Skip to content

Commit

Permalink
Write submissions in remote db using protos (#2556)
Browse files Browse the repository at this point in the history
* Update proto dep to latest

* Add getter/setter for other text

* Add helper method to convert submission mutation model to proto

* Remove unused parenthesis

* Keep public methods together

* Update tests in SubmissionMutationConverterTest

* Update remaining "delete" and "unknown" test scenarios

* Replace SubmissionMutationConverter with the model --> proto --> firebase converter

* Cleanup dead code

* Add quotations

* fix import order

* Add a todo for converting timestamp to UTC

* Fix import order

* Add test cases for date/time task
  • Loading branch information
shobhitagarwal1612 authored Jul 24, 2024
1 parent 5c2f48b commit b1340ef
Show file tree
Hide file tree
Showing 11 changed files with 278 additions and 669 deletions.
2 changes: 1 addition & 1 deletion ground/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ dependencies {
api("com.google.protobuf:protobuf-kotlin-lite:4.26.1")

// Pulls protodefs from the specified commit in the ground-platform repo.
groundProtoJar "com.github.google:ground-platform:7420132@jar"
groundProtoJar "com.github.google:ground-platform:3e9162a@jar"
}

protobuf {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ class MultipleChoiceTaskData(
val selectedOptionIds: List<String>,
) : TaskData {

// TODO: Reuse the key value here and in the view model
fun hasOtherText(): Boolean = selectedOptionIds.contains("OTHER_ID")

fun getOtherText(): String {
val otherId = selectedOptionIds.firstOrNull { it == "OTHER_ID" } ?: return ""
return multipleChoice?.getOptionById(otherId)?.label ?: ""
}

// TODO: Make these inner classes non-static and access Task directly.
override fun getDetailsText(): String =
selectedOptionIds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ internal object ValueJsonConverter {
private fun toJsonArray(response: MultipleChoiceTaskData): JSONArray =
JSONArray().apply { response.selectedOptionIds.forEach { this.put(it) } }

// TODO: Replace with proto conversion logic if this is still necessary
fun toResponse(task: Task, obj: Any): TaskData? {
if (JSONObject.NULL === obj) {
return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,74 @@ import com.google.android.ground.model.geometry.Polygon
import com.google.android.ground.model.locationofinterest.LoiProperties
import com.google.android.ground.model.mutation.LocationOfInterestMutation
import com.google.android.ground.model.mutation.Mutation
import com.google.android.ground.model.mutation.SubmissionMutation
import com.google.android.ground.model.submission.CaptureLocationTaskData
import com.google.android.ground.model.submission.DateTaskData
import com.google.android.ground.model.submission.GeometryTaskData
import com.google.android.ground.model.submission.MultipleChoiceTaskData
import com.google.android.ground.model.submission.NumberTaskData
import com.google.android.ground.model.submission.PhotoTaskData
import com.google.android.ground.model.submission.TextTaskData
import com.google.android.ground.model.submission.TimeTaskData
import com.google.android.ground.model.submission.ValueDelta
import com.google.android.ground.model.submission.isNotNullOrEmpty
import com.google.android.ground.model.task.Task
import com.google.android.ground.proto.LinearRing as LinearRingProto
import com.google.android.ground.proto.LocationOfInterest.Property
import com.google.android.ground.proto.LocationOfInterest.Source
import com.google.android.ground.proto.LocationOfInterestKt.property
import com.google.android.ground.proto.MultiPolygon as MultiPolygonProto
import com.google.android.ground.proto.Polygon as PolygonProto
import com.google.android.ground.proto.TaskDataKt.captureLocationResult
import com.google.android.ground.proto.TaskDataKt.dateTimeResponse
import com.google.android.ground.proto.TaskDataKt.drawGeometryResult
import com.google.android.ground.proto.TaskDataKt.multipleChoiceResponses
import com.google.android.ground.proto.TaskDataKt.numberResponse
import com.google.android.ground.proto.TaskDataKt.takePhotoResult
import com.google.android.ground.proto.TaskDataKt.textResponse
import com.google.android.ground.proto.auditInfo
import com.google.android.ground.proto.coordinates
import com.google.android.ground.proto.geometry
import com.google.android.ground.proto.locationOfInterest
import com.google.android.ground.proto.point
import com.google.android.ground.proto.submission
import com.google.android.ground.proto.taskData
import com.google.protobuf.timestamp
import java.util.Date
import kotlinx.collections.immutable.toImmutableMap

// TODO: Add test coverage
fun SubmissionMutation.createSubmissionMessage(user: User) = submission {
assert(userId == user.id) { "UserId doesn't match: expected $userId, found ${user.id}" }

val me = this@createSubmissionMessage
id = submissionId
loiId = locationOfInterestId
jobId = job.id
ownerId = me.userId

deltas.forEach {
if (it.newTaskData.isNotNullOrEmpty()) {
taskData.add(it.toMessage())
}
}

val auditInfo = createAuditInfoMessage(user, clientTimestamp)
when (type) {
Mutation.Type.CREATE -> {
created = auditInfo
lastModified = auditInfo
}
Mutation.Type.UPDATE -> {
lastModified = auditInfo
}
Mutation.Type.DELETE,
Mutation.Type.UNKNOWN -> {
throw UnsupportedOperationException()
}
}
}

fun LocationOfInterestMutation.createLoiMessage(user: User) = locationOfInterest {
assert(userId == user.id) { "UserId doesn't match: expected $userId, found ${user.id}" }

Expand Down Expand Up @@ -75,6 +128,50 @@ fun LocationOfInterestMutation.createLoiMessage(user: User) = locationOfInterest
}
}

private fun ValueDelta.toMessage() = taskData {
val me = this@toMessage
// TODO: What should be the ID?
taskId = me.taskId
// TODO: Add "skipped" field
when (taskType) {
Task.Type.TEXT -> textResponse = textResponse { text = (newTaskData as TextTaskData).text }
Task.Type.NUMBER -> numberResponse = numberResponse {
number = (newTaskData as NumberTaskData).value
}
// TODO: Ensure the dates are always converted to UTC time zone.
Task.Type.DATE -> dateTimeResponse = dateTimeResponse {
dateTime = timestamp { seconds = (newTaskData as DateTaskData).date.time * 1000 }
}
// TODO: Ensure the dates are always converted to UTC time zone.
Task.Type.TIME -> dateTimeResponse = dateTimeResponse {
dateTime = timestamp { seconds = (newTaskData as TimeTaskData).time.time * 1000 }
}
Task.Type.MULTIPLE_CHOICE -> multipleChoiceResponses = multipleChoiceResponses {
(newTaskData as MultipleChoiceTaskData).selectedOptionIds.forEach {
selectedOptionIds.add(it)
}
if (newTaskData.hasOtherText()) {
otherText = newTaskData.getOtherText()
}
}
Task.Type.DROP_PIN,
Task.Type.DRAW_AREA -> drawGeometryResult = drawGeometryResult {
geometry = (newTaskData as GeometryTaskData).geometry.toMessage()
}
Task.Type.CAPTURE_LOCATION -> captureLocationResult = captureLocationResult {
val data = newTaskData as CaptureLocationTaskData
data.altitude?.let { altitude = it }
data.accuracy?.let { accuracy = it }
coordinates = data.location.coordinates.toMessage()
// TODO: Add timestamp
}
Task.Type.PHOTO -> takePhotoResult = takePhotoResult {
photoPath = (newTaskData as PhotoTaskData).path
}
Task.Type.UNKNOWN -> error("Unknown task type")
}
}

private fun createAuditInfoMessage(user: User, timestamp: Date) = auditInfo {
userId = user.id
displayName = user.displayName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@
package com.google.android.ground.persistence.remote.firebase.schema

import com.google.android.ground.model.AuditInfo
import com.google.android.ground.model.User
import com.google.android.ground.model.mutation.Mutation
import com.google.android.ground.persistence.remote.DataStoreException
import com.google.firebase.Timestamp

/** Converts between Firestore nested objects and [AuditInfo] instances. */
internal object AuditInfoConverter {
Expand All @@ -34,12 +31,4 @@ internal object AuditInfoConverter {
doc.serverTimestamp?.toDate(),
)
}

@JvmStatic
fun fromMutationAndUser(mutation: Mutation, user: User): AuditInfoNestedObject =
AuditInfoNestedObject(
UserConverter.toNestedObject(user),
Timestamp(mutation.clientTimestamp),
null,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,11 @@
*/
package com.google.android.ground.persistence.remote.firebase.schema

import com.google.android.ground.model.geometry.Point
import com.google.android.ground.model.submission.CaptureLocationTaskData

/** Converts between [CaptureLocationTaskData] and its equivalent remote representation. */
object CaptureLocationResultConverter {
const val ACCURACY_KEY = "accuracy"
const val ALTITUDE_KEY = "altitude"
const val GEOMETRY_KEY = "geometry"

fun toFirestoreMap(result: CaptureLocationTaskData): Result<Map<String, Any>> =
Result.runCatching {
mapOf(
ACCURACY_KEY to result.accuracy!!,
ALTITUDE_KEY to result.altitude!!,
GEOMETRY_KEY to GeometryConverter.toFirestoreMap(result.geometry).getOrThrow(),
)
}

fun fromFirestoreMap(map: Map<String, *>?): Result<CaptureLocationTaskData> =
Result.runCatching {
val accuracy = map?.get(ACCURACY_KEY) as? Double
val altitude = map?.get(ALTITUDE_KEY) as? Double
val geometry = GeometryConverter.fromFirestoreMap(map?.get(GEOMETRY_KEY) as? Map<String, *>)
CaptureLocationTaskData(geometry.getOrThrow() as Point, altitude, accuracy)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import com.google.android.ground.model.User
import com.google.android.ground.model.mutation.Mutation
import com.google.android.ground.model.mutation.SubmissionMutation
import com.google.android.ground.persistence.remote.firebase.base.FluentDocumentReference
import com.google.android.ground.persistence.remote.firebase.protobuf.createSubmissionMessage
import com.google.android.ground.persistence.remote.firebase.protobuf.toFirestoreMap
import com.google.firebase.firestore.DocumentReference
import com.google.firebase.firestore.WriteBatch

Expand All @@ -30,7 +32,7 @@ class SubmissionDocumentReference internal constructor(ref: DocumentReference) :
fun addMutationToBatch(mutation: SubmissionMutation, user: User, batch: WriteBatch) {
when (mutation.type) {
Mutation.Type.CREATE,
Mutation.Type.UPDATE -> merge(SubmissionMutationConverter.toMap(mutation, user), batch)
Mutation.Type.UPDATE -> merge(mutation.createSubmissionMessage(user).toFirestoreMap(), batch)
Mutation.Type.DELETE -> delete(batch)
else -> throw IllegalArgumentException("Unknown mutation type ${mutation.type}")
}
Expand Down
Loading

0 comments on commit b1340ef

Please sign in to comment.