From 6aa5b0f31acfd7cd79f58abafe1be27c3ee2443f Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Thu, 27 Aug 2026 22:26:09 +0530 Subject: [PATCH 01/10] feat: implement DataModule in core data layer and update application build configuration --- app/build.gradle | 1 + core/data/build.gradle.kts | 51 +++++++++++++++++++ .../org/groundplatform/data/DataModule.kt | 22 ++++++++ .../org/groundplatform/data/DataModuleTest.kt | 27 ++++++++++ settings.gradle | 1 + 5 files changed, 102 insertions(+) create mode 100644 core/data/build.gradle.kts create mode 100644 core/data/src/commonMain/kotlin/org/groundplatform/data/DataModule.kt create mode 100644 core/data/src/commonTest/kotlin/org/groundplatform/data/DataModuleTest.kt diff --git a/app/build.gradle b/app/build.gradle index 585c31acf3..adc830d9dc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -189,6 +189,7 @@ configurations { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation project(':core:domain') + implementation project(':core:data') implementation project(':core:ui') implementation project(':feature:pdf') implementation libs.androidx.multidex diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts new file mode 100644 index 0000000000..854260c5fc --- /dev/null +++ b/core/data/build.gradle.kts @@ -0,0 +1,51 @@ +/* + * Copyright 2026 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. + */ + +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.serialization) +} + +apply(from = "../../config/jacoco/jacoco.gradle") + +kotlin { + jvm() + jvmToolchain(libs.versions.jvmToolchainVersion.get().toInt()) + + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain { + dependencies { + implementation(project(":core:domain")) + implementation(libs.kermit) + implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.collections.immutable) + implementation(libs.kotlinx.coroutines.core) + } + } + + commonTest { + dependencies { + implementation(project(":core:testing")) + implementation(libs.kotlin.test) + implementation(libs.kotlinx.coroutines.test) + } + } + } +} diff --git a/core/data/src/commonMain/kotlin/org/groundplatform/data/DataModule.kt b/core/data/src/commonMain/kotlin/org/groundplatform/data/DataModule.kt new file mode 100644 index 0000000000..63fc3d9888 --- /dev/null +++ b/core/data/src/commonMain/kotlin/org/groundplatform/data/DataModule.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2026 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 org.groundplatform.data + +/** Marker object representing the core data layer module configuration. */ +object DataModule { + const val MODULE_NAME: String = "core:data" +} diff --git a/core/data/src/commonTest/kotlin/org/groundplatform/data/DataModuleTest.kt b/core/data/src/commonTest/kotlin/org/groundplatform/data/DataModuleTest.kt new file mode 100644 index 0000000000..44a0f70892 --- /dev/null +++ b/core/data/src/commonTest/kotlin/org/groundplatform/data/DataModuleTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2026 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 org.groundplatform.data + +import kotlin.test.Test +import kotlin.test.assertEquals + +class DataModuleTest { + @Test + fun moduleName_isCorrect() { + assertEquals("core:data", DataModule.MODULE_NAME) + } +} diff --git a/settings.gradle b/settings.gradle index 8b0871680a..0bbeba6d14 100644 --- a/settings.gradle +++ b/settings.gradle @@ -29,5 +29,6 @@ plugins { include ':app', ':e2eTest' include ':core:ui' include ':core:domain' +include ':core:data' include ':core:testing' include ':feature:pdf' From 496cc4f6f20e5b9774c83cf5418a2ba5fb9304b2 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Thu, 27 Aug 2026 22:43:10 +0530 Subject: [PATCH 02/10] refactor: decouple local stores from room-specific mutation sync status types by using domain-level SyncStatus enums --- .../room/dao/LocationOfInterestMutationDao.kt | 2 +- .../local/room/dao/SubmissionMutationDao.kt | 4 ++-- .../stores/RoomLocationOfInterestStore.kt | 8 ++++--- .../local/room/stores/RoomSubmissionStore.kt | 23 ++++++++++++------- .../stores/LocalLocationOfInterestStore.kt | 7 +++--- .../data/local/stores/LocalSubmissionStore.kt | 9 ++++---- .../data/local/stores/LocalUserStore.kt | 1 - .../local/LocalLocationOfInterestStoreTest.kt | 5 ++-- .../data/local/LocalSubmissionStoreTest.kt | 5 ++-- 9 files changed, 34 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/data/local/room/dao/LocationOfInterestMutationDao.kt b/app/src/main/java/org/groundplatform/android/data/local/room/dao/LocationOfInterestMutationDao.kt index 03db3d0d39..67e788a987 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/room/dao/LocationOfInterestMutationDao.kt +++ b/app/src/main/java/org/groundplatform/android/data/local/room/dao/LocationOfInterestMutationDao.kt @@ -37,6 +37,6 @@ interface LocationOfInterestMutationDao : BaseDao, ): List } diff --git a/app/src/main/java/org/groundplatform/android/data/local/room/dao/SubmissionMutationDao.kt b/app/src/main/java/org/groundplatform/android/data/local/room/dao/SubmissionMutationDao.kt index a7b347665e..976bdedcdd 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/room/dao/SubmissionMutationDao.kt +++ b/app/src/main/java/org/groundplatform/android/data/local/room/dao/SubmissionMutationDao.kt @@ -34,7 +34,7 @@ interface SubmissionMutationDao : BaseDao { ) suspend fun findByLocationOfInterestId( locationOfInterestId: String, - vararg allowedStates: MutationEntitySyncStatus, + allowedStates: List, ): List @Query( @@ -63,6 +63,6 @@ interface SubmissionMutationDao : BaseDao { ) fun findByLoiIdFlow( locationOfInterestId: String, - vararg allowedStates: MutationEntitySyncStatus, + allowedStates: List, ): Flow> } diff --git a/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomLocationOfInterestStore.kt b/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomLocationOfInterestStore.kt index d34b17853b..e7e0b2c5a2 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomLocationOfInterestStore.kt +++ b/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomLocationOfInterestStore.kt @@ -126,9 +126,11 @@ class RoomLocationOfInterestStore @Inject internal constructor() : LocalLocation override suspend fun findByLocationOfInterestId( id: String, - vararg states: MutationEntitySyncStatus, - ): List = - locationOfInterestMutationDao.getMutations(id, *states) + vararg states: Mutation.SyncStatus, + ): List = + locationOfInterestMutationDao + .getMutations(id, states.map { MutationEntitySyncStatus.fromMutationSyncStatus(it) }) + .map { it.toModelObject() } override suspend fun insertOrUpdate(loi: LocationOfInterest) { require(!loi.geometry.isEmpty()) { "Cannot save LOI with empty geometry" } diff --git a/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomSubmissionStore.kt b/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomSubmissionStore.kt index 0ca293719f..b2b86b43a4 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomSubmissionStore.kt +++ b/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomSubmissionStore.kt @@ -198,12 +198,14 @@ class RoomSubmissionStore @Inject internal constructor() : LocalSubmissionStore override fun getSubmissionMutationsByLoiIdFlow( survey: Survey, locationOfInterestId: String, - vararg allowedStates: MutationEntitySyncStatus, + vararg allowedStates: Mutation.SyncStatus, ): Flow> = - submissionMutationDao.findByLoiIdFlow(locationOfInterestId, *allowedStates).map { - list: List -> - list.map { it.toModelObject(survey) } - } + submissionMutationDao + .findByLoiIdFlow( + locationOfInterestId, + allowedStates.map { MutationEntitySyncStatus.fromMutationSyncStatus(it) }, + ) + .map { list: List -> list.map { it.toModelObject(survey) } } override suspend fun applyAndEnqueue(mutation: SubmissionMutation) { try { @@ -239,9 +241,14 @@ class RoomSubmissionStore @Inject internal constructor() : LocalSubmissionStore override suspend fun findByLocationOfInterestId( loidId: String, - vararg states: MutationEntitySyncStatus, - ): List = - submissionMutationDao.findByLocationOfInterestId(loidId, *states) + vararg states: Mutation.SyncStatus, + ): List = + submissionMutationDao + .findByLocationOfInterestId( + loidId, + states.map { MutationEntitySyncStatus.fromMutationSyncStatus(it) }, + ) + .mapNotNull { convertMutation(it) } override suspend fun getPendingCreateCount(loiId: String): Int = submissionMutationDao.getSubmissionMutationCount( diff --git a/app/src/main/java/org/groundplatform/android/data/local/stores/LocalLocationOfInterestStore.kt b/app/src/main/java/org/groundplatform/android/data/local/stores/LocalLocationOfInterestStore.kt index 1ae85fae0d..db0e84f8f5 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/stores/LocalLocationOfInterestStore.kt +++ b/app/src/main/java/org/groundplatform/android/data/local/stores/LocalLocationOfInterestStore.kt @@ -16,11 +16,10 @@ package org.groundplatform.android.data.local.stores import kotlinx.coroutines.flow.Flow -import org.groundplatform.android.data.local.room.entity.LocationOfInterestMutationEntity -import org.groundplatform.android.data.local.room.fields.MutationEntitySyncStatus import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.locationofinterest.LocationOfInterest import org.groundplatform.domain.model.mutation.LocationOfInterestMutation +import org.groundplatform.domain.model.mutation.Mutation.SyncStatus interface LocalLocationOfInterestStore : LocalMutationStore { @@ -57,8 +56,8 @@ interface LocalLocationOfInterestStore : suspend fun findByLocationOfInterestId( id: String, - vararg states: MutationEntitySyncStatus, - ): List + vararg states: SyncStatus, + ): List suspend fun insertOrUpdate(loi: LocationOfInterest) diff --git a/app/src/main/java/org/groundplatform/android/data/local/stores/LocalSubmissionStore.kt b/app/src/main/java/org/groundplatform/android/data/local/stores/LocalSubmissionStore.kt index 86b55a3097..d023479ffa 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/stores/LocalSubmissionStore.kt +++ b/app/src/main/java/org/groundplatform/android/data/local/stores/LocalSubmissionStore.kt @@ -16,10 +16,9 @@ package org.groundplatform.android.data.local.stores import kotlinx.coroutines.flow.Flow -import org.groundplatform.android.data.local.room.entity.SubmissionMutationEntity -import org.groundplatform.android.data.local.room.fields.MutationEntitySyncStatus import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.locationofinterest.LocationOfInterest +import org.groundplatform.domain.model.mutation.Mutation.SyncStatus import org.groundplatform.domain.model.mutation.SubmissionMutation import org.groundplatform.domain.model.submission.DraftSubmission import org.groundplatform.domain.model.submission.Submission @@ -50,7 +49,7 @@ interface LocalSubmissionStore : LocalMutationStore> /** @@ -63,8 +62,8 @@ interface LocalSubmissionStore : LocalMutationStore + vararg states: SyncStatus, + ): List suspend fun getPendingCreateCount(loiId: String): Int diff --git a/app/src/main/java/org/groundplatform/android/data/local/stores/LocalUserStore.kt b/app/src/main/java/org/groundplatform/android/data/local/stores/LocalUserStore.kt index 399c24bab3..6365064409 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/stores/LocalUserStore.kt +++ b/app/src/main/java/org/groundplatform/android/data/local/stores/LocalUserStore.kt @@ -15,7 +15,6 @@ */ package org.groundplatform.android.data.local.stores -import org.groundplatform.android.data.local.room.LocalDataStoreException import org.groundplatform.domain.model.User /** Provides access to [User] data in local storage. */ diff --git a/app/src/test/java/org/groundplatform/android/data/local/LocalLocationOfInterestStoreTest.kt b/app/src/test/java/org/groundplatform/android/data/local/LocalLocationOfInterestStoreTest.kt index 88c68cc915..5d638dcca7 100644 --- a/app/src/test/java/org/groundplatform/android/data/local/LocalLocationOfInterestStoreTest.kt +++ b/app/src/test/java/org/groundplatform/android/data/local/LocalLocationOfInterestStoreTest.kt @@ -32,7 +32,6 @@ import org.groundplatform.android.data.local.room.converter.toLocalDataStoreObje import org.groundplatform.android.data.local.room.dao.LocationOfInterestDao import org.groundplatform.android.data.local.room.dao.MAX_SQL_VARIABLES import org.groundplatform.android.data.local.room.fields.EntityDeletionState -import org.groundplatform.android.data.local.room.fields.MutationEntitySyncStatus import org.groundplatform.android.data.local.room.stores.RoomLocationOfInterestStore import org.groundplatform.android.data.local.stores.LocalLocationOfInterestStore import org.groundplatform.android.data.local.stores.LocalSubmissionStore @@ -376,14 +375,14 @@ class LocalLocationOfInterestStoreTest : BaseHiltTest() { assertThat( localLoiStore.findByLocationOfInterestId( FakeData.LOI_ID, - MutationEntitySyncStatus.PENDING, + Mutation.SyncStatus.PENDING, ) ) .hasSize(1) assertThat( localSubmissionStore.findByLocationOfInterestId( FakeData.LOI_ID, - MutationEntitySyncStatus.PENDING, + Mutation.SyncStatus.PENDING, ) ) .hasSize(1) diff --git a/app/src/test/java/org/groundplatform/android/data/local/LocalSubmissionStoreTest.kt b/app/src/test/java/org/groundplatform/android/data/local/LocalSubmissionStoreTest.kt index a8b2150320..f39b03f373 100644 --- a/app/src/test/java/org/groundplatform/android/data/local/LocalSubmissionStoreTest.kt +++ b/app/src/test/java/org/groundplatform/android/data/local/LocalSubmissionStoreTest.kt @@ -26,7 +26,6 @@ import org.groundplatform.android.FakeData.FAKE_GENERAL_ACCESS import org.groundplatform.android.data.local.room.LocalDataStoreException import org.groundplatform.android.data.local.room.dao.SubmissionDao import org.groundplatform.android.data.local.room.fields.EntityDeletionState -import org.groundplatform.android.data.local.room.fields.MutationEntitySyncStatus import org.groundplatform.android.data.local.stores.LocalLocationOfInterestStore import org.groundplatform.android.data.local.stores.LocalSubmissionStore import org.groundplatform.android.data.local.stores.LocalSurveyStore @@ -75,7 +74,7 @@ class LocalSubmissionStoreTest : BaseHiltTest() { .getSubmissionMutationsByLoiIdFlow( TEST_SURVEY, TEST_LOI_MUTATION.locationOfInterestId, - MutationEntitySyncStatus.PENDING, + Mutation.SyncStatus.PENDING, ) .test { assertThat(expectMostRecentItem()).isEqualTo(listOf(TEST_SUBMISSION_MUTATION)) } val loi = localLoiStore.getLocationOfInterest(TEST_SURVEY, FakeData.LOI_ID)!! @@ -100,7 +99,7 @@ class LocalSubmissionStoreTest : BaseHiltTest() { .getSubmissionMutationsByLoiIdFlow( TEST_SURVEY, TEST_LOI_MUTATION.locationOfInterestId, - MutationEntitySyncStatus.PENDING, + Mutation.SyncStatus.PENDING, ) .test { assertThat(expectMostRecentItem()).isEqualTo(listOf(TEST_SUBMISSION_MUTATION, mutation)) From 7ec0bbe6dd4c6c039d7a543c7743d18f02e29cb7 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Thu, 27 Aug 2026 22:53:02 +0530 Subject: [PATCH 03/10] refactor: migrate data store interfaces to the org.groundplatform.data.stores package --- .../local/room/stores/RoomLocationOfInterestStore.kt | 2 +- .../data/local/room/stores/RoomOfflineAreaStore.kt | 2 +- .../data/local/room/stores/RoomSubmissionStore.kt | 2 +- .../android/data/local/room/stores/RoomSurveyStore.kt | 2 +- .../android/data/local/room/stores/RoomUserStore.kt | 2 +- .../android/data/remote/firebase/FirestoreDataStore.kt | 2 +- .../groundplatform/android/di/LocalDataStoreModule.kt | 10 +++++----- .../android/di/RemotePersistenceModule.kt | 2 +- .../android/repository/LocationOfInterestRepository.kt | 6 +++--- .../android/repository/MutationRepository.kt | 6 +++--- .../android/repository/OfflineAreaRepository.kt | 2 +- .../android/repository/SubmissionRepository.kt | 2 +- .../android/repository/SurveyRepository.kt | 4 ++-- .../android/repository/TermsOfServiceRepository.kt | 2 +- .../android/repository/UserRepository.kt | 4 ++-- .../groundplatform/android/TestRemoteStorageModule.kt | 2 +- .../data/local/LocalLocationOfInterestStoreTest.kt | 8 ++++---- .../android/data/local/LocalOfflineAreaStore.kt | 2 +- .../android/data/local/LocalSubmissionStoreTest.kt | 8 ++++---- .../android/data/local/LocalSurveyStoreTest.kt | 2 +- .../android/data/local/LocalUserStoreTest.kt | 2 +- .../android/data/remote/FakeRemoteDataStore.kt | 1 + .../android/data/sync/LocalMutationSyncWorkerTest.kt | 8 ++++---- .../android/data/sync/MediaUploadWorkerTest.kt | 8 ++++---- .../repository/LocationOfInterestRepositoryTest.kt | 2 +- .../android/repository/MutationRepositoryTest.kt | 6 +++--- .../android/repository/OfflineAreaRepositoryTest.kt | 2 +- .../android/repository/SubmissionRepositoryTest.kt | 2 +- .../android/repository/SurveyRepositoryTest.kt | 2 +- .../android/repository/UserRepositoryTest.kt | 4 ++-- .../android/ui/home/HomeScreenFragmentTest.kt | 2 +- .../ui/offlineareas/OfflineAreasFragmentTest.kt | 2 +- .../viewer/OfflineAreaViewerFragmentTest.kt | 2 +- .../viewer/OfflineAreaViewerViewModelTest.kt | 2 +- .../android/ui/syncstatus/SyncStatusFragmentTest.kt | 8 ++++---- .../data}/stores/LocalLocationOfInterestStore.kt | 2 +- .../groundplatform/data}/stores/LocalMutationStore.kt | 2 +- .../data}/stores/LocalOfflineAreaStore.kt | 2 +- .../data}/stores/LocalSubmissionStore.kt | 2 +- .../groundplatform/data}/stores/LocalSurveyStore.kt | 2 +- .../org/groundplatform/data}/stores/LocalUserStore.kt | 2 +- .../org/groundplatform/data/stores}/RemoteDataStore.kt | 2 +- 42 files changed, 70 insertions(+), 69 deletions(-) rename {app/src/main/java/org/groundplatform/android/data/local => core/data/src/commonMain/kotlin/org/groundplatform/data}/stores/LocalLocationOfInterestStore.kt (98%) rename {app/src/main/java/org/groundplatform/android/data/local => core/data/src/commonMain/kotlin/org/groundplatform/data}/stores/LocalMutationStore.kt (96%) rename {app/src/main/java/org/groundplatform/android/data/local => core/data/src/commonMain/kotlin/org/groundplatform/data}/stores/LocalOfflineAreaStore.kt (96%) rename {app/src/main/java/org/groundplatform/android/data/local => core/data/src/commonMain/kotlin/org/groundplatform/data}/stores/LocalSubmissionStore.kt (98%) rename {app/src/main/java/org/groundplatform/android/data/local => core/data/src/commonMain/kotlin/org/groundplatform/data}/stores/LocalSurveyStore.kt (95%) rename {app/src/main/java/org/groundplatform/android/data/local => core/data/src/commonMain/kotlin/org/groundplatform/data}/stores/LocalUserStore.kt (95%) rename {app/src/main/java/org/groundplatform/android/data/remote => core/data/src/commonMain/kotlin/org/groundplatform/data/stores}/RemoteDataStore.kt (98%) diff --git a/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomLocationOfInterestStore.kt b/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomLocationOfInterestStore.kt index e7e0b2c5a2..ad33378066 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomLocationOfInterestStore.kt +++ b/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomLocationOfInterestStore.kt @@ -32,8 +32,8 @@ import org.groundplatform.android.data.local.room.entity.LocationOfInterestEntit import org.groundplatform.android.data.local.room.entity.LocationOfInterestMutationEntity import org.groundplatform.android.data.local.room.fields.EntityDeletionState import org.groundplatform.android.data.local.room.fields.MutationEntitySyncStatus -import org.groundplatform.android.data.local.stores.LocalLocationOfInterestStore import org.groundplatform.android.util.Debug.logOnFailure +import org.groundplatform.data.stores.LocalLocationOfInterestStore import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.locationofinterest.LocationOfInterest import org.groundplatform.domain.model.mutation.LocationOfInterestMutation diff --git a/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomOfflineAreaStore.kt b/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomOfflineAreaStore.kt index 6bcf7319b4..cc48ff169c 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomOfflineAreaStore.kt +++ b/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomOfflineAreaStore.kt @@ -24,7 +24,7 @@ import org.groundplatform.android.data.local.room.converter.toOfflineAreaEntity import org.groundplatform.android.data.local.room.dao.OfflineAreaDao import org.groundplatform.android.data.local.room.dao.insertOrUpdate import org.groundplatform.android.data.local.room.entity.OfflineAreaEntity -import org.groundplatform.android.data.local.stores.LocalOfflineAreaStore +import org.groundplatform.data.stores.LocalOfflineAreaStore import org.groundplatform.domain.model.imagery.OfflineArea @Singleton diff --git a/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomSubmissionStore.kt b/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomSubmissionStore.kt index b2b86b43a4..29c4bbd91d 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomSubmissionStore.kt +++ b/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomSubmissionStore.kt @@ -37,8 +37,8 @@ import org.groundplatform.android.data.local.room.fields.EntityDeletionState import org.groundplatform.android.data.local.room.fields.MutationEntitySyncStatus import org.groundplatform.android.data.local.room.fields.MutationEntityType import org.groundplatform.android.data.local.room.fields.UserDetails -import org.groundplatform.android.data.local.stores.LocalSubmissionStore import org.groundplatform.android.util.Debug.logOnFailure +import org.groundplatform.data.stores.LocalSubmissionStore import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.User import org.groundplatform.domain.model.job.Job diff --git a/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomSurveyStore.kt b/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomSurveyStore.kt index 891a063df2..271e376430 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomSurveyStore.kt +++ b/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomSurveyStore.kt @@ -31,7 +31,7 @@ import org.groundplatform.android.data.local.room.dao.OptionDao import org.groundplatform.android.data.local.room.dao.SurveyDao import org.groundplatform.android.data.local.room.dao.TaskDao import org.groundplatform.android.data.local.room.dao.insertOrUpdate -import org.groundplatform.android.data.local.stores.LocalSurveyStore +import org.groundplatform.data.stores.LocalSurveyStore import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.job.Job import org.groundplatform.domain.model.task.Condition diff --git a/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomUserStore.kt b/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomUserStore.kt index 6a8278244f..b3446f4908 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomUserStore.kt +++ b/app/src/main/java/org/groundplatform/android/data/local/room/stores/RoomUserStore.kt @@ -22,7 +22,7 @@ import org.groundplatform.android.data.local.room.converter.toLocalDataStoreObje import org.groundplatform.android.data.local.room.converter.toModelObject import org.groundplatform.android.data.local.room.dao.UserDao import org.groundplatform.android.data.local.room.dao.insertOrUpdate -import org.groundplatform.android.data.local.stores.LocalUserStore +import org.groundplatform.data.stores.LocalUserStore import org.groundplatform.domain.model.User /** Manages access to [User] objects persisted in local storage. */ diff --git a/app/src/main/java/org/groundplatform/android/data/remote/firebase/FirestoreDataStore.kt b/app/src/main/java/org/groundplatform/android/data/remote/firebase/FirestoreDataStore.kt index 1c7addd08d..1f68dd04b1 100644 --- a/app/src/main/java/org/groundplatform/android/data/remote/firebase/FirestoreDataStore.kt +++ b/app/src/main/java/org/groundplatform/android/data/remote/firebase/FirestoreDataStore.kt @@ -32,10 +32,10 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext import org.groundplatform.android.BuildConfig.USE_EMULATORS -import org.groundplatform.android.data.remote.RemoteDataStore import org.groundplatform.android.data.remote.firebase.schema.GroundFirestore import org.groundplatform.android.data.remote.firebase.schema.LoiCollectionReference import org.groundplatform.android.di.coroutines.IoDispatcher +import org.groundplatform.data.stores.RemoteDataStore import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.SurveyListItem import org.groundplatform.domain.model.TermsOfService diff --git a/app/src/main/java/org/groundplatform/android/di/LocalDataStoreModule.kt b/app/src/main/java/org/groundplatform/android/di/LocalDataStoreModule.kt index 729e3928a7..21a4c2bccf 100644 --- a/app/src/main/java/org/groundplatform/android/di/LocalDataStoreModule.kt +++ b/app/src/main/java/org/groundplatform/android/di/LocalDataStoreModule.kt @@ -41,11 +41,11 @@ import org.groundplatform.android.data.local.room.stores.RoomOfflineAreaStore import org.groundplatform.android.data.local.room.stores.RoomSubmissionStore import org.groundplatform.android.data.local.room.stores.RoomSurveyStore import org.groundplatform.android.data.local.room.stores.RoomUserStore -import org.groundplatform.android.data.local.stores.LocalLocationOfInterestStore -import org.groundplatform.android.data.local.stores.LocalOfflineAreaStore -import org.groundplatform.android.data.local.stores.LocalSubmissionStore -import org.groundplatform.android.data.local.stores.LocalSurveyStore -import org.groundplatform.android.data.local.stores.LocalUserStore +import org.groundplatform.data.stores.LocalLocationOfInterestStore +import org.groundplatform.data.stores.LocalOfflineAreaStore +import org.groundplatform.data.stores.LocalSubmissionStore +import org.groundplatform.data.stores.LocalSurveyStore +import org.groundplatform.data.stores.LocalUserStore @InstallIn(SingletonComponent::class) @Module diff --git a/app/src/main/java/org/groundplatform/android/di/RemotePersistenceModule.kt b/app/src/main/java/org/groundplatform/android/di/RemotePersistenceModule.kt index aeba5afc5e..80dd0882b0 100644 --- a/app/src/main/java/org/groundplatform/android/di/RemotePersistenceModule.kt +++ b/app/src/main/java/org/groundplatform/android/di/RemotePersistenceModule.kt @@ -30,12 +30,12 @@ import javax.inject.Singleton import org.groundplatform.android.BuildConfig.EMULATOR_HOST import org.groundplatform.android.BuildConfig.FIRESTORE_EMULATOR_PORT import org.groundplatform.android.BuildConfig.USE_EMULATORS -import org.groundplatform.android.data.remote.RemoteDataStore import org.groundplatform.android.data.remote.RemoteStorageManager import org.groundplatform.android.data.remote.firebase.FirebaseStorageManager import org.groundplatform.android.data.remote.firebase.FirestoreDataStore import org.groundplatform.android.data.remote.firebase.FirestoreUuidGenerator import org.groundplatform.android.data.uuid.OfflineUuidGenerator +import org.groundplatform.data.stores.RemoteDataStore @InstallIn(SingletonComponent::class) @Module diff --git a/app/src/main/java/org/groundplatform/android/repository/LocationOfInterestRepository.kt b/app/src/main/java/org/groundplatform/android/repository/LocationOfInterestRepository.kt index bc276f01ad..4215235179 100644 --- a/app/src/main/java/org/groundplatform/android/repository/LocationOfInterestRepository.kt +++ b/app/src/main/java/org/groundplatform/android/repository/LocationOfInterestRepository.kt @@ -21,13 +21,13 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map -import org.groundplatform.android.data.local.stores.LocalLocationOfInterestStore -import org.groundplatform.android.data.local.stores.LocalSurveyStore -import org.groundplatform.android.data.remote.RemoteDataStore import org.groundplatform.android.data.sync.MutationSyncWorkManager import org.groundplatform.android.data.uuid.OfflineUuidGenerator import org.groundplatform.android.system.auth.AuthenticationManager import org.groundplatform.android.ui.map.gms.GmsExt.contains +import org.groundplatform.data.stores.LocalLocationOfInterestStore +import org.groundplatform.data.stores.LocalSurveyStore +import org.groundplatform.data.stores.RemoteDataStore import org.groundplatform.domain.model.Role import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.geometry.Geometry diff --git a/app/src/main/java/org/groundplatform/android/repository/MutationRepository.kt b/app/src/main/java/org/groundplatform/android/repository/MutationRepository.kt index 3a7866557e..571f9fec10 100644 --- a/app/src/main/java/org/groundplatform/android/repository/MutationRepository.kt +++ b/app/src/main/java/org/groundplatform/android/repository/MutationRepository.kt @@ -21,11 +21,11 @@ import javax.inject.Singleton import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first -import org.groundplatform.android.data.local.stores.LocalLocationOfInterestStore -import org.groundplatform.android.data.local.stores.LocalSubmissionStore -import org.groundplatform.android.data.remote.RemoteDataStore import org.groundplatform.android.system.auth.AuthenticationManager import org.groundplatform.android.util.priority +import org.groundplatform.data.stores.LocalLocationOfInterestStore +import org.groundplatform.data.stores.LocalSubmissionStore +import org.groundplatform.data.stores.RemoteDataStore import org.groundplatform.domain.model.User import org.groundplatform.domain.model.mutation.LocationOfInterestMutation import org.groundplatform.domain.model.mutation.Mutation diff --git a/app/src/main/java/org/groundplatform/android/repository/OfflineAreaRepository.kt b/app/src/main/java/org/groundplatform/android/repository/OfflineAreaRepository.kt index 8dfd1c98ab..89688363d8 100644 --- a/app/src/main/java/org/groundplatform/android/repository/OfflineAreaRepository.kt +++ b/app/src/main/java/org/groundplatform/android/repository/OfflineAreaRepository.kt @@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.mapNotNull -import org.groundplatform.android.data.local.stores.LocalOfflineAreaStore import org.groundplatform.android.data.uuid.OfflineUuidGenerator import org.groundplatform.android.system.GeocodingManager import org.groundplatform.android.ui.map.gms.mog.MogClient @@ -32,6 +31,7 @@ import org.groundplatform.android.ui.map.gms.mog.getTilePath import org.groundplatform.android.ui.util.FileUtil import org.groundplatform.android.util.deleteIfEmpty import org.groundplatform.android.util.rangeOf +import org.groundplatform.data.stores.LocalOfflineAreaStore import org.groundplatform.domain.model.imagery.LocalTileSource import org.groundplatform.domain.model.imagery.OfflineArea import org.groundplatform.domain.model.imagery.TileSource diff --git a/app/src/main/java/org/groundplatform/android/repository/SubmissionRepository.kt b/app/src/main/java/org/groundplatform/android/repository/SubmissionRepository.kt index f3c42499d4..52fc6a01f9 100644 --- a/app/src/main/java/org/groundplatform/android/repository/SubmissionRepository.kt +++ b/app/src/main/java/org/groundplatform/android/repository/SubmissionRepository.kt @@ -18,9 +18,9 @@ package org.groundplatform.android.repository import javax.inject.Inject import javax.inject.Singleton import org.groundplatform.android.data.local.LocalValueStore -import org.groundplatform.android.data.local.stores.LocalSubmissionStore import org.groundplatform.android.data.sync.MutationSyncWorkManager import org.groundplatform.android.data.uuid.OfflineUuidGenerator +import org.groundplatform.data.stores.LocalSubmissionStore import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.locationofinterest.LocationOfInterest import org.groundplatform.domain.model.mutation.Mutation diff --git a/app/src/main/java/org/groundplatform/android/repository/SurveyRepository.kt b/app/src/main/java/org/groundplatform/android/repository/SurveyRepository.kt index 0539a2edc4..0bdcf8a938 100644 --- a/app/src/main/java/org/groundplatform/android/repository/SurveyRepository.kt +++ b/app/src/main/java/org/groundplatform/android/repository/SurveyRepository.kt @@ -33,9 +33,9 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.withTimeout import org.groundplatform.android.FirebaseCrashLogger import org.groundplatform.android.data.local.LocalValueStore -import org.groundplatform.android.data.local.stores.LocalSurveyStore -import org.groundplatform.android.data.remote.RemoteDataStore import org.groundplatform.android.di.coroutines.ApplicationScope +import org.groundplatform.data.stores.LocalSurveyStore +import org.groundplatform.data.stores.RemoteDataStore import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.SurveyListItem import org.groundplatform.domain.model.User diff --git a/app/src/main/java/org/groundplatform/android/repository/TermsOfServiceRepository.kt b/app/src/main/java/org/groundplatform/android/repository/TermsOfServiceRepository.kt index 66977debc8..51b1da48a3 100644 --- a/app/src/main/java/org/groundplatform/android/repository/TermsOfServiceRepository.kt +++ b/app/src/main/java/org/groundplatform/android/repository/TermsOfServiceRepository.kt @@ -21,7 +21,7 @@ import javax.inject.Singleton import kotlinx.coroutines.withTimeout import org.groundplatform.android.data.local.LocalValueStore import org.groundplatform.android.data.remote.DataStoreException -import org.groundplatform.android.data.remote.RemoteDataStore +import org.groundplatform.data.stores.RemoteDataStore import org.groundplatform.domain.model.TermsOfService import org.groundplatform.domain.repository.TermsOfServiceRepositoryInterface import org.groundplatform.domain.system.NetworkManagerInterface diff --git a/app/src/main/java/org/groundplatform/android/repository/UserRepository.kt b/app/src/main/java/org/groundplatform/android/repository/UserRepository.kt index 5a27f5ae6b..28bb3e45ef 100644 --- a/app/src/main/java/org/groundplatform/android/repository/UserRepository.kt +++ b/app/src/main/java/org/groundplatform/android/repository/UserRepository.kt @@ -20,9 +20,9 @@ import javax.inject.Singleton import kotlinx.coroutines.flow.Flow import org.groundplatform.android.data.local.LocalValueStore import org.groundplatform.android.data.local.room.LocalDatabase -import org.groundplatform.android.data.local.stores.LocalUserStore -import org.groundplatform.android.data.remote.RemoteDataStore import org.groundplatform.android.system.auth.AuthenticationManager +import org.groundplatform.data.stores.LocalUserStore +import org.groundplatform.data.stores.RemoteDataStore import org.groundplatform.domain.model.Role import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.User diff --git a/app/src/test/java/org/groundplatform/android/TestRemoteStorageModule.kt b/app/src/test/java/org/groundplatform/android/TestRemoteStorageModule.kt index 35e83f586a..0c8e88e845 100644 --- a/app/src/test/java/org/groundplatform/android/TestRemoteStorageModule.kt +++ b/app/src/test/java/org/groundplatform/android/TestRemoteStorageModule.kt @@ -22,11 +22,11 @@ import dagger.hilt.testing.TestInstallIn import javax.inject.Singleton import org.groundplatform.android.data.remote.FakeRemoteDataStore import org.groundplatform.android.data.remote.FakeRemoteStorageManager -import org.groundplatform.android.data.remote.RemoteDataStore import org.groundplatform.android.data.remote.RemoteStorageManager import org.groundplatform.android.data.uuid.FakeUuidGenerator import org.groundplatform.android.data.uuid.OfflineUuidGenerator import org.groundplatform.android.di.RemotePersistenceModule +import org.groundplatform.data.stores.RemoteDataStore @Module @TestInstallIn( diff --git a/app/src/test/java/org/groundplatform/android/data/local/LocalLocationOfInterestStoreTest.kt b/app/src/test/java/org/groundplatform/android/data/local/LocalLocationOfInterestStoreTest.kt index 5d638dcca7..16139e83bb 100644 --- a/app/src/test/java/org/groundplatform/android/data/local/LocalLocationOfInterestStoreTest.kt +++ b/app/src/test/java/org/groundplatform/android/data/local/LocalLocationOfInterestStoreTest.kt @@ -33,12 +33,12 @@ import org.groundplatform.android.data.local.room.dao.LocationOfInterestDao import org.groundplatform.android.data.local.room.dao.MAX_SQL_VARIABLES import org.groundplatform.android.data.local.room.fields.EntityDeletionState import org.groundplatform.android.data.local.room.stores.RoomLocationOfInterestStore -import org.groundplatform.android.data.local.stores.LocalLocationOfInterestStore -import org.groundplatform.android.data.local.stores.LocalSubmissionStore -import org.groundplatform.android.data.local.stores.LocalSurveyStore -import org.groundplatform.android.data.local.stores.LocalUserStore import org.groundplatform.android.proto.geometry import org.groundplatform.android.ui.map.gms.GmsExt.getShellCoordinates +import org.groundplatform.data.stores.LocalLocationOfInterestStore +import org.groundplatform.data.stores.LocalSubmissionStore +import org.groundplatform.data.stores.LocalSurveyStore +import org.groundplatform.data.stores.LocalUserStore import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.User import org.groundplatform.domain.model.geometry.Coordinates diff --git a/app/src/test/java/org/groundplatform/android/data/local/LocalOfflineAreaStore.kt b/app/src/test/java/org/groundplatform/android/data/local/LocalOfflineAreaStore.kt index f1c5720956..a6cd73a188 100644 --- a/app/src/test/java/org/groundplatform/android/data/local/LocalOfflineAreaStore.kt +++ b/app/src/test/java/org/groundplatform/android/data/local/LocalOfflineAreaStore.kt @@ -20,7 +20,7 @@ import com.google.common.truth.Truth.assertThat import dagger.hilt.android.testing.HiltAndroidTest import javax.inject.Inject import org.groundplatform.android.BaseHiltTest -import org.groundplatform.android.data.local.stores.LocalOfflineAreaStore +import org.groundplatform.data.stores.LocalOfflineAreaStore import org.groundplatform.domain.model.imagery.OfflineArea import org.groundplatform.domain.model.map.Bounds import org.junit.Test diff --git a/app/src/test/java/org/groundplatform/android/data/local/LocalSubmissionStoreTest.kt b/app/src/test/java/org/groundplatform/android/data/local/LocalSubmissionStoreTest.kt index f39b03f373..854a1150cd 100644 --- a/app/src/test/java/org/groundplatform/android/data/local/LocalSubmissionStoreTest.kt +++ b/app/src/test/java/org/groundplatform/android/data/local/LocalSubmissionStoreTest.kt @@ -26,10 +26,10 @@ import org.groundplatform.android.FakeData.FAKE_GENERAL_ACCESS import org.groundplatform.android.data.local.room.LocalDataStoreException import org.groundplatform.android.data.local.room.dao.SubmissionDao import org.groundplatform.android.data.local.room.fields.EntityDeletionState -import org.groundplatform.android.data.local.stores.LocalLocationOfInterestStore -import org.groundplatform.android.data.local.stores.LocalSubmissionStore -import org.groundplatform.android.data.local.stores.LocalSurveyStore -import org.groundplatform.android.data.local.stores.LocalUserStore +import org.groundplatform.data.stores.LocalLocationOfInterestStore +import org.groundplatform.data.stores.LocalSubmissionStore +import org.groundplatform.data.stores.LocalSurveyStore +import org.groundplatform.data.stores.LocalUserStore import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.User import org.groundplatform.domain.model.geometry.Coordinates diff --git a/app/src/test/java/org/groundplatform/android/data/local/LocalSurveyStoreTest.kt b/app/src/test/java/org/groundplatform/android/data/local/LocalSurveyStoreTest.kt index 9ba342a061..eebd177b0f 100644 --- a/app/src/test/java/org/groundplatform/android/data/local/LocalSurveyStoreTest.kt +++ b/app/src/test/java/org/groundplatform/android/data/local/LocalSurveyStoreTest.kt @@ -22,7 +22,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.first import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.FakeData.SURVEY -import org.groundplatform.android.data.local.stores.LocalSurveyStore +import org.groundplatform.data.stores.LocalSurveyStore import org.groundplatform.domain.model.job.Job import org.groundplatform.domain.model.job.Style import org.groundplatform.domain.model.task.Condition diff --git a/app/src/test/java/org/groundplatform/android/data/local/LocalUserStoreTest.kt b/app/src/test/java/org/groundplatform/android/data/local/LocalUserStoreTest.kt index dada5359f6..99698a5fce 100644 --- a/app/src/test/java/org/groundplatform/android/data/local/LocalUserStoreTest.kt +++ b/app/src/test/java/org/groundplatform/android/data/local/LocalUserStoreTest.kt @@ -20,7 +20,7 @@ import dagger.hilt.android.testing.HiltAndroidTest import javax.inject.Inject import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.FakeData -import org.groundplatform.android.data.local.stores.LocalUserStore +import org.groundplatform.data.stores.LocalUserStore import org.groundplatform.domain.model.User import org.junit.Test import org.junit.runner.RunWith diff --git a/app/src/test/java/org/groundplatform/android/data/remote/FakeRemoteDataStore.kt b/app/src/test/java/org/groundplatform/android/data/remote/FakeRemoteDataStore.kt index 7569879dc4..21bceee58c 100644 --- a/app/src/test/java/org/groundplatform/android/data/remote/FakeRemoteDataStore.kt +++ b/app/src/test/java/org/groundplatform/android/data/remote/FakeRemoteDataStore.kt @@ -20,6 +20,7 @@ import javax.inject.Singleton import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf +import org.groundplatform.data.stores.RemoteDataStore import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.SurveyListItem import org.groundplatform.domain.model.TermsOfService diff --git a/app/src/test/java/org/groundplatform/android/data/sync/LocalMutationSyncWorkerTest.kt b/app/src/test/java/org/groundplatform/android/data/sync/LocalMutationSyncWorkerTest.kt index 1a08d5ea3a..f53958105d 100644 --- a/app/src/test/java/org/groundplatform/android/data/sync/LocalMutationSyncWorkerTest.kt +++ b/app/src/test/java/org/groundplatform/android/data/sync/LocalMutationSyncWorkerTest.kt @@ -32,13 +32,13 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.FakeData -import org.groundplatform.android.data.local.stores.LocalLocationOfInterestStore -import org.groundplatform.android.data.local.stores.LocalSubmissionStore -import org.groundplatform.android.data.local.stores.LocalSurveyStore -import org.groundplatform.android.data.local.stores.LocalUserStore import org.groundplatform.android.data.remote.FakeRemoteDataStore import org.groundplatform.android.di.coroutines.IoDispatcher import org.groundplatform.android.system.auth.FakeAuthenticationManager +import org.groundplatform.data.stores.LocalLocationOfInterestStore +import org.groundplatform.data.stores.LocalSubmissionStore +import org.groundplatform.data.stores.LocalSurveyStore +import org.groundplatform.data.stores.LocalUserStore import org.groundplatform.domain.model.geometry.Point import org.groundplatform.domain.model.mutation.Mutation import org.groundplatform.domain.model.mutation.Mutation.SyncStatus.COMPLETED diff --git a/app/src/test/java/org/groundplatform/android/data/sync/MediaUploadWorkerTest.kt b/app/src/test/java/org/groundplatform/android/data/sync/MediaUploadWorkerTest.kt index a5c9ddd8a9..8d5f94575f 100644 --- a/app/src/test/java/org/groundplatform/android/data/sync/MediaUploadWorkerTest.kt +++ b/app/src/test/java/org/groundplatform/android/data/sync/MediaUploadWorkerTest.kt @@ -29,13 +29,13 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.first import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.FakeData -import org.groundplatform.android.data.local.stores.LocalLocationOfInterestStore -import org.groundplatform.android.data.local.stores.LocalSubmissionStore -import org.groundplatform.android.data.local.stores.LocalSurveyStore -import org.groundplatform.android.data.local.stores.LocalUserStore import org.groundplatform.android.data.remote.FakeRemoteDataStore import org.groundplatform.android.data.remote.FakeRemoteStorageManager import org.groundplatform.android.di.coroutines.IoDispatcher +import org.groundplatform.data.stores.LocalLocationOfInterestStore +import org.groundplatform.data.stores.LocalSubmissionStore +import org.groundplatform.data.stores.LocalSurveyStore +import org.groundplatform.data.stores.LocalUserStore import org.groundplatform.domain.model.mutation.Mutation import org.groundplatform.domain.model.mutation.Mutation.SyncStatus.COMPLETED import org.groundplatform.domain.model.mutation.Mutation.SyncStatus.FAILED diff --git a/app/src/test/java/org/groundplatform/android/repository/LocationOfInterestRepositoryTest.kt b/app/src/test/java/org/groundplatform/android/repository/LocationOfInterestRepositoryTest.kt index 5e8c04239f..0c3b352306 100644 --- a/app/src/test/java/org/groundplatform/android/repository/LocationOfInterestRepositoryTest.kt +++ b/app/src/test/java/org/groundplatform/android/repository/LocationOfInterestRepositoryTest.kt @@ -28,10 +28,10 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceUntilIdle import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.FakeData -import org.groundplatform.android.data.local.stores.LocalLocationOfInterestStore import org.groundplatform.android.data.remote.FakeRemoteDataStore import org.groundplatform.android.data.sync.MutationSyncWorkManager import org.groundplatform.android.system.auth.FakeAuthenticationManager +import org.groundplatform.data.stores.LocalLocationOfInterestStore import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.geometry.Coordinates import org.groundplatform.domain.model.geometry.LinearRing diff --git a/app/src/test/java/org/groundplatform/android/repository/MutationRepositoryTest.kt b/app/src/test/java/org/groundplatform/android/repository/MutationRepositoryTest.kt index 1d6e04d1ef..c2cbf1b096 100644 --- a/app/src/test/java/org/groundplatform/android/repository/MutationRepositoryTest.kt +++ b/app/src/test/java/org/groundplatform/android/repository/MutationRepositoryTest.kt @@ -19,10 +19,10 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest -import org.groundplatform.android.data.local.stores.LocalLocationOfInterestStore -import org.groundplatform.android.data.local.stores.LocalSubmissionStore -import org.groundplatform.android.data.remote.RemoteDataStore import org.groundplatform.android.system.auth.AuthenticationManager +import org.groundplatform.data.stores.LocalLocationOfInterestStore +import org.groundplatform.data.stores.LocalSubmissionStore +import org.groundplatform.data.stores.RemoteDataStore import org.groundplatform.domain.model.User import org.groundplatform.domain.model.mutation.LocationOfInterestMutation import org.groundplatform.domain.model.mutation.Mutation diff --git a/app/src/test/java/org/groundplatform/android/repository/OfflineAreaRepositoryTest.kt b/app/src/test/java/org/groundplatform/android/repository/OfflineAreaRepositoryTest.kt index 6c6e39433d..bd3f29d5d0 100644 --- a/app/src/test/java/org/groundplatform/android/repository/OfflineAreaRepositoryTest.kt +++ b/app/src/test/java/org/groundplatform/android/repository/OfflineAreaRepositoryTest.kt @@ -22,12 +22,12 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest -import org.groundplatform.android.data.local.stores.LocalOfflineAreaStore import org.groundplatform.android.data.uuid.OfflineUuidGenerator import org.groundplatform.android.system.GeocodingManager import org.groundplatform.android.ui.map.gms.mog.MogClient import org.groundplatform.android.ui.map.gms.mog.getTilePath import org.groundplatform.android.ui.util.FileUtil +import org.groundplatform.data.stores.LocalOfflineAreaStore import org.groundplatform.domain.model.imagery.LocalTileSource import org.groundplatform.domain.model.imagery.MogCollection import org.groundplatform.domain.model.imagery.MogSource diff --git a/app/src/test/java/org/groundplatform/android/repository/SubmissionRepositoryTest.kt b/app/src/test/java/org/groundplatform/android/repository/SubmissionRepositoryTest.kt index 2138cc3716..b3422b7636 100644 --- a/app/src/test/java/org/groundplatform/android/repository/SubmissionRepositoryTest.kt +++ b/app/src/test/java/org/groundplatform/android/repository/SubmissionRepositoryTest.kt @@ -18,9 +18,9 @@ package org.groundplatform.android.repository import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.groundplatform.android.data.local.LocalValueStore -import org.groundplatform.android.data.local.stores.LocalSubmissionStore import org.groundplatform.android.data.sync.MutationSyncWorkManager import org.groundplatform.android.data.uuid.OfflineUuidGenerator +import org.groundplatform.data.stores.LocalSubmissionStore import org.groundplatform.domain.model.User import org.groundplatform.domain.model.locationofinterest.AuditInfo import org.groundplatform.domain.model.locationofinterest.LocationOfInterest diff --git a/app/src/test/java/org/groundplatform/android/repository/SurveyRepositoryTest.kt b/app/src/test/java/org/groundplatform/android/repository/SurveyRepositoryTest.kt index b95b089a4c..eaea73ea0d 100644 --- a/app/src/test/java/org/groundplatform/android/repository/SurveyRepositoryTest.kt +++ b/app/src/test/java/org/groundplatform/android/repository/SurveyRepositoryTest.kt @@ -23,8 +23,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.FakeData.SURVEY -import org.groundplatform.android.data.local.stores.LocalSurveyStore import org.groundplatform.android.data.remote.FakeRemoteDataStore +import org.groundplatform.data.stores.LocalSurveyStore import org.groundplatform.domain.repository.SurveyRepositoryInterface import org.groundplatform.domain.usecases.survey.ActivateSurveyUseCase import org.junit.Before diff --git a/app/src/test/java/org/groundplatform/android/repository/UserRepositoryTest.kt b/app/src/test/java/org/groundplatform/android/repository/UserRepositoryTest.kt index 9f93e973b8..e92df290f3 100644 --- a/app/src/test/java/org/groundplatform/android/repository/UserRepositoryTest.kt +++ b/app/src/test/java/org/groundplatform/android/repository/UserRepositoryTest.kt @@ -25,11 +25,11 @@ import kotlinx.coroutines.test.advanceUntilIdle import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.FakeData import org.groundplatform.android.data.local.LocalValueStore -import org.groundplatform.android.data.local.stores.LocalSurveyStore -import org.groundplatform.android.data.local.stores.LocalUserStore import org.groundplatform.android.data.remote.FakeRemoteDataStore import org.groundplatform.android.system.NetworkManager import org.groundplatform.android.system.auth.FakeAuthenticationManager +import org.groundplatform.data.stores.LocalSurveyStore +import org.groundplatform.data.stores.LocalUserStore import org.groundplatform.domain.model.Role import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.auth.SignInState diff --git a/app/src/test/java/org/groundplatform/android/ui/home/HomeScreenFragmentTest.kt b/app/src/test/java/org/groundplatform/android/ui/home/HomeScreenFragmentTest.kt index 6fe1ea20b2..bbba345e43 100644 --- a/app/src/test/java/org/groundplatform/android/ui/home/HomeScreenFragmentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/home/HomeScreenFragmentTest.kt @@ -57,9 +57,9 @@ import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.BuildConfig import org.groundplatform.android.FakeData import org.groundplatform.android.R -import org.groundplatform.android.data.local.stores.LocalSurveyStore import org.groundplatform.android.testrules.FragmentScenarioRule import org.groundplatform.android.ui.components.MapFloatingActionButtonType +import org.groundplatform.data.stores.LocalSurveyStore import org.groundplatform.domain.model.Survey import org.groundplatform.domain.repository.SurveyRepositoryInterface import org.junit.Before diff --git a/app/src/test/java/org/groundplatform/android/ui/offlineareas/OfflineAreasFragmentTest.kt b/app/src/test/java/org/groundplatform/android/ui/offlineareas/OfflineAreasFragmentTest.kt index 8b7afa59a9..8bfc32609a 100644 --- a/app/src/test/java/org/groundplatform/android/ui/offlineareas/OfflineAreasFragmentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/offlineareas/OfflineAreasFragmentTest.kt @@ -38,8 +38,8 @@ import kotlinx.coroutines.test.advanceUntilIdle import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.FakeData.OFFLINE_AREA import org.groundplatform.android.R -import org.groundplatform.android.data.local.stores.LocalOfflineAreaStore import org.groundplatform.android.testrules.FragmentScenarioRule +import org.groundplatform.data.stores.LocalOfflineAreaStore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith diff --git a/app/src/test/java/org/groundplatform/android/ui/offlineareas/viewer/OfflineAreaViewerFragmentTest.kt b/app/src/test/java/org/groundplatform/android/ui/offlineareas/viewer/OfflineAreaViewerFragmentTest.kt index 15e1c46011..25dd49ba53 100644 --- a/app/src/test/java/org/groundplatform/android/ui/offlineareas/viewer/OfflineAreaViewerFragmentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/offlineareas/viewer/OfflineAreaViewerFragmentTest.kt @@ -32,9 +32,9 @@ import kotlinx.coroutines.test.advanceUntilIdle import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.FakeData.OFFLINE_AREA import org.groundplatform.android.R -import org.groundplatform.android.data.local.stores.LocalOfflineAreaStore import org.groundplatform.android.testrules.FragmentScenarioRule import org.groundplatform.android.util.view.isGone +import org.groundplatform.data.stores.LocalOfflineAreaStore import org.groundplatform.domain.model.map.MapType import org.groundplatform.ui.map.MapConfig import org.junit.Rule diff --git a/app/src/test/java/org/groundplatform/android/ui/offlineareas/viewer/OfflineAreaViewerViewModelTest.kt b/app/src/test/java/org/groundplatform/android/ui/offlineareas/viewer/OfflineAreaViewerViewModelTest.kt index ac786cbc85..cec62523c2 100644 --- a/app/src/test/java/org/groundplatform/android/ui/offlineareas/viewer/OfflineAreaViewerViewModelTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/offlineareas/viewer/OfflineAreaViewerViewModelTest.kt @@ -21,7 +21,7 @@ import javax.inject.Inject import kotlinx.coroutines.test.advanceUntilIdle import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.FakeData.OFFLINE_AREA -import org.groundplatform.android.data.local.stores.LocalOfflineAreaStore +import org.groundplatform.data.stores.LocalOfflineAreaStore import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner diff --git a/app/src/test/java/org/groundplatform/android/ui/syncstatus/SyncStatusFragmentTest.kt b/app/src/test/java/org/groundplatform/android/ui/syncstatus/SyncStatusFragmentTest.kt index 9a92f07417..3466e331f8 100644 --- a/app/src/test/java/org/groundplatform/android/ui/syncstatus/SyncStatusFragmentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/syncstatus/SyncStatusFragmentTest.kt @@ -31,12 +31,12 @@ import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.FakeData.SURVEY import org.groundplatform.android.FakeData.USER import org.groundplatform.android.R -import org.groundplatform.android.data.local.stores.LocalLocationOfInterestStore -import org.groundplatform.android.data.local.stores.LocalSubmissionStore -import org.groundplatform.android.data.local.stores.LocalSurveyStore -import org.groundplatform.android.data.local.stores.LocalUserStore import org.groundplatform.android.data.remote.FakeRemoteDataStore import org.groundplatform.android.testrules.FragmentScenarioRule +import org.groundplatform.data.stores.LocalLocationOfInterestStore +import org.groundplatform.data.stores.LocalSubmissionStore +import org.groundplatform.data.stores.LocalSurveyStore +import org.groundplatform.data.stores.LocalUserStore import org.groundplatform.domain.model.geometry.Coordinates import org.groundplatform.domain.model.geometry.Point import org.groundplatform.domain.repository.SurveyRepositoryInterface diff --git a/app/src/main/java/org/groundplatform/android/data/local/stores/LocalLocationOfInterestStore.kt b/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalLocationOfInterestStore.kt similarity index 98% rename from app/src/main/java/org/groundplatform/android/data/local/stores/LocalLocationOfInterestStore.kt rename to core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalLocationOfInterestStore.kt index db0e84f8f5..98188b104d 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/stores/LocalLocationOfInterestStore.kt +++ b/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalLocationOfInterestStore.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.android.data.local.stores +package org.groundplatform.data.stores import kotlinx.coroutines.flow.Flow import org.groundplatform.domain.model.Survey diff --git a/app/src/main/java/org/groundplatform/android/data/local/stores/LocalMutationStore.kt b/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalMutationStore.kt similarity index 96% rename from app/src/main/java/org/groundplatform/android/data/local/stores/LocalMutationStore.kt rename to core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalMutationStore.kt index e7863321cd..8ef188069d 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/stores/LocalMutationStore.kt +++ b/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalMutationStore.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.android.data.local.stores +package org.groundplatform.data.stores import org.groundplatform.domain.model.mutation.Mutation diff --git a/app/src/main/java/org/groundplatform/android/data/local/stores/LocalOfflineAreaStore.kt b/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalOfflineAreaStore.kt similarity index 96% rename from app/src/main/java/org/groundplatform/android/data/local/stores/LocalOfflineAreaStore.kt rename to core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalOfflineAreaStore.kt index 02f06129ab..19a90d956a 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/stores/LocalOfflineAreaStore.kt +++ b/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalOfflineAreaStore.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.android.data.local.stores +package org.groundplatform.data.stores import kotlinx.coroutines.flow.Flow import org.groundplatform.domain.model.imagery.OfflineArea diff --git a/app/src/main/java/org/groundplatform/android/data/local/stores/LocalSubmissionStore.kt b/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalSubmissionStore.kt similarity index 98% rename from app/src/main/java/org/groundplatform/android/data/local/stores/LocalSubmissionStore.kt rename to core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalSubmissionStore.kt index d023479ffa..2f2c52ae80 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/stores/LocalSubmissionStore.kt +++ b/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalSubmissionStore.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.android.data.local.stores +package org.groundplatform.data.stores import kotlinx.coroutines.flow.Flow import org.groundplatform.domain.model.Survey diff --git a/app/src/main/java/org/groundplatform/android/data/local/stores/LocalSurveyStore.kt b/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalSurveyStore.kt similarity index 95% rename from app/src/main/java/org/groundplatform/android/data/local/stores/LocalSurveyStore.kt rename to core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalSurveyStore.kt index b198dc1f70..92f692d791 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/stores/LocalSurveyStore.kt +++ b/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalSurveyStore.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.android.data.local.stores +package org.groundplatform.data.stores import kotlinx.coroutines.flow.Flow import org.groundplatform.domain.model.Survey diff --git a/app/src/main/java/org/groundplatform/android/data/local/stores/LocalUserStore.kt b/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalUserStore.kt similarity index 95% rename from app/src/main/java/org/groundplatform/android/data/local/stores/LocalUserStore.kt rename to core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalUserStore.kt index 6365064409..271da4fa4f 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/stores/LocalUserStore.kt +++ b/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalUserStore.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.android.data.local.stores +package org.groundplatform.data.stores import org.groundplatform.domain.model.User diff --git a/app/src/main/java/org/groundplatform/android/data/remote/RemoteDataStore.kt b/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/RemoteDataStore.kt similarity index 98% rename from app/src/main/java/org/groundplatform/android/data/remote/RemoteDataStore.kt rename to core/data/src/commonMain/kotlin/org/groundplatform/data/stores/RemoteDataStore.kt index b791d91934..8f981c7c86 100644 --- a/app/src/main/java/org/groundplatform/android/data/remote/RemoteDataStore.kt +++ b/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/RemoteDataStore.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.android.data.remote +package org.groundplatform.data.stores import kotlinx.coroutines.flow.Flow import org.groundplatform.domain.model.Survey From 0907bd787483b2dd7848641927d337f21fa84373 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Thu, 27 Aug 2026 23:00:55 +0530 Subject: [PATCH 04/10] refactor: move LocalValueStore to core data module and rename implementation to SharedPreferencesLocalValueStore --- .../android/GroundApplication.kt | 2 +- ...kt => SharedPreferencesLocalValueStore.kt} | 59 ++++++-------- .../data/sync/MediaUploadWorkManager.kt | 2 +- .../android/di/LocalDataStoreModule.kt | 6 ++ .../android/repository/MapStateRepository.kt | 2 +- .../repository/SubmissionRepository.kt | 2 +- .../android/repository/SurveyRepository.kt | 2 +- .../repository/TermsOfServiceRepository.kt | 2 +- .../android/repository/UserRepository.kt | 2 +- .../deeplink/PlayInstallReferrerService.kt | 2 +- .../tasks/point/DropPinTaskViewModel.kt | 2 +- .../tasks/polygon/DrawAreaTaskViewModel.kt | 2 +- .../local/LocalLocationOfInterestStoreTest.kt | 1 + .../android/data/local/LocalValueStoreTest.kt | 3 +- .../repository/MapStateRepositoryTest.kt | 2 +- .../repository/SubmissionRepositoryTest.kt | 2 +- .../android/repository/UserRepositoryTest.kt | 2 +- .../PlayInstallReferrerServiceTest.kt | 2 +- .../tasks/point/DropPinTaskScreenTest.kt | 2 +- .../tasks/polygon/DrawAreaTaskScreenTest.kt | 2 +- .../polygon/DrawAreaTaskViewModelTest.kt | 2 +- .../data/stores/LocalValueStore.kt | 78 +++++++++++++++++++ 22 files changed, 127 insertions(+), 54 deletions(-) rename app/src/main/java/org/groundplatform/android/data/local/{LocalValueStore.kt => SharedPreferencesLocalValueStore.kt} (78%) create mode 100644 core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalValueStore.kt diff --git a/app/src/main/java/org/groundplatform/android/GroundApplication.kt b/app/src/main/java/org/groundplatform/android/GroundApplication.kt index bac8f4aebc..dcc39b3e7f 100644 --- a/app/src/main/java/org/groundplatform/android/GroundApplication.kt +++ b/app/src/main/java/org/groundplatform/android/GroundApplication.kt @@ -27,7 +27,7 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfig import dagger.hilt.android.HiltAndroidApp import javax.inject.Inject import org.groundplatform.android.common.Constants.isReleaseBuild -import org.groundplatform.android.data.local.LocalValueStore +import org.groundplatform.data.stores.LocalValueStore import timber.log.Timber @HiltAndroidApp diff --git a/app/src/main/java/org/groundplatform/android/data/local/LocalValueStore.kt b/app/src/main/java/org/groundplatform/android/data/local/SharedPreferencesLocalValueStore.kt similarity index 78% rename from app/src/main/java/org/groundplatform/android/data/local/LocalValueStore.kt rename to app/src/main/java/org/groundplatform/android/data/local/SharedPreferencesLocalValueStore.kt index c63c21f086..90ef6288e7 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/LocalValueStore.kt +++ b/app/src/main/java/org/groundplatform/android/data/local/SharedPreferencesLocalValueStore.kt @@ -23,6 +23,7 @@ import javax.inject.Singleton import org.groundplatform.android.common.PrefKeys import org.groundplatform.android.util.allowThreadDiskReads import org.groundplatform.android.util.allowThreadDiskWrites +import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.domain.model.geometry.Coordinates import org.groundplatform.domain.model.map.Bounds import org.groundplatform.domain.model.map.CameraPosition @@ -30,21 +31,14 @@ import org.groundplatform.domain.model.settings.MeasurementUnits import org.groundplatform.domain.util.Constants.DEFAULT_MAP_TYPE import timber.log.Timber -/** - * Simple value store persisted locally on device. Unlike [LocalDataStoreModule], this class - * provides a concrete implementation using the Android SDK, and therefore does not require a - * database-specific implementation. - */ +/** Implementation of [LocalValueStore] using Android SDK [SharedPreferences]. */ @Singleton -class LocalValueStore +class SharedPreferencesLocalValueStore @Inject -constructor(private val preferences: SharedPreferences, private val locale: Locale) { +constructor(private val preferences: SharedPreferences, private val locale: Locale) : + LocalValueStore { - /** - * Id of the last survey successfully activated by the user. This value is only updated after the - * survey activation process is complete. - */ - var lastActiveSurveyId: String + override var lastActiveSurveyId: String get() = allowThreadDiskReads { preferences.getString(PrefKeys.ACTIVE_SURVEY_ID_KEY, "").orEmpty() } @@ -52,34 +46,29 @@ constructor(private val preferences: SharedPreferences, private val locale: Loca preferences.edit { putString(PrefKeys.ACTIVE_SURVEY_ID_KEY, id) } } - /** The last map type selected. */ - var mapType: Int + override var mapType: Int get() = allowThreadDiskReads { preferences.getInt(PrefKeys.MAP_TYPE, DEFAULT_MAP_TYPE.ordinal) } set(value) = allowThreadDiskWrites { preferences.edit { putInt(PrefKeys.MAP_TYPE, value) } } - /** Whether location lock is enabled or not. */ - var isLocationLockEnabled: Boolean + override var isLocationLockEnabled: Boolean get() = allowThreadDiskReads { preferences.getBoolean(PrefKeys.LOCATION_LOCK_ENABLED, false) } set(value) = allowThreadDiskWrites { preferences.edit { putBoolean(PrefKeys.LOCATION_LOCK_ENABLED, value) } } - /** Terms of service acceptance state for the currently signed in user. */ - var isTermsOfServiceAccepted: Boolean + override var isTermsOfServiceAccepted: Boolean get() = allowThreadDiskReads { preferences.getBoolean(PrefKeys.TOS_ACCEPTED, false) } set(value) = allowThreadDiskWrites { preferences.edit { putBoolean(PrefKeys.TOS_ACCEPTED, value) } } - /** Whether to overlay offline map imagery. */ - var isOfflineImageryEnabled: Boolean + override var isOfflineImageryEnabled: Boolean get() = allowThreadDiskReads { preferences.getBoolean(PrefKeys.OFFLINE_MAP_IMAGERY, true) } set(value) = allowThreadDiskReads { preferences.edit { putBoolean(PrefKeys.OFFLINE_MAP_IMAGERY, value) } } - /** Whether to display instructions when loading a draw area task. */ - var drawAreaInstructionsShown: Boolean + override var drawAreaInstructionsShown: Boolean get() = allowThreadDiskReads { preferences.getBoolean(PrefKeys.DRAW_AREA_INSTRUCTIONS_SHOWN, false) } @@ -87,8 +76,7 @@ constructor(private val preferences: SharedPreferences, private val locale: Loca preferences.edit { putBoolean(PrefKeys.DRAW_AREA_INSTRUCTIONS_SHOWN, value) } } - /** Whether to display instructions when loading a drop pin task. */ - var dropPinInstructionsShown: Boolean + override var dropPinInstructionsShown: Boolean get() = allowThreadDiskReads { preferences.getBoolean(PrefKeys.DROP_PIN_INSTRUCTIONS_SHOWN, false) } @@ -96,19 +84,19 @@ constructor(private val preferences: SharedPreferences, private val locale: Loca preferences.edit { putBoolean(PrefKeys.DROP_PIN_INSTRUCTIONS_SHOWN, value) } } - var draftSubmissionId: String? + override var draftSubmissionId: String? get() = allowThreadDiskReads { preferences.getString(PrefKeys.DRAFT_SUBMISSION_ID, null) } set(value) = allowThreadDiskReads { preferences.edit { putString(PrefKeys.DRAFT_SUBMISSION_ID, value) } } - var selectedLanguage: String + override var selectedLanguage: String get() = allowThreadDiskReads { preferences.getString(PrefKeys.LANGUAGE, locale.language) ?: locale.language } set(value) = allowThreadDiskReads { preferences.edit { putString(PrefKeys.LANGUAGE, value) } } - var selectedLengthUnit: String + override var selectedLengthUnit: String get() = allowThreadDiskReads { preferences.getString(PrefKeys.MEASUREMENT_UNITS, MeasurementUnits.METRIC.name) ?: MeasurementUnits.METRIC.name @@ -117,13 +105,13 @@ constructor(private val preferences: SharedPreferences, private val locale: Loca preferences.edit { putString(PrefKeys.MEASUREMENT_UNITS, value) } } - var shouldUploadMediaOverUnmeteredConnectionOnly: Boolean + override var shouldUploadMediaOverUnmeteredConnectionOnly: Boolean get() = allowThreadDiskReads { preferences.getBoolean(PrefKeys.UPLOAD_MEDIA, false) } set(value) = allowThreadDiskWrites { preferences.edit { putBoolean(PrefKeys.UPLOAD_MEDIA, value) } } - var isDeferredDeeplinkConsumed: Boolean + override var isDeferredDeeplinkConsumed: Boolean get() = allowThreadDiskReads { preferences.getBoolean(PrefKeys.DEFERRED_DEEPLINK_CONSUMED, false) } @@ -131,14 +119,13 @@ constructor(private val preferences: SharedPreferences, private val locale: Loca preferences.edit { putBoolean(PrefKeys.DEFERRED_DEEPLINK_CONSUMED, value) } } - /** Removes all values stored in the local store. */ - fun clear() = allowThreadDiskWrites { preferences.edit { clear() } } + override fun clear() = allowThreadDiskWrites { preferences.edit { clear() } } - fun clearLastCameraPosition(surveyId: String) = allowThreadDiskReads { + override fun clearLastCameraPosition(surveyId: String) = allowThreadDiskReads { preferences.edit { remove(PrefKeys.LAST_VIEWPORT_PREFIX + surveyId) } } - fun setLastCameraPosition(surveyId: String, cameraPosition: CameraPosition) = + override fun setLastCameraPosition(surveyId: String, cameraPosition: CameraPosition) = allowThreadDiskReads { preferences.edit { putString( @@ -148,7 +135,7 @@ constructor(private val preferences: SharedPreferences, private val locale: Loca } } - fun getLastCameraPosition(surveyId: String): CameraPosition? = allowThreadDiskReads { + override fun getLastCameraPosition(surveyId: String): CameraPosition? = allowThreadDiskReads { try { val stringVal = preferences.getString(PrefKeys.LAST_VIEWPORT_PREFIX + surveyId, "").orEmpty() getDeserializedCameraPosition(stringVal) @@ -161,11 +148,11 @@ constructor(private val preferences: SharedPreferences, private val locale: Loca } } - fun setDataSharingConsent(surveyId: String, consent: Boolean) { + override fun setDataSharingConsent(surveyId: String, consent: Boolean) { preferences.edit { putBoolean(PrefKeys.DATA_SHARING_CONSENT_PREFIX + surveyId, consent) } } - fun getDataSharingConsent(surveyId: String): Boolean = allowThreadDiskReads { + override fun getDataSharingConsent(surveyId: String): Boolean = allowThreadDiskReads { return preferences.getBoolean(PrefKeys.DATA_SHARING_CONSENT_PREFIX + surveyId, false) } diff --git a/app/src/main/java/org/groundplatform/android/data/sync/MediaUploadWorkManager.kt b/app/src/main/java/org/groundplatform/android/data/sync/MediaUploadWorkManager.kt index 30c6e48a3c..52e695e48d 100644 --- a/app/src/main/java/org/groundplatform/android/data/sync/MediaUploadWorkManager.kt +++ b/app/src/main/java/org/groundplatform/android/data/sync/MediaUploadWorkManager.kt @@ -19,7 +19,7 @@ import androidx.work.ExistingWorkPolicy import androidx.work.NetworkType import androidx.work.WorkManager import javax.inject.Inject -import org.groundplatform.android.data.local.LocalValueStore +import org.groundplatform.data.stores.LocalValueStore /** Enqueues media upload work to be performed in the background. */ class MediaUploadWorkManager diff --git a/app/src/main/java/org/groundplatform/android/di/LocalDataStoreModule.kt b/app/src/main/java/org/groundplatform/android/di/LocalDataStoreModule.kt index 21a4c2bccf..02741e47f3 100644 --- a/app/src/main/java/org/groundplatform/android/di/LocalDataStoreModule.kt +++ b/app/src/main/java/org/groundplatform/android/di/LocalDataStoreModule.kt @@ -21,6 +21,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +import org.groundplatform.android.data.local.SharedPreferencesLocalValueStore import org.groundplatform.android.data.local.room.LocalDatabase import org.groundplatform.android.data.local.room.dao.ConditionDao import org.groundplatform.android.data.local.room.dao.DraftSubmissionDao @@ -46,6 +47,7 @@ import org.groundplatform.data.stores.LocalOfflineAreaStore import org.groundplatform.data.stores.LocalSubmissionStore import org.groundplatform.data.stores.LocalSurveyStore import org.groundplatform.data.stores.LocalUserStore +import org.groundplatform.data.stores.LocalValueStore @InstallIn(SingletonComponent::class) @Module @@ -67,6 +69,10 @@ abstract class LocalDataStoreModule { @Binds @Singleton abstract fun userStore(store: RoomUserStore): LocalUserStore + @Binds + @Singleton + abstract fun localValueStore(store: SharedPreferencesLocalValueStore): LocalValueStore + companion object { @Provides fun draftSubmissionDao(localDatabase: LocalDatabase): DraftSubmissionDao { diff --git a/app/src/main/java/org/groundplatform/android/repository/MapStateRepository.kt b/app/src/main/java/org/groundplatform/android/repository/MapStateRepository.kt index eedfdb86e4..8f43406ad2 100644 --- a/app/src/main/java/org/groundplatform/android/repository/MapStateRepository.kt +++ b/app/src/main/java/org/groundplatform/android/repository/MapStateRepository.kt @@ -21,7 +21,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import org.groundplatform.android.data.local.LocalValueStore +import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.domain.model.map.CameraPosition import org.groundplatform.domain.model.map.MapType import org.groundplatform.domain.repository.MapStateRepositoryInterface diff --git a/app/src/main/java/org/groundplatform/android/repository/SubmissionRepository.kt b/app/src/main/java/org/groundplatform/android/repository/SubmissionRepository.kt index 52fc6a01f9..aca5bdd49a 100644 --- a/app/src/main/java/org/groundplatform/android/repository/SubmissionRepository.kt +++ b/app/src/main/java/org/groundplatform/android/repository/SubmissionRepository.kt @@ -17,10 +17,10 @@ package org.groundplatform.android.repository import javax.inject.Inject import javax.inject.Singleton -import org.groundplatform.android.data.local.LocalValueStore import org.groundplatform.android.data.sync.MutationSyncWorkManager import org.groundplatform.android.data.uuid.OfflineUuidGenerator import org.groundplatform.data.stores.LocalSubmissionStore +import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.locationofinterest.LocationOfInterest import org.groundplatform.domain.model.mutation.Mutation diff --git a/app/src/main/java/org/groundplatform/android/repository/SurveyRepository.kt b/app/src/main/java/org/groundplatform/android/repository/SurveyRepository.kt index 0bdcf8a938..7f85695167 100644 --- a/app/src/main/java/org/groundplatform/android/repository/SurveyRepository.kt +++ b/app/src/main/java/org/groundplatform/android/repository/SurveyRepository.kt @@ -32,9 +32,9 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.withTimeout import org.groundplatform.android.FirebaseCrashLogger -import org.groundplatform.android.data.local.LocalValueStore import org.groundplatform.android.di.coroutines.ApplicationScope import org.groundplatform.data.stores.LocalSurveyStore +import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.data.stores.RemoteDataStore import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.SurveyListItem diff --git a/app/src/main/java/org/groundplatform/android/repository/TermsOfServiceRepository.kt b/app/src/main/java/org/groundplatform/android/repository/TermsOfServiceRepository.kt index 51b1da48a3..8dd8dcad09 100644 --- a/app/src/main/java/org/groundplatform/android/repository/TermsOfServiceRepository.kt +++ b/app/src/main/java/org/groundplatform/android/repository/TermsOfServiceRepository.kt @@ -19,8 +19,8 @@ package org.groundplatform.android.repository import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.withTimeout -import org.groundplatform.android.data.local.LocalValueStore import org.groundplatform.android.data.remote.DataStoreException +import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.data.stores.RemoteDataStore import org.groundplatform.domain.model.TermsOfService import org.groundplatform.domain.repository.TermsOfServiceRepositoryInterface diff --git a/app/src/main/java/org/groundplatform/android/repository/UserRepository.kt b/app/src/main/java/org/groundplatform/android/repository/UserRepository.kt index 28bb3e45ef..a90e04a24a 100644 --- a/app/src/main/java/org/groundplatform/android/repository/UserRepository.kt +++ b/app/src/main/java/org/groundplatform/android/repository/UserRepository.kt @@ -18,10 +18,10 @@ package org.groundplatform.android.repository import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.flow.Flow -import org.groundplatform.android.data.local.LocalValueStore import org.groundplatform.android.data.local.room.LocalDatabase import org.groundplatform.android.system.auth.AuthenticationManager import org.groundplatform.data.stores.LocalUserStore +import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.data.stores.RemoteDataStore import org.groundplatform.domain.model.Role import org.groundplatform.domain.model.Survey diff --git a/app/src/main/java/org/groundplatform/android/system/deeplink/PlayInstallReferrerService.kt b/app/src/main/java/org/groundplatform/android/system/deeplink/PlayInstallReferrerService.kt index df6a2c18a0..963c72d327 100644 --- a/app/src/main/java/org/groundplatform/android/system/deeplink/PlayInstallReferrerService.kt +++ b/app/src/main/java/org/groundplatform/android/system/deeplink/PlayInstallReferrerService.kt @@ -25,7 +25,7 @@ import javax.inject.Singleton import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.withTimeoutOrNull -import org.groundplatform.android.data.local.LocalValueStore +import org.groundplatform.data.stores.LocalValueStore import timber.log.Timber @Singleton diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskViewModel.kt index 9538fc311a..bece9382e6 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskViewModel.kt @@ -21,7 +21,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import org.groundplatform.android.data.local.LocalValueStore import org.groundplatform.android.data.uuid.OfflineUuidGenerator import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.components.ButtonActionState @@ -30,6 +29,7 @@ import org.groundplatform.android.ui.datacollection.tasks.DataCollectionEvent import org.groundplatform.android.ui.datacollection.tasks.TaskPositionInterface import org.groundplatform.android.ui.map.Feature import org.groundplatform.android.ui.util.getDefaultColor +import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.domain.model.geometry.Point import org.groundplatform.domain.model.job.Job import org.groundplatform.domain.model.submission.DropPinTaskData diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModel.kt index d13a5248ce..37c4160fd8 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModel.kt @@ -31,7 +31,6 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.groundplatform.android.R -import org.groundplatform.android.data.local.LocalValueStore import org.groundplatform.android.data.uuid.OfflineUuidGenerator import org.groundplatform.android.ui.common.SharedViewModel import org.groundplatform.android.ui.datacollection.components.ButtonAction @@ -45,6 +44,7 @@ import org.groundplatform.android.ui.util.VibrationHelper import org.groundplatform.android.ui.util.getDefaultColor import org.groundplatform.android.util.distanceTo import org.groundplatform.android.util.penult +import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.domain.model.geometry.Coordinates import org.groundplatform.domain.model.geometry.LineString import org.groundplatform.domain.model.geometry.LinearRing diff --git a/app/src/test/java/org/groundplatform/android/data/local/LocalLocationOfInterestStoreTest.kt b/app/src/test/java/org/groundplatform/android/data/local/LocalLocationOfInterestStoreTest.kt index 16139e83bb..f1963f3082 100644 --- a/app/src/test/java/org/groundplatform/android/data/local/LocalLocationOfInterestStoreTest.kt +++ b/app/src/test/java/org/groundplatform/android/data/local/LocalLocationOfInterestStoreTest.kt @@ -39,6 +39,7 @@ import org.groundplatform.data.stores.LocalLocationOfInterestStore import org.groundplatform.data.stores.LocalSubmissionStore import org.groundplatform.data.stores.LocalSurveyStore import org.groundplatform.data.stores.LocalUserStore +import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.User import org.groundplatform.domain.model.geometry.Coordinates diff --git a/app/src/test/java/org/groundplatform/android/data/local/LocalValueStoreTest.kt b/app/src/test/java/org/groundplatform/android/data/local/LocalValueStoreTest.kt index 42c8795e99..8eccf9b35c 100644 --- a/app/src/test/java/org/groundplatform/android/data/local/LocalValueStoreTest.kt +++ b/app/src/test/java/org/groundplatform/android/data/local/LocalValueStoreTest.kt @@ -21,6 +21,7 @@ import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat import java.util.Locale import org.groundplatform.android.common.PrefKeys +import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.domain.model.geometry.Coordinates import org.groundplatform.domain.model.map.Bounds import org.groundplatform.domain.model.map.CameraPosition @@ -41,7 +42,7 @@ class LocalValueStoreTest { ApplicationProvider.getApplicationContext() .getSharedPreferences("test_prefs", android.content.Context.MODE_PRIVATE) sharedPreferences.edit { clear() } - localValueStore = LocalValueStore(sharedPreferences, Locale.getDefault()) + localValueStore = SharedPreferencesLocalValueStore(sharedPreferences, Locale.getDefault()) } @Test diff --git a/app/src/test/java/org/groundplatform/android/repository/MapStateRepositoryTest.kt b/app/src/test/java/org/groundplatform/android/repository/MapStateRepositoryTest.kt index 1241d999e4..e5b1bac1b4 100644 --- a/app/src/test/java/org/groundplatform/android/repository/MapStateRepositoryTest.kt +++ b/app/src/test/java/org/groundplatform/android/repository/MapStateRepositoryTest.kt @@ -21,7 +21,7 @@ import dagger.hilt.android.testing.HiltAndroidTest import javax.inject.Inject import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.FakeData -import org.groundplatform.android.data.local.LocalValueStore +import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.domain.model.map.CameraPosition import org.groundplatform.domain.model.map.MapType import org.groundplatform.domain.repository.MapStateRepositoryInterface diff --git a/app/src/test/java/org/groundplatform/android/repository/SubmissionRepositoryTest.kt b/app/src/test/java/org/groundplatform/android/repository/SubmissionRepositoryTest.kt index b3422b7636..706e6c700c 100644 --- a/app/src/test/java/org/groundplatform/android/repository/SubmissionRepositoryTest.kt +++ b/app/src/test/java/org/groundplatform/android/repository/SubmissionRepositoryTest.kt @@ -17,10 +17,10 @@ package org.groundplatform.android.repository import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest -import org.groundplatform.android.data.local.LocalValueStore import org.groundplatform.android.data.sync.MutationSyncWorkManager import org.groundplatform.android.data.uuid.OfflineUuidGenerator import org.groundplatform.data.stores.LocalSubmissionStore +import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.domain.model.User import org.groundplatform.domain.model.locationofinterest.AuditInfo import org.groundplatform.domain.model.locationofinterest.LocationOfInterest diff --git a/app/src/test/java/org/groundplatform/android/repository/UserRepositoryTest.kt b/app/src/test/java/org/groundplatform/android/repository/UserRepositoryTest.kt index e92df290f3..47d262ecc1 100644 --- a/app/src/test/java/org/groundplatform/android/repository/UserRepositoryTest.kt +++ b/app/src/test/java/org/groundplatform/android/repository/UserRepositoryTest.kt @@ -24,12 +24,12 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.advanceUntilIdle import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.FakeData -import org.groundplatform.android.data.local.LocalValueStore import org.groundplatform.android.data.remote.FakeRemoteDataStore import org.groundplatform.android.system.NetworkManager import org.groundplatform.android.system.auth.FakeAuthenticationManager import org.groundplatform.data.stores.LocalSurveyStore import org.groundplatform.data.stores.LocalUserStore +import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.domain.model.Role import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.auth.SignInState diff --git a/app/src/test/java/org/groundplatform/android/system/deeplink/PlayInstallReferrerServiceTest.kt b/app/src/test/java/org/groundplatform/android/system/deeplink/PlayInstallReferrerServiceTest.kt index e1e62465b1..ae6d2c9132 100644 --- a/app/src/test/java/org/groundplatform/android/system/deeplink/PlayInstallReferrerServiceTest.kt +++ b/app/src/test/java/org/groundplatform/android/system/deeplink/PlayInstallReferrerServiceTest.kt @@ -25,7 +25,7 @@ import com.android.installreferrer.api.ReferrerDetails import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest -import org.groundplatform.android.data.local.LocalValueStore +import org.groundplatform.data.stores.LocalValueStore import org.junit.After import org.junit.Before import org.junit.Test diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt index e80744e8e9..ebd8d5f55d 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/point/DropPinTaskScreenTest.kt @@ -26,12 +26,12 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.FakeData.JOB import org.groundplatform.android.R -import org.groundplatform.android.data.local.LocalValueStore import org.groundplatform.android.getString import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.ButtonActionStateChecker import org.groundplatform.android.ui.datacollection.tasks.TaskPositionInterface +import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.domain.model.geometry.Coordinates import org.groundplatform.domain.model.geometry.Point import org.groundplatform.domain.model.map.CameraPosition diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreenTest.kt index 7f6f9f8aae..49dc48659c 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskScreenTest.kt @@ -27,12 +27,12 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.FakeData.JOB import org.groundplatform.android.R -import org.groundplatform.android.data.local.LocalValueStore import org.groundplatform.android.getString import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.components.ButtonActionState import org.groundplatform.android.ui.datacollection.tasks.ButtonActionStateChecker import org.groundplatform.android.ui.datacollection.tasks.TaskPositionInterface +import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.domain.model.geometry.Coordinates import org.groundplatform.domain.model.geometry.LineString import org.groundplatform.domain.model.geometry.LinearRing diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModelTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModelTest.kt index 39848e50b2..5fa3ac7c19 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModelTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModelTest.kt @@ -37,12 +37,12 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.advanceUntilIdle import org.groundplatform.android.BaseHiltTest -import org.groundplatform.android.data.local.LocalValueStore import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.tasks.TaskPositionInterface import org.groundplatform.android.ui.datacollection.tasks.polygon.PolygonDrawingSession.Companion.DISTANCE_THRESHOLD_DP import org.groundplatform.android.ui.map.Feature import org.groundplatform.android.ui.map.gms.GmsExt.getShellCoordinates +import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.domain.model.geometry.Coordinates import org.groundplatform.domain.model.geometry.LineString import org.groundplatform.domain.model.geometry.LinearRing diff --git a/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalValueStore.kt b/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalValueStore.kt new file mode 100644 index 0000000000..23d16d2dd1 --- /dev/null +++ b/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalValueStore.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2026 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 org.groundplatform.data.stores + +import org.groundplatform.domain.model.map.CameraPosition + +/** Interface for storing key-value pairs locally on device. */ +interface LocalValueStore { + /** + * Id of the last survey successfully activated by the user. This value is only updated after the + * survey activation process is complete. + */ + var lastActiveSurveyId: String + + /** The last map type selected. */ + var mapType: Int + + /** Whether location lock is enabled or not. */ + var isLocationLockEnabled: Boolean + + /** Terms of service acceptance state for the currently signed in user. */ + var isTermsOfServiceAccepted: Boolean + + /** Whether to overlay offline map imagery. */ + var isOfflineImageryEnabled: Boolean + + /** Whether to display instructions when loading a draw area task. */ + var drawAreaInstructionsShown: Boolean + + /** Whether to display instructions when loading a drop pin task. */ + var dropPinInstructionsShown: Boolean + + /** Id of the active draft submission, or null if none. */ + var draftSubmissionId: String? + + /** The currently selected UI language tag. */ + var selectedLanguage: String + + /** The preferred measurement unit system (e.g. Metric or Imperial). */ + var selectedLengthUnit: String + + /** Whether media uploads should only occur over unmetered (e.g. Wi-Fi) connections. */ + var shouldUploadMediaOverUnmeteredConnectionOnly: Boolean + + /** Whether a deferred deep link has already been handled. */ + var isDeferredDeeplinkConsumed: Boolean + + /** Removes all values stored in the local store. */ + fun clear() + + /** Clears the last saved camera position for the given [surveyId]. */ + fun clearLastCameraPosition(surveyId: String) + + /** Saves the last [cameraPosition] viewed for the given [surveyId]. */ + fun setLastCameraPosition(surveyId: String, cameraPosition: CameraPosition) + + /** Returns the last saved camera position for the given [surveyId], or null if none was saved. */ + fun getLastCameraPosition(surveyId: String): CameraPosition? + + /** Sets the user's data sharing consent for the given [surveyId]. */ + fun setDataSharingConsent(surveyId: String, consent: Boolean) + + /** Returns whether data sharing consent was granted for the given [surveyId]. */ + fun getDataSharingConsent(surveyId: String): Boolean +} From 0a2c89bdbe9ce0360e76416e425929e3bd8cdaee Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Thu, 27 Aug 2026 23:12:42 +0530 Subject: [PATCH 05/10] refactor: migrate DataStoreException to core module and move TermsOfServiceRepository to common data layer --- .../converter/SubmissionDeltasConverter.kt | 2 +- .../room/converter/ValueJsonConverter.kt | 30 ++-- .../android/data/remote/DataStoreException.kt | 43 ------ .../remote/firebase/schema/JobConverter.kt | 2 +- .../remote/firebase/schema/LoiConverter.kt | 2 +- .../firebase/schema/LoiGeometryConverter.kt | 2 +- .../remote/firebase/schema/SurveyConverter.kt | 2 +- .../android/di/RepositoryModule.kt | 19 ++- .../schema/LoiGeometryConverterTest.kt | 2 +- .../TermsOfServiceRepositoryTest.kt | 2 +- .../groundplatform/data/DataStoreException.kt | 24 +++ .../repository/TermsOfServiceRepository.kt | 15 +- .../TermsOfServiceRepositoryTest.kt | 139 ++++++++++++++++++ 13 files changed, 208 insertions(+), 76 deletions(-) delete mode 100644 app/src/main/java/org/groundplatform/android/data/remote/DataStoreException.kt create mode 100644 core/data/src/commonMain/kotlin/org/groundplatform/data/DataStoreException.kt rename {app/src/main/java/org/groundplatform/android => core/data/src/commonMain/kotlin/org/groundplatform/data}/repository/TermsOfServiceRepository.kt (85%) create mode 100644 core/data/src/commonTest/kotlin/org/groundplatform/data/repository/TermsOfServiceRepositoryTest.kt diff --git a/app/src/main/java/org/groundplatform/android/data/local/room/converter/SubmissionDeltasConverter.kt b/app/src/main/java/org/groundplatform/android/data/local/room/converter/SubmissionDeltasConverter.kt index c8d54a7f15..76f41b41c7 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/room/converter/SubmissionDeltasConverter.kt +++ b/app/src/main/java/org/groundplatform/android/data/local/room/converter/SubmissionDeltasConverter.kt @@ -18,8 +18,8 @@ package org.groundplatform.android.data.local.room.converter import kotlinx.collections.immutable.toPersistentList import org.groundplatform.android.data.local.LocalDataConsistencyException -import org.groundplatform.android.data.remote.DataStoreException import org.groundplatform.android.util.Enums.toEnum +import org.groundplatform.data.DataStoreException import org.groundplatform.domain.model.job.Job import org.groundplatform.domain.model.submission.ValueDelta import org.groundplatform.domain.model.task.Task diff --git a/app/src/main/java/org/groundplatform/android/data/local/room/converter/ValueJsonConverter.kt b/app/src/main/java/org/groundplatform/android/data/local/room/converter/ValueJsonConverter.kt index 77d98cb22f..4c28b8e920 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/room/converter/ValueJsonConverter.kt +++ b/app/src/main/java/org/groundplatform/android/data/local/room/converter/ValueJsonConverter.kt @@ -17,9 +17,9 @@ package org.groundplatform.android.data.local.room.converter import kotlinx.collections.immutable.toPersistentList -import org.groundplatform.android.data.remote.DataStoreException import org.groundplatform.android.data.remote.firebase.schema.CaptureLocationResultConverter.toCaptureLocationTaskData import org.groundplatform.android.data.remote.firebase.schema.CaptureLocationResultConverter.toJSONObject +import org.groundplatform.data.DataStoreException import org.groundplatform.domain.model.geometry.LineString import org.groundplatform.domain.model.geometry.Point import org.groundplatform.domain.model.geometry.Polygon @@ -75,28 +75,28 @@ internal object ValueJsonConverter { return when (task.type) { Task.Type.TEXT -> { - DataStoreException.checkType(String::class.java, obj) + checkType(String::class.java, obj) TextTaskData.fromString(obj as String) } Task.Type.PHOTO -> { - DataStoreException.checkType(String::class.java, obj) + checkType(String::class.java, obj) PhotoTaskData(obj as String) } Task.Type.MULTIPLE_CHOICE -> { - DataStoreException.checkType(JSONArray::class.java, obj) + checkType(JSONArray::class.java, obj) MultipleChoiceTaskData.fromList(task.multipleChoice, toList(obj as JSONArray)) } Task.Type.NUMBER -> { - DataStoreException.checkType(Number::class.java, obj) + checkType(Number::class.java, obj) NumberTaskData.fromNumber(obj.toString()) } Task.Type.DATE, Task.Type.TIME -> { - DataStoreException.checkType(Long::class.java, obj) + checkType(Long::class.java, obj) DateTimeTaskData.fromMillis(obj as Long) } Task.Type.DRAW_AREA -> { - DataStoreException.checkType(String::class.java, obj) + checkType(String::class.java, obj) val geometry = GeometryWrapperTypeConverter.fromString(obj as String)?.getGeometry() DataStoreException.checkNotNull(geometry, "Missing geometry in draw area task result") when (geometry) { @@ -109,14 +109,14 @@ internal object ValueJsonConverter { } } Task.Type.DROP_PIN -> { - DataStoreException.checkType(String::class.java, obj) + checkType(String::class.java, obj) val geometry = GeometryWrapperTypeConverter.fromString(obj as String)?.getGeometry() DataStoreException.checkNotNull(geometry, "Missing geometry in drop pin task result") - DataStoreException.checkType(Point::class.java, geometry!!) + checkType(Point::class.java, geometry!!) DropPinTaskData(geometry as Point) } Task.Type.CAPTURE_LOCATION -> { - DataStoreException.checkType(JSONObject::class.java, obj) + checkType(JSONObject::class.java, obj) (obj as JSONObject).toCaptureLocationTaskData() } Task.Type.INSTRUCTIONS -> { @@ -128,6 +128,16 @@ internal object ValueJsonConverter { } } + private fun checkType(expectedType: Class<*>, obj: T): T { + if (obj.javaClass == java.lang.Long::class.java && expectedType == Long::class.java) { + return obj + } + if (!expectedType.isAssignableFrom(obj.javaClass)) { + throw DataStoreException("Expected ${expectedType.name}, got ${obj.javaClass.name}") + } + return obj + } + private fun toList(jsonArray: JSONArray): List { val list: MutableList = ArrayList(jsonArray.length()) for (i in 0 until jsonArray.length()) { diff --git a/app/src/main/java/org/groundplatform/android/data/remote/DataStoreException.kt b/app/src/main/java/org/groundplatform/android/data/remote/DataStoreException.kt deleted file mode 100644 index 4a4c2c624c..0000000000 --- a/app/src/main/java/org/groundplatform/android/data/remote/DataStoreException.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2020 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 org.groundplatform.android.data.remote - -open class DataStoreException(message: String?) : RuntimeException(message) { - companion object { - @JvmStatic - @Throws(DataStoreException::class) - fun checkNotNull(reference: T?, field: String): T = - reference ?: throw DataStoreException("Missing $field") - - /** - * Checks if the provided object is of the same type as (or a subtype of) the specified type. If - * not, a `DataStoreException` is thrown with relevant details. - */ - @JvmStatic - @Throws(DataStoreException::class) - fun checkType(expectedType: Class<*>, obj: T): T { - // TODO: Handle Kotlin Long (java.lang.Long) vs Java primitive long (long) - // Issue URL: https://github.com/google/ground-android/issues/2743 - if (obj.javaClass == java.lang.Long::class.java && expectedType == Long::class.java) { - return obj - } - if (!expectedType.isAssignableFrom(obj.javaClass)) { - throw DataStoreException("Expected ${expectedType.name}, got ${obj.javaClass.name}") - } - return obj - } - } -} diff --git a/app/src/main/java/org/groundplatform/android/data/remote/firebase/schema/JobConverter.kt b/app/src/main/java/org/groundplatform/android/data/remote/firebase/schema/JobConverter.kt index ef538f7731..924a56c8a1 100644 --- a/app/src/main/java/org/groundplatform/android/data/remote/firebase/schema/JobConverter.kt +++ b/app/src/main/java/org/groundplatform/android/data/remote/firebase/schema/JobConverter.kt @@ -18,9 +18,9 @@ package org.groundplatform.android.data.remote.firebase.schema import com.google.firebase.firestore.DocumentSnapshot import kotlinx.collections.immutable.toPersistentMap -import org.groundplatform.android.data.remote.DataStoreException import org.groundplatform.android.data.remote.firebase.protobuf.parseFrom import org.groundplatform.android.proto.Job as JobProto +import org.groundplatform.data.DataStoreException import org.groundplatform.domain.model.job.Job import org.groundplatform.domain.model.job.Style as StyleModel diff --git a/app/src/main/java/org/groundplatform/android/data/remote/firebase/schema/LoiConverter.kt b/app/src/main/java/org/groundplatform/android/data/remote/firebase/schema/LoiConverter.kt index f42c2131d9..5b02111326 100644 --- a/app/src/main/java/org/groundplatform/android/data/remote/firebase/schema/LoiConverter.kt +++ b/app/src/main/java/org/groundplatform/android/data/remote/firebase/schema/LoiConverter.kt @@ -16,10 +16,10 @@ package org.groundplatform.android.data.remote.firebase.schema import com.google.firebase.firestore.DocumentSnapshot -import org.groundplatform.android.data.remote.DataStoreException import org.groundplatform.android.data.remote.firebase.protobuf.parseFrom import org.groundplatform.android.proto.LocationOfInterest as LocationOfInterestProto import org.groundplatform.android.proto.LocationOfInterest.Source +import org.groundplatform.data.DataStoreException import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.locationofinterest.LOI_ID_PROPERTY import org.groundplatform.domain.model.locationofinterest.LOI_NAME_PROPERTY diff --git a/app/src/main/java/org/groundplatform/android/data/remote/firebase/schema/LoiGeometryConverter.kt b/app/src/main/java/org/groundplatform/android/data/remote/firebase/schema/LoiGeometryConverter.kt index 16269651be..4c8bc81dc9 100644 --- a/app/src/main/java/org/groundplatform/android/data/remote/firebase/schema/LoiGeometryConverter.kt +++ b/app/src/main/java/org/groundplatform/android/data/remote/firebase/schema/LoiGeometryConverter.kt @@ -15,13 +15,13 @@ */ package org.groundplatform.android.data.remote.firebase.schema -import org.groundplatform.android.data.remote.DataStoreException import org.groundplatform.android.proto.Coordinates as CoordinatesProto import org.groundplatform.android.proto.Geometry as GeometryProto import org.groundplatform.android.proto.LinearRing as LinearRingProto import org.groundplatform.android.proto.MultiPolygon as MultiPolygonProto import org.groundplatform.android.proto.Point as PointProto import org.groundplatform.android.proto.Polygon as PolygonProto +import org.groundplatform.data.DataStoreException import org.groundplatform.domain.model.geometry.Coordinates import org.groundplatform.domain.model.geometry.Geometry import org.groundplatform.domain.model.geometry.LinearRing diff --git a/app/src/main/java/org/groundplatform/android/data/remote/firebase/schema/SurveyConverter.kt b/app/src/main/java/org/groundplatform/android/data/remote/firebase/schema/SurveyConverter.kt index 9af85b414c..996ea90cd9 100644 --- a/app/src/main/java/org/groundplatform/android/data/remote/firebase/schema/SurveyConverter.kt +++ b/app/src/main/java/org/groundplatform/android/data/remote/firebase/schema/SurveyConverter.kt @@ -18,11 +18,11 @@ package org.groundplatform.android.data.remote.firebase.schema import com.google.firebase.firestore.DocumentSnapshot import kotlinx.collections.immutable.toPersistentMap -import org.groundplatform.android.data.remote.DataStoreException import org.groundplatform.android.data.remote.firebase.protobuf.parseFrom import org.groundplatform.android.data.remote.firebase.protobuf.toModel import org.groundplatform.android.proto.Survey as SurveyProto import org.groundplatform.android.proto.Survey.DataVisibility +import org.groundplatform.data.DataStoreException import org.groundplatform.domain.model.Survey as SurveyModel import org.groundplatform.domain.model.job.Job diff --git a/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt b/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt index 59225f5fd1..03ef2f52be 100644 --- a/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt +++ b/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt @@ -17,6 +17,7 @@ package org.groundplatform.android.di import dagger.Binds import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import jakarta.inject.Singleton @@ -26,9 +27,11 @@ import org.groundplatform.android.repository.MutationRepository import org.groundplatform.android.repository.OfflineAreaRepository import org.groundplatform.android.repository.SubmissionRepository import org.groundplatform.android.repository.SurveyRepository -import org.groundplatform.android.repository.TermsOfServiceRepository import org.groundplatform.android.repository.UserMediaRepository import org.groundplatform.android.repository.UserRepository +import org.groundplatform.data.repository.TermsOfServiceRepository +import org.groundplatform.data.stores.LocalValueStore +import org.groundplatform.data.stores.RemoteDataStore import org.groundplatform.domain.repository.LocationOfInterestRepositoryInterface import org.groundplatform.domain.repository.MapStateRepositoryInterface import org.groundplatform.domain.repository.MutationRepositoryInterface @@ -38,6 +41,7 @@ import org.groundplatform.domain.repository.SurveyRepositoryInterface import org.groundplatform.domain.repository.TermsOfServiceRepositoryInterface import org.groundplatform.domain.repository.UserMediaRepositoryInterface import org.groundplatform.domain.repository.UserRepositoryInterface +import org.groundplatform.domain.system.NetworkManagerInterface @Module @InstallIn(SingletonComponent::class) @@ -89,12 +93,15 @@ abstract class MutationRepositoryModule { @Module @InstallIn(SingletonComponent::class) -abstract class TermsOfServiceRepositoryModule { - @Binds +object TermsOfServiceRepositoryModule { + @Provides @Singleton - abstract fun bindTermsOfServiceRepository( - impl: TermsOfServiceRepository - ): TermsOfServiceRepositoryInterface + fun provideTermsOfServiceRepository( + networkManager: NetworkManagerInterface, + remoteDataStore: RemoteDataStore, + localValueStore: LocalValueStore, + ): TermsOfServiceRepositoryInterface = + TermsOfServiceRepository(networkManager, remoteDataStore, localValueStore) } @Module diff --git a/app/src/test/java/org/groundplatform/android/data/remote/firebase/schema/LoiGeometryConverterTest.kt b/app/src/test/java/org/groundplatform/android/data/remote/firebase/schema/LoiGeometryConverterTest.kt index e13e271fc8..140c985019 100644 --- a/app/src/test/java/org/groundplatform/android/data/remote/firebase/schema/LoiGeometryConverterTest.kt +++ b/app/src/test/java/org/groundplatform/android/data/remote/firebase/schema/LoiGeometryConverterTest.kt @@ -17,13 +17,13 @@ package org.groundplatform.android.data.remote.firebase.schema import com.google.common.truth.Truth.assertThat import kotlin.test.assertFailsWith -import org.groundplatform.android.data.remote.DataStoreException import org.groundplatform.android.proto.Coordinates as CoordinatesProto import org.groundplatform.android.proto.Geometry as GeometryProto import org.groundplatform.android.proto.LinearRing as LinearRingProto import org.groundplatform.android.proto.MultiPolygon as MultiPolygonProto import org.groundplatform.android.proto.Point as PointProto import org.groundplatform.android.proto.Polygon as PolygonProto +import org.groundplatform.data.DataStoreException import org.groundplatform.domain.model.geometry.Coordinates import org.groundplatform.domain.model.geometry.LinearRing import org.groundplatform.domain.model.geometry.MultiPolygon diff --git a/app/src/test/java/org/groundplatform/android/repository/TermsOfServiceRepositoryTest.kt b/app/src/test/java/org/groundplatform/android/repository/TermsOfServiceRepositoryTest.kt index e537ba98f7..03af1bfd37 100644 --- a/app/src/test/java/org/groundplatform/android/repository/TermsOfServiceRepositoryTest.kt +++ b/app/src/test/java/org/groundplatform/android/repository/TermsOfServiceRepositoryTest.kt @@ -23,9 +23,9 @@ import javax.inject.Inject import kotlinx.coroutines.runBlocking import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.FakeData -import org.groundplatform.android.data.remote.DataStoreException import org.groundplatform.android.data.remote.FakeRemoteDataStore import org.groundplatform.android.system.NetworkManager +import org.groundplatform.data.DataStoreException import org.groundplatform.domain.repository.TermsOfServiceRepositoryInterface import org.junit.Assert.assertThrows import org.junit.Test diff --git a/core/data/src/commonMain/kotlin/org/groundplatform/data/DataStoreException.kt b/core/data/src/commonMain/kotlin/org/groundplatform/data/DataStoreException.kt new file mode 100644 index 0000000000..a278608f7e --- /dev/null +++ b/core/data/src/commonMain/kotlin/org/groundplatform/data/DataStoreException.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2026 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 org.groundplatform.data + +open class DataStoreException(message: String? = null, cause: Throwable? = null) : + RuntimeException(message, cause) { + companion object { + fun checkNotNull(reference: T?, field: String): T = + reference ?: throw DataStoreException("Missing $field") + } +} diff --git a/app/src/main/java/org/groundplatform/android/repository/TermsOfServiceRepository.kt b/core/data/src/commonMain/kotlin/org/groundplatform/data/repository/TermsOfServiceRepository.kt similarity index 85% rename from app/src/main/java/org/groundplatform/android/repository/TermsOfServiceRepository.kt rename to core/data/src/commonMain/kotlin/org/groundplatform/data/repository/TermsOfServiceRepository.kt index 8dd8dcad09..46f518ee2c 100644 --- a/app/src/main/java/org/groundplatform/android/repository/TermsOfServiceRepository.kt +++ b/core/data/src/commonMain/kotlin/org/groundplatform/data/repository/TermsOfServiceRepository.kt @@ -14,25 +14,20 @@ * limitations under the License. */ -package org.groundplatform.android.repository +package org.groundplatform.data.repository -import javax.inject.Inject -import javax.inject.Singleton +import co.touchlab.kermit.Logger import kotlinx.coroutines.withTimeout -import org.groundplatform.android.data.remote.DataStoreException +import org.groundplatform.data.DataStoreException import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.data.stores.RemoteDataStore import org.groundplatform.domain.model.TermsOfService import org.groundplatform.domain.repository.TermsOfServiceRepositoryInterface import org.groundplatform.domain.system.NetworkManagerInterface -import timber.log.Timber private const val LOAD_REMOTE_SURVEY_TERMS_OF_SERVICE_TIMEOUT_MILLIS: Long = 30 * 1000 -@Singleton -class TermsOfServiceRepository -@Inject -constructor( +class TermsOfServiceRepository( private val networkManager: NetworkManagerInterface, private val remoteDataStore: RemoteDataStore, private val localValueStore: LocalValueStore, @@ -50,7 +45,7 @@ constructor( } return withTimeout(LOAD_REMOTE_SURVEY_TERMS_OF_SERVICE_TIMEOUT_MILLIS) { - Timber.d("Loading Terms of Service") + Logger.d("Loading Terms of Service") remoteDataStore.loadTermsOfService() } } diff --git a/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/TermsOfServiceRepositoryTest.kt b/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/TermsOfServiceRepositoryTest.kt new file mode 100644 index 0000000000..c15eb55528 --- /dev/null +++ b/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/TermsOfServiceRepositoryTest.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2026 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 org.groundplatform.data.repository + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.test.runTest +import org.groundplatform.data.DataStoreException +import org.groundplatform.data.stores.LocalValueStore +import org.groundplatform.data.stores.RemoteDataStore +import org.groundplatform.domain.model.Survey +import org.groundplatform.domain.model.SurveyListItem +import org.groundplatform.domain.model.TermsOfService +import org.groundplatform.domain.model.User +import org.groundplatform.domain.model.locationofinterest.LocationOfInterest +import org.groundplatform.domain.model.map.CameraPosition +import org.groundplatform.domain.model.mutation.Mutation +import org.groundplatform.domain.system.NetworkStatus +import org.groundplatform.testing.FakeNetworkManager + +class TermsOfServiceRepositoryTest { + private val fakeNetworkManager = FakeNetworkManager(NetworkStatus.AVAILABLE) + private val fakeRemoteDataStore = TestRemoteDataStore() + private val fakeLocalValueStore = TestLocalValueStore() + private val repository = + TermsOfServiceRepository(fakeNetworkManager, fakeRemoteDataStore, fakeLocalValueStore) + + private val testTermsOfService = TermsOfService("tos_1", "Test Terms of Service") + + @Test + fun getTermsOfService_success() = runTest { + fakeRemoteDataStore.termsOfServiceResult = Result.success(testTermsOfService) + + assertEquals(testTermsOfService, repository.getTermsOfService()) + } + + @Test + fun getTermsOfService_whenMissing_returnsNull() = runTest { + fakeRemoteDataStore.termsOfServiceResult = Result.success(null) + + assertNull(repository.getTermsOfService()) + } + + @Test + fun getTermsOfService_whenOffline_throwsDataStoreException() = runTest { + fakeNetworkManager.networkStatusStateFlow.value = NetworkStatus.UNAVAILABLE + + assertFailsWith { repository.getTermsOfService() } + } + + @Test + fun getTermsOfService_whenRequestFails_throwsError() = runTest { + fakeRemoteDataStore.termsOfServiceResult = Result.failure(RuntimeException("Network error")) + + assertFailsWith { repository.getTermsOfService() } + } + + @Test + fun termsOfServiceAccepted_roundTrip() { + repository.isTermsOfServiceAccepted = true + assertTrue(repository.isTermsOfServiceAccepted) + + repository.isTermsOfServiceAccepted = false + assertFalse(repository.isTermsOfServiceAccepted) + } + + private class TestRemoteDataStore : RemoteDataStore { + var termsOfServiceResult: Result = Result.success(null) + + override suspend fun loadTermsOfService(): TermsOfService? = termsOfServiceResult.getOrThrow() + + override fun getRestrictedSurveyList(user: User): Flow> = emptyFlow() + + override fun getPublicSurveyList(): Flow> = emptyFlow() + + override suspend fun loadSurvey(surveyId: String): Survey? = null + + override fun loadPredefinedLois(survey: Survey): Flow> = emptyFlow() + + override fun loadUserLois(survey: Survey, ownerUserId: String): Flow> = + emptyFlow() + + override fun loadSharedLois(survey: Survey): Flow> = emptyFlow() + + override suspend fun applyMutations(mutations: List, user: User) = Unit + + override suspend fun subscribeToSurveyUpdates(surveyId: String) = Unit + + override suspend fun unsubscribeFromSurveyUpdates(surveyId: String) = Unit + + override suspend fun refreshUserProfile() = Unit + } + + private class TestLocalValueStore : LocalValueStore { + override var lastActiveSurveyId: String = "" + override var mapType: Int = 0 + override var isLocationLockEnabled: Boolean = false + override var isTermsOfServiceAccepted: Boolean = false + override var isOfflineImageryEnabled: Boolean = true + override var drawAreaInstructionsShown: Boolean = false + override var dropPinInstructionsShown: Boolean = false + override var draftSubmissionId: String? = null + override var selectedLanguage: String = "en" + override var selectedLengthUnit: String = "m" + override var shouldUploadMediaOverUnmeteredConnectionOnly: Boolean = false + override var isDeferredDeeplinkConsumed: Boolean = false + + override fun clear() = Unit + + override fun clearLastCameraPosition(surveyId: String) = Unit + + override fun setLastCameraPosition(surveyId: String, cameraPosition: CameraPosition) = Unit + + override fun getLastCameraPosition(surveyId: String): CameraPosition? = null + + override fun setDataSharingConsent(surveyId: String, consent: Boolean) = Unit + + override fun getDataSharingConsent(surveyId: String): Boolean = false + } +} From f347c111ab2f1190c2023a2993bea455c157ae8b Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Thu, 27 Aug 2026 23:17:35 +0530 Subject: [PATCH 06/10] feat: migrate MapStateRepository to core-data and add comprehensive unit tests --- .../android/di/RepositoryModule.kt | 9 +- core/data/build.gradle.kts | 1 + .../data}/repository/MapStateRepository.kt | 7 +- .../data/FakeLocalValueStore.kt | 72 ++++++++++++++ .../data/repository/MapStateRepositoryTest.kt | 93 +++++++++++++++++++ .../TermsOfServiceRepositoryTest.kt | 32 +------ 6 files changed, 175 insertions(+), 39 deletions(-) rename {app/src/main/java/org/groundplatform/android => core/data/src/commonMain/kotlin/org/groundplatform/data}/repository/MapStateRepository.kt (91%) create mode 100644 core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalValueStore.kt create mode 100644 core/data/src/commonTest/kotlin/org/groundplatform/data/repository/MapStateRepositoryTest.kt diff --git a/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt b/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt index 03ef2f52be..c94c1b8ed1 100644 --- a/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt +++ b/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt @@ -22,13 +22,13 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import jakarta.inject.Singleton import org.groundplatform.android.repository.LocationOfInterestRepository -import org.groundplatform.android.repository.MapStateRepository import org.groundplatform.android.repository.MutationRepository import org.groundplatform.android.repository.OfflineAreaRepository import org.groundplatform.android.repository.SubmissionRepository import org.groundplatform.android.repository.SurveyRepository import org.groundplatform.android.repository.UserMediaRepository import org.groundplatform.android.repository.UserRepository +import org.groundplatform.data.repository.MapStateRepository import org.groundplatform.data.repository.TermsOfServiceRepository import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.data.stores.RemoteDataStore @@ -77,10 +77,11 @@ abstract class SubmissionRepositoryModule { @Module @InstallIn(SingletonComponent::class) -abstract class MapStateRepositoryModule { - @Binds +object MapStateRepositoryModule { + @Provides @Singleton - abstract fun bindMapStateRepository(impl: MapStateRepository): MapStateRepositoryInterface + fun provideMapStateRepository(localValueStore: LocalValueStore): MapStateRepositoryInterface = + MapStateRepository(localValueStore) } @Module diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index 854260c5fc..4259e70522 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -45,6 +45,7 @@ kotlin { implementation(project(":core:testing")) implementation(libs.kotlin.test) implementation(libs.kotlinx.coroutines.test) + implementation(libs.turbine) } } } diff --git a/app/src/main/java/org/groundplatform/android/repository/MapStateRepository.kt b/core/data/src/commonMain/kotlin/org/groundplatform/data/repository/MapStateRepository.kt similarity index 91% rename from app/src/main/java/org/groundplatform/android/repository/MapStateRepository.kt rename to core/data/src/commonMain/kotlin/org/groundplatform/data/repository/MapStateRepository.kt index 8f43406ad2..e297fe69b6 100644 --- a/app/src/main/java/org/groundplatform/android/repository/MapStateRepository.kt +++ b/core/data/src/commonMain/kotlin/org/groundplatform/data/repository/MapStateRepository.kt @@ -13,10 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.android.repository +package org.groundplatform.data.repository -import javax.inject.Inject -import javax.inject.Singleton import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -26,8 +24,7 @@ import org.groundplatform.domain.model.map.CameraPosition import org.groundplatform.domain.model.map.MapType import org.groundplatform.domain.repository.MapStateRepositoryInterface -@Singleton -class MapStateRepository @Inject constructor(private val localValueStore: LocalValueStore) : +class MapStateRepository(private val localValueStore: LocalValueStore) : MapStateRepositoryInterface { private val _mapType = MutableStateFlow(mapType) diff --git a/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalValueStore.kt b/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalValueStore.kt new file mode 100644 index 0000000000..5483f0b116 --- /dev/null +++ b/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalValueStore.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2026 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 org.groundplatform.data + +import org.groundplatform.data.stores.LocalValueStore +import org.groundplatform.domain.model.map.CameraPosition +import org.groundplatform.domain.util.Constants.DEFAULT_MAP_TYPE + +class FakeLocalValueStore : LocalValueStore { + override var lastActiveSurveyId: String = "" + override var mapType: Int = DEFAULT_MAP_TYPE.ordinal + override var isLocationLockEnabled: Boolean = false + override var isTermsOfServiceAccepted: Boolean = false + override var isOfflineImageryEnabled: Boolean = true + override var drawAreaInstructionsShown: Boolean = false + override var dropPinInstructionsShown: Boolean = false + override var draftSubmissionId: String? = null + override var selectedLanguage: String = "en" + override var selectedLengthUnit: String = "m" + override var shouldUploadMediaOverUnmeteredConnectionOnly: Boolean = false + override var isDeferredDeeplinkConsumed: Boolean = false + + private val cameraPositions = mutableMapOf() + private val dataSharingConsents = mutableMapOf() + + override fun clear() { + lastActiveSurveyId = "" + mapType = DEFAULT_MAP_TYPE.ordinal + isLocationLockEnabled = false + isTermsOfServiceAccepted = false + isOfflineImageryEnabled = true + drawAreaInstructionsShown = false + dropPinInstructionsShown = false + draftSubmissionId = null + selectedLanguage = "en" + selectedLengthUnit = "m" + shouldUploadMediaOverUnmeteredConnectionOnly = false + isDeferredDeeplinkConsumed = false + cameraPositions.clear() + dataSharingConsents.clear() + } + + override fun clearLastCameraPosition(surveyId: String) { + cameraPositions.remove(surveyId) + } + + override fun setLastCameraPosition(surveyId: String, cameraPosition: CameraPosition) { + cameraPositions[surveyId] = cameraPosition + } + + override fun getLastCameraPosition(surveyId: String): CameraPosition? = cameraPositions[surveyId] + + override fun setDataSharingConsent(surveyId: String, consent: Boolean) { + dataSharingConsents[surveyId] = consent + } + + override fun getDataSharingConsent(surveyId: String): Boolean = + dataSharingConsents[surveyId] ?: false +} diff --git a/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/MapStateRepositoryTest.kt b/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/MapStateRepositoryTest.kt new file mode 100644 index 0000000000..ad35c01799 --- /dev/null +++ b/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/MapStateRepositoryTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2026 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 org.groundplatform.data.repository + +import app.cash.turbine.test +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlinx.coroutines.test.runTest +import org.groundplatform.data.FakeLocalValueStore +import org.groundplatform.domain.model.geometry.Coordinates +import org.groundplatform.domain.model.map.CameraPosition +import org.groundplatform.domain.model.map.MapType + +class MapStateRepositoryTest { + private val fakeLocalValueStore = FakeLocalValueStore() + private val mapStateRepository = MapStateRepository(fakeLocalValueStore) + + private val surveyId = "survey_1" + private val coordinates = Coordinates(10.0, 20.0) + + @Test + fun getMapType_defaultIsTerrain() { + assertEquals(MapType.TERRAIN, mapStateRepository.mapType) + } + + @Test + fun getMapType_whenSet_returnsUpdatedValue() { + mapStateRepository.mapType = MapType.SATELLITE + assertEquals(MapType.SATELLITE, mapStateRepository.mapType) + } + + @Test + fun mapTypeFlow_emitsUpdatedValue() = runTest { + mapStateRepository.mapType = MapType.SATELLITE + + mapStateRepository.mapTypeFlow.test { assertEquals(MapType.SATELLITE, expectMostRecentItem()) } + } + + @Test + fun isOfflineImageryEnabled_defaultIsTrue() { + assertTrue(mapStateRepository.isOfflineImageryEnabled) + } + + @Test + fun isOfflineImageryEnabled_whenSet_updatesFlow() = runTest { + mapStateRepository.isOfflineImageryEnabled = false + + mapStateRepository.offlineImageryEnabledFlow.test { assertFalse(expectMostRecentItem()) } + } + + @Test + fun isLocationLockEnabled_toggle() { + assertFalse(mapStateRepository.isLocationLockEnabled) + + mapStateRepository.isLocationLockEnabled = true + assertTrue(mapStateRepository.isLocationLockEnabled) + } + + @Test + fun cameraPosition_setAndGet() { + fakeLocalValueStore.lastActiveSurveyId = surveyId + val position = CameraPosition(coordinates = coordinates) + + mapStateRepository.setCameraPosition(position) + assertEquals(position, mapStateRepository.getCameraPosition(surveyId)) + } + + @Test + fun clearCameraPosition_removesValue() { + fakeLocalValueStore.lastActiveSurveyId = surveyId + val position = CameraPosition(coordinates = coordinates) + + mapStateRepository.setCameraPosition(position) + mapStateRepository.clearCameraPosition(surveyId) + assertNull(mapStateRepository.getCameraPosition(surveyId)) + } +} diff --git a/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/TermsOfServiceRepositoryTest.kt b/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/TermsOfServiceRepositoryTest.kt index c15eb55528..22951e17e9 100644 --- a/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/TermsOfServiceRepositoryTest.kt +++ b/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/TermsOfServiceRepositoryTest.kt @@ -25,14 +25,13 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.runTest import org.groundplatform.data.DataStoreException -import org.groundplatform.data.stores.LocalValueStore +import org.groundplatform.data.FakeLocalValueStore import org.groundplatform.data.stores.RemoteDataStore import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.SurveyListItem import org.groundplatform.domain.model.TermsOfService import org.groundplatform.domain.model.User import org.groundplatform.domain.model.locationofinterest.LocationOfInterest -import org.groundplatform.domain.model.map.CameraPosition import org.groundplatform.domain.model.mutation.Mutation import org.groundplatform.domain.system.NetworkStatus import org.groundplatform.testing.FakeNetworkManager @@ -40,7 +39,7 @@ import org.groundplatform.testing.FakeNetworkManager class TermsOfServiceRepositoryTest { private val fakeNetworkManager = FakeNetworkManager(NetworkStatus.AVAILABLE) private val fakeRemoteDataStore = TestRemoteDataStore() - private val fakeLocalValueStore = TestLocalValueStore() + private val fakeLocalValueStore = FakeLocalValueStore() private val repository = TermsOfServiceRepository(fakeNetworkManager, fakeRemoteDataStore, fakeLocalValueStore) @@ -109,31 +108,4 @@ class TermsOfServiceRepositoryTest { override suspend fun refreshUserProfile() = Unit } - - private class TestLocalValueStore : LocalValueStore { - override var lastActiveSurveyId: String = "" - override var mapType: Int = 0 - override var isLocationLockEnabled: Boolean = false - override var isTermsOfServiceAccepted: Boolean = false - override var isOfflineImageryEnabled: Boolean = true - override var drawAreaInstructionsShown: Boolean = false - override var dropPinInstructionsShown: Boolean = false - override var draftSubmissionId: String? = null - override var selectedLanguage: String = "en" - override var selectedLengthUnit: String = "m" - override var shouldUploadMediaOverUnmeteredConnectionOnly: Boolean = false - override var isDeferredDeeplinkConsumed: Boolean = false - - override fun clear() = Unit - - override fun clearLastCameraPosition(surveyId: String) = Unit - - override fun setLastCameraPosition(surveyId: String, cameraPosition: CameraPosition) = Unit - - override fun getLastCameraPosition(surveyId: String): CameraPosition? = null - - override fun setDataSharingConsent(surveyId: String, consent: Boolean) = Unit - - override fun getDataSharingConsent(surveyId: String): Boolean = false - } } From 6e185b6465b4446041d09c03f22d8d54a766ad5c Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Thu, 27 Aug 2026 23:24:56 +0530 Subject: [PATCH 07/10] feat: define CrashLogger interface, implement DI, and add test doubles for repository and data store testing. --- .../android/FirebaseCrashLogger.kt | 7 +- .../android/di/RepositoryModule.kt | 19 ++- .../groundplatform/android/di/SystemModule.kt | 8 + .../data}/repository/SurveyRepository.kt | 22 +-- .../data/FakeLocalSurveyStore.kt | 40 +++++ .../data/FakeRemoteDataStore.kt | 65 ++++++++ .../data/repository/SurveyRepositoryTest.kt | 145 ++++++++++++++++++ .../TermsOfServiceRepositoryTest.kt | 41 +---- .../domain/system/CrashLogger.kt | 26 ++++ .../groundplatform/testing/FakeCrashLogger.kt | 33 ++++ 10 files changed, 348 insertions(+), 58 deletions(-) rename {app/src/main/java/org/groundplatform/android => core/data/src/commonMain/kotlin/org/groundplatform/data}/repository/SurveyRepository.kt (90%) create mode 100644 core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalSurveyStore.kt create mode 100644 core/data/src/commonTest/kotlin/org/groundplatform/data/FakeRemoteDataStore.kt create mode 100644 core/data/src/commonTest/kotlin/org/groundplatform/data/repository/SurveyRepositoryTest.kt create mode 100644 core/domain/src/commonMain/kotlin/org/groundplatform/domain/system/CrashLogger.kt create mode 100644 core/testing/src/commonMain/kotlin/org/groundplatform/testing/FakeCrashLogger.kt diff --git a/app/src/main/java/org/groundplatform/android/FirebaseCrashLogger.kt b/app/src/main/java/org/groundplatform/android/FirebaseCrashLogger.kt index 602260be31..b4af19c4ee 100644 --- a/app/src/main/java/org/groundplatform/android/FirebaseCrashLogger.kt +++ b/app/src/main/java/org/groundplatform/android/FirebaseCrashLogger.kt @@ -20,9 +20,10 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics import javax.inject.Inject import javax.inject.Singleton import org.groundplatform.android.common.Constants.isReleaseBuild +import org.groundplatform.domain.system.CrashLogger @Singleton -class FirebaseCrashLogger @Inject constructor() { +class FirebaseCrashLogger @Inject constructor() : CrashLogger { private val crashlytics by lazy { FirebaseCrashlytics.getInstance() } @@ -33,11 +34,11 @@ class FirebaseCrashLogger @Inject constructor() { } } - fun setSelectedSurveyId(surveyId: String?) { + override fun setSelectedSurveyId(surveyId: String?) { setCustomKeys { key("selectedSurveyId", surveyId ?: "") } } - fun setScreenName(viewClass: String) { + override fun setScreenName(viewClass: String) { setCustomKeys { key("screenName", viewClass) } } diff --git a/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt b/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt index c94c1b8ed1..2dad8f71be 100644 --- a/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt +++ b/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt @@ -21,15 +21,18 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import jakarta.inject.Singleton +import kotlinx.coroutines.CoroutineScope +import org.groundplatform.android.di.coroutines.ApplicationScope import org.groundplatform.android.repository.LocationOfInterestRepository import org.groundplatform.android.repository.MutationRepository import org.groundplatform.android.repository.OfflineAreaRepository import org.groundplatform.android.repository.SubmissionRepository -import org.groundplatform.android.repository.SurveyRepository import org.groundplatform.android.repository.UserMediaRepository import org.groundplatform.android.repository.UserRepository import org.groundplatform.data.repository.MapStateRepository +import org.groundplatform.data.repository.SurveyRepository import org.groundplatform.data.repository.TermsOfServiceRepository +import org.groundplatform.data.stores.LocalSurveyStore import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.data.stores.RemoteDataStore import org.groundplatform.domain.repository.LocationOfInterestRepositoryInterface @@ -41,6 +44,7 @@ import org.groundplatform.domain.repository.SurveyRepositoryInterface import org.groundplatform.domain.repository.TermsOfServiceRepositoryInterface import org.groundplatform.domain.repository.UserMediaRepositoryInterface import org.groundplatform.domain.repository.UserRepositoryInterface +import org.groundplatform.domain.system.CrashLogger import org.groundplatform.domain.system.NetworkManagerInterface @Module @@ -61,10 +65,17 @@ abstract class UserRepositoryModule { @Module @InstallIn(SingletonComponent::class) -abstract class SurveyRepositoryModule { - @Binds +object SurveyRepositoryModule { + @Provides @Singleton - abstract fun bindSurveyRepository(impl: SurveyRepository): SurveyRepositoryInterface + fun provideSurveyRepository( + @ApplicationScope externalScope: CoroutineScope, + crashLogger: CrashLogger, + localSurveyStore: LocalSurveyStore, + localValueStore: LocalValueStore, + remoteDataStore: RemoteDataStore, + ): SurveyRepositoryInterface = + SurveyRepository(externalScope, crashLogger, localSurveyStore, localValueStore, remoteDataStore) } @Module diff --git a/app/src/main/java/org/groundplatform/android/di/SystemModule.kt b/app/src/main/java/org/groundplatform/android/di/SystemModule.kt index b632b042f5..0a8a455f7c 100644 --- a/app/src/main/java/org/groundplatform/android/di/SystemModule.kt +++ b/app/src/main/java/org/groundplatform/android/di/SystemModule.kt @@ -24,7 +24,9 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +import org.groundplatform.android.FirebaseCrashLogger import org.groundplatform.android.system.NetworkManager +import org.groundplatform.domain.system.CrashLogger import org.groundplatform.domain.system.NetworkManagerInterface @InstallIn(SingletonComponent::class) @@ -45,3 +47,9 @@ abstract class NetworkManagerModule { @Singleton abstract fun bindNetworkManager(networkManager: NetworkManager): NetworkManagerInterface } + +@InstallIn(SingletonComponent::class) +@Module +abstract class CrashLoggerModule { + @Binds @Singleton abstract fun bindCrashLogger(impl: FirebaseCrashLogger): CrashLogger +} diff --git a/app/src/main/java/org/groundplatform/android/repository/SurveyRepository.kt b/core/data/src/commonMain/kotlin/org/groundplatform/data/repository/SurveyRepository.kt similarity index 90% rename from app/src/main/java/org/groundplatform/android/repository/SurveyRepository.kt rename to core/data/src/commonMain/kotlin/org/groundplatform/data/repository/SurveyRepository.kt index 7f85695167..736cecda6a 100644 --- a/app/src/main/java/org/groundplatform/android/repository/SurveyRepository.kt +++ b/core/data/src/commonMain/kotlin/org/groundplatform/data/repository/SurveyRepository.kt @@ -13,10 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.android.repository +package org.groundplatform.data.repository -import javax.inject.Inject -import javax.inject.Singleton +import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.TimeoutCancellationException @@ -31,8 +30,6 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.withTimeout -import org.groundplatform.android.FirebaseCrashLogger -import org.groundplatform.android.di.coroutines.ApplicationScope import org.groundplatform.data.stores.LocalSurveyStore import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.data.stores.RemoteDataStore @@ -40,18 +37,15 @@ import org.groundplatform.domain.model.Survey import org.groundplatform.domain.model.SurveyListItem import org.groundplatform.domain.model.User import org.groundplatform.domain.repository.SurveyRepositoryInterface -import timber.log.Timber +import org.groundplatform.domain.system.CrashLogger private const val ACTIVATE_SURVEY_TIMEOUT_MILLS: Long = 3 * 1000 private const val LOAD_REMOTE_SURVEY_TIMEOUT_MILLS: Long = 30 * 1000 @OptIn(ExperimentalCoroutinesApi::class) -@Singleton -class SurveyRepository -@Inject -constructor( - @ApplicationScope private val externalScope: CoroutineScope, - private val firebaseCrashLogger: FirebaseCrashLogger, +class SurveyRepository( + private val externalScope: CoroutineScope, + private val crashLogger: CrashLogger, private val localSurveyStore: LocalSurveyStore, private val localValueStore: LocalValueStore, private val remoteDataStore: RemoteDataStore, @@ -112,11 +106,11 @@ constructor( } } } catch (e: TimeoutCancellationException) { - Timber.e(e, "Failed to get survey due to timeout") + Logger.e("Failed to get survey due to timeout", e) } if (isSurveyActive(surveyId) || surveyId.isBlank()) { - firebaseCrashLogger.setSelectedSurveyId(surveyId) + crashLogger.setSelectedSurveyId(surveyId) localValueStore.lastActiveSurveyId = surveyId } } diff --git a/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalSurveyStore.kt b/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalSurveyStore.kt new file mode 100644 index 0000000000..b6231e5953 --- /dev/null +++ b/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalSurveyStore.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2026 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 org.groundplatform.data + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import org.groundplatform.data.stores.LocalSurveyStore +import org.groundplatform.domain.model.Survey + +class FakeLocalSurveyStore : LocalSurveyStore { + private val surveyMap = MutableStateFlow>(emptyMap()) + + override val surveys: Flow> = surveyMap.map { it.values.toList() } + + override fun survey(id: String): Flow = surveyMap.map { it[id] } + + override suspend fun getSurveyById(id: String): Survey? = surveyMap.value[id] + + override suspend fun deleteSurvey(survey: Survey) { + surveyMap.value = surveyMap.value - survey.id + } + + override suspend fun insertOrUpdateSurvey(survey: Survey) { + surveyMap.value = surveyMap.value + (survey.id to survey) + } +} diff --git a/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeRemoteDataStore.kt b/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeRemoteDataStore.kt new file mode 100644 index 0000000000..c47628349c --- /dev/null +++ b/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeRemoteDataStore.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2026 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 org.groundplatform.data + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import org.groundplatform.data.stores.RemoteDataStore +import org.groundplatform.domain.model.Survey +import org.groundplatform.domain.model.SurveyListItem +import org.groundplatform.domain.model.TermsOfService +import org.groundplatform.domain.model.User +import org.groundplatform.domain.model.locationofinterest.LocationOfInterest +import org.groundplatform.domain.model.mutation.Mutation + +class FakeRemoteDataStore : RemoteDataStore { + var termsOfServiceResult: Result = Result.success(null) + var surveys: List = emptyList() + val subscribedSurveyUpdates = mutableListOf() + val unsubscribedSurveyUpdates = mutableListOf() + val appliedMutations = mutableListOf() + + override suspend fun loadTermsOfService(): TermsOfService? = termsOfServiceResult.getOrThrow() + + override fun getRestrictedSurveyList(user: User): Flow> = emptyFlow() + + override fun getPublicSurveyList(): Flow> = emptyFlow() + + override suspend fun loadSurvey(surveyId: String): Survey? = surveys.firstOrNull { + it.id == surveyId + } + + override fun loadPredefinedLois(survey: Survey): Flow> = emptyFlow() + + override fun loadUserLois(survey: Survey, ownerUserId: String): Flow> = + emptyFlow() + + override fun loadSharedLois(survey: Survey): Flow> = emptyFlow() + + override suspend fun applyMutations(mutations: List, user: User) { + appliedMutations.addAll(mutations) + } + + override suspend fun subscribeToSurveyUpdates(surveyId: String) { + subscribedSurveyUpdates.add(surveyId) + } + + override suspend fun unsubscribeFromSurveyUpdates(surveyId: String) { + unsubscribedSurveyUpdates.add(surveyId) + } + + override suspend fun refreshUserProfile() = Unit +} diff --git a/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/SurveyRepositoryTest.kt b/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/SurveyRepositoryTest.kt new file mode 100644 index 0000000000..31be0744b5 --- /dev/null +++ b/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/SurveyRepositoryTest.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2026 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 org.groundplatform.data.repository + +import app.cash.turbine.test +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.groundplatform.data.FakeLocalSurveyStore +import org.groundplatform.data.FakeLocalValueStore +import org.groundplatform.data.FakeRemoteDataStore +import org.groundplatform.domain.model.Survey +import org.groundplatform.domain.model.Survey.GeneralAccess +import org.groundplatform.testing.FakeCrashLogger + +@OptIn(ExperimentalCoroutinesApi::class) +class SurveyRepositoryTest { + + private val testScope = TestScope() + private val fakeCrashLogger = FakeCrashLogger() + private val fakeLocalSurveyStore = FakeLocalSurveyStore() + private val fakeLocalValueStore = FakeLocalValueStore() + private val fakeRemoteDataStore = FakeRemoteDataStore() + + private val repository = + SurveyRepository( + externalScope = testScope.backgroundScope, + crashLogger = fakeCrashLogger, + localSurveyStore = fakeLocalSurveyStore, + localValueStore = fakeLocalValueStore, + remoteDataStore = fakeRemoteDataStore, + ) + + private val testSurvey = + Survey( + id = "survey_123", + title = "Test Survey", + description = "Test Description", + jobMap = emptyMap(), + generalAccess = GeneralAccess.PUBLIC, + ) + + @Test + fun activateSurvey_updatesActiveSurveyAndCrashLogger() = testScope.runTest { + fakeLocalSurveyStore.insertOrUpdateSurvey(testSurvey) + + repository.activateSurvey(testSurvey.id) + advanceUntilIdle() + + assertEquals(testSurvey, repository.activeSurvey) + repository.activeSurveyFlow.test { assertEquals(testSurvey, expectMostRecentItem()) } + assertEquals(testSurvey.id, fakeCrashLogger.lastSelectedSurveyId) + assertEquals(testSurvey.id, fakeLocalValueStore.lastActiveSurveyId) + assertTrue(repository.isSurveyActive(testSurvey.id)) + } + + @Test + fun clearActiveSurvey_resetsActiveSurvey() = testScope.runTest { + fakeLocalSurveyStore.insertOrUpdateSurvey(testSurvey) + repository.activateSurvey(testSurvey.id) + advanceUntilIdle() + + repository.clearActiveSurvey() + advanceUntilIdle() + + assertNull(repository.activeSurvey) + repository.activeSurveyFlow.test { assertNull(expectMostRecentItem()) } + assertEquals("", fakeCrashLogger.lastSelectedSurveyId) + assertEquals("", fakeLocalValueStore.lastActiveSurveyId) + assertFalse(repository.isSurveyActive(testSurvey.id)) + } + + @Test + fun saveSurvey_persistsToLocalSurveyStore() = testScope.runTest { + repository.saveSurvey(testSurvey) + + assertEquals(testSurvey, fakeLocalSurveyStore.getSurveyById(testSurvey.id)) + } + + @Test + fun getOfflineSurvey_returnsSavedSurvey() = testScope.runTest { + fakeLocalSurveyStore.insertOrUpdateSurvey(testSurvey) + + assertEquals(testSurvey, repository.getOfflineSurvey(testSurvey.id)) + } + + @Test + fun removeOfflineSurvey_deletesFromLocalSurveyStore() = testScope.runTest { + fakeLocalSurveyStore.insertOrUpdateSurvey(testSurvey) + assertEquals(testSurvey, repository.getOfflineSurvey(testSurvey.id)) + + repository.removeOfflineSurvey(testSurvey.id) + + assertNull(repository.getOfflineSurvey(testSurvey.id)) + } + + @Test + fun getRemoteSurvey_loadsFromRemoteDataStore() = testScope.runTest { + fakeRemoteDataStore.surveys = listOf(testSurvey) + + val result = repository.getRemoteSurvey(testSurvey.id) + assertEquals(testSurvey, result) + } + + @Test + fun subscribeToSurveyUpdates_delegatesToRemoteDataStore() = testScope.runTest { + repository.subscribeToSurveyUpdates("survey_abc") + + assertTrue(fakeRemoteDataStore.subscribedSurveyUpdates.contains("survey_abc")) + } + + @Test + fun unsubscribeFromSurveyUpdates_delegatesToRemoteDataStore() = testScope.runTest { + repository.unsubscribeFromSurveyUpdates("survey_abc") + + assertTrue(fakeRemoteDataStore.unsubscribedSurveyUpdates.contains("survey_abc")) + } + + @Test + fun dataSharingConsent_roundTrip() { + assertFalse(repository.getDataSharingConsent(testSurvey.id)) + + repository.setDataSharingConsent(testSurvey.id, true) + assertTrue(repository.getDataSharingConsent(testSurvey.id)) + } +} diff --git a/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/TermsOfServiceRepositoryTest.kt b/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/TermsOfServiceRepositoryTest.kt index 22951e17e9..c8bb7d47f9 100644 --- a/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/TermsOfServiceRepositoryTest.kt +++ b/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/TermsOfServiceRepositoryTest.kt @@ -21,24 +21,17 @@ import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.runTest import org.groundplatform.data.DataStoreException import org.groundplatform.data.FakeLocalValueStore -import org.groundplatform.data.stores.RemoteDataStore -import org.groundplatform.domain.model.Survey -import org.groundplatform.domain.model.SurveyListItem +import org.groundplatform.data.FakeRemoteDataStore import org.groundplatform.domain.model.TermsOfService -import org.groundplatform.domain.model.User -import org.groundplatform.domain.model.locationofinterest.LocationOfInterest -import org.groundplatform.domain.model.mutation.Mutation import org.groundplatform.domain.system.NetworkStatus import org.groundplatform.testing.FakeNetworkManager class TermsOfServiceRepositoryTest { private val fakeNetworkManager = FakeNetworkManager(NetworkStatus.AVAILABLE) - private val fakeRemoteDataStore = TestRemoteDataStore() + private val fakeRemoteDataStore = FakeRemoteDataStore() private val fakeLocalValueStore = FakeLocalValueStore() private val repository = TermsOfServiceRepository(fakeNetworkManager, fakeRemoteDataStore, fakeLocalValueStore) @@ -68,7 +61,8 @@ class TermsOfServiceRepositoryTest { @Test fun getTermsOfService_whenRequestFails_throwsError() = runTest { - fakeRemoteDataStore.termsOfServiceResult = Result.failure(RuntimeException("Network error")) + fakeRemoteDataStore.termsOfServiceResult = + Result.failure(RuntimeException("Network error")) assertFailsWith { repository.getTermsOfService() } } @@ -81,31 +75,4 @@ class TermsOfServiceRepositoryTest { repository.isTermsOfServiceAccepted = false assertFalse(repository.isTermsOfServiceAccepted) } - - private class TestRemoteDataStore : RemoteDataStore { - var termsOfServiceResult: Result = Result.success(null) - - override suspend fun loadTermsOfService(): TermsOfService? = termsOfServiceResult.getOrThrow() - - override fun getRestrictedSurveyList(user: User): Flow> = emptyFlow() - - override fun getPublicSurveyList(): Flow> = emptyFlow() - - override suspend fun loadSurvey(surveyId: String): Survey? = null - - override fun loadPredefinedLois(survey: Survey): Flow> = emptyFlow() - - override fun loadUserLois(survey: Survey, ownerUserId: String): Flow> = - emptyFlow() - - override fun loadSharedLois(survey: Survey): Flow> = emptyFlow() - - override suspend fun applyMutations(mutations: List, user: User) = Unit - - override suspend fun subscribeToSurveyUpdates(surveyId: String) = Unit - - override suspend fun unsubscribeFromSurveyUpdates(surveyId: String) = Unit - - override suspend fun refreshUserProfile() = Unit - } } diff --git a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/system/CrashLogger.kt b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/system/CrashLogger.kt new file mode 100644 index 0000000000..b626f32383 --- /dev/null +++ b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/system/CrashLogger.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2026 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 org.groundplatform.domain.system + +/** Interface for recording crash reporting context and breadcrumbs. */ +interface CrashLogger { + /** Sets the ID of the currently selected survey in the crash report. */ + fun setSelectedSurveyId(surveyId: String?) + + /** Sets the current screen name in the crash report. */ + fun setScreenName(viewClass: String) +} diff --git a/core/testing/src/commonMain/kotlin/org/groundplatform/testing/FakeCrashLogger.kt b/core/testing/src/commonMain/kotlin/org/groundplatform/testing/FakeCrashLogger.kt new file mode 100644 index 0000000000..e13d4b59d9 --- /dev/null +++ b/core/testing/src/commonMain/kotlin/org/groundplatform/testing/FakeCrashLogger.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2026 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 org.groundplatform.testing + +import org.groundplatform.domain.system.CrashLogger + +/** In-memory test double for [CrashLogger]. */ +class FakeCrashLogger : CrashLogger { + var lastSelectedSurveyId: String? = null + var lastScreenName: String? = null + + override fun setSelectedSurveyId(surveyId: String?) { + lastSelectedSurveyId = surveyId + } + + override fun setScreenName(viewClass: String) { + lastScreenName = viewClass + } +} From d444223274642fe35113e74e438b36513bf0e20f Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Fri, 28 Aug 2026 07:44:28 +0530 Subject: [PATCH 08/10] refactor: migrate MutationRepository to data module --- .../android/di/RepositoryModule.kt | 21 +++++++++++++++---- .../repository/MutationRepositoryTest.kt | 5 +---- .../data}/repository/MutationRepository.kt | 20 ++++++------------ 3 files changed, 24 insertions(+), 22 deletions(-) rename {app/src/main/java/org/groundplatform/android => core/data/src/commonMain/kotlin/org/groundplatform/data}/repository/MutationRepository.kt (93%) diff --git a/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt b/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt index 2dad8f71be..234721ef6f 100644 --- a/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt +++ b/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt @@ -24,14 +24,16 @@ import jakarta.inject.Singleton import kotlinx.coroutines.CoroutineScope import org.groundplatform.android.di.coroutines.ApplicationScope import org.groundplatform.android.repository.LocationOfInterestRepository -import org.groundplatform.android.repository.MutationRepository import org.groundplatform.android.repository.OfflineAreaRepository import org.groundplatform.android.repository.SubmissionRepository import org.groundplatform.android.repository.UserMediaRepository import org.groundplatform.android.repository.UserRepository import org.groundplatform.data.repository.MapStateRepository +import org.groundplatform.data.repository.MutationRepository import org.groundplatform.data.repository.SurveyRepository import org.groundplatform.data.repository.TermsOfServiceRepository +import org.groundplatform.data.stores.LocalLocationOfInterestStore +import org.groundplatform.data.stores.LocalSubmissionStore import org.groundplatform.data.stores.LocalSurveyStore import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.data.stores.RemoteDataStore @@ -97,10 +99,21 @@ object MapStateRepositoryModule { @Module @InstallIn(SingletonComponent::class) -abstract class MutationRepositoryModule { - @Binds +object MutationRepositoryModule { + @Provides @Singleton - abstract fun bindMutationRepository(impl: MutationRepository): MutationRepositoryInterface + fun provideMutationRepository( + localLocationOfInterestStore: LocalLocationOfInterestStore, + localSubmissionStore: LocalSubmissionStore, + remoteDataStore: RemoteDataStore, + userRepository: UserRepositoryInterface, + ): MutationRepositoryInterface = + MutationRepository( + localLocationOfInterestStore, + localSubmissionStore, + remoteDataStore, + userRepository, + ) } @Module diff --git a/app/src/test/java/org/groundplatform/android/repository/MutationRepositoryTest.kt b/app/src/test/java/org/groundplatform/android/repository/MutationRepositoryTest.kt index c2cbf1b096..c0e476fbc9 100644 --- a/app/src/test/java/org/groundplatform/android/repository/MutationRepositoryTest.kt +++ b/app/src/test/java/org/groundplatform/android/repository/MutationRepositoryTest.kt @@ -19,7 +19,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest -import org.groundplatform.android.system.auth.AuthenticationManager +import org.groundplatform.data.repository.MutationRepository import org.groundplatform.data.stores.LocalLocationOfInterestStore import org.groundplatform.data.stores.LocalSubmissionStore import org.groundplatform.data.stores.RemoteDataStore @@ -55,7 +55,6 @@ import org.mockito.kotlin.whenever @RunWith(MockitoJUnitRunner::class) class MutationRepositoryTest { - @Mock private lateinit var authenticationManager: AuthenticationManager @Mock private lateinit var localLoiStore: LocalLocationOfInterestStore @Mock private lateinit var localSubmissionStore: LocalSubmissionStore @Mock private lateinit var remoteDataStore: RemoteDataStore @@ -67,7 +66,6 @@ class MutationRepositoryTest { fun setUp() { repository = MutationRepository( - authenticationManager, localLoiStore, localSubmissionStore, remoteDataStore, @@ -321,7 +319,6 @@ class MutationRepositoryTest { loiMutations: List = emptyList(), submissionMutations: List = emptyList(), ) { - whenever(authenticationManager.getAuthenticatedUser()).thenReturn(user) whenever(userRepository.getAuthenticatedUser()).thenReturn(user) whenever(localLoiStore.getAllMutationsFlow()).thenReturn(flowOf(loiMutations)) whenever(localSubmissionStore.getAllMutationsFlow()).thenReturn(flowOf(submissionMutations)) diff --git a/app/src/main/java/org/groundplatform/android/repository/MutationRepository.kt b/core/data/src/commonMain/kotlin/org/groundplatform/data/repository/MutationRepository.kt similarity index 93% rename from app/src/main/java/org/groundplatform/android/repository/MutationRepository.kt rename to core/data/src/commonMain/kotlin/org/groundplatform/data/repository/MutationRepository.kt index 571f9fec10..f9ac152ece 100644 --- a/app/src/main/java/org/groundplatform/android/repository/MutationRepository.kt +++ b/core/data/src/commonMain/kotlin/org/groundplatform/data/repository/MutationRepository.kt @@ -14,15 +14,12 @@ * limitations under the License. */ -package org.groundplatform.android.repository +package org.groundplatform.data.repository -import javax.inject.Inject -import javax.inject.Singleton +import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first -import org.groundplatform.android.system.auth.AuthenticationManager -import org.groundplatform.android.util.priority import org.groundplatform.data.stores.LocalLocationOfInterestStore import org.groundplatform.data.stores.LocalSubmissionStore import org.groundplatform.data.stores.RemoteDataStore @@ -42,13 +39,8 @@ import org.groundplatform.domain.model.mutation.SubmissionMutation import org.groundplatform.domain.model.submission.UploadQueueEntry import org.groundplatform.domain.repository.MutationRepositoryInterface import org.groundplatform.domain.repository.UserRepositoryInterface -import timber.log.Timber -@Singleton -class MutationRepository -@Inject -constructor( - private val authenticationManager: AuthenticationManager, +class MutationRepository( private val localLocationOfInterestStore: LocalLocationOfInterestStore, private val localSubmissionStore: LocalSubmissionStore, private val remoteDataStore: RemoteDataStore, @@ -81,7 +73,7 @@ constructor( loiMutations: List, submissionMutations: List, ): List { - val user = authenticationManager.getAuthenticatedUser() + val user = userRepository.getAuthenticatedUser() val loiMutationMap = loiMutations.filterByUser(user).associateBy { it.collectionId } val submissionMutationMap = submissionMutations.filterByUser(user).associateBy { it.collectionId } @@ -101,7 +93,7 @@ constructor( private fun List.filterByUser(user: User): List { val (validMutations, invalidMutations) = partition { it.userId == user.id } if (invalidMutations.isNotEmpty()) { - Timber.e("Mutation(s) not deleted on sign-out") + Logger.e("Mutation(s) not deleted on sign-out") } return validMutations } @@ -134,7 +126,7 @@ constructor( // Mark all mutations as having failed since the remote datastore only commits when all // mutations have succeeded. markAsFailed(mutations, t) - Timber.log(t.priority(), t, "Failed to sync local data") + Logger.e("Failed to sync local data", t) MutationRepositoryInterface.MutationResult.Failure } From b94b83899be3daafb430ab752fad35818f50f977 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Fri, 28 Aug 2026 07:56:00 +0530 Subject: [PATCH 09/10] refactor: introduce common authentication and local database interfaces with associated test doubles --- .../data/local/room/RoomLocalDatabase.kt | 28 +++ .../android/di/LocalDataStoreModule.kt | 7 + .../android/di/RepositoryModule.kt | 28 ++- .../system/auth/AuthenticationManager.kt | 18 +- .../data}/repository/UserRepository.kt | 23 +- .../data/stores/LocalDatabase.kt | 23 ++ .../groundplatform/data/FakeLocalDatabase.kt | 27 +++ .../groundplatform/data/FakeLocalUserStore.kt | 32 +++ .../data/FakeRemoteDataStore.kt | 5 +- .../data/repository/UserRepositoryTest.kt | 215 ++++++++++++++++++ .../system/auth/AuthenticationManager.kt | 34 +++ .../testing/FakeAuthenticationManager.kt | 52 +++++ 12 files changed, 457 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/org/groundplatform/android/data/local/room/RoomLocalDatabase.kt rename {app/src/main/java/org/groundplatform/android => core/data/src/commonMain/kotlin/org/groundplatform/data}/repository/UserRepository.kt (90%) create mode 100644 core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalDatabase.kt create mode 100644 core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalDatabase.kt create mode 100644 core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalUserStore.kt create mode 100644 core/data/src/commonTest/kotlin/org/groundplatform/data/repository/UserRepositoryTest.kt create mode 100644 core/domain/src/commonMain/kotlin/org/groundplatform/domain/system/auth/AuthenticationManager.kt create mode 100644 core/testing/src/commonMain/kotlin/org/groundplatform/testing/FakeAuthenticationManager.kt diff --git a/app/src/main/java/org/groundplatform/android/data/local/room/RoomLocalDatabase.kt b/app/src/main/java/org/groundplatform/android/data/local/room/RoomLocalDatabase.kt new file mode 100644 index 0000000000..e23c036051 --- /dev/null +++ b/app/src/main/java/org/groundplatform/android/data/local/room/RoomLocalDatabase.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2026 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 org.groundplatform.android.data.local.room + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RoomLocalDatabase @Inject constructor(private val roomDb: LocalDatabase) : + org.groundplatform.data.stores.LocalDatabase { + override fun clearAllTables() { + roomDb.clearAllTables() + } +} diff --git a/app/src/main/java/org/groundplatform/android/di/LocalDataStoreModule.kt b/app/src/main/java/org/groundplatform/android/di/LocalDataStoreModule.kt index 02741e47f3..d921a1c18b 100644 --- a/app/src/main/java/org/groundplatform/android/di/LocalDataStoreModule.kt +++ b/app/src/main/java/org/groundplatform/android/di/LocalDataStoreModule.kt @@ -23,6 +23,7 @@ import dagger.hilt.components.SingletonComponent import javax.inject.Singleton import org.groundplatform.android.data.local.SharedPreferencesLocalValueStore import org.groundplatform.android.data.local.room.LocalDatabase +import org.groundplatform.android.data.local.room.RoomLocalDatabase import org.groundplatform.android.data.local.room.dao.ConditionDao import org.groundplatform.android.data.local.room.dao.DraftSubmissionDao import org.groundplatform.android.data.local.room.dao.ExpressionDao @@ -73,6 +74,12 @@ abstract class LocalDataStoreModule { @Singleton abstract fun localValueStore(store: SharedPreferencesLocalValueStore): LocalValueStore + @Binds + @Singleton + abstract fun localDatabase( + database: RoomLocalDatabase + ): org.groundplatform.data.stores.LocalDatabase + companion object { @Provides fun draftSubmissionDao(localDatabase: LocalDatabase): DraftSubmissionDao { diff --git a/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt b/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt index 234721ef6f..7b7d21d28b 100644 --- a/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt +++ b/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt @@ -27,14 +27,16 @@ import org.groundplatform.android.repository.LocationOfInterestRepository import org.groundplatform.android.repository.OfflineAreaRepository import org.groundplatform.android.repository.SubmissionRepository import org.groundplatform.android.repository.UserMediaRepository -import org.groundplatform.android.repository.UserRepository import org.groundplatform.data.repository.MapStateRepository import org.groundplatform.data.repository.MutationRepository import org.groundplatform.data.repository.SurveyRepository import org.groundplatform.data.repository.TermsOfServiceRepository +import org.groundplatform.data.repository.UserRepository +import org.groundplatform.data.stores.LocalDatabase import org.groundplatform.data.stores.LocalLocationOfInterestStore import org.groundplatform.data.stores.LocalSubmissionStore import org.groundplatform.data.stores.LocalSurveyStore +import org.groundplatform.data.stores.LocalUserStore import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.data.stores.RemoteDataStore import org.groundplatform.domain.repository.LocationOfInterestRepositoryInterface @@ -48,6 +50,7 @@ import org.groundplatform.domain.repository.UserMediaRepositoryInterface import org.groundplatform.domain.repository.UserRepositoryInterface import org.groundplatform.domain.system.CrashLogger import org.groundplatform.domain.system.NetworkManagerInterface +import org.groundplatform.domain.system.auth.AuthenticationManager @Module @InstallIn(SingletonComponent::class) @@ -61,8 +64,27 @@ abstract class LocationOfInterestRepositoryModule { @Module @InstallIn(SingletonComponent::class) -abstract class UserRepositoryModule { - @Binds @Singleton abstract fun bindUserRepository(impl: UserRepository): UserRepositoryInterface +object UserRepositoryModule { + @Provides + @Singleton + fun provideUserRepository( + authenticationManager: AuthenticationManager, + localValueStore: LocalValueStore, + localUserStore: LocalUserStore, + networkManager: NetworkManagerInterface, + surveyRepository: SurveyRepositoryInterface, + remoteDataStore: RemoteDataStore, + localDatabase: LocalDatabase, + ): UserRepositoryInterface = + UserRepository( + authenticationManager, + localValueStore, + localUserStore, + networkManager, + surveyRepository, + remoteDataStore, + localDatabase, + ) } @Module diff --git a/app/src/main/java/org/groundplatform/android/system/auth/AuthenticationManager.kt b/app/src/main/java/org/groundplatform/android/system/auth/AuthenticationManager.kt index dbd36d6da1..5116ce75db 100644 --- a/app/src/main/java/org/groundplatform/android/system/auth/AuthenticationManager.kt +++ b/app/src/main/java/org/groundplatform/android/system/auth/AuthenticationManager.kt @@ -15,20 +15,4 @@ */ package org.groundplatform.android.system.auth -import kotlinx.coroutines.flow.Flow -import org.groundplatform.domain.model.User -import org.groundplatform.domain.model.auth.SignInState - -interface AuthenticationManager { - val signInState: Flow - - /** Must be called before looking up auth state or logged-in user. */ - fun init() - - fun signIn() - - fun signOut() - - /** Returns the logged-in user. */ - suspend fun getAuthenticatedUser(): User -} +typealias AuthenticationManager = org.groundplatform.domain.system.auth.AuthenticationManager diff --git a/app/src/main/java/org/groundplatform/android/repository/UserRepository.kt b/core/data/src/commonMain/kotlin/org/groundplatform/data/repository/UserRepository.kt similarity index 90% rename from app/src/main/java/org/groundplatform/android/repository/UserRepository.kt rename to core/data/src/commonMain/kotlin/org/groundplatform/data/repository/UserRepository.kt index a90e04a24a..d536405ec5 100644 --- a/app/src/main/java/org/groundplatform/android/repository/UserRepository.kt +++ b/core/data/src/commonMain/kotlin/org/groundplatform/data/repository/UserRepository.kt @@ -13,13 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.android.repository +package org.groundplatform.data.repository -import javax.inject.Inject -import javax.inject.Singleton +import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow -import org.groundplatform.android.data.local.room.LocalDatabase -import org.groundplatform.android.system.auth.AuthenticationManager +import org.groundplatform.data.stores.LocalDatabase import org.groundplatform.data.stores.LocalUserStore import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.data.stores.RemoteDataStore @@ -33,16 +31,13 @@ import org.groundplatform.domain.model.settings.UserSettings import org.groundplatform.domain.repository.SurveyRepositoryInterface import org.groundplatform.domain.repository.UserRepositoryInterface import org.groundplatform.domain.system.NetworkManagerInterface -import timber.log.Timber +import org.groundplatform.domain.system.auth.AuthenticationManager /** * Coordinates persistence of [User] instance in local data store. For more details on this pattern * and overall architecture, see https://developer.android.com/jetpack/docs/guide. */ -@Singleton -class UserRepository -@Inject -constructor( +class UserRepository( private val authenticationManager: AuthenticationManager, private val localValueStore: LocalValueStore, private val localUserStore: LocalUserStore, @@ -70,14 +65,14 @@ constructor( /** Attempts to refresh current user's profile in remote database if network is available. */ private suspend fun updateRemoteUserInfo(user: User) { if (!networkManager.isNetworkConnected()) { - Timber.d("Skipped refreshing user profile as device is offline.") + Logger.d { "Skipped refreshing user profile as device is offline." } return } if (!user.isAnonymous) { runCatching { remoteDataStore.refreshUserProfile() } .fold( - { Timber.i("Profile refreshed") }, - { throwable -> Timber.e(throwable, "Failed to refresh profile") }, + { Logger.i { "Profile refreshed" } }, + { throwable -> Logger.e(throwable) { "Failed to refresh profile" } }, ) } } @@ -115,7 +110,7 @@ constructor( return try { surveyRepository.activeSurvey?.getRole(user.email) != Role.VIEWER } catch (e: IllegalStateException) { - Timber.e(e, "Error getting role for user $user") + Logger.e(e) { "Error getting role for user $user" } false } } diff --git a/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalDatabase.kt b/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalDatabase.kt new file mode 100644 index 0000000000..ea21a58984 --- /dev/null +++ b/core/data/src/commonMain/kotlin/org/groundplatform/data/stores/LocalDatabase.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2026 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 org.groundplatform.data.stores + +/** Provides local database level management operations. */ +interface LocalDatabase { + /** Clears all tables in the local database. */ + fun clearAllTables() +} diff --git a/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalDatabase.kt b/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalDatabase.kt new file mode 100644 index 0000000000..3798993cc2 --- /dev/null +++ b/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalDatabase.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2026 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 org.groundplatform.data + +import org.groundplatform.data.stores.LocalDatabase + +class FakeLocalDatabase : LocalDatabase { + var clearAllTablesCalled = false + + override fun clearAllTables() { + clearAllTablesCalled = true + } +} diff --git a/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalUserStore.kt b/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalUserStore.kt new file mode 100644 index 0000000000..08bb908eac --- /dev/null +++ b/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalUserStore.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2026 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 org.groundplatform.data + +import org.groundplatform.data.stores.LocalUserStore +import org.groundplatform.domain.model.User + +class FakeLocalUserStore : LocalUserStore { + private val users = mutableMapOf() + + override suspend fun insertOrUpdateUser(user: User) { + users[user.id] = user + } + + override suspend fun getUser(id: String): User = users[id] ?: error("User not found: $id") + + override suspend fun getUserOrNull(id: String): User? = users[id] +} diff --git a/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeRemoteDataStore.kt b/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeRemoteDataStore.kt index c47628349c..654708f826 100644 --- a/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeRemoteDataStore.kt +++ b/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeRemoteDataStore.kt @@ -31,6 +31,7 @@ class FakeRemoteDataStore : RemoteDataStore { val subscribedSurveyUpdates = mutableListOf() val unsubscribedSurveyUpdates = mutableListOf() val appliedMutations = mutableListOf() + var refreshUserProfileCount = 0 override suspend fun loadTermsOfService(): TermsOfService? = termsOfServiceResult.getOrThrow() @@ -61,5 +62,7 @@ class FakeRemoteDataStore : RemoteDataStore { unsubscribedSurveyUpdates.add(surveyId) } - override suspend fun refreshUserProfile() = Unit + override suspend fun refreshUserProfile() { + refreshUserProfileCount++ + } } diff --git a/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/UserRepositoryTest.kt b/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/UserRepositoryTest.kt new file mode 100644 index 0000000000..290901679f --- /dev/null +++ b/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/UserRepositoryTest.kt @@ -0,0 +1,215 @@ +/* + * Copyright 2026 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 org.groundplatform.data.repository + +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.groundplatform.data.FakeLocalDatabase +import org.groundplatform.data.FakeLocalUserStore +import org.groundplatform.data.FakeLocalValueStore +import org.groundplatform.data.FakeRemoteDataStore +import org.groundplatform.domain.model.Role +import org.groundplatform.domain.model.Survey +import org.groundplatform.domain.model.User +import org.groundplatform.domain.model.auth.SignInState +import org.groundplatform.domain.model.geometry.Coordinates +import org.groundplatform.domain.model.geometry.Point +import org.groundplatform.domain.model.job.Job +import org.groundplatform.domain.model.locationofinterest.AuditInfo +import org.groundplatform.domain.model.locationofinterest.LocationOfInterest +import org.groundplatform.domain.model.settings.MeasurementUnits +import org.groundplatform.domain.model.settings.UserSettings +import org.groundplatform.domain.system.NetworkStatus +import org.groundplatform.testing.FakeAuthenticationManager +import org.groundplatform.testing.FakeNetworkManager +import org.groundplatform.testing.FakeSurveyRepository + +class UserRepositoryTest { + + private val testUser = User(id = "user1", email = "user1@gmail.com", displayName = "User 1") + private val fakeAuthManager = FakeAuthenticationManager(testUser) + private val fakeValueStore = FakeLocalValueStore() + private val fakeUserStore = FakeLocalUserStore() + private val fakeNetworkManager = FakeNetworkManager() + private val fakeSurveyRepository = FakeSurveyRepository() + private val fakeRemoteDataStore = FakeRemoteDataStore() + private val fakeLocalDatabase = FakeLocalDatabase() + + private lateinit var userRepository: UserRepository + + @BeforeTest + fun setUp() { + userRepository = + UserRepository( + authenticationManager = fakeAuthManager, + localValueStore = fakeValueStore, + localUserStore = fakeUserStore, + networkManager = fakeNetworkManager, + surveyRepository = fakeSurveyRepository, + remoteDataStore = fakeRemoteDataStore, + localDatabase = fakeLocalDatabase, + ) + } + + @Test + fun getAuthenticatedUser_returnsUserFromAuthenticationManager() = runTest { + assertEquals(testUser, userRepository.getAuthenticatedUser()) + } + + @Test + fun getSignInState_returnsFlowFromAuthenticationManager() = runTest { + assertEquals(SignInState.SignedIn(testUser), userRepository.getSignInState().first()) + } + + @Test + fun init_delegatesToAuthenticationManager() { + userRepository.init() + assertTrue(fakeAuthManager.initCalled) + } + + @Test + fun signIn_delegatesToAuthenticationManager() { + userRepository.signIn() + assertTrue(fakeAuthManager.signInCalled) + } + + @Test + fun signOut_delegatesToAuthenticationManager() { + userRepository.signOut() + assertTrue(fakeAuthManager.signOutCalled) + } + + @Test + fun saveUserDetails_savesLocallyAndRefreshesProfileWhenOnline() = runTest { + fakeNetworkManager.setNetworkStatus(NetworkStatus.AVAILABLE) + + userRepository.saveUserDetails(testUser) + + assertEquals(testUser, fakeUserStore.getUser(testUser.id)) + assertEquals(1, fakeRemoteDataStore.refreshUserProfileCount) + } + + @Test + fun saveUserDetails_savesLocallyAndSkipsRemoteRefreshWhenOffline() = runTest { + fakeNetworkManager.setNetworkStatus(NetworkStatus.UNAVAILABLE) + + userRepository.saveUserDetails(testUser) + + assertEquals(testUser, fakeUserStore.getUser(testUser.id)) + assertEquals(0, fakeRemoteDataStore.refreshUserProfileCount) + } + + @Test + fun getUser_returnsUserFromLocalUserStore() = runTest { + fakeUserStore.insertOrUpdateUser(testUser) + assertEquals(testUser, userRepository.getUser(testUser.id)) + } + + @Test + fun clearUserData_clearsValueStoreAndLocalDatabaseTables() = runTest { + fakeValueStore.selectedLanguage = "es" + fakeValueStore.isDeferredDeeplinkConsumed = false + + userRepository.clearUserData() + + assertTrue(fakeLocalDatabase.clearAllTablesCalled) + assertEquals("en", fakeValueStore.selectedLanguage) + } + + @Test + fun clearUserData_preservesIsDeferredDeeplinkConsumedWhenTrue() = runTest { + fakeValueStore.isDeferredDeeplinkConsumed = true + + userRepository.clearUserData() + + assertTrue(fakeLocalDatabase.clearAllTablesCalled) + assertTrue(fakeValueStore.isDeferredDeeplinkConsumed) + } + + @Test + fun canUserSubmitData_returnsTrueForPublicSurvey() = runTest { + val survey = + Survey( + id = "s1", + title = "Public Survey", + description = "Description", + jobMap = emptyMap(), + generalAccess = Survey.GeneralAccess.PUBLIC, + ) + fakeSurveyRepository.saveSurvey(survey) + fakeSurveyRepository.activateSurvey("s1") + + assertTrue(userRepository.canUserSubmitData()) + } + + @Test + fun canUserSubmitData_returnsFalseForViewerRole() = runTest { + val survey = + Survey( + id = "s2", + title = "Restricted Survey", + description = "Description", + jobMap = emptyMap(), + acl = mapOf(testUser.email to Role.VIEWER.name.lowercase()), + generalAccess = Survey.GeneralAccess.RESTRICTED, + ) + fakeSurveyRepository.saveSurvey(survey) + fakeSurveyRepository.activateSurvey("s2") + + assertFalse(userRepository.canUserSubmitData()) + } + + @Test + fun canDeleteLoi_returnsTrueForOwnerAndFalseForPredefined() = runTest { + val userLoi = + LocationOfInterest( + id = "loi1", + surveyId = "s1", + created = AuditInfo(testUser), + lastModified = AuditInfo(testUser), + geometry = Point(Coordinates(0.0, 0.0)), + job = Job(id = "job1"), + isPredefined = false, + ) + val predefinedLoi = userLoi.copy(id = "loi2", isPredefined = true) + + assertTrue(userRepository.canDeleteLoi(userLoi)) + assertFalse(userRepository.canDeleteLoi(predefinedLoi)) + } + + @Test + fun userSettings_getAndSetUpdateLocalValueStore() { + val settings = + UserSettings( + language = "fr", + measurementUnits = MeasurementUnits.METRIC, + shouldUploadPhotosOnWifiOnly = true, + ) + + userRepository.setUserSettings(settings) + + assertEquals(settings, userRepository.getUserSettings()) + assertEquals("fr", fakeValueStore.selectedLanguage) + assertEquals(MeasurementUnits.METRIC.name, fakeValueStore.selectedLengthUnit) + assertTrue(fakeValueStore.shouldUploadMediaOverUnmeteredConnectionOnly) + } +} diff --git a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/system/auth/AuthenticationManager.kt b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/system/auth/AuthenticationManager.kt new file mode 100644 index 0000000000..d0872f2868 --- /dev/null +++ b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/system/auth/AuthenticationManager.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 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 org.groundplatform.domain.system.auth + +import kotlinx.coroutines.flow.Flow +import org.groundplatform.domain.model.User +import org.groundplatform.domain.model.auth.SignInState + +interface AuthenticationManager { + val signInState: Flow + + /** Must be called before looking up auth state or logged-in user. */ + fun init() + + fun signIn() + + fun signOut() + + /** Returns the logged-in user. */ + suspend fun getAuthenticatedUser(): User +} diff --git a/core/testing/src/commonMain/kotlin/org/groundplatform/testing/FakeAuthenticationManager.kt b/core/testing/src/commonMain/kotlin/org/groundplatform/testing/FakeAuthenticationManager.kt new file mode 100644 index 0000000000..30efe9ec8c --- /dev/null +++ b/core/testing/src/commonMain/kotlin/org/groundplatform/testing/FakeAuthenticationManager.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2026 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 org.groundplatform.testing + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.groundplatform.domain.model.User +import org.groundplatform.domain.model.auth.SignInState +import org.groundplatform.domain.system.auth.AuthenticationManager + +/** In-memory test double for [AuthenticationManager]. */ +class FakeAuthenticationManager( + var authenticatedUser: User = User("user_id", "user@example.com", "Test User") +) : AuthenticationManager { + private val _signInState = MutableStateFlow(SignInState.SignedIn(authenticatedUser)) + override val signInState: Flow = _signInState.asStateFlow() + + var signInCalled = false + var signOutCalled = false + var initCalled = false + + override fun init() { + initCalled = true + } + + override fun signIn() { + signInCalled = true + _signInState.value = SignInState.SignedIn(authenticatedUser) + } + + override fun signOut() { + signOutCalled = true + _signInState.value = SignInState.SignedOut + } + + override suspend fun getAuthenticatedUser(): User = authenticatedUser +} From 8dc09ba5695c6b671c61a0b7d5a2c924375501a7 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Fri, 28 Aug 2026 08:17:02 +0530 Subject: [PATCH 10/10] refactor: migrate repository dependency injection to provides methods and unify sync and UUID interfaces across modules --- .../data/sync/MutationSyncWorkManager.kt | 8 +- .../android/data/uuid/OfflineUuidGenerator.kt | 13 +- .../android/di/RepositoryModule.kt | 52 ++++- .../android/ui/common/BaseMapViewModel.kt | 1 - .../android/ui/map/gms/GmsExt.kt | 15 -- .../local/LocalLocationOfInterestStoreTest.kt | 1 - .../LocationOfInterestRepositoryTest.kt | 6 +- .../repository/SubmissionRepositoryTest.kt | 5 +- .../polygon/DrawAreaTaskViewModelTest.kt | 1 - .../LocationOfInterestRepository.kt | 28 +-- .../data}/repository/SubmissionRepository.kt | 23 +- .../data/FakeLocalLocationOfInterestStore.kt | 90 ++++++++ .../data/FakeLocalSubmissionStore.kt | 102 +++++++++ .../LocationOfInterestRepositoryTest.kt | 198 ++++++++++++++++++ .../repository/SubmissionRepositoryTest.kt | 187 +++++++++++++++++ .../domain/model/geometry/Geometry.kt | 15 +- .../groundplatform/domain/model/map/Bounds.kt | 13 ++ .../domain/system/sync/MutationSyncManager.kt | 23 ++ .../system/uuid/OfflineUuidGenerator.kt | 30 +++ .../testing/FakeMutationSyncManager.kt | 27 +++ .../testing/FakeOfflineUuidGenerator.kt | 23 ++ 21 files changed, 782 insertions(+), 79 deletions(-) rename {app/src/main/java/org/groundplatform/android => core/data/src/commonMain/kotlin/org/groundplatform/data}/repository/LocationOfInterestRepository.kt (90%) rename {app/src/main/java/org/groundplatform/android => core/data/src/commonMain/kotlin/org/groundplatform/data}/repository/SubmissionRepository.kt (88%) create mode 100644 core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalLocationOfInterestStore.kt create mode 100644 core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalSubmissionStore.kt create mode 100644 core/data/src/commonTest/kotlin/org/groundplatform/data/repository/LocationOfInterestRepositoryTest.kt create mode 100644 core/data/src/commonTest/kotlin/org/groundplatform/data/repository/SubmissionRepositoryTest.kt create mode 100644 core/domain/src/commonMain/kotlin/org/groundplatform/domain/system/sync/MutationSyncManager.kt create mode 100644 core/domain/src/commonMain/kotlin/org/groundplatform/domain/system/uuid/OfflineUuidGenerator.kt create mode 100644 core/testing/src/commonMain/kotlin/org/groundplatform/testing/FakeMutationSyncManager.kt create mode 100644 core/testing/src/commonMain/kotlin/org/groundplatform/testing/FakeOfflineUuidGenerator.kt diff --git a/app/src/main/java/org/groundplatform/android/data/sync/MutationSyncWorkManager.kt b/app/src/main/java/org/groundplatform/android/data/sync/MutationSyncWorkManager.kt index ac593f4e38..b10b315517 100644 --- a/app/src/main/java/org/groundplatform/android/data/sync/MutationSyncWorkManager.kt +++ b/app/src/main/java/org/groundplatform/android/data/sync/MutationSyncWorkManager.kt @@ -18,9 +18,11 @@ package org.groundplatform.android.data.sync import androidx.work.ExistingWorkPolicy import androidx.work.WorkManager import javax.inject.Inject +import org.groundplatform.domain.system.sync.MutationSyncManager /** Enqueues data sync work to be done in the background. */ -class MutationSyncWorkManager @Inject constructor(private val workManager: WorkManager) { +class MutationSyncWorkManager @Inject constructor(private val workManager: WorkManager) : + MutationSyncManager { /** * Enqueues a worker that sends changes made locally to the remote data store once a network @@ -31,7 +33,7 @@ class MutationSyncWorkManager @Inject constructor(private val workManager: WorkM * worker again on each new mutation. This simplifies the worker implementation and avoids race * conditions in the rare event the worker finishes just when new mutations are added to the db. */ - fun enqueueSyncWorker() { + override fun enqueueSync() { val request = WorkRequestBuilder().setWorkerClass(LocalMutationSyncWorker::class.java).buildWorkerRequest() workManager.enqueueUniqueWork( @@ -40,4 +42,6 @@ class MutationSyncWorkManager @Inject constructor(private val workManager: WorkM request, ) } + + fun enqueueSyncWorker() = enqueueSync() } diff --git a/app/src/main/java/org/groundplatform/android/data/uuid/OfflineUuidGenerator.kt b/app/src/main/java/org/groundplatform/android/data/uuid/OfflineUuidGenerator.kt index 82b13a9c91..f818289152 100644 --- a/app/src/main/java/org/groundplatform/android/data/uuid/OfflineUuidGenerator.kt +++ b/app/src/main/java/org/groundplatform/android/data/uuid/OfflineUuidGenerator.kt @@ -15,15 +15,4 @@ */ package org.groundplatform.android.data.uuid -/** - * Generator for unique identifier strings while offline. Implementations must assume the network - * will be unavailable when invoked. - */ -interface OfflineUuidGenerator { - /** - * Returns an identifier that is universally unique for all practical intents and purposes. - * Implementations should ensure that the probability of collision is so small to be considered - * insignificant. - */ - suspend fun generateUuid(): String -} +typealias OfflineUuidGenerator = org.groundplatform.domain.system.uuid.OfflineUuidGenerator diff --git a/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt b/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt index 7b7d21d28b..6c0834a6cf 100644 --- a/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt +++ b/app/src/main/java/org/groundplatform/android/di/RepositoryModule.kt @@ -22,13 +22,14 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import jakarta.inject.Singleton import kotlinx.coroutines.CoroutineScope +import org.groundplatform.android.data.sync.MutationSyncWorkManager import org.groundplatform.android.di.coroutines.ApplicationScope -import org.groundplatform.android.repository.LocationOfInterestRepository import org.groundplatform.android.repository.OfflineAreaRepository -import org.groundplatform.android.repository.SubmissionRepository import org.groundplatform.android.repository.UserMediaRepository +import org.groundplatform.data.repository.LocationOfInterestRepository import org.groundplatform.data.repository.MapStateRepository import org.groundplatform.data.repository.MutationRepository +import org.groundplatform.data.repository.SubmissionRepository import org.groundplatform.data.repository.SurveyRepository import org.groundplatform.data.repository.TermsOfServiceRepository import org.groundplatform.data.repository.UserRepository @@ -51,15 +52,31 @@ import org.groundplatform.domain.repository.UserRepositoryInterface import org.groundplatform.domain.system.CrashLogger import org.groundplatform.domain.system.NetworkManagerInterface import org.groundplatform.domain.system.auth.AuthenticationManager +import org.groundplatform.domain.system.uuid.OfflineUuidGenerator @Module @InstallIn(SingletonComponent::class) -abstract class LocationOfInterestRepositoryModule { - @Binds +object LocationOfInterestRepositoryModule { + @Provides @Singleton - abstract fun bindLocationOfInterestRepository( - impl: LocationOfInterestRepository - ): LocationOfInterestRepositoryInterface + fun provideLocationOfInterestRepository( + localSurveyStore: LocalSurveyStore, + localLoiStore: LocalLocationOfInterestStore, + remoteDataStore: RemoteDataStore, + mutationSyncWorkManager: MutationSyncWorkManager, + userRepository: UserRepositoryInterface, + uuidGenerator: OfflineUuidGenerator, + authenticationManager: AuthenticationManager, + ): LocationOfInterestRepositoryInterface = + LocationOfInterestRepository( + localSurveyStore, + localLoiStore, + remoteDataStore, + mutationSyncWorkManager, + userRepository, + uuidGenerator, + authenticationManager, + ) } @Module @@ -104,10 +121,25 @@ object SurveyRepositoryModule { @Module @InstallIn(SingletonComponent::class) -abstract class SubmissionRepositoryModule { - @Binds +object SubmissionRepositoryModule { + @Provides @Singleton - abstract fun bindSubmissionRepository(impl: SubmissionRepository): SubmissionRepositoryInterface + fun provideSubmissionRepository( + localSubmissionStore: LocalSubmissionStore, + localValueStore: LocalValueStore, + locationOfInterestRepository: LocationOfInterestRepositoryInterface, + mutationSyncWorkManager: MutationSyncWorkManager, + userRepository: UserRepositoryInterface, + uuidGenerator: OfflineUuidGenerator, + ): SubmissionRepositoryInterface = + SubmissionRepository( + localSubmissionStore, + localValueStore, + locationOfInterestRepository, + mutationSyncWorkManager, + userRepository, + uuidGenerator, + ) } @Module diff --git a/app/src/main/java/org/groundplatform/android/ui/common/BaseMapViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/common/BaseMapViewModel.kt index a0228d9ebc..14be4ba81c 100644 --- a/app/src/main/java/org/groundplatform/android/ui/common/BaseMapViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/common/BaseMapViewModel.kt @@ -56,7 +56,6 @@ import org.groundplatform.android.ui.map.Feature import org.groundplatform.android.ui.map.NewCameraPositionViaBounds import org.groundplatform.android.ui.map.NewCameraPositionViaCoordinates import org.groundplatform.android.ui.map.NewCameraPositionViaCoordinatesAndZoomLevel -import org.groundplatform.android.ui.map.gms.GmsExt.contains import org.groundplatform.android.ui.map.gms.GmsExt.toBounds import org.groundplatform.android.ui.map.gms.toCoordinates import org.groundplatform.android.ui.util.getDefaultColor diff --git a/app/src/main/java/org/groundplatform/android/ui/map/gms/GmsExt.kt b/app/src/main/java/org/groundplatform/android/ui/map/gms/GmsExt.kt index 8f1d72ad9f..06c9a2e8ec 100644 --- a/app/src/main/java/org/groundplatform/android/ui/map/gms/GmsExt.kt +++ b/app/src/main/java/org/groundplatform/android/ui/map/gms/GmsExt.kt @@ -29,11 +29,6 @@ import org.groundplatform.domain.model.map.Bounds /** Extensions for indirectly using GMS functions in map-provider agnostic codebase. */ object GmsExt { - fun Bounds.contains(geometry: Geometry): Boolean { - val latLngBounds = toGoogleMapsObject() - return geometry.getShellCoordinates().any { latLngBounds.contains(it.toLatLng()) } - } - fun Bounds.center(): Coordinates = toGoogleMapsObject().center.toModelObject() fun List.toBounds(): Bounds? { @@ -49,16 +44,6 @@ object GmsExt { return null } - /** Returns the list of [Coordinates] in the geometry or in the outer shell of the geometry. */ - fun Geometry.getShellCoordinates(): List = - when (this) { - is Point -> listOf(coordinates) - is LineString -> coordinates - is LinearRing -> coordinates - is Polygon -> getShellCoordinates() - is MultiPolygon -> polygons.flatMap { it.getShellCoordinates() } - } - fun Geometry.area(): Double = when (this) { is Point -> 0.0 diff --git a/app/src/test/java/org/groundplatform/android/data/local/LocalLocationOfInterestStoreTest.kt b/app/src/test/java/org/groundplatform/android/data/local/LocalLocationOfInterestStoreTest.kt index f1963f3082..c90f432850 100644 --- a/app/src/test/java/org/groundplatform/android/data/local/LocalLocationOfInterestStoreTest.kt +++ b/app/src/test/java/org/groundplatform/android/data/local/LocalLocationOfInterestStoreTest.kt @@ -34,7 +34,6 @@ import org.groundplatform.android.data.local.room.dao.MAX_SQL_VARIABLES import org.groundplatform.android.data.local.room.fields.EntityDeletionState import org.groundplatform.android.data.local.room.stores.RoomLocationOfInterestStore import org.groundplatform.android.proto.geometry -import org.groundplatform.android.ui.map.gms.GmsExt.getShellCoordinates import org.groundplatform.data.stores.LocalLocationOfInterestStore import org.groundplatform.data.stores.LocalSubmissionStore import org.groundplatform.data.stores.LocalSurveyStore diff --git a/app/src/test/java/org/groundplatform/android/repository/LocationOfInterestRepositoryTest.kt b/app/src/test/java/org/groundplatform/android/repository/LocationOfInterestRepositoryTest.kt index 0c3b352306..75f572d47b 100644 --- a/app/src/test/java/org/groundplatform/android/repository/LocationOfInterestRepositoryTest.kt +++ b/app/src/test/java/org/groundplatform/android/repository/LocationOfInterestRepositoryTest.kt @@ -128,12 +128,12 @@ class LocationOfInterestRepositoryTest : BaseHiltTest() { fun `apply and enqueue when enqueues worker`() = runWithTestDispatcher { locationOfInterestRepository.applyAndEnqueue(mutation) - verify(mockWorkManager).enqueueSyncWorker() + verify(mockWorkManager).enqueueSync() } @Test fun `apply and enqueue when returns error on worker sync failure`() = runWithTestDispatcher { - `when`(mockWorkManager.enqueueSyncWorker()).thenThrow(Error()) + `when`(mockWorkManager.enqueueSync()).thenThrow(Error()) assertFailsWith { locationOfInterestRepository.applyAndEnqueue( @@ -141,7 +141,7 @@ class LocationOfInterestRepositoryTest : BaseHiltTest() { ) } - verify(mockWorkManager, times(1)).enqueueSyncWorker() + verify(mockWorkManager, times(1)).enqueueSync() } @Test diff --git a/app/src/test/java/org/groundplatform/android/repository/SubmissionRepositoryTest.kt b/app/src/test/java/org/groundplatform/android/repository/SubmissionRepositoryTest.kt index 706e6c700c..bcd579ea41 100644 --- a/app/src/test/java/org/groundplatform/android/repository/SubmissionRepositoryTest.kt +++ b/app/src/test/java/org/groundplatform/android/repository/SubmissionRepositoryTest.kt @@ -19,6 +19,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.groundplatform.android.data.sync.MutationSyncWorkManager import org.groundplatform.android.data.uuid.OfflineUuidGenerator +import org.groundplatform.data.repository.SubmissionRepository import org.groundplatform.data.stores.LocalSubmissionStore import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.domain.model.User @@ -94,7 +95,7 @@ class SubmissionRepositoryTest { assertThat(userId).isEqualTo(TEST_USER.id) assertThat(collectionId).isEqualTo(collectionId) } - verify(mutationSyncWorkManager).enqueueSyncWorker() + verify(mutationSyncWorkManager).enqueueSync() } @Test @@ -109,7 +110,7 @@ class SubmissionRepositoryTest { ) verify(localSubmissionStore, never()).applyAndEnqueue(any()) - verify(mutationSyncWorkManager, never()).enqueueSyncWorker() + verify(mutationSyncWorkManager, never()).enqueueSync() } @Test diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModelTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModelTest.kt index 5fa3ac7c19..6b8f3089d5 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModelTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/tasks/polygon/DrawAreaTaskViewModelTest.kt @@ -41,7 +41,6 @@ import org.groundplatform.android.ui.datacollection.components.ButtonAction import org.groundplatform.android.ui.datacollection.tasks.TaskPositionInterface import org.groundplatform.android.ui.datacollection.tasks.polygon.PolygonDrawingSession.Companion.DISTANCE_THRESHOLD_DP import org.groundplatform.android.ui.map.Feature -import org.groundplatform.android.ui.map.gms.GmsExt.getShellCoordinates import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.domain.model.geometry.Coordinates import org.groundplatform.domain.model.geometry.LineString diff --git a/app/src/main/java/org/groundplatform/android/repository/LocationOfInterestRepository.kt b/core/data/src/commonMain/kotlin/org/groundplatform/data/repository/LocationOfInterestRepository.kt similarity index 90% rename from app/src/main/java/org/groundplatform/android/repository/LocationOfInterestRepository.kt rename to core/data/src/commonMain/kotlin/org/groundplatform/data/repository/LocationOfInterestRepository.kt index 4215235179..f860b03841 100644 --- a/app/src/main/java/org/groundplatform/android/repository/LocationOfInterestRepository.kt +++ b/core/data/src/commonMain/kotlin/org/groundplatform/data/repository/LocationOfInterestRepository.kt @@ -13,18 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.android.repository +package org.groundplatform.data.repository -import javax.inject.Inject -import javax.inject.Singleton +import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map -import org.groundplatform.android.data.sync.MutationSyncWorkManager -import org.groundplatform.android.data.uuid.OfflineUuidGenerator -import org.groundplatform.android.system.auth.AuthenticationManager -import org.groundplatform.android.ui.map.gms.GmsExt.contains import org.groundplatform.data.stores.LocalLocationOfInterestStore import org.groundplatform.data.stores.LocalSurveyStore import org.groundplatform.data.stores.RemoteDataStore @@ -40,21 +35,20 @@ import org.groundplatform.domain.model.mutation.Mutation import org.groundplatform.domain.model.mutation.Mutation.SyncStatus import org.groundplatform.domain.repository.LocationOfInterestRepositoryInterface import org.groundplatform.domain.repository.UserRepositoryInterface -import timber.log.Timber +import org.groundplatform.domain.system.auth.AuthenticationManager +import org.groundplatform.domain.system.sync.MutationSyncManager +import org.groundplatform.domain.system.uuid.OfflineUuidGenerator /** * Coordinates persistence and retrieval of [LocationOfInterest] instances from remote, local, and * in memory data stores. For more details on this pattern and overall architecture, see * https://developer.android.com/jetpack/docs/guide. */ -@Singleton -class LocationOfInterestRepository -@Inject -constructor( +class LocationOfInterestRepository( private val localSurveyStore: LocalSurveyStore, private val localLoiStore: LocalLocationOfInterestStore, private val remoteDataStore: RemoteDataStore, - private val mutationSyncWorkManager: MutationSyncWorkManager, + private val mutationSyncManager: MutationSyncManager, private val userRepository: UserRepositoryInterface, private val uuidGenerator: OfflineUuidGenerator, private val authenticationManager: AuthenticationManager, @@ -104,9 +98,9 @@ constructor( val locationOfInterest = survey?.let { localLoiStore.getLocationOfInterest(it, loiId) } if (survey == null) { - Timber.e("Survey not found: $surveyId") + Logger.e { "Survey not found: $surveyId" } } else if (locationOfInterest == null) { - Timber.e("LOI not found for survey $surveyId: LOI ID $loiId") + Logger.e { "LOI not found for survey $surveyId: LOI ID $loiId" } } return locationOfInterest } @@ -164,7 +158,7 @@ constructor( } localLoiStore.applyAndEnqueue(mutation) - mutationSyncWorkManager.enqueueSyncWorker() + mutationSyncManager.enqueueSync() } override suspend fun hasValidLois(surveyId: String): Boolean = @@ -177,7 +171,7 @@ constructor( .filter { loi -> val isValid = !loi.geometry.isEmpty() if (!isValid) { - Timber.w("Filtering out LOI ${loi.id} with empty coordinates: $loi") + Logger.w { "Filtering out LOI ${loi.id} with empty coordinates: $loi" } } isValid } diff --git a/app/src/main/java/org/groundplatform/android/repository/SubmissionRepository.kt b/core/data/src/commonMain/kotlin/org/groundplatform/data/repository/SubmissionRepository.kt similarity index 88% rename from app/src/main/java/org/groundplatform/android/repository/SubmissionRepository.kt rename to core/data/src/commonMain/kotlin/org/groundplatform/data/repository/SubmissionRepository.kt index aca5bdd49a..0a76d86e11 100644 --- a/app/src/main/java/org/groundplatform/android/repository/SubmissionRepository.kt +++ b/core/data/src/commonMain/kotlin/org/groundplatform/data/repository/SubmissionRepository.kt @@ -13,12 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.groundplatform.android.repository +package org.groundplatform.data.repository -import javax.inject.Inject -import javax.inject.Singleton -import org.groundplatform.android.data.sync.MutationSyncWorkManager -import org.groundplatform.android.data.uuid.OfflineUuidGenerator +import co.touchlab.kermit.Logger import org.groundplatform.data.stores.LocalSubmissionStore import org.groundplatform.data.stores.LocalValueStore import org.groundplatform.domain.model.Survey @@ -31,16 +28,14 @@ import org.groundplatform.domain.model.submission.ValueDelta import org.groundplatform.domain.repository.LocationOfInterestRepositoryInterface import org.groundplatform.domain.repository.SubmissionRepositoryInterface import org.groundplatform.domain.repository.UserRepositoryInterface -import timber.log.Timber +import org.groundplatform.domain.system.sync.MutationSyncManager +import org.groundplatform.domain.system.uuid.OfflineUuidGenerator -@Singleton -class SubmissionRepository -@Inject -constructor( +class SubmissionRepository( private val localSubmissionStore: LocalSubmissionStore, private val localValueStore: LocalValueStore, private val locationOfInterestRepository: LocationOfInterestRepositoryInterface, - private val mutationSyncWorkManager: MutationSyncWorkManager, + private val mutationSyncManager: MutationSyncManager, private val userRepository: UserRepositoryInterface, private val uuidGenerator: OfflineUuidGenerator, ) : SubmissionRepositoryInterface { @@ -67,7 +62,7 @@ constructor( collectionId = collectionId, ) applyAndEnqueue(mutation) - } ?: run { Timber.w("Job not found for survey $surveyId and LOI $locationOfInterestId") } + } ?: run { Logger.w { "Job not found for survey $surveyId and LOI $locationOfInterestId" } } } override suspend fun getDraftSubmission(survey: Survey): DraftSubmission? { @@ -76,7 +71,7 @@ constructor( if (draftId.isNullOrEmpty()) null else localSubmissionStore.getDraftSubmission(draftSubmissionId = draftId, survey = survey) if (draft != null && draft.surveyId != survey.id) { - Timber.e("Skipping draft submission, survey id doesn't match") + Logger.e { "Skipping draft submission, survey id doesn't match" } return null } return draft @@ -112,7 +107,7 @@ constructor( private suspend fun applyAndEnqueue(mutation: SubmissionMutation) { localSubmissionStore.applyAndEnqueue(mutation) - mutationSyncWorkManager.enqueueSyncWorker() + mutationSyncManager.enqueueSync() } override suspend fun getTotalSubmissionCount(loi: LocationOfInterest) = diff --git a/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalLocationOfInterestStore.kt b/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalLocationOfInterestStore.kt new file mode 100644 index 0000000000..1175353c0b --- /dev/null +++ b/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalLocationOfInterestStore.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2026 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 org.groundplatform.data + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.map +import org.groundplatform.data.stores.LocalLocationOfInterestStore +import org.groundplatform.domain.model.Survey +import org.groundplatform.domain.model.locationofinterest.LocationOfInterest +import org.groundplatform.domain.model.mutation.LocationOfInterestMutation +import org.groundplatform.domain.model.mutation.Mutation.SyncStatus + +class FakeLocalLocationOfInterestStore : LocalLocationOfInterestStore { + val appliedMutations = mutableListOf() + val loisFlow = MutableStateFlow>(emptyMap()) + + override suspend fun getLoiCount(surveyId: String): Int = + loisFlow.value.values.count { it.surveyId == surveyId } + + override fun getValidLois(survey: Survey): Flow> = loisFlow.map { + it.values.filter { loi -> loi.surveyId == survey.id }.toSet() + } + + override suspend fun getLocationOfInterest( + survey: Survey, + locationOfInterestId: String, + ): LocationOfInterest? = loisFlow.value[locationOfInterestId]?.takeIf { it.surveyId == survey.id } + + override suspend fun deleteLocationOfInterest(locationOfInterestId: String) { + loisFlow.value = loisFlow.value - locationOfInterestId + } + + override fun getAllSurveyMutations(survey: Survey): Flow> = + emptyFlow() + + override fun getAllMutationsFlow(): Flow> = emptyFlow() + + override suspend fun findByLocationOfInterestId( + id: String, + vararg states: SyncStatus, + ): List = emptyList() + + override suspend fun insertOrUpdate(loi: LocationOfInterest) { + loisFlow.value = loisFlow.value + (loi.id to loi) + } + + override suspend fun insertOrUpdateAll(lois: List) { + loisFlow.value = loisFlow.value + lois.associateBy { it.id } + } + + override suspend fun deleteNotIn(surveyId: String, ids: List) { + val idsSet = ids.toSet() + loisFlow.value = + loisFlow.value.filterNot { (id, loi) -> loi.surveyId == surveyId && id !in idsSet } + } + + override suspend fun merge(model: LocationOfInterest) = insertOrUpdate(model) + + override suspend fun enqueue(mutation: LocationOfInterestMutation) { + appliedMutations.add(mutation) + } + + override suspend fun apply(mutation: LocationOfInterestMutation) { + appliedMutations.add(mutation) + } + + override suspend fun updateAll(mutations: List) { + appliedMutations.addAll(mutations) + } + + override suspend fun applyAndEnqueue(mutation: LocationOfInterestMutation) { + appliedMutations.add(mutation) + } +} diff --git a/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalSubmissionStore.kt b/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalSubmissionStore.kt new file mode 100644 index 0000000000..f2e57adc50 --- /dev/null +++ b/core/data/src/commonTest/kotlin/org/groundplatform/data/FakeLocalSubmissionStore.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2026 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 org.groundplatform.data + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import org.groundplatform.data.stores.LocalSubmissionStore +import org.groundplatform.domain.model.Survey +import org.groundplatform.domain.model.locationofinterest.LocationOfInterest +import org.groundplatform.domain.model.mutation.Mutation.SyncStatus +import org.groundplatform.domain.model.mutation.SubmissionMutation +import org.groundplatform.domain.model.submission.DraftSubmission +import org.groundplatform.domain.model.submission.Submission + +class FakeLocalSubmissionStore : LocalSubmissionStore { + val appliedMutations = mutableListOf() + val draftSubmissions = mutableMapOf() + var pendingCreateCount: Int = 0 + var pendingDeleteCount: Int = 0 + var submissionsToReturn: List = emptyList() + + override suspend fun getSubmissions( + locationOfInterest: LocationOfInterest, + jobId: String, + ): List = submissionsToReturn + + override suspend fun getSubmission( + locationOfInterest: LocationOfInterest, + submissionId: String, + ): Submission = submissionsToReturn.first { it.id == submissionId } + + override suspend fun deleteSubmission(submissionId: String) { + submissionsToReturn = submissionsToReturn.filterNot { it.id == submissionId } + } + + override fun getSubmissionMutationsByLoiIdFlow( + survey: Survey, + locationOfInterestId: String, + vararg allowedStates: SyncStatus, + ): Flow> = emptyFlow() + + override fun getAllSurveyMutationsFlow(survey: Survey): Flow> = + emptyFlow() + + override fun getAllMutationsFlow(): Flow> = emptyFlow() + + override suspend fun findByLocationOfInterestId( + loidId: String, + vararg states: SyncStatus, + ): List = emptyList() + + override suspend fun getPendingCreateCount(loiId: String): Int = pendingCreateCount + + override suspend fun getPendingDeleteCount(loiId: String): Int = pendingDeleteCount + + override suspend fun getDraftSubmission( + draftSubmissionId: String, + survey: Survey, + ): DraftSubmission? = draftSubmissions[draftSubmissionId]?.takeIf { it.surveyId == survey.id } + + override suspend fun saveDraftSubmission(draftSubmission: DraftSubmission) { + draftSubmissions[draftSubmission.id] = draftSubmission + } + + override suspend fun deleteDraftSubmissions() { + draftSubmissions.clear() + } + + override suspend fun countDraftSubmissions(): Int = draftSubmissions.size + + override suspend fun merge(model: Submission) = Unit + + override suspend fun enqueue(mutation: SubmissionMutation) { + appliedMutations.add(mutation) + } + + override suspend fun apply(mutation: SubmissionMutation) { + appliedMutations.add(mutation) + } + + override suspend fun updateAll(mutations: List) { + appliedMutations.addAll(mutations) + } + + override suspend fun applyAndEnqueue(mutation: SubmissionMutation) { + appliedMutations.add(mutation) + } +} diff --git a/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/LocationOfInterestRepositoryTest.kt b/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/LocationOfInterestRepositoryTest.kt new file mode 100644 index 0000000000..3933c2c959 --- /dev/null +++ b/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/LocationOfInterestRepositoryTest.kt @@ -0,0 +1,198 @@ +/* + * Copyright 2026 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 org.groundplatform.data.repository + +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.groundplatform.data.FakeLocalLocationOfInterestStore +import org.groundplatform.data.FakeLocalSurveyStore +import org.groundplatform.data.FakeRemoteDataStore +import org.groundplatform.domain.model.geometry.Coordinates +import org.groundplatform.domain.model.geometry.Point +import org.groundplatform.domain.model.job.Job +import org.groundplatform.domain.model.locationofinterest.AuditInfo +import org.groundplatform.domain.model.map.Bounds +import org.groundplatform.domain.model.mutation.Mutation +import org.groundplatform.testing.FakeAuthenticationManager +import org.groundplatform.testing.FakeDataGenerator +import org.groundplatform.testing.FakeMutationSyncManager +import org.groundplatform.testing.FakeOfflineUuidGenerator +import org.groundplatform.testing.FakeUserRepository + +class LocationOfInterestRepositoryTest { + + private val fakeSurveyStore = FakeLocalSurveyStore() + private val fakeLoiStore = FakeLocalLocationOfInterestStore() + private val fakeRemoteDataStore = FakeRemoteDataStore() + private val fakeMutationSyncManager = FakeMutationSyncManager() + private val fakeUserRepository = FakeUserRepository() + private val fakeUuidGenerator = FakeOfflineUuidGenerator("test-loi-id") + private val fakeAuthenticationManager = FakeAuthenticationManager() + + private lateinit var loiRepository: LocationOfInterestRepository + + @BeforeTest + fun setUp() { + loiRepository = + LocationOfInterestRepository( + localSurveyStore = fakeSurveyStore, + localLoiStore = fakeLoiStore, + remoteDataStore = fakeRemoteDataStore, + mutationSyncManager = fakeMutationSyncManager, + userRepository = fakeUserRepository, + uuidGenerator = fakeUuidGenerator, + authenticationManager = fakeAuthenticationManager, + ) + } + + @Test + fun saveLoi_createsMutationAndEnqueuesSync() = runTest { + val point = Point(Coordinates(10.0, 20.0)) + val job = Job(id = "job1") + + val loiId = + loiRepository.saveLoi( + geometry = point, + job = job, + surveyId = "survey1", + loiName = "My LOI", + collectionId = "c1", + ) + + assertEquals("test-loi-id", loiId) + assertEquals(1, fakeLoiStore.appliedMutations.size) + val mutation = fakeLoiStore.appliedMutations.first() + assertEquals("test-loi-id", mutation.locationOfInterestId) + assertEquals(Mutation.Type.CREATE, mutation.type) + assertEquals(1, fakeMutationSyncManager.enqueueSyncCount) + } + + @Test + fun getOfflineLoi_returnsLoiWhenFound() = runTest { + val survey = FakeDataGenerator.newSurvey(id = "survey1") + val loi = FakeDataGenerator.newLocationOfInterest(id = "loi1", surveyId = "survey1") + fakeSurveyStore.insertOrUpdateSurvey(survey) + fakeLoiStore.insertOrUpdate(loi) + + val result = loiRepository.getOfflineLoi("survey1", "loi1") + + assertEquals(loi, result) + } + + @Test + fun getOfflineLoi_returnsNullWhenNotFound() = runTest { + val survey = FakeDataGenerator.newSurvey(id = "survey1") + fakeSurveyStore.insertOrUpdateSurvey(survey) + + val result = loiRepository.getOfflineLoi("survey1", "nonexistent") + + assertNull(result) + } + + @Test + fun hasValidLois_returnsTrueWhenCountPositive() = runTest { + val loi = FakeDataGenerator.newLocationOfInterest(id = "loi1", surveyId = "survey1") + fakeLoiStore.insertOrUpdate(loi) + + assertTrue(loiRepository.hasValidLois("survey1")) + } + + @Test + fun hasValidLois_returnsFalseWhenCountZero() = runTest { + assertFalse(loiRepository.hasValidLois("survey1")) + } + + @Test + fun getWithinBounds_filtersLoisWithinBounds() = runTest { + val survey = FakeDataGenerator.newSurvey(id = "survey1") + val insideLoi = + FakeDataGenerator.newLocationOfInterest( + id = "loi-inside", + surveyId = "survey1", + geometry = Point(Coordinates(10.0, 10.0)), + ) + val outsideLoi = + FakeDataGenerator.newLocationOfInterest( + id = "loi-outside", + surveyId = "survey1", + geometry = Point(Coordinates(50.0, 50.0)), + ) + fakeLoiStore.insertOrUpdate(insideLoi) + fakeLoiStore.insertOrUpdate(outsideLoi) + + val bounds = Bounds(southwest = Coordinates(0.0, 0.0), northeast = Coordinates(20.0, 20.0)) + val result = loiRepository.getWithinBounds(survey, bounds).first() + + assertEquals(listOf(insideLoi), result) + } + + @Test + fun deleteLoi_failsForPredefinedLoi() = runTest { + val loi = + FakeDataGenerator.newLocationOfInterest(id = "loi1", surveyId = "survey1") + .copy(isPredefined = true) + + assertFailsWith { loiRepository.deleteLoi(loi) } + } + + @Test + fun deleteLoi_failsWhenUserNotOwnerOrOrganizer() = runTest { + val owner = FakeDataGenerator.newUser(id = "owner1") + val otherUser = FakeDataGenerator.newUser(id = "other1") + fakeUserRepository.currentUser = otherUser + + val loi = + FakeDataGenerator.newLocationOfInterest( + id = "loi1", + surveyId = "survey1", + created = AuditInfo(owner), + ) + val survey = FakeDataGenerator.newSurvey(id = "survey1", acl = emptyMap()) + fakeSurveyStore.insertOrUpdateSurvey(survey) + + assertFailsWith { loiRepository.deleteLoi(loi) } + } + + @Test + fun deleteLoi_createsDeleteMutationAndEnqueuesSync() = runTest { + val user = FakeDataGenerator.newUser(id = "user1") + fakeUserRepository.currentUser = user + + val loi = + FakeDataGenerator.newLocationOfInterest( + id = "loi1", + surveyId = "survey1", + created = AuditInfo(user), + ) + val survey = FakeDataGenerator.newSurvey(id = "survey1") + fakeSurveyStore.insertOrUpdateSurvey(survey) + + loiRepository.deleteLoi(loi) + + assertEquals(1, fakeLoiStore.appliedMutations.size) + val mutation = fakeLoiStore.appliedMutations.first() + assertEquals(Mutation.Type.DELETE, mutation.type) + assertEquals(1, fakeMutationSyncManager.enqueueSyncCount) + } +} diff --git a/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/SubmissionRepositoryTest.kt b/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/SubmissionRepositoryTest.kt new file mode 100644 index 0000000000..ebe1e5972e --- /dev/null +++ b/core/data/src/commonTest/kotlin/org/groundplatform/data/repository/SubmissionRepositoryTest.kt @@ -0,0 +1,187 @@ +/* + * Copyright 2026 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 org.groundplatform.data.repository + +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlinx.coroutines.test.runTest +import org.groundplatform.data.FakeLocalSubmissionStore +import org.groundplatform.data.FakeLocalValueStore +import org.groundplatform.domain.model.Survey +import org.groundplatform.domain.model.job.Job +import org.groundplatform.domain.model.mutation.Mutation +import org.groundplatform.domain.model.submission.DraftSubmission +import org.groundplatform.domain.model.submission.ValueDelta +import org.groundplatform.domain.model.task.Task +import org.groundplatform.testing.FakeDataGenerator +import org.groundplatform.testing.FakeLocationOfInterestRepository +import org.groundplatform.testing.FakeMutationSyncManager +import org.groundplatform.testing.FakeOfflineUuidGenerator +import org.groundplatform.testing.FakeUserRepository + +class SubmissionRepositoryTest { + + private val fakeSubmissionStore = FakeLocalSubmissionStore() + private val fakeValueStore = FakeLocalValueStore() + private val fakeLoiRepository = FakeLocationOfInterestRepository() + private val fakeMutationSyncManager = FakeMutationSyncManager() + private val fakeUserRepository = FakeUserRepository() + private val fakeUuidGenerator = FakeOfflineUuidGenerator("test-uuid") + + private lateinit var submissionRepository: SubmissionRepository + + @BeforeTest + fun setUp() { + submissionRepository = + SubmissionRepository( + localSubmissionStore = fakeSubmissionStore, + localValueStore = fakeValueStore, + locationOfInterestRepository = fakeLoiRepository, + mutationSyncManager = fakeMutationSyncManager, + userRepository = fakeUserRepository, + uuidGenerator = fakeUuidGenerator, + ) + } + + @Test + fun saveSubmission_createsMutationAndEnqueuesSync() = runTest { + val loi = FakeDataGenerator.newLocationOfInterest(job = Job(id = "job1")) + fakeLoiRepository.offlineLoi = loi + + submissionRepository.saveSubmission( + surveyId = "s1", + locationOfInterestId = loi.id, + deltas = listOf(ValueDelta("task1", Task.Type.TEXT, null)), + collectionId = "c1", + ) + + assertEquals(1, fakeSubmissionStore.appliedMutations.size) + val mutation = fakeSubmissionStore.appliedMutations.first() + assertEquals("test-uuid", mutation.submissionId) + assertEquals(Mutation.Type.CREATE, mutation.type) + assertEquals(1, fakeMutationSyncManager.enqueueSyncCount) + } + + @Test + fun getDraftSubmission_returnsDraftWhenSurveyMatches() = runTest { + val survey = + Survey( + id = "s1", + title = "Survey 1", + description = "Description", + jobMap = emptyMap(), + generalAccess = Survey.GeneralAccess.PUBLIC, + ) + val draft = + DraftSubmission( + id = "draft1", + jobId = "job1", + loiId = "loi1", + loiName = "LOI 1", + surveyId = "s1", + deltas = emptyList(), + currentTaskId = "task1", + ) + fakeValueStore.draftSubmissionId = "draft1" + fakeSubmissionStore.saveDraftSubmission(draft) + + val result = submissionRepository.getDraftSubmission(survey) + + assertEquals(draft, result) + } + + @Test + fun getDraftSubmission_returnsNullWhenSurveyMismatch() = runTest { + val survey1 = + Survey( + id = "s1", + title = "Survey 1", + description = "Description", + jobMap = emptyMap(), + generalAccess = Survey.GeneralAccess.PUBLIC, + ) + val draft = + DraftSubmission( + id = "draft1", + jobId = "job1", + loiId = "loi1", + loiName = "LOI 1", + surveyId = "other_survey", + deltas = emptyList(), + currentTaskId = "task1", + ) + fakeValueStore.draftSubmissionId = "draft1" + fakeSubmissionStore.saveDraftSubmission(draft) + + val result = submissionRepository.getDraftSubmission(survey1) + + assertNull(result) + } + + @Test + fun saveDraftSubmission_savesDraftAndSetsIdInValueStore() = runTest { + fakeUuidGenerator.nextUuid = "draft-123" + + submissionRepository.saveDraftSubmission( + jobId = "job1", + loiId = "loi1", + surveyId = "s1", + deltas = emptyList(), + loiName = "Test LOI", + currentTaskId = "task1", + ) + + assertEquals("draft-123", fakeValueStore.draftSubmissionId) + assertEquals(1, fakeSubmissionStore.countDraftSubmissions()) + } + + @Test + fun deleteDraftSubmission_deletesFromStoreAndClearsValueStore() = runTest { + fakeValueStore.draftSubmissionId = "draft-123" + fakeSubmissionStore.saveDraftSubmission( + DraftSubmission( + id = "draft-123", + jobId = "job1", + loiId = "loi1", + loiName = "LOI", + surveyId = "s1", + deltas = emptyList(), + currentTaskId = "task1", + ) + ) + + submissionRepository.deleteDraftSubmission() + + assertNull(fakeValueStore.draftSubmissionId) + assertEquals(0, fakeSubmissionStore.countDraftSubmissions()) + } + + @Test + fun getTotalSubmissionCount_calculatesCountCorrectly() = runTest { + val loi = + FakeDataGenerator.newLocationOfInterest(id = "loi1", job = Job(id = "job1")) + .copy(submissionCount = 5) + fakeSubmissionStore.pendingCreateCount = 3 + fakeSubmissionStore.pendingDeleteCount = 1 + + val total = submissionRepository.getTotalSubmissionCount(loi) + + assertEquals(7, total) + } +} diff --git a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/geometry/Geometry.kt b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/geometry/Geometry.kt index c03ca88c61..ac45b2177b 100644 --- a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/geometry/Geometry.kt +++ b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/geometry/Geometry.kt @@ -31,6 +31,9 @@ sealed interface Geometry { /** Returns true if there are one or more vertices in the geometry. */ fun isEmpty(): Boolean + /** Returns the list of [Coordinates] in the geometry or in the outer shell of the geometry. */ + fun getShellCoordinates(): List + /** Validates that the current [Geometry] is well-formed. */ fun validate() { // default no-op implementation @@ -49,7 +52,7 @@ data class Polygon(val shell: LinearRing, val holes: List = listOf() override fun isEmpty() = shell.isEmpty() - fun getShellCoordinates() = shell.coordinates + override fun getShellCoordinates(): List = shell.coordinates } /** Represents a single point. */ @@ -60,6 +63,8 @@ data class Point(val coordinates: Coordinates) : Geometry { override fun center(): Coordinates = coordinates override fun isEmpty() = false + + override fun getShellCoordinates(): List = listOf(coordinates) } /** A collection of [Polygon]s. */ @@ -70,6 +75,10 @@ data class MultiPolygon(val polygons: List) : Geometry { override fun center(): Coordinates = polygons.map { it.center() }.centerOrError() override fun isEmpty() = polygons.all { it.isEmpty() } + + override fun getShellCoordinates(): List = polygons.flatMap { + it.getShellCoordinates() + } } /** A sequence of two or more vertices modelling an OCG style line string. */ @@ -81,6 +90,8 @@ data class LineString(val coordinates: List) : Geometry { override fun isEmpty() = coordinates.isEmpty() + override fun getShellCoordinates(): List = coordinates + fun isClosed(): Boolean = isClosed(coordinates) companion object { @@ -104,6 +115,8 @@ data class LinearRing(val coordinates: List) : Geometry { override fun isEmpty() = coordinates.isEmpty() + override fun getShellCoordinates(): List = coordinates + override fun validate() { // TODO: Check for vertices count > 3 // Issue URL: https://github.com/google/ground-android/issues/1647 diff --git a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/map/Bounds.kt b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/map/Bounds.kt index a03aeb2db8..aabdf49e1f 100644 --- a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/map/Bounds.kt +++ b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/map/Bounds.kt @@ -16,6 +16,7 @@ package org.groundplatform.domain.model.map import org.groundplatform.domain.model.geometry.Coordinates +import org.groundplatform.domain.model.geometry.Geometry /** * Represents a rectangular bound on a map. A bounds may be constructed using only southwest and @@ -67,4 +68,16 @@ data class Bounds(val southwest: Coordinates, val northeast: Coordinates) { Coordinates(north - latOffset, east - lngOffset), ) } + + /** Returns true if the given [Coordinates] is within these bounds. */ + fun contains(coordinates: Coordinates): Boolean { + val lat = coordinates.lat + val lng = coordinates.lng + val latInRange = lat in south..north + val lngInRange = if (west <= east) lng in west..east else lng >= west || lng <= east + return latInRange && lngInRange + } + + /** Returns true if any vertex of the given [Geometry] is within these bounds. */ + fun contains(geometry: Geometry): Boolean = geometry.getShellCoordinates().any { contains(it) } } diff --git a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/system/sync/MutationSyncManager.kt b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/system/sync/MutationSyncManager.kt new file mode 100644 index 0000000000..4af6a213ae --- /dev/null +++ b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/system/sync/MutationSyncManager.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2026 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 org.groundplatform.domain.system.sync + +/** Coordinates background synchronization of pending mutations. */ +interface MutationSyncManager { + /** Enqueues or schedules background synchronization of pending mutations. */ + fun enqueueSync() +} diff --git a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/system/uuid/OfflineUuidGenerator.kt b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/system/uuid/OfflineUuidGenerator.kt new file mode 100644 index 0000000000..7812958028 --- /dev/null +++ b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/system/uuid/OfflineUuidGenerator.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2026 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 org.groundplatform.domain.system.uuid + +/** + * Generator for unique identifier strings while offline. Implementations must assume the network + * will be unavailable when invoked. + */ +interface OfflineUuidGenerator { + /** + * Returns an identifier that is universally unique for all practical intents and purposes. + * Implementations should ensure that the probability of collision is so small to be considered + * insignificant. + */ + suspend fun generateUuid(): String +} diff --git a/core/testing/src/commonMain/kotlin/org/groundplatform/testing/FakeMutationSyncManager.kt b/core/testing/src/commonMain/kotlin/org/groundplatform/testing/FakeMutationSyncManager.kt new file mode 100644 index 0000000000..0a8b6908da --- /dev/null +++ b/core/testing/src/commonMain/kotlin/org/groundplatform/testing/FakeMutationSyncManager.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2026 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 org.groundplatform.testing + +import org.groundplatform.domain.system.sync.MutationSyncManager + +class FakeMutationSyncManager : MutationSyncManager { + var enqueueSyncCount = 0 + + override fun enqueueSync() { + enqueueSyncCount++ + } +} diff --git a/core/testing/src/commonMain/kotlin/org/groundplatform/testing/FakeOfflineUuidGenerator.kt b/core/testing/src/commonMain/kotlin/org/groundplatform/testing/FakeOfflineUuidGenerator.kt new file mode 100644 index 0000000000..24cf85f18e --- /dev/null +++ b/core/testing/src/commonMain/kotlin/org/groundplatform/testing/FakeOfflineUuidGenerator.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2026 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 org.groundplatform.testing + +import org.groundplatform.domain.system.uuid.OfflineUuidGenerator + +class FakeOfflineUuidGenerator(var nextUuid: String = "uuid") : OfflineUuidGenerator { + override suspend fun generateUuid(): String = nextUuid +}