Skip to content

Commit

Permalink
Add Condition and Expression local database tables and local support.
Browse files Browse the repository at this point in the history
  - Also formatting etc.
  • Loading branch information
sufyanAbbasi committed Feb 27, 2024
1 parent 5ec779b commit afb6907
Show file tree
Hide file tree
Showing 19 changed files with 543 additions and 122 deletions.
2 changes: 1 addition & 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,7 +26,7 @@ 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ constructor(
/** The expressions to evaluate to fulfill the condition. */
val expressions: List<Expression> = listOf(),
) {
private val <T> T.exhaustive: T
get() = this

/** Match type names as they appear in the remote database. */
enum class MatchType {
Expand All @@ -44,17 +46,13 @@ constructor(
}

/** Given the user's task selections, determine whether the condition is fulfilled. */
fun fulfilledBy(taskSelections: TaskSelections): Boolean {
return when (matchType) {
fun fulfilledBy(taskSelections: TaskSelections) =
when (matchType) {
MatchType.MATCH_ANY -> expressions.any { it.fulfilledBy(taskSelections) }
MatchType.MATCH_ALL -> expressions.all { it.fulfilledBy(taskSelections) }
MatchType.MATCH_ONE -> expressions.filter { it.fulfilledBy(taskSelections) }.size == 1
else -> throw IllegalArgumentException("Unknown match type: $matchType")
}.exhaustive
}

private val <T> T.exhaustive: T
get() = this
}

data class Expression
Expand All @@ -76,20 +74,18 @@ constructor(
ONE_OF_SELECTED,
}

private val <T> T.exhaustive: T
get() = this

/** Given the selected options for this task, determine whether the expression is fulfilled. */
fun fulfilledBy(taskSelections: TaskSelections): Boolean {
return taskSelections[this.taskId]?.let { selection -> this.fulfilled(selection) } ?: false
}
fun fulfilledBy(taskSelections: TaskSelections): Boolean =
taskSelections[this.taskId]?.let { selection -> this.fulfilled(selection) } ?: false

private fun fulfilled(selectedOptions: Set<OptionId>): Boolean {
return when (expressionType) {
private fun fulfilled(selectedOptions: Set<OptionId>): Boolean =
when (expressionType) {
ExpressionType.ANY_OF_SELECTED -> optionIds.any { it in selectedOptions }
ExpressionType.ALL_OF_SELECTED -> selectedOptions.containsAll(optionIds)
ExpressionType.ONE_OF_SELECTED -> selectedOptions.intersect(optionIds).size == 1
else -> throw IllegalArgumentException("Unknown expression type: $expressionType")
}.exhaustive
}

private val <T> T.exhaustive: T
get() = this
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ abstract class LocalDataStoreModule {
abstract fun localLocationOfInterestStore(
store: RoomLocationOfInterestStore
): LocalLocationOfInterestStore

@Binds
@Singleton
abstract fun offlineAreaStore(store: RoomOfflineAreaStore): LocalOfflineAreaStore
Expand Down Expand Up @@ -102,5 +103,15 @@ abstract class LocalDataStoreModule {
fun userDao(localDatabase: LocalDatabase): UserDao {
return localDatabase.userDao()
}

@Provides
fun conditionDao(localDatabase: LocalDatabase): ConditionDao {
return localDatabase.conditionDao()
}

@Provides
fun expressionDao(localDatabase: LocalDatabase): ExpressionDao {
return localDatabase.expressionDao()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,43 @@ import com.google.android.ground.persistence.local.room.converter.JsonArrayTypeC
import com.google.android.ground.persistence.local.room.converter.JsonObjectTypeConverter
import com.google.android.ground.persistence.local.room.converter.LoiPropertiesMapConverter
import com.google.android.ground.persistence.local.room.converter.StyleTypeConverter
import com.google.android.ground.persistence.local.room.dao.*
import com.google.android.ground.persistence.local.room.entity.*
import com.google.android.ground.persistence.local.room.fields.*
import com.google.android.ground.persistence.local.room.dao.ConditionDao
import com.google.android.ground.persistence.local.room.dao.ExpressionDao
import com.google.android.ground.persistence.local.room.dao.JobDao
import com.google.android.ground.persistence.local.room.dao.LocationOfInterestDao
import com.google.android.ground.persistence.local.room.dao.LocationOfInterestMutationDao
import com.google.android.ground.persistence.local.room.dao.MultipleChoiceDao
import com.google.android.ground.persistence.local.room.dao.OfflineAreaDao
import com.google.android.ground.persistence.local.room.dao.OptionDao
import com.google.android.ground.persistence.local.room.dao.SubmissionDao
import com.google.android.ground.persistence.local.room.dao.SubmissionMutationDao
import com.google.android.ground.persistence.local.room.dao.SurveyDao
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.UserDao
import com.google.android.ground.persistence.local.room.entity.ConditionEntity
import com.google.android.ground.persistence.local.room.entity.ExpressionEntity
import com.google.android.ground.persistence.local.room.entity.JobEntity
import com.google.android.ground.persistence.local.room.entity.LocationOfInterestEntity
import com.google.android.ground.persistence.local.room.entity.LocationOfInterestMutationEntity
import com.google.android.ground.persistence.local.room.entity.MultipleChoiceEntity
import com.google.android.ground.persistence.local.room.entity.OfflineAreaEntity
import com.google.android.ground.persistence.local.room.entity.OptionEntity
import com.google.android.ground.persistence.local.room.entity.SubmissionEntity
import com.google.android.ground.persistence.local.room.entity.SubmissionMutationEntity
import com.google.android.ground.persistence.local.room.entity.SurveyEntity
import com.google.android.ground.persistence.local.room.entity.TaskEntity
import com.google.android.ground.persistence.local.room.entity.TileSourceEntity
import com.google.android.ground.persistence.local.room.entity.UserEntity
import com.google.android.ground.persistence.local.room.fields.EntityState
import com.google.android.ground.persistence.local.room.fields.ExpressionEntityType
import com.google.android.ground.persistence.local.room.fields.MatchEntityType
import com.google.android.ground.persistence.local.room.fields.MultipleChoiceEntityType
import com.google.android.ground.persistence.local.room.fields.MutationEntitySyncStatus
import com.google.android.ground.persistence.local.room.fields.MutationEntityType
import com.google.android.ground.persistence.local.room.fields.OfflineAreaEntityState
import com.google.android.ground.persistence.local.room.fields.TaskEntityType
import com.google.android.ground.persistence.local.room.fields.TileSetEntityState

/**
* Main entry point to local database API, exposing data access objects (DAOs) for interacting with
Expand All @@ -49,14 +83,18 @@ import com.google.android.ground.persistence.local.room.fields.*
SubmissionEntity::class,
SubmissionMutationEntity::class,
OfflineAreaEntity::class,
UserEntity::class
UserEntity::class,
ConditionEntity::class,
ExpressionEntity::class,
],
version = Config.DB_VERSION,
exportSchema = false
)
@TypeConverters(
TaskEntityType::class,
MultipleChoiceEntityType::class,
MatchEntityType::class,
ExpressionEntityType::class,
MutationEntityType::class,
EntityState::class,
GeometryWrapperTypeConverter::class,
Expand All @@ -81,4 +119,6 @@ abstract class LocalDatabase : RoomDatabase() {
abstract fun submissionMutationDao(): SubmissionMutationDao
abstract fun offlineAreaDao(): OfflineAreaDao
abstract fun userDao(): UserDao
abstract fun conditionDao(): ConditionDao
abstract fun expressionDao(): ExpressionDao
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Google LLC
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -29,23 +29,27 @@ import com.google.android.ground.model.mutation.LocationOfInterestMutation
import com.google.android.ground.model.mutation.SubmissionMutation
import com.google.android.ground.model.submission.Submission
import com.google.android.ground.model.submission.SubmissionData
import com.google.android.ground.model.task.Condition
import com.google.android.ground.model.task.Expression
import com.google.android.ground.model.task.MultipleChoice
import com.google.android.ground.model.task.Option
import com.google.android.ground.model.task.Task
import com.google.android.ground.model.task.TaskId
import com.google.android.ground.persistence.local.LocalDataConsistencyException
import com.google.android.ground.persistence.local.room.entity.*
import com.google.android.ground.persistence.local.room.fields.*
import com.google.android.ground.persistence.local.room.relations.ConditionEntityAndRelations
import com.google.android.ground.persistence.local.room.relations.JobEntityAndRelations
import com.google.android.ground.persistence.local.room.relations.SurveyEntityAndRelations
import com.google.android.ground.persistence.local.room.relations.TaskEntityAndRelations
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 +117,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,9 +167,9 @@ 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}"
)
?: throw LocalDataConsistencyException(
"Unknown jobId ${this.jobId} in location of interest ${this.id}"
)
)
}

Expand Down Expand Up @@ -421,14 +425,24 @@ fun TaskEntityAndRelations.toModelObject(): Task {
multipleChoice = multipleChoiceEntities[0].toModelObject(optionEntities)
}

var condition: Condition? = null

if (conditionEntityAndRelations.isNotEmpty()) {
if (conditionEntityAndRelations.size > 1) {
Timber.e("More than 1 condition found for task")
}
condition = conditionEntityAndRelations[0].toModelObject()
}

return Task(
taskEntity.id,
taskEntity.index,
taskEntity.taskType.toTaskType(),
taskEntity.label!!,
taskEntity.isRequired,
multipleChoice,
taskEntity.isAddLoiTask
taskEntity.isAddLoiTask,
condition = condition,
)
}

Expand All @@ -437,3 +451,36 @@ fun User.toLocalDataStoreObject() =

fun UserEntity.toModelObject() =
User(id = id, email = email, displayName = displayName, photoUrl = photoUrl)

fun Condition.toLocalDataStoreObject(parentTaskId: TaskId) =
ConditionEntity(parentTaskId = parentTaskId, matchType = MatchEntityType.fromMatchType(matchType))

fun ConditionEntityAndRelations.toModelObject(): Condition? {
val expressions: List<Expression>?

if (expressionEntities.isEmpty()) {
return null
} else {
expressions = expressionEntities.map { it.toModelObject() }
}

return Condition(
conditionEntity.matchType.toMatchType(),
expressions = expressions,
)
}

fun Expression.toLocalDataStoreObject(parentTaskId: TaskId): ExpressionEntity =
ExpressionEntity(
parentTaskId = parentTaskId,
expressionType = ExpressionEntityType.fromExpressionType(expressionType),
taskId = taskId,
optionIds = optionIds.joinToString(",")
)

fun ExpressionEntity.toModelObject(): Expression =
Expression(
expressionType = expressionType.toExpressionType(),
taskId = taskId,
optionIds = optionIds?.split(',')?.toSet() ?: setOf(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.ground.persistence.local.room.dao

import androidx.room.Dao
import com.google.android.ground.persistence.local.room.entity.ConditionEntity

@Dao
interface ConditionDao : BaseDao<ConditionEntity>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.ground.persistence.local.room.dao

import androidx.room.Dao
import com.google.android.ground.persistence.local.room.entity.ExpressionEntity

@Dao
interface ExpressionDao : BaseDao<ExpressionEntity>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.ground.persistence.local.room.entity

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.android.ground.persistence.local.room.fields.MatchEntityType

@Entity(
tableName = "condition",
foreignKeys =
[
ForeignKey(
entity = TaskEntity::class,
parentColumns = ["id"],
childColumns = ["parent_task_id"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index("parent_task_id")]
)
data class ConditionEntity(
@ColumnInfo(name = "parent_task_id") @PrimaryKey val parentTaskId: String,
@ColumnInfo(name = "match_type") val matchType: MatchEntityType,
)
Loading

0 comments on commit afb6907

Please sign in to comment.