Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Write submissions in remote db using protos #2556

Merged
merged 14 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 }
shobhitagarwal1612 marked this conversation as resolved.
Show resolved Hide resolved
}
// 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 }
sufyanAbbasi marked this conversation as resolved.
Show resolved Hide resolved
}
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>> =
sufyanAbbasi marked this conversation as resolved.
Show resolved Hide resolved
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