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

Minor local LOI persistency renames #2861

Merged
merged 6 commits into from
Nov 21, 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
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import com.google.android.ground.persistence.local.room.entity.SubmissionMutatio
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.UserEntity
import com.google.android.ground.persistence.local.room.fields.EntityState
import com.google.android.ground.persistence.local.room.fields.EntityDeletionState
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
Expand Down Expand Up @@ -96,7 +96,7 @@ import com.google.android.ground.persistence.local.room.fields.TileSetEntityStat
MatchEntityType::class,
ExpressionEntityType::class,
MutationEntityType::class,
EntityState::class,
EntityDeletionState::class,
GeometryWrapperTypeConverter::class,
JsonArrayTypeConverter::class,
JsonObjectTypeConverter::class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ fun LocationOfInterest.toLocalDataStoreObject() =
id = id,
surveyId = surveyId,
jobId = job.id,
state = EntityState.DEFAULT,
deletionState = EntityDeletionState.DEFAULT,
created = created.toLocalDataStoreObject(),
lastModified = lastModified.toLocalDataStoreObject(),
geometry = geometry.toLocalDataStoreObject(),
Expand Down Expand Up @@ -170,7 +170,7 @@ fun LocationOfInterestMutation.toLocalDataStoreObject(user: User): LocationOfInt
id = locationOfInterestId,
surveyId = surveyId,
jobId = jobId,
state = EntityState.DEFAULT,
deletionState = EntityDeletionState.DEFAULT,
// TODO(#1562): Preserve creation audit info for UPDATE mutations.
created = auditInfo,
lastModified = auditInfo,
Expand Down Expand Up @@ -306,7 +306,7 @@ fun Submission.toLocalDataStoreObject() =
id = this.id,
jobId = this.job.id,
locationOfInterestId = this.locationOfInterest.id,
state = EntityState.DEFAULT,
deletionState = EntityDeletionState.DEFAULT,
data = SubmissionDataConverter.toString(this.data),
created = this.created.toLocalDataStoreObject(),
lastModified = this.lastModified.toLocalDataStoreObject(),
Expand All @@ -319,7 +319,7 @@ fun SubmissionMutation.toLocalDataStoreObject(created: AuditInfo): SubmissionEnt
id = this.submissionId,
jobId = this.job.id,
locationOfInterestId = this.locationOfInterestId,
state = EntityState.DEFAULT,
deletionState = EntityDeletionState.DEFAULT,
data = SubmissionDataConverter.toString(SubmissionData().copyWithDeltas(this.deltas)),
// TODO(#1562): Preserve creation audit info for UPDATE mutations.
created = auditInfo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,20 @@ package com.google.android.ground.persistence.local.room.dao
import androidx.room.Dao
import androidx.room.Query
import com.google.android.ground.persistence.local.room.entity.LocationOfInterestEntity
import com.google.android.ground.persistence.local.room.fields.EntityState
import com.google.android.ground.persistence.local.room.fields.EntityDeletionState
import kotlinx.coroutines.flow.Flow

/** Provides low-level read/write operations of [LocationOfInterestEntity] to/from the local db. */
@Dao
interface LocationOfInterestDao : BaseDao<LocationOfInterestEntity> {

@Query("SELECT * FROM location_of_interest WHERE survey_id = :surveyId AND state = :state")
fun findByState(surveyId: String, state: EntityState): Flow<List<LocationOfInterestEntity>>
@Query(
"SELECT * FROM location_of_interest WHERE survey_id = :surveyId AND state = :deletionState"
)
fun getByDeletionState(
surveyId: String,
deletionState: EntityDeletionState,
): Flow<List<LocationOfInterestEntity>>

@Query("SELECT * FROM location_of_interest WHERE id = :id")
suspend fun findById(id: String): LocationOfInterestEntity?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package com.google.android.ground.persistence.local.room.dao
import androidx.room.Dao
import androidx.room.Query
import com.google.android.ground.persistence.local.room.entity.SubmissionEntity
import com.google.android.ground.persistence.local.room.fields.EntityState
import com.google.android.ground.persistence.local.room.fields.EntityDeletionState

@Dao
interface SubmissionDao : BaseDao<SubmissionEntity> {
Expand All @@ -33,11 +33,11 @@ interface SubmissionDao : BaseDao<SubmissionEntity> {
@Query(
"SELECT * FROM submission " +
"WHERE location_of_interest_id = :locationOfInterestId " +
"AND job_id = :jobId AND state = :state"
"AND job_id = :jobId AND state = :deletionState"
)
suspend fun findByLocationOfInterestId(
locationOfInterestId: String,
jobId: String,
state: EntityState,
deletionState: EntityDeletionState,
): List<SubmissionEntity>?
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.android.ground.model.locationofinterest.LoiProperties
import com.google.android.ground.persistence.local.room.fields.EntityState
import com.google.android.ground.persistence.local.room.fields.EntityDeletionState

/**
* Defines how Room persists LOIs in the local db. By default, Room uses the name of object fields
Expand All @@ -32,7 +32,7 @@ data class LocationOfInterestEntity(
@ColumnInfo(name = "id") @PrimaryKey val id: String,
@ColumnInfo(name = "survey_id") val surveyId: String,
@ColumnInfo(name = "job_id") val jobId: String,
@ColumnInfo(name = "state") val state: EntityState, // TODO: Rename to DeletionState.
@ColumnInfo(name = "state") val deletionState: EntityDeletionState,
@Embedded(prefix = "created_") val created: AuditInfoEntity,
@Embedded(prefix = "modified_") val lastModified: AuditInfoEntity,
val geometry: GeometryWrapper?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ package com.google.android.ground.persistence.local.room.entity

import androidx.room.*
import com.google.android.ground.model.submission.Submission
import com.google.android.ground.persistence.local.room.fields.EntityState
import com.google.android.ground.persistence.local.room.fields.EntityDeletionState

/** Representation of a [Submission] in local db. */
@Entity(
Expand All @@ -37,7 +37,7 @@ data class SubmissionEntity(
@ColumnInfo(name = "id") @PrimaryKey val id: String,
@ColumnInfo(name = "location_of_interest_id") val locationOfInterestId: String,
@ColumnInfo(name = "job_id") val jobId: String,
@ColumnInfo(name = "state") val state: EntityState,
@ColumnInfo(name = "state") val deletionState: EntityDeletionState,
@ColumnInfo(name = "data") val data: String?,
@Embedded(prefix = "created_") val created: AuditInfoEntity,
@Embedded(prefix = "modified_") val lastModified: AuditInfoEntity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ import com.google.android.ground.persistence.local.room.IntEnum.Companion.fromIn
import com.google.android.ground.persistence.local.room.IntEnum.Companion.toInt

/** Mutually exclusive entity states shared by LOIs and Submissions. */
enum class EntityState(private val intValue: Int) : IntEnum {
enum class EntityDeletionState(private val intValue: Int) : IntEnum {
UNKNOWN(0),
DEFAULT(1),
DELETED(2);

override fun intValue() = intValue

companion object {
@JvmStatic @TypeConverter fun toInt(value: EntityState?) = toInt(value, UNKNOWN)
@JvmStatic @TypeConverter fun toInt(value: EntityDeletionState?) = toInt(value, UNKNOWN)

@JvmStatic
@TypeConverter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import com.google.android.ground.persistence.local.room.dao.LocationOfInterestMu
import com.google.android.ground.persistence.local.room.dao.insertOrUpdate
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.fields.EntityState
import com.google.android.ground.persistence.local.room.fields.EntityDeletionState
import com.google.android.ground.persistence.local.room.fields.MutationEntitySyncStatus
import com.google.android.ground.persistence.local.stores.LocalLocationOfInterestStore
import com.google.android.ground.util.Debug.logOnFailure
Expand All @@ -49,8 +49,8 @@ class RoomLocationOfInterestStore @Inject internal constructor() : LocalLocation
* local database and returns a [Flow] that continually emits the complete set anew any time the
* underlying table changes (insertions, deletions, updates).
*/
override fun findLocationsOfInterest(survey: Survey) =
locationOfInterestDao.findByState(survey.id, EntityState.DEFAULT).map {
override fun getValidLois(survey: Survey): Flow<Set<LocationOfInterest>> =
locationOfInterestDao.getByDeletionState(survey.id, EntityDeletionState.DEFAULT).map {
toLocationsOfInterest(survey, it)
}

Expand Down Expand Up @@ -83,7 +83,7 @@ class RoomLocationOfInterestStore @Inject internal constructor() : LocalLocation
Mutation.Type.DELETE -> {
val loiId = mutation.locationOfInterestId
val entity = checkNotNull(locationOfInterestDao.findById(loiId))
locationOfInterestDao.update(entity.copy(state = EntityState.DELETED))
locationOfInterestDao.update(entity.copy(deletionState = EntityDeletionState.DELETED))
}
Mutation.Type.UNKNOWN -> {
throw LocalDataStoreException("Unknown Mutation.Type")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import com.google.android.ground.persistence.local.room.dao.insertOrUpdate
import com.google.android.ground.persistence.local.room.entity.AuditInfoEntity
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.fields.EntityState
import com.google.android.ground.persistence.local.room.fields.EntityDeletionState
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.UserDetails
Expand All @@ -50,9 +50,7 @@ import javax.inject.Singleton
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import timber.log.Timber

/** Manages access to [Submission] objects persisted in local storage. */
Expand Down Expand Up @@ -86,7 +84,7 @@ class RoomSubmissionStore @Inject internal constructor() : LocalSubmissionStore
jobId: String,
): List<Submission> =
submissionDao
.findByLocationOfInterestId(locationOfInterest.id, jobId, EntityState.DEFAULT)
.findByLocationOfInterestId(locationOfInterest.id, jobId, EntityDeletionState.DEFAULT)
?.mapNotNull { logOnFailure { it.toModelObject(locationOfInterest) } } ?: listOf()

override suspend fun merge(model: Submission) {
Expand Down Expand Up @@ -121,7 +119,7 @@ class RoomSubmissionStore @Inject internal constructor() : LocalSubmissionStore
}
Mutation.Type.DELETE -> {
val entity = checkNotNull(submissionDao.findById(mutation.submissionId))
submissionDao.update(entity.copy(state = EntityState.DELETED))
submissionDao.update(entity.copy(deletionState = EntityDeletionState.DELETED))
}
Mutation.Type.UNKNOWN -> {
throw LocalDataStoreException("Unknown Mutation.Type")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface LocalLocationOfInterestStore :
* Returns a main-safe flow that emits the full set of LOIs for a survey on subscribe, and
* continues to return the full set each time a LOI is added/changed/removed.
*/
fun findLocationsOfInterest(survey: Survey): Flow<Set<LocationOfInterest>>
fun getValidLois(survey: Survey): Flow<Set<LocationOfInterest>>

/** Returns the LOI with the specified UUID from the local data store, if found. */
suspend fun getLocationOfInterest(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,17 +140,17 @@
mutationSyncWorkManager.enqueueSyncWorker(mutation.locationOfInterestId)
}

/** Returns a flow of all [LocationOfInterest] associated with the given [Survey]. */
fun getLocationsOfInterests(survey: Survey): Flow<Set<LocationOfInterest>> =
localLoiStore.findLocationsOfInterest(survey)
/** Returns a flow of all valid (not deleted) [LocationOfInterest] in the given [Survey]. */
fun getValidLois(survey: Survey): Flow<Set<LocationOfInterest>> =
localLoiStore.getValidLois(survey)

/** Returns a list of geometries associated with the given [Survey]. */
suspend fun getAllGeometries(survey: Survey): List<Geometry> =
getLocationsOfInterests(survey).first().map { it.geometry }
getValidLois(survey).first().map { it.geometry }

Check warning on line 149 in ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt#L149

Added line #L149 was not covered by tests

/** Returns a flow of all [LocationOfInterest] within the map bounds (viewport). */
fun getWithinBounds(survey: Survey, bounds: Bounds): Flow<List<LocationOfInterest>> =
getLocationsOfInterests(survey)
getValidLois(survey)
.map { lois -> lois.filter { bounds.contains(it.geometry) } }
.distinctUntilChanged()
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
*/
val surveyUpdateFlow: Flow<SurveyProperties> =
activeSurvey.filterNotNull().map { survey ->
val lois = loiRepository.getLocationsOfInterests(survey).first()
val lois = loiRepository.getValidLois(survey).first()
val addLoiPermitted = survey.jobs.any { job -> job.canDataCollectorsAddLois }
SurveyProperties(addLoiPermitted = addLoiPermitted, noLois = lois.isEmpty())
}
Expand Down Expand Up @@ -221,9 +221,7 @@
localValueStore.setDataSharingConsent(survey.id, dataSharingTerms)

private fun getLocationOfInterestFeatures(survey: Survey): Flow<Set<Feature>> =
loiRepository.getLocationsOfInterests(survey).map {
it.map { loi -> loi.toFeature() }.toPersistentSet()
}
loiRepository.getValidLois(survey).map { it.map { loi -> loi.toFeature() }.toPersistentSet() }

Check warning on line 224 in ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt#L224

Added line #L224 was not covered by tests

private suspend fun LocationOfInterest.toFeature() =
Feature(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import com.google.android.ground.persistence.local.room.converter.formatVertices
import com.google.android.ground.persistence.local.room.converter.parseVertices
import com.google.android.ground.persistence.local.room.dao.LocationOfInterestDao
import com.google.android.ground.persistence.local.room.dao.SubmissionDao
import com.google.android.ground.persistence.local.room.fields.EntityState
import com.google.android.ground.persistence.local.room.fields.EntityDeletionState
import com.google.android.ground.persistence.local.room.fields.MutationEntitySyncStatus
import com.google.android.ground.persistence.local.stores.LocalLocationOfInterestStore
import com.google.android.ground.persistence.local.stores.LocalOfflineAreaStore
Expand Down Expand Up @@ -178,7 +178,7 @@ class LocalDataStoreTests : BaseHiltTest() {

val loi = localLoiStore.getLocationOfInterest(TEST_SURVEY, FakeData.LOI_ID)

localLoiStore.findLocationsOfInterest(TEST_SURVEY).test {
localLoiStore.getValidLois(TEST_SURVEY).test {
assertThat(expectMostRecentItem()).isEqualTo(setOf(loi))
}
}
Expand Down Expand Up @@ -287,7 +287,8 @@ class LocalDataStoreTests : BaseHiltTest() {
localSubmissionStore.applyAndEnqueue(mutation)

// Verify that local entity exists and its state is updated.
assertThat(submissionDao.findById("submission id")?.state).isEqualTo(EntityState.DELETED)
assertThat(submissionDao.findById("submission id")?.deletionState)
.isEqualTo(EntityDeletionState.DELETED)

// Verify that the local submission doesn't end up in getSubmissions().
val loi = localLoiStore.getLocationOfInterest(TEST_SURVEY, FakeData.LOI_ID)!!
Expand All @@ -311,7 +312,7 @@ class LocalDataStoreTests : BaseHiltTest() {

// Assert that one LOI is streamed.
val loi = localLoiStore.getLocationOfInterest(TEST_SURVEY, FakeData.LOI_ID)!!
localLoiStore.findLocationsOfInterest(TEST_SURVEY).test {
localLoiStore.getValidLois(TEST_SURVEY).test {
assertThat(expectMostRecentItem()).isEqualTo(setOf(loi))
}
val mutation = TEST_LOI_MUTATION.copy(id = null, type = Mutation.Type.DELETE)
Expand All @@ -320,13 +321,11 @@ class LocalDataStoreTests : BaseHiltTest() {
localLoiStore.applyAndEnqueue(mutation)

// Verify that local entity exists but its state is updated to DELETED.
assertThat(locationOfInterestDao.findById(FakeData.LOI_ID)?.state)
.isEqualTo(EntityState.DELETED)
assertThat(locationOfInterestDao.findById(FakeData.LOI_ID)?.deletionState)
.isEqualTo(EntityDeletionState.DELETED)

// Verify that the local LOI is now removed from the latest LOI stream.
localLoiStore.findLocationsOfInterest(TEST_SURVEY).test {
assertThat(expectMostRecentItem()).isEmpty()
}
localLoiStore.getValidLois(TEST_SURVEY).test { assertThat(expectMostRecentItem()).isEmpty() }

// After successful remote sync, delete LOI is called by LocalMutationSyncWorker.
localLoiStore.deleteLocationOfInterest(FakeData.LOI_ID)
Expand Down