Skip to content

Commit

Permalink
Merge branch 'master' into sufy/2300/labeled-lois
Browse files Browse the repository at this point in the history
  • Loading branch information
sufyanAbbasi authored Mar 5, 2024
2 parents 21080ae + b70c871 commit 7dddadb
Show file tree
Hide file tree
Showing 25 changed files with 1,127 additions and 45 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
@@ -0,0 +1,87 @@
/*
* 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.model.task

import com.google.android.ground.model.submission.MultipleChoiceResponse
import com.google.android.ground.model.submission.Value

/** The task ID. */
typealias TaskId = String
/** The selected values keyed by task ID. */
typealias TaskSelections = Map<String, Value>

/**
* Describes a user-defined condition on a task, which determines whether the given task should be
* hidden due to failure of fulfillment based on the input expressions.
*/
data class Condition(
/** Determines the evaluation condition for fulfillment (e.g. all or some expressions). */
val matchType: MatchType = MatchType.UNKNOWN,
/** The expressions to evaluate to fulfill the condition. */
val expressions: List<Expression> = listOf(),
) {

/** Match type names as they appear in the remote database. */
enum class MatchType {
UNKNOWN,
MATCH_ANY,
MATCH_ALL,
MATCH_ONE,
}

/** Given the user's task selections, determine whether the condition is fulfilled. */
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
MatchType.UNKNOWN -> throw IllegalArgumentException("Unknown match type: $matchType")
}
}

data class Expression(
/** Determines the evaluation condition for the expression (e.g. all or some selected options). */
val expressionType: ExpressionType = ExpressionType.UNKNOWN,
/** The task ID associated with this expression. */
val taskId: String,
/** The option IDs that need to be selected to fulfill the condition. */
val optionIds: Set<String> = setOf(),
) {

/** Task type names as they appear in the remote database. */
enum class ExpressionType {
UNKNOWN,
ANY_OF_SELECTED,
ALL_OF_SELECTED,
ONE_OF_SELECTED,
}

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

private fun fulfilled(value: Value): Boolean {
if (value !is MultipleChoiceResponse) return false
val selectedOptions = value.selectedOptionIds.toSet()
return 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
ExpressionType.UNKNOWN ->
throw IllegalArgumentException("Unknown expression type: $expressionType")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ constructor(
val label: String,
val isRequired: Boolean,
val multipleChoice: MultipleChoice? = null,
val isAddLoiTask: Boolean = false
val isAddLoiTask: Boolean = false,
val condition: Condition? = null,
) {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,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 Down Expand Up @@ -92,4 +130,8 @@ abstract class LocalDatabase : RoomDatabase() {
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
Expand Up @@ -29,12 +29,16 @@ 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
Expand Down Expand Up @@ -418,20 +422,30 @@ fun TaskEntityAndRelations.toModelObject(): Task {

if (multipleChoiceEntities.isNotEmpty()) {
if (multipleChoiceEntities.size > 1) {
Timber.e("More than 1 multiple choice found for task")
Timber.e("More than 1 multiple choice found for task: $taskEntity")
}

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: $taskEntity")
}
condition = conditionEntityAndRelations[0].toModelObject()
}

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

Expand All @@ -440,3 +454,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,21 @@
/*
* 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,21 @@
/*
* 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 7dddadb

Please sign in to comment.