diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/LocalDatabase.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/LocalDatabase.kt index 7377012475..e56efde3a1 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/LocalDatabase.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/LocalDatabase.kt @@ -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 @@ -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, diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt index 4dacbfb87f..c063be1fc1 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt @@ -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(), @@ -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, @@ -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(), @@ -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, diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/dao/LocationOfInterestDao.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/dao/LocationOfInterestDao.kt index 484761e048..68c17655e0 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/dao/LocationOfInterestDao.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/dao/LocationOfInterestDao.kt @@ -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 { - @Query("SELECT * FROM location_of_interest WHERE survey_id = :surveyId AND state = :state") - fun findByState(surveyId: String, state: EntityState): Flow> + @Query( + "SELECT * FROM location_of_interest WHERE survey_id = :surveyId AND state = :deletionState" + ) + fun getByDeletionState( + surveyId: String, + deletionState: EntityDeletionState, + ): Flow> @Query("SELECT * FROM location_of_interest WHERE id = :id") suspend fun findById(id: String): LocationOfInterestEntity? diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/dao/SubmissionDao.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/dao/SubmissionDao.kt index b1b13ab9f4..88cd265efa 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/dao/SubmissionDao.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/dao/SubmissionDao.kt @@ -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 { @@ -33,11 +33,11 @@ interface SubmissionDao : BaseDao { @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? } diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/LocationOfInterestEntity.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/LocationOfInterestEntity.kt index 58238a9363..81d8cf3dad 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/LocationOfInterestEntity.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/LocationOfInterestEntity.kt @@ -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 @@ -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?, diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/SubmissionEntity.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/SubmissionEntity.kt index d463a093cd..757dd41d56 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/SubmissionEntity.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/SubmissionEntity.kt @@ -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( @@ -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, diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/fields/EntityState.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/fields/EntityDeletionState.kt similarity index 88% rename from ground/src/main/java/com/google/android/ground/persistence/local/room/fields/EntityState.kt rename to ground/src/main/java/com/google/android/ground/persistence/local/room/fields/EntityDeletionState.kt index 1ee2b64e06..09bf62ee9d 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/fields/EntityState.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/fields/EntityDeletionState.kt @@ -21,7 +21,7 @@ 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); @@ -29,7 +29,7 @@ enum class EntityState(private val intValue: Int) : IntEnum { 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 diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomLocationOfInterestStore.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomLocationOfInterestStore.kt index 4c3ae043de..26796d6fdf 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomLocationOfInterestStore.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomLocationOfInterestStore.kt @@ -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 @@ -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> = + locationOfInterestDao.getByDeletionState(survey.id, EntityDeletionState.DEFAULT).map { toLocationsOfInterest(survey, it) } @@ -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") diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomSubmissionStore.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomSubmissionStore.kt index 6311e95268..5c57aa86b7 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomSubmissionStore.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomSubmissionStore.kt @@ -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 @@ -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. */ @@ -86,7 +84,7 @@ class RoomSubmissionStore @Inject internal constructor() : LocalSubmissionStore jobId: String, ): List = 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) { @@ -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") diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/stores/LocalLocationOfInterestStore.kt b/ground/src/main/java/com/google/android/ground/persistence/local/stores/LocalLocationOfInterestStore.kt index 01c4a3a680..a162f1d7c4 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/stores/LocalLocationOfInterestStore.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/stores/LocalLocationOfInterestStore.kt @@ -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> + fun getValidLois(survey: Survey): Flow> /** Returns the LOI with the specified UUID from the local data store, if found. */ suspend fun getLocationOfInterest( diff --git a/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt b/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt index 4d27dbeaec..defe4a8a53 100644 --- a/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt +++ b/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt @@ -140,17 +140,17 @@ constructor( mutationSyncWorkManager.enqueueSyncWorker(mutation.locationOfInterestId) } - /** Returns a flow of all [LocationOfInterest] associated with the given [Survey]. */ - fun getLocationsOfInterests(survey: Survey): Flow> = - localLoiStore.findLocationsOfInterest(survey) + /** Returns a flow of all valid (not deleted) [LocationOfInterest] in the given [Survey]. */ + fun getValidLois(survey: Survey): Flow> = + localLoiStore.getValidLois(survey) /** Returns a list of geometries associated with the given [Survey]. */ suspend fun getAllGeometries(survey: Survey): List = - getLocationsOfInterests(survey).first().map { it.geometry } + getValidLois(survey).first().map { it.geometry } /** Returns a flow of all [LocationOfInterest] within the map bounds (viewport). */ fun getWithinBounds(survey: Survey, bounds: Bounds): Flow> = - getLocationsOfInterests(survey) + getValidLois(survey) .map { lois -> lois.filter { bounds.contains(it.geometry) } } .distinctUntilChanged() } diff --git a/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt index b514e046ca..8af6139816 100644 --- a/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt @@ -92,7 +92,7 @@ internal constructor( */ val surveyUpdateFlow: Flow = 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()) } @@ -221,9 +221,7 @@ internal constructor( localValueStore.setDataSharingConsent(survey.id, dataSharingTerms) private fun getLocationOfInterestFeatures(survey: Survey): Flow> = - loiRepository.getLocationsOfInterests(survey).map { - it.map { loi -> loi.toFeature() }.toPersistentSet() - } + loiRepository.getValidLois(survey).map { it.map { loi -> loi.toFeature() }.toPersistentSet() } private suspend fun LocationOfInterest.toFeature() = Feature( diff --git a/ground/src/test/java/com/google/android/ground/persistence/local/LocalDataStoreTests.kt b/ground/src/test/java/com/google/android/ground/persistence/local/LocalDataStoreTests.kt index 57b27a0fbd..9c5d68a22a 100644 --- a/ground/src/test/java/com/google/android/ground/persistence/local/LocalDataStoreTests.kt +++ b/ground/src/test/java/com/google/android/ground/persistence/local/LocalDataStoreTests.kt @@ -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 @@ -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)) } } @@ -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)!! @@ -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) @@ -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)