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

Use both task ID and job ID as primary keys and refactor to use $addLoi as the task ID. #2276

Closed
wants to merge 7 commits into from
5 changes: 4 additions & 1 deletion ground/src/main/java/com/google/android/ground/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ object Config {

// Local db settings.
// TODO(#128): Reset version to 1 before releasing.
const val DB_VERSION = 113
const val DB_VERSION = 114
const val DB_NAME = "ground.db"

// Firebase Cloud Firestore settings.
const val FIRESTORE_LOGGING_ENABLED = true

// Tasks.
const val LOI_TASK_ID_PREFIX = "\$addLoi"

// Photos
const val PHOTO_EXT = ".jpg"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ data class Job(
class TaskNotFoundException(taskId: String) : Throwable(message = "unknown task $taskId")

val canDataCollectorsAddLois: Boolean
get() = strategy != DataCollectionStrategy.PREDEFINED
get() = strategy != DataCollectionStrategy.PREDEFINED && getAddLoiTask() != null

val tasksSorted: List<Task>
get() = tasks.values.sortedBy { it.index }
Expand All @@ -48,7 +48,7 @@ data class Job(
/** Job must contain at-most 1 `AddLoiTask`. */
fun getAddLoiTask(): Task? =
tasks.values
.filter { it.isAddLoiTask }
.filter { it.isAddLoiTask() }
.apply { check(size <= 1) { "Expected 0 or 1, found $size AddLoiTasks" } }
.firstOrNull()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package com.google.android.ground.model.task

import com.google.android.ground.Config.LOI_TASK_ID_PREFIX

/**
* Describes a user-defined task.
*
Expand All @@ -31,7 +33,6 @@ constructor(
val label: String,
val isRequired: Boolean,
val multipleChoice: MultipleChoice? = null,
val isAddLoiTask: Boolean = false
) {

/**
Expand All @@ -50,4 +51,6 @@ constructor(
DRAW_AREA,
CAPTURE_LOCATION
}

fun isAddLoiTask(): Boolean = id.startsWith(LOI_TASK_ID_PREFIX)
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ import com.google.android.ground.persistence.local.room.relations.TaskEntityAndR
import com.google.android.ground.ui.map.Bounds
import com.google.common.reflect.TypeToken
import com.google.gson.Gson
import java.util.*
import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentMap
import org.json.JSONObject
import timber.log.Timber
import java.util.*

fun AuditInfo.toLocalDataStoreObject(): AuditInfoEntity =
AuditInfoEntity(
Expand Down Expand Up @@ -113,14 +113,14 @@ fun JobEntityAndRelations.toModelObject(): Job {
style = jobEntity.style?.toModelObject(),
name = jobEntity.name,
strategy =
jobEntity.strategy.let {
try {
DataCollectionStrategy.valueOf(it)
} catch (e: IllegalArgumentException) {
Timber.e("unknown data collection strategy $it")
DataCollectionStrategy.UNKNOWN
}
},
jobEntity.strategy.let {
try {
DataCollectionStrategy.valueOf(it)
} catch (e: IllegalArgumentException) {
Timber.e("unknown data collection strategy $it")
DataCollectionStrategy.UNKNOWN
}
},
tasks = taskMap.toPersistentMap()
)
}
Expand Down Expand Up @@ -163,10 +163,10 @@ fun LocationOfInterestEntity.toModelObject(survey: Survey): LocationOfInterest =
submissionCount = submissionCount,
properties = properties,
job =
survey.getJob(jobId = jobId)
?: throw LocalDataConsistencyException(
"Unknown jobId ${this.jobId} in location of interest ${this.id}"
)
survey.getJob(jobId = jobId)
?: throw LocalDataConsistencyException(
"Unknown jobId ${this.jobId} in location of interest ${this.id}"
)
)
}

Expand Down Expand Up @@ -236,8 +236,8 @@ fun MultipleChoiceEntity.toModelObject(optionEntities: List<OptionEntity>): Mult
return MultipleChoice(options.toPersistentList(), this.type.toCardinality())
}

fun MultipleChoice.toLocalDataStoreObject(taskId: String): MultipleChoiceEntity =
MultipleChoiceEntity(taskId, MultipleChoiceEntityType.fromCardinality(this.cardinality))
fun MultipleChoice.toLocalDataStoreObject(taskId: String, jobId: String): MultipleChoiceEntity =
MultipleChoiceEntity(taskId, jobId, MultipleChoiceEntityType.fromCardinality(this.cardinality))

private fun OfflineAreaEntityState.toModelObject() =
when (this) {
Expand Down Expand Up @@ -282,8 +282,8 @@ fun OfflineAreaEntity.toModelObject(): OfflineArea {
)
}

fun Option.toLocalDataStoreObject(taskId: String) =
OptionEntity(id = this.id, code = this.code, label = this.label, taskId = taskId)
fun Option.toLocalDataStoreObject(taskId: String, jobId: String) =
OptionEntity(id = this.id, code = this.code, label = this.label, taskId = taskId, jobId = jobId)

fun OptionEntity.toModelObject() = Option(id = this.id, code = this.code, label = this.label)

Expand Down Expand Up @@ -400,15 +400,16 @@ fun Survey.toLocalDataStoreObject() =
acl = JSONObject(acl as Map<*, *>)
)

fun Task.toLocalDataStoreObject(jobId: String?) =
fun Task.toLocalDataStoreObject(jobId: String) =
TaskEntity(
id = id,
jobId = jobId,
index = index,
label = label,
isRequired = isRequired,
taskType = TaskEntityType.fromTaskType(type),
isAddLoiTask = isAddLoiTask
// Deprecated.
isAddLoiTask = false,
)

fun TaskEntityAndRelations.toModelObject(): Task {
Expand All @@ -429,7 +430,6 @@ fun TaskEntityAndRelations.toModelObject(): Task {
taskEntity.label!!,
taskEntity.isRequired,
multipleChoice,
taskEntity.isAddLoiTask
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,28 @@
*/
package com.google.android.ground.persistence.local.room.entity

import androidx.room.*
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import com.google.android.ground.persistence.local.room.fields.MultipleChoiceEntityType

@Entity(
tableName = "multiple_choice",
foreignKeys =
[
ForeignKey(
entity = TaskEntity::class,
parentColumns = ["id"],
childColumns = ["task_id"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index("task_id")]
[
ForeignKey(
entity = TaskEntity::class,
parentColumns = ["id", "job_id"],
childColumns = ["task_id", "job_id"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index("task_id"), Index("job_id")],
primaryKeys = ["task_id", "job_id"],
)
data class MultipleChoiceEntity(
@ColumnInfo(name = "task_id") @PrimaryKey val taskId: String,
@ColumnInfo(name = "task_id") val taskId: String,
@ColumnInfo(name = "job_id") val jobId: String,
@ColumnInfo(name = "type") val type: MultipleChoiceEntityType
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,29 @@
*/
package com.google.android.ground.persistence.local.room.entity

import androidx.room.*
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index

@Entity(
tableName = "option",
foreignKeys =
[
ForeignKey(
entity = TaskEntity::class,
parentColumns = ["id"],
childColumns = ["task_id"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index("task_id")],
primaryKeys = ["id"]
[
ForeignKey(
entity = TaskEntity::class,
parentColumns = ["id", "job_id"],
childColumns = ["task_id", "job_id"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index("task_id"), Index("job_id")],
primaryKeys = ["task_id", "job_id"],
)
data class OptionEntity(
@ColumnInfo(name = "id") val id: String,
@ColumnInfo(name = "code") val code: String,
@ColumnInfo(name = "label") val label: String,
@ColumnInfo(name = "task_id") val taskId: String
@ColumnInfo(name = "task_id") val taskId: String,
@ColumnInfo(name = "job_id") val jobId: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,33 @@
*/
package com.google.android.ground.persistence.local.room.entity

import androidx.room.*
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import com.google.android.ground.persistence.local.room.fields.TaskEntityType

@Entity(
tableName = "task",
foreignKeys =
[
ForeignKey(
entity = JobEntity::class,
parentColumns = ["id"],
childColumns = ["job_id"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index("job_id")]
[
ForeignKey(
entity = JobEntity::class,
parentColumns = ["id"],
childColumns = ["job_id"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index("id"), Index("job_id")],
primaryKeys = ["id", "job_id"],
)
data class TaskEntity(
@ColumnInfo(name = "id") @PrimaryKey val id: String,
@ColumnInfo(name = "id") val id: String,
@ColumnInfo(name = "index") val index: Int,
@ColumnInfo(name = "task_type") val taskType: TaskEntityType,
@ColumnInfo(name = "label") val label: String?,
@ColumnInfo(name = "is_required") val isRequired: Boolean,
@ColumnInfo(name = "job_id") val jobId: String?,
@ColumnInfo(name = "job_id") val jobId: String,
// Deprecated.
@ColumnInfo(name = "is_add_loi_task") val isAddLoiTask: Boolean
)
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,31 @@ import com.google.android.ground.persistence.local.room.dao.TaskDao
import com.google.android.ground.persistence.local.room.dao.TileSourceDao
import com.google.android.ground.persistence.local.room.dao.insertOrUpdate
import com.google.android.ground.persistence.local.stores.LocalSurveyStore
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton

/** Manages access to [Survey] objects persisted in local storage. */
@Singleton
class RoomSurveyStore @Inject internal constructor() : LocalSurveyStore {
@Inject lateinit var optionDao: OptionDao
@Inject lateinit var multipleChoiceDao: MultipleChoiceDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var jobDao: JobDao
@Inject lateinit var surveyDao: SurveyDao
@Inject lateinit var tileSourceDao: TileSourceDao
@Inject
lateinit var optionDao: OptionDao

@Inject
lateinit var multipleChoiceDao: MultipleChoiceDao

@Inject
lateinit var taskDao: TaskDao

@Inject
lateinit var jobDao: JobDao

@Inject
lateinit var surveyDao: SurveyDao

@Inject
lateinit var tileSourceDao: TileSourceDao

override val surveys: Flow<List<Survey>>
get() = surveyDao.getAll().map { surveyEntities -> surveyEntities.map { it.toModelObject() } }
Expand Down Expand Up @@ -73,22 +84,26 @@ class RoomSurveyStore @Inject internal constructor() : LocalSurveyStore {
override suspend fun deleteSurvey(survey: Survey) =
surveyDao.delete(survey.toLocalDataStoreObject())

private suspend fun insertOrUpdateOption(taskId: String, option: Option) =
optionDao.insertOrUpdate(option.toLocalDataStoreObject(taskId))
private suspend fun insertOrUpdateOption(taskId: String, jobId: String, option: Option) =
optionDao.insertOrUpdate(option.toLocalDataStoreObject(taskId, jobId))

private suspend fun insertOrUpdateOptions(taskId: String, options: List<Option>) {
options.forEach { insertOrUpdateOption(taskId, it) }
private suspend fun insertOrUpdateOptions(taskId: String, jobId: String, options: List<Option>) {
options.forEach { insertOrUpdateOption(taskId, jobId, it) }
}

private suspend fun insertOrUpdateMultipleChoice(taskId: String, multipleChoice: MultipleChoice) {
multipleChoiceDao.insertOrUpdate(multipleChoice.toLocalDataStoreObject(taskId))
insertOrUpdateOptions(taskId, multipleChoice.options)
private suspend fun insertOrUpdateMultipleChoice(
taskId: String,
jobId: String,
multipleChoice: MultipleChoice
) {
multipleChoiceDao.insertOrUpdate(multipleChoice.toLocalDataStoreObject(taskId, jobId))
insertOrUpdateOptions(taskId, jobId, multipleChoice.options)
}

private suspend fun insertOrUpdateTask(jobId: String, task: Task) {
taskDao.insertOrUpdate(task.toLocalDataStoreObject(jobId))
if (task.multipleChoice != null) {
insertOrUpdateMultipleChoice(task.id, task.multipleChoice)
insertOrUpdateMultipleChoice(task.id, jobId, task.multipleChoice)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ internal object TaskConverter {
em.label!!,
em.required != null && em.required,
multipleChoice,
em.addLoiTask ?: false
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,4 @@ data class TaskNestedObject(
val label: String? = null,
val options: Map<String, OptionNestedObject>? = null,
val required: Boolean? = null,
val addLoiTask: Boolean? = false
)
Loading