Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() }

Expand All @@ -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) }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,92 +23,80 @@ 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
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()
}
set(id) = allowThreadDiskWrites {
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)
}
set(value) = allowThreadDiskReads {
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)
}
set(value) = allowThreadDiskReads {
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
Expand All @@ -117,28 +105,27 @@ 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)
}
set(value) = allowThreadDiskWrites {
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(
Expand All @@ -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)
Expand All @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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 -> {
Expand All @@ -128,6 +128,16 @@ internal object ValueJsonConverter {
}
}

private fun <T : Any> 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<String> {
val list: MutableList<String> = ArrayList(jsonArray.length())
for (i in 0 until jsonArray.length()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@ interface LocationOfInterestMutationDao : BaseDao<LocationOfInterestMutationEnti
)
suspend fun getMutations(
locationOfInterestId: String,
vararg allowedStates: MutationEntitySyncStatus,
allowedStates: List<MutationEntitySyncStatus>,
): List<LocationOfInterestMutationEntity>
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ interface SubmissionMutationDao : BaseDao<SubmissionMutationEntity> {
)
suspend fun findByLocationOfInterestId(
locationOfInterestId: String,
vararg allowedStates: MutationEntitySyncStatus,
allowedStates: List<MutationEntitySyncStatus>,
): List<SubmissionMutationEntity>

@Query(
Expand Down Expand Up @@ -63,6 +63,6 @@ interface SubmissionMutationDao : BaseDao<SubmissionMutationEntity> {
)
fun findByLoiIdFlow(
locationOfInterestId: String,
vararg allowedStates: MutationEntitySyncStatus,
allowedStates: List<MutationEntitySyncStatus>,
): Flow<List<SubmissionMutationEntity>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -126,9 +126,11 @@ class RoomLocationOfInterestStore @Inject internal constructor() : LocalLocation

override suspend fun findByLocationOfInterestId(
id: String,
vararg states: MutationEntitySyncStatus,
): List<LocationOfInterestMutationEntity> =
locationOfInterestMutationDao.getMutations(id, *states)
vararg states: Mutation.SyncStatus,
): List<LocationOfInterestMutation> =
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" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading