diff --git a/ground/src/main/java/com/google/android/ground/model/job/Job.kt b/ground/src/main/java/com/google/android/ground/model/job/Job.kt index e1d2aaaaf5..8a6f5e65e5 100644 --- a/ground/src/main/java/com/google/android/ground/model/job/Job.kt +++ b/ground/src/main/java/com/google/android/ground/model/job/Job.kt @@ -15,7 +15,10 @@ */ package com.google.android.ground.model.job +import android.graphics.Color import com.google.android.ground.model.task.Task +import java.lang.IllegalArgumentException +import timber.log.Timber /** * @param suggestLoiTaskType the type of task used to suggest the LOI for this Job. Null if the job @@ -23,7 +26,7 @@ import com.google.android.ground.model.task.Task */ data class Job( val id: String, - val style: Style, + val style: Style? = null, val name: String? = null, val tasks: Map = mapOf(), val suggestLoiTaskType: Task.Type? = null, @@ -35,3 +38,11 @@ data class Job( fun hasData(): Boolean = tasks.isNotEmpty() } + +fun Job.getDefaultColor(): Int = + try { + Color.parseColor(style?.color ?: "") + } catch (e: IllegalArgumentException) { + Timber.w(e, "Invalid or missing color ${style?.color} in job $id") + 0 + } diff --git a/ground/src/main/java/com/google/android/ground/model/job/Style.kt b/ground/src/main/java/com/google/android/ground/model/job/Style.kt index 5069172a98..034e95d3d1 100644 --- a/ground/src/main/java/com/google/android/ground/model/job/Style.kt +++ b/ground/src/main/java/com/google/android/ground/model/job/Style.kt @@ -15,4 +15,4 @@ */ package com.google.android.ground.model.job -data class Style @JvmOverloads constructor(val color: String = "#ff9131") +data class Style constructor(val color: String) diff --git a/ground/src/main/java/com/google/android/ground/model/submission/LocationTaskData.kt b/ground/src/main/java/com/google/android/ground/model/submission/LocationTaskData.kt index 351ace95d7..88d4a8c8c1 100644 --- a/ground/src/main/java/com/google/android/ground/model/submission/LocationTaskData.kt +++ b/ground/src/main/java/com/google/android/ground/model/submission/LocationTaskData.kt @@ -34,9 +34,10 @@ constructor( // TODO: Move to strings.xml for i18n val df = DecimalFormat("#.##") df.roundingMode = RoundingMode.DOWN - return "${LatLngConverter.processCoordinates(geometry.coordinates)}\n" + - "Altitude: ${df.format(altitude)}m\n" + - "Accuracy: ${df.format(accuracy)}m" + val coordinatesString = LatLngConverter.formatCoordinates(geometry.coordinates) + val altitudeString = altitude?.let { df.format(it) } ?: "?" + val accuracyString = accuracy?.let { df.format(it) } ?: "?" + return "$coordinatesString\nAltitude: $altitudeString m\nAccuracy: $accuracyString m" } override fun isEmpty(): Boolean = geometry == null diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt index 498f167d9b..da67330127 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/converter/ConverterExt.kt @@ -102,21 +102,25 @@ fun Job.toLocalDataStoreObject(surveyId: String): JobEntity = surveyId = surveyId, name = name, suggestLoiTaskType = suggestLoiTaskType?.toString(), - style = style.toLocalDataStoreObject() + style = style?.toLocalDataStoreObject() ) fun JobEntityAndRelations.toModelObject(): Job { val taskMap = taskEntityAndRelations.map { it.toModelObject() }.associateBy { it.id } return Job( jobEntity.id, - jobEntity.style.toModelObject(), + jobEntity.style?.toModelObject(), jobEntity.name, taskMap.toPersistentMap(), jobEntity.suggestLoiTaskType?.let { Task.Type.valueOf(it) } ) } -fun StyleEntity.toModelObject() = color?.let { Style(it) } ?: Style() +/** + * Returns the equivalent model object, setting the style color to #000 if it was missing in the + * local db. + */ +fun StyleEntity.toModelObject() = color?.let { Style(it) } ?: Style("#000000") fun Style.toLocalDataStoreObject() = StyleEntity(color) diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/JobEntity.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/JobEntity.kt index 65f8f416d4..1c66111636 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/JobEntity.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/entity/JobEntity.kt @@ -35,5 +35,5 @@ data class JobEntity( @ColumnInfo(name = "name") val name: String?, @ColumnInfo(name = "survey_id") val surveyId: String?, @ColumnInfo(name = "suggest_loi_task_type") val suggestLoiTaskType: String?, - @Embedded(prefix = "style_") val style: StyleEntity + @Embedded(prefix = "style_") val style: StyleEntity? ) diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomSubmissionStore.kt b/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomSubmissionStore.kt index 8ccfe11ce9..1c17bdae55 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomSubmissionStore.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/room/stores/RoomSubmissionStore.kt @@ -225,14 +225,14 @@ class RoomSubmissionStore @Inject internal constructor() : LocalSubmissionStore ): List = submissionMutationDao.findByLocationOfInterestId(loidId, *states) - override suspend fun getPendingSubmissionCountByLocationOfInterestId(loiId: String): Int = + override suspend fun getPendingCreateCount(loiId: String): Int = submissionMutationDao.getSubmissionMutationCount( loiId, MutationEntityType.CREATE, MutationEntitySyncStatus.PENDING ) - override suspend fun getPendingSubmissionDeletionCountByLocationOfInterestId(loiId: String): Int = + override suspend fun getPendingDeleteCount(loiId: String): Int = submissionMutationDao.getSubmissionMutationCount( loiId, MutationEntityType.DELETE, diff --git a/ground/src/main/java/com/google/android/ground/persistence/local/stores/LocalSubmissionStore.kt b/ground/src/main/java/com/google/android/ground/persistence/local/stores/LocalSubmissionStore.kt index adbf2a34b3..9ccb3989da 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/local/stores/LocalSubmissionStore.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/local/stores/LocalSubmissionStore.kt @@ -59,11 +59,11 @@ interface LocalSubmissionStore : LocalMutationStore - suspend fun getPendingSubmissionCountByLocationOfInterestId( + suspend fun getPendingCreateCount( loiId: String, ): Int - suspend fun getPendingSubmissionDeletionCountByLocationOfInterestId( + suspend fun getPendingDeleteCount( loiId: String, ): Int } diff --git a/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/JobConverter.kt b/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/JobConverter.kt index 4b0b5a5027..a5b86dcdd3 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/JobConverter.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/JobConverter.kt @@ -33,7 +33,7 @@ internal object JobConverter { } return Job( id, - obj.defaultStyle.toStyle(), + obj.defaultStyle?.toStyle(), obj.name, taskMap.toPersistentMap(), TaskConverter.toSuggestLoiTaskType(obj.suggestLoiTaskType) diff --git a/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/JobNestedObject.kt b/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/JobNestedObject.kt index e616a68824..85941e77e9 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/JobNestedObject.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/JobNestedObject.kt @@ -21,7 +21,7 @@ import com.google.firebase.firestore.IgnoreExtraProperties /** Firestore representation of map layers. */ @IgnoreExtraProperties data class JobNestedObject( - val defaultStyle: StyleNestedObject = StyleNestedObject(), + val defaultStyle: StyleNestedObject? = null, val name: String? = null, val tasks: Map? = null, val suggestLoiTaskType: String? = null diff --git a/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/StyleNestedObject.kt b/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/StyleNestedObject.kt index cdd1861f52..dd6090cd74 100644 --- a/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/StyleNestedObject.kt +++ b/ground/src/main/java/com/google/android/ground/persistence/remote/firebase/schema/StyleNestedObject.kt @@ -19,6 +19,6 @@ import com.google.android.ground.model.job.Style import com.google.firebase.firestore.IgnoreExtraProperties /** Firestore representation of map layers. */ -@IgnoreExtraProperties data class StyleNestedObject(val color: String = "#ff9131") +@IgnoreExtraProperties data class StyleNestedObject(val color: String = "") fun StyleNestedObject.toStyle(): Style = Style(color) diff --git a/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt b/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt index e1833abb0b..de12950aae 100644 --- a/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt +++ b/ground/src/main/java/com/google/android/ground/repository/LocationOfInterestRepository.kt @@ -24,7 +24,6 @@ import com.google.android.ground.model.mutation.LocationOfInterestMutation import com.google.android.ground.model.mutation.Mutation.SyncStatus import com.google.android.ground.persistence.local.room.fields.MutationEntitySyncStatus import com.google.android.ground.persistence.local.stores.LocalLocationOfInterestStore -import com.google.android.ground.persistence.local.stores.LocalSubmissionStore import com.google.android.ground.persistence.local.stores.LocalSurveyStore import com.google.android.ground.persistence.remote.NotFoundException import com.google.android.ground.persistence.remote.RemoteDataStore @@ -33,15 +32,11 @@ import com.google.android.ground.persistence.uuid.OfflineUuidGenerator import com.google.android.ground.rx.annotations.Cold import com.google.android.ground.system.auth.AuthenticationManager import com.google.android.ground.ui.map.Bounds -import com.google.android.ground.ui.map.Feature -import com.google.android.ground.ui.map.FeatureType import com.google.android.ground.ui.map.gms.GmsExt.contains import io.reactivex.Flowable import io.reactivex.Single import javax.inject.Inject import javax.inject.Singleton -import kotlinx.collections.immutable.toPersistentSet -import kotlinx.coroutines.flow.map import kotlinx.coroutines.reactive.awaitFirst /** @@ -55,7 +50,6 @@ class LocationOfInterestRepository constructor( private val localSurveyStore: LocalSurveyStore, private val localLoiStore: LocalLocationOfInterestStore, - private val localSubmissionStore: LocalSubmissionStore, private val remoteDataStore: RemoteDataStore, private val mutationSyncWorkManager: MutationSyncWorkManager, private val authManager: AuthenticationManager, @@ -133,31 +127,7 @@ constructor( survey: Survey ): Flowable> = localLoiStore.getLocationsOfInterestOnceAndStream(survey) - private fun findLocationsOfInterest(survey: Survey) = - localLoiStore.findLocationsOfInterest(survey) - - fun findLocationsOfInterestFeatures(survey: Survey) = - findLocationsOfInterest(survey).map { toLocationOfInterestFeatures(it) } - - private suspend fun toLocationOfInterestFeatures( - locationsOfInterest: Set - ): Set = // TODO: Add support for polylines similar to mapPins. - locationsOfInterest - .map { - val pendingSubmissions = - localSubmissionStore.getPendingSubmissionCountByLocationOfInterestId(it.id) - - localSubmissionStore.getPendingSubmissionDeletionCountByLocationOfInterestId(it.id) - val submissionCount = it.submissionCount + pendingSubmissions - Feature( - id = it.id, - type = FeatureType.LOCATION_OF_INTEREST.ordinal, - flag = submissionCount > 0, - geometry = it.geometry, - style = it.job.style, - clusterable = true - ) - } - .toPersistentSet() + fun getLocationsOfInterest(survey: Survey) = localLoiStore.findLocationsOfInterest(survey) /** Returns a list of geometries associated with the given [Survey]. */ suspend fun getAllGeometries(survey: Survey): List = diff --git a/ground/src/main/java/com/google/android/ground/repository/SubmissionRepository.kt b/ground/src/main/java/com/google/android/ground/repository/SubmissionRepository.kt index b29fcec8f6..28d3417e70 100644 --- a/ground/src/main/java/com/google/android/ground/repository/SubmissionRepository.kt +++ b/ground/src/main/java/com/google/android/ground/repository/SubmissionRepository.kt @@ -182,4 +182,10 @@ constructor( MutationEntitySyncStatus.FAILED ) } + + suspend fun getPendingCreateCount(loiId: String) = + localSubmissionStore.getPendingCreateCount(loiId) + + suspend fun getPendingDeleteCount(loiId: String) = + localSubmissionStore.getPendingDeleteCount(loiId) } diff --git a/ground/src/main/java/com/google/android/ground/ui/common/AbstractMapFragmentWithControls.kt b/ground/src/main/java/com/google/android/ground/ui/common/AbstractMapFragmentWithControls.kt index d406180d4b..8da9b34174 100644 --- a/ground/src/main/java/com/google/android/ground/ui/common/AbstractMapFragmentWithControls.kt +++ b/ground/src/main/java/com/google/android/ground/ui/common/AbstractMapFragmentWithControls.kt @@ -84,7 +84,7 @@ abstract class AbstractMapFragmentWithControls : AbstractMapContainerFragment() return } val target = position.target - val processedCoordinates = LatLngConverter.processCoordinates(target) + val processedCoordinates = LatLngConverter.formatCoordinates(target) setCurrentLocationAsInfoCard(processedCoordinates) } } diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt index 21ba438382..01ecb194a4 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionViewModel.kt @@ -148,7 +148,7 @@ internal constructor( } val viewModel = viewModelFactory.create(getViewModelClass(task.type)) // TODO(#1146): Pass in the existing taskData if there is one - viewModel.initialize(task, null) + viewModel.initialize(job, task, null) addTaskViewModel(viewModel) return viewModel } diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/AbstractTaskViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/AbstractTaskViewModel.kt index cf84ae56bc..faff78f61a 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/AbstractTaskViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/AbstractTaskViewModel.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.toLiveData import androidx.lifecycle.viewModelScope import com.google.android.ground.R +import com.google.android.ground.model.job.Job import com.google.android.ground.model.submission.TaskData import com.google.android.ground.model.submission.isNullOrEmpty import com.google.android.ground.model.task.Task @@ -65,7 +66,7 @@ open class AbstractTaskViewModel internal constructor(private val resources: Res } // TODO: Add a reference of Task in TaskData for simplification. - fun initialize(task: Task, taskData: TaskData?) { + open fun initialize(job: Job, task: Task, taskData: TaskData?) { this.task = task setResponse(taskData) } diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskViewModel.kt index 815ac85920..2cf503ed26 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskViewModel.kt @@ -18,7 +18,11 @@ package com.google.android.ground.ui.datacollection.tasks.point import android.content.res.Resources import androidx.lifecycle.MutableLiveData import com.google.android.ground.model.geometry.Point +import com.google.android.ground.model.job.Job +import com.google.android.ground.model.job.getDefaultColor import com.google.android.ground.model.submission.GeometryData +import com.google.android.ground.model.submission.TaskData +import com.google.android.ground.model.task.Task import com.google.android.ground.persistence.uuid.OfflineUuidGenerator import com.google.android.ground.rx.annotations.Hot import com.google.android.ground.ui.datacollection.tasks.AbstractTaskViewModel @@ -32,9 +36,15 @@ class DropAPinTaskViewModel constructor(resources: Resources, private val uuidGenerator: OfflineUuidGenerator) : AbstractTaskViewModel(resources) { + private var pinColor: Int = 0 private var lastCameraPosition: CameraPosition? = null val features: @Hot MutableLiveData> = MutableLiveData() + override fun initialize(job: Job, task: Task, taskData: TaskData?) { + super.initialize(job, task, taskData) + pinColor = job.getDefaultColor() + } + fun updateCameraPosition(position: CameraPosition) { lastCameraPosition = position } @@ -55,6 +65,8 @@ constructor(resources: Resources, private val uuidGenerator: OfflineUuidGenerato id = uuidGenerator.generateUuid(), type = FeatureType.USER_POINT.ordinal, geometry = point, + // TODO: Set correct pin color. + style = Feature.Style(pinColor), clusterable = false ) diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/LatLngConverter.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/LatLngConverter.kt index ff6d0289ff..cca60eb669 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/LatLngConverter.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/LatLngConverter.kt @@ -26,7 +26,7 @@ import kotlin.math.abs object LatLngConverter { /** Converts the given coordinates in decimal format to D°M′S″ format. */ - fun processCoordinates(coordinates: Coordinates?): String? = + fun formatCoordinates(coordinates: Coordinates?): String? = coordinates?.let { "${convertLatToDMS(it.lat)} ${convertLongToDMS(it.lng)}" } private fun convertLatToDMS(lat: Double): String { diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt index 1dd1acfbc7..bb70a84cea 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingViewModel.kt @@ -24,7 +24,11 @@ import com.google.android.ground.model.geometry.LineString import com.google.android.ground.model.geometry.LinearRing import com.google.android.ground.model.geometry.Point import com.google.android.ground.model.geometry.Polygon +import com.google.android.ground.model.job.Job +import com.google.android.ground.model.job.getDefaultColor import com.google.android.ground.model.submission.GeometryData +import com.google.android.ground.model.submission.TaskData +import com.google.android.ground.model.task.Task import com.google.android.ground.persistence.uuid.OfflineUuidGenerator import com.google.android.ground.ui.common.SharedViewModel import com.google.android.ground.ui.datacollection.tasks.AbstractTaskViewModel @@ -61,6 +65,13 @@ internal constructor(private val uuidGenerator: OfflineUuidGenerator, resources: /** Represents whether the user has completed drawing the polygon or not. */ private var isMarkedComplete: Boolean = false + private var strokeColor: Int = 0 + + override fun initialize(job: Job, task: Task, taskData: TaskData?) { + super.initialize(job, task, taskData) + strokeColor = job.getDefaultColor() + } + fun isMarkedComplete(): Boolean = isMarkedComplete /** @@ -147,15 +158,16 @@ internal constructor(private val uuidGenerator: OfflineUuidGenerator, resources: } /** Returns a set of [Feature] to be drawn on map for the given [Polygon]. */ - private fun refreshFeatures(points: List, isMarkedComplete: Boolean) { + private fun refreshFeatures(vertices: List, isMarkedComplete: Boolean) { featureFlow.value = - if (points.isEmpty()) { + if (vertices.isEmpty()) { null } else { Feature( id = uuidGenerator.generateUuid(), type = FeatureType.USER_POLYGON.ordinal, - geometry = createGeometry(points, isMarkedComplete), + geometry = createGeometry(vertices, isMarkedComplete), + style = Feature.Style(strokeColor, Feature.VertexStyle.CIRCLE), clusterable = false ) } diff --git a/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt index 6ac758797d..526863e5c4 100644 --- a/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt @@ -19,11 +19,14 @@ import androidx.lifecycle.viewModelScope import com.google.android.ground.Config.CLUSTERING_ZOOM_THRESHOLD import com.google.android.ground.Config.ZOOM_LEVEL_THRESHOLD import com.google.android.ground.coroutines.IoDispatcher +import com.google.android.ground.model.Survey import com.google.android.ground.model.job.Job +import com.google.android.ground.model.job.getDefaultColor import com.google.android.ground.model.locationofinterest.LocationOfInterest import com.google.android.ground.repository.LocationOfInterestRepository import com.google.android.ground.repository.MapStateRepository import com.google.android.ground.repository.OfflineAreaRepository +import com.google.android.ground.repository.SubmissionRepository import com.google.android.ground.repository.SurveyRepository import com.google.android.ground.rx.Nil import com.google.android.ground.rx.annotations.Hot @@ -34,10 +37,12 @@ import com.google.android.ground.ui.common.BaseMapViewModel import com.google.android.ground.ui.common.SharedViewModel import com.google.android.ground.ui.map.CameraPosition import com.google.android.ground.ui.map.Feature +import com.google.android.ground.ui.map.FeatureType import io.reactivex.Observable import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject import javax.inject.Inject +import kotlinx.collections.immutable.toPersistentSet import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -61,6 +66,7 @@ class HomeScreenMapContainerViewModel internal constructor( private val loiRepository: LocationOfInterestRepository, private val mapStateRepository: MapStateRepository, + private val submissionRepository: SubmissionRepository, locationManager: LocationManager, settingsManager: SettingsManager, offlineAreaRepository: OfflineAreaRepository, @@ -110,7 +116,7 @@ internal constructor( mapLoiFeatures = activeSurvey.flatMapLatest { - if (it == null) flowOf(setOf()) else loiRepository.findLocationsOfInterestFeatures(it) + if (it == null) flowOf(setOf()) else getLocationOfInterestFeatures(it) } val isZoomedInFlow = @@ -169,4 +175,21 @@ internal constructor( } fun getZoomThresholdCrossed(): Observable = zoomThresholdCrossed + + private fun getLocationOfInterestFeatures(survey: Survey): Flow> = + loiRepository.getLocationsOfInterest(survey).map { + it.map { loi -> loi.toFeature() }.toPersistentSet() + } + + private suspend fun LocationOfInterest.toFeature() = + Feature( + id = id, + type = FeatureType.LOCATION_OF_INTEREST.ordinal, + flag = + submissionCount + submissionRepository.getPendingCreateCount(id) - + submissionRepository.getPendingDeleteCount(id) > 0, + geometry = geometry, + style = Feature.Style(job.getDefaultColor()), + clusterable = true + ) } diff --git a/ground/src/main/java/com/google/android/ground/ui/map/Feature.kt b/ground/src/main/java/com/google/android/ground/ui/map/Feature.kt index 5002652f83..5af282da8f 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/Feature.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/Feature.kt @@ -15,9 +15,8 @@ */ package com.google.android.ground.ui.map -import android.graphics.Color +import androidx.annotation.ColorInt import com.google.android.ground.model.geometry.Geometry -import com.google.android.ground.model.job.Style /** Represents an individual feature on a map with a given [Geometry] and [Tag]. */ data class Feature( @@ -31,7 +30,7 @@ data class Feature( type: Int, geometry: Geometry, flag: Boolean = false, - style: Style = Style(), + style: Style, clusterable: Boolean ) : this(Tag(id, type, flag), geometry, style, clusterable) @@ -45,9 +44,14 @@ data class Feature( */ val type: Int, /** An arbitrary slot for boolean flag. The interpretation of this field is type-dependent. */ - // TODO: This is not part of the unique identifer for the feature - should not live in Tag! + // TODO: This is not part of the unique identifier for the feature - should not live in Tag! val flag: Boolean = false ) -} -fun Feature.colorInt(): Int = Color.parseColor(style.color) + data class Style(@ColorInt val color: Int, val vertexStyle: VertexStyle? = VertexStyle.NONE) + + enum class VertexStyle { + NONE, + CIRCLE + } +} diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt index 477a8cede1..e48f2b2205 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/FeatureClusterRenderer.kt @@ -24,8 +24,8 @@ import com.google.android.ground.model.geometry.MultiPolygon import com.google.android.ground.model.geometry.Point import com.google.android.ground.model.geometry.Polygon import com.google.android.ground.ui.IconFactory -import com.google.android.ground.ui.map.gms.renderer.PointRenderer -import com.google.android.ground.ui.map.gms.renderer.PolygonRenderer +import com.google.android.ground.ui.map.gms.renderer.PointFeatureManager +import com.google.android.ground.ui.map.gms.renderer.PolygonFeatureManager import com.google.maps.android.clustering.Cluster import com.google.maps.android.clustering.view.DefaultClusterRenderer import timber.log.Timber @@ -38,12 +38,11 @@ import timber.log.Timber * individual markers for each cluster item. */ class FeatureClusterRenderer( - // TODO: Inject. context: Context, - private val map: GoogleMap, + map: GoogleMap, private val clusterManager: FeatureClusterManager, - private val pointRenderer: PointRenderer, - private val polygonRenderer: PolygonRenderer, + private val pointFeatureManager: PointFeatureManager, + private val polygonFeatureManager: PolygonFeatureManager, private val clusteringZoomThreshold: Float, /** * The current zoom level to compare against the renderer's threshold. @@ -62,14 +61,14 @@ class FeatureClusterRenderer( override fun onBeforeClusterItemRendered(item: FeatureClusterItem, markerOptions: MarkerOptions) { when (item.feature.geometry) { is Point -> { - pointRenderer.setMarkerOptions(markerOptions, item.isSelected(), item.style.color) + pointFeatureManager.setMarkerOptions(markerOptions, item.isSelected(), item.style.color) } is Polygon, is MultiPolygon -> { // Don't render marker if this item is a polygon. markerOptions.visible(false) // Add polygon or multi-polygon when zooming in. - polygonRenderer.addFeature(item.feature, item.isSelected()) + polygonFeatureManager.addFeature(item.feature, item.isSelected()) } else -> { throw UnsupportedOperationException( @@ -82,11 +81,12 @@ class FeatureClusterRenderer( override fun onClusterItemUpdated(item: FeatureClusterItem, marker: Marker) { val feature = item.feature when (feature.geometry) { - is Point -> marker.setIcon(pointRenderer.getMarkerIcon(item.isSelected(), item.style.color)) + is Point -> + marker.setIcon(pointFeatureManager.getMarkerIcon(item.isSelected(), item.style.color)) is Polygon, is MultiPolygon -> // Update polygon or multi-polygon on change. - polygonRenderer.updateFeature(feature, item.isSelected()) + polygonFeatureManager.updateFeature(feature, item.isSelected()) else -> throw UnsupportedOperationException( "Unsupported feature type ${feature.geometry.javaClass.simpleName}" @@ -113,7 +113,7 @@ class FeatureClusterRenderer( cluster.items .map { it.feature } .filter { it.geometry !is Point } - .forEach { feature -> polygonRenderer.removeFeature(feature) } + .forEach { feature -> polygonFeatureManager.removeFeature(feature) } super.onBeforeClusterRendered(cluster, markerOptions) Timber.d("MARKER_RENDER: onBeforeClusterRendered") with(markerOptions) { diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt index 3dc7f994d6..b99d2f4e2b 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/GoogleMapsFragment.kt @@ -33,7 +33,6 @@ import com.google.android.gms.maps.GoogleMap.OnCameraMoveStartedListener import com.google.android.gms.maps.SupportMapFragment import com.google.android.gms.maps.model.* import com.google.android.ground.Config -import com.google.android.ground.R import com.google.android.ground.model.geometry.* import com.google.android.ground.model.geometry.Polygon import com.google.android.ground.model.imagery.TileSource @@ -45,9 +44,10 @@ import com.google.android.ground.ui.map.CameraPosition import com.google.android.ground.ui.map.gms.GmsExt.toBounds import com.google.android.ground.ui.map.gms.mog.MogCollection import com.google.android.ground.ui.map.gms.mog.MogTileProvider -import com.google.android.ground.ui.map.gms.renderer.PointRenderer -import com.google.android.ground.ui.map.gms.renderer.PolygonRenderer -import com.google.android.ground.ui.map.gms.renderer.PolylineRenderer +import com.google.android.ground.ui.map.gms.renderer.FeatureManager +import com.google.android.ground.ui.map.gms.renderer.PointFeatureManager +import com.google.android.ground.ui.map.gms.renderer.PolygonFeatureManager +import com.google.android.ground.ui.map.gms.renderer.PolylineFeatureManager import com.google.android.ground.ui.util.BitmapUtil import com.google.android.ground.util.invert import com.google.maps.android.PolyUtil @@ -66,6 +66,7 @@ const val TILE_OVERLAY_Z = 0f const val POLYGON_Z = 1f const val CLUSTER_Z = 2f const val MARKER_Z = 3f + /** * Customization of Google Maps API Fragment that automatically adjusts the Google watermark based * on window insets. @@ -81,31 +82,24 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { /** Camera move events. Emits items after the camera has stopped moving. */ override val cameraMovedEvents = MutableSharedFlow() - private lateinit var pointRenderer: PointRenderer - private lateinit var polylineRenderer: PolylineRenderer - private lateinit var polygonRenderer: PolygonRenderer - + @Inject lateinit var pointFeatureManager: PointFeatureManager + @Inject lateinit var polylineFeatureManager: PolylineFeatureManager + @Inject lateinit var polygonFeatureManager: PolygonFeatureManager @Inject lateinit var bitmapUtil: BitmapUtil + private val featureManagers: List + get() = listOf(pointFeatureManager, polylineFeatureManager, polygonFeatureManager) + private lateinit var map: GoogleMap private lateinit var clusterManager: FeatureClusterManager - /** - * References to Google Maps SDK CustomCap present on the map. Used to set the custom drawable to - * start and end of polygon. - */ - private var customCap: CustomCap? = null - override val supportedMapTypes: List = IDS_BY_MAP_TYPE.keys.toList() private val tileOverlays = mutableListOf() override val featureClicks = MutableSharedFlow>() - private val polylineStrokeWidth: Float - get() = resources.getDimension(R.dimen.polyline_stroke_width) - override var mapType: MapType get() = MAP_TYPES_BY_ID[map.mapType]!! set(mapType) { @@ -173,19 +167,15 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { private fun onMapReady(map: GoogleMap) { this.map = map - pointRenderer = PointRenderer(requireContext(), map) - polylineRenderer = PolylineRenderer(map, getCustomCap(), polylineStrokeWidth) - polygonRenderer = - PolygonRenderer(map, polylineStrokeWidth, resources.getColor(R.color.polyLineColor)) - + featureManagers.forEach { it.onMapReady(map) } clusterManager = FeatureClusterManager(requireContext(), map) clusterRenderer = FeatureClusterRenderer( requireContext(), map, clusterManager, - pointRenderer, - polygonRenderer, + pointFeatureManager, + polygonFeatureManager, Config.CLUSTERING_ZOOM_THRESHOLD, map.cameraPosition.zoom ) @@ -241,14 +231,6 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { private fun moveCamera(cameraUpdate: CameraUpdate, shouldAnimate: Boolean) = if (shouldAnimate) map.animateCamera(cameraUpdate) else map.moveCamera(cameraUpdate) - private fun getCustomCap(): CustomCap { - if (customCap == null) { - val bitmap = bitmapUtil.fromVector(R.drawable.ic_endpoint) - customCap = CustomCap(BitmapDescriptorFactory.fromBitmap(bitmap)) - } - return checkNotNull(customCap) - } - private fun onMapClick(latLng: LatLng) { val clickedPolygons = getPolygonFeaturesContaining(latLng) if (clickedPolygons.isNotEmpty()) { @@ -257,7 +239,7 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { } private fun getPolygonFeaturesContaining(latLng: LatLng) = - polygonRenderer + polygonFeatureManager .getPolygonsByFeature() .filterValues { polygons -> polygons.any { PolyUtil.containsLocation(latLng, it.points, false) } @@ -274,17 +256,13 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { private fun removeStaleFeatures(features: Set) { Timber.d("Removing stale features from map") clusterManager.removeStaleFeatures(features) - pointRenderer.removeStaleFeatures(features) - polylineRenderer.removeStaleFeatures(features) - polygonRenderer.removeStaleFeatures(features) + featureManagers.forEach { it.removeStaleFeatures(features) } } private fun removeAllFeatures() { Timber.d("Removing all features from map") clusterManager.removeAllFeatures() - pointRenderer.removeAllFeatures() - polylineRenderer.removeAllFeatures() - polygonRenderer.removeAllFeatures() + featureManagers.forEach { it.removeAllFeatures() } } private fun addOrUpdateFeature(feature: Feature) { @@ -293,11 +271,11 @@ class GoogleMapsFragment : Hilt_GoogleMapsFragment(), MapFragment { return } when (feature.geometry) { - is Point -> pointRenderer.addFeature(feature) + is Point -> pointFeatureManager.addFeature(feature) is LineString, - is LinearRing -> polylineRenderer.addFeature(feature) + is LinearRing -> polylineFeatureManager.addFeature(feature) is Polygon, - is MultiPolygon -> polygonRenderer.addFeature(feature) + is MultiPolygon -> polygonFeatureManager.addFeature(feature) } } diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureManager.kt similarity index 80% rename from ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureRenderer.kt rename to ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureManager.kt index 853fa893a8..19cfa25233 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/FeatureManager.kt @@ -18,8 +18,17 @@ package com.google.android.ground.ui.map.gms.renderer import com.google.android.gms.maps.GoogleMap import com.google.android.ground.ui.map.Feature -sealed class FeatureRenderer(val map: GoogleMap) { +/** Keeps track of features on a map and implement basic related add/remove operations. */ +sealed class FeatureManager { + protected lateinit var map: GoogleMap + abstract fun addFeature(feature: Feature, isSelected: Boolean = false) + abstract fun removeStaleFeatures(features: Set) + abstract fun removeAllFeatures() + + fun onMapReady(map: GoogleMap) { + this.map = map + } } diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointFeatureManager.kt similarity index 83% rename from ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt rename to ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointFeatureManager.kt index cf076c69df..b8c853e760 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PointFeatureManager.kt @@ -17,7 +17,7 @@ package com.google.android.ground.ui.map.gms.renderer import android.content.Context -import com.google.android.gms.maps.GoogleMap +import androidx.annotation.ColorInt import com.google.android.gms.maps.model.BitmapDescriptor import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.MarkerOptions @@ -25,10 +25,12 @@ import com.google.android.ground.model.geometry.Point import com.google.android.ground.ui.IconFactory import com.google.android.ground.ui.map.Feature import com.google.android.ground.ui.map.gms.MARKER_Z -import com.google.android.ground.ui.map.gms.parseColor import com.google.android.ground.ui.map.gms.toLatLng +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject -class PointRenderer(val context: Context, map: GoogleMap) : FeatureRenderer(map) { +class PointFeatureManager @Inject constructor(@ApplicationContext val context: Context) : + FeatureManager() { private val markerIconFactory: IconFactory = IconFactory(context) private val markersByTag = HashMap() @@ -43,19 +45,15 @@ class PointRenderer(val context: Context, map: GoogleMap) : FeatureRenderer(map) markersByTag[feature.tag] = marker } - fun setMarkerOptions(markerOptions: MarkerOptions, isSelected: Boolean, color: String) { + fun setMarkerOptions(markerOptions: MarkerOptions, isSelected: Boolean, @ColorInt color: Int) { with(markerOptions) { icon(getMarkerIcon(isSelected, color)) zIndex(MARKER_Z) } } - fun getMarkerIcon(isSelected: Boolean = false, color: String): BitmapDescriptor = - markerIconFactory.getMarkerIcon( - color.parseColor(context.resources), - map.cameraPosition.zoom, - isSelected - ) + fun getMarkerIcon(isSelected: Boolean = false, @ColorInt color: Int): BitmapDescriptor = + markerIconFactory.getMarkerIcon(color, map.cameraPosition.zoom, isSelected) override fun removeStaleFeatures(features: Set) = (markersByTag.keys - features.map { it.tag }.toSet()).forEach { remove(it) } diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonFeatureManager.kt similarity index 86% rename from ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonRenderer.kt rename to ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonFeatureManager.kt index 79aeb0759d..fa302f665d 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolygonFeatureManager.kt @@ -15,33 +15,32 @@ */ package com.google.android.ground.ui.map.gms.renderer -import com.google.android.gms.maps.GoogleMap +import android.content.Context import com.google.android.gms.maps.model.JointType import com.google.android.gms.maps.model.Polygon as MapsPolygon import com.google.android.gms.maps.model.PolygonOptions +import com.google.android.ground.R import com.google.android.ground.model.geometry.MultiPolygon import com.google.android.ground.model.geometry.Polygon import com.google.android.ground.model.locationofinterest.LocationOfInterest import com.google.android.ground.ui.map.Feature -import com.google.android.ground.ui.map.colorInt import com.google.android.ground.ui.map.gms.POLYGON_Z import com.google.android.ground.ui.map.gms.toLatLng import com.google.android.ground.ui.map.gms.toLatLngList +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject import timber.log.Timber -class PolygonRenderer( - map: GoogleMap, - private val strokeWidth: Float, - private val fillColor: Int, -) : FeatureRenderer(map) { - +class PolygonFeatureManager @Inject constructor(@ApplicationContext context: Context) : + FeatureManager() { private val polygonsByFeature: MutableMap> = HashMap() + private val lineWidth = context.resources.getDimension(R.dimen.line_geometry_width) override fun addFeature(feature: Feature, isSelected: Boolean) { when (feature.geometry) { - is Polygon -> render(feature, feature.geometry, feature.colorInt(), isSelected) + is Polygon -> render(feature, feature.geometry, feature.style.color, isSelected) is MultiPolygon -> - feature.geometry.polygons.map { render(feature, it, feature.colorInt(), isSelected) } + feature.geometry.polygons.map { render(feature, it, feature.style.color, isSelected) } else -> throw IllegalArgumentException( "PolylineRendered expected Polygon or MultiPolygon, but got ${feature.geometry::class.simpleName}" @@ -64,8 +63,7 @@ class PolygonRenderer( val strokeScale = if (isSelected) 2f else 1f with(mapsPolygon) { tag = Pair(feature.tag.id, LocationOfInterest::javaClass) - strokeWidth = this@PolygonRenderer.strokeWidth * strokeScale - fillColor = this@PolygonRenderer.fillColor + strokeWidth = lineWidth * strokeScale strokeColor = color strokeJointType = JointType.ROUND zIndex = POLYGON_Z diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineRenderer.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineFeatureManager.kt similarity index 73% rename from ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineRenderer.kt rename to ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineFeatureManager.kt index ecbce2fc58..dd62abfdb4 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineRenderer.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/renderer/PolylineFeatureManager.kt @@ -15,26 +15,34 @@ */ package com.google.android.ground.ui.map.gms.renderer -import com.google.android.gms.maps.GoogleMap +import android.content.Context +import com.google.android.gms.maps.model.BitmapDescriptorFactory import com.google.android.gms.maps.model.CustomCap import com.google.android.gms.maps.model.JointType import com.google.android.gms.maps.model.Polyline import com.google.android.gms.maps.model.PolylineOptions +import com.google.android.ground.R import com.google.android.ground.model.geometry.Coordinates import com.google.android.ground.model.geometry.LineString import com.google.android.ground.model.geometry.LinearRing import com.google.android.ground.ui.map.Feature -import com.google.android.ground.ui.map.colorInt import com.google.android.ground.ui.map.gms.toLatLngList +import com.google.android.ground.ui.util.BitmapUtil +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject import timber.log.Timber -class PolylineRenderer( - map: GoogleMap, - private val customCap: CustomCap, - private val strokeWidth: Float -) : FeatureRenderer(map) { - +class PolylineFeatureManager +@Inject +constructor(@ApplicationContext context: Context, bitmapUtil: BitmapUtil) : FeatureManager() { private val polylines: MutableMap> = HashMap() + private val lineWidth = context.resources.getDimension(R.dimen.line_geometry_width) + private val circleCap by lazy { + // This must be done lazily since resources are not available before the app completes + // initialization. + val bitmap = bitmapUtil.fromVector(R.drawable.ic_endpoint) + CustomCap(BitmapDescriptorFactory.fromBitmap(bitmap)) + } override fun addFeature(feature: Feature, isSelected: Boolean) { when (feature.geometry) { @@ -57,12 +65,15 @@ class PolylineRenderer( } val polyline = map.addPolyline(options) val strokeScale = if (isSelected) 2f else 1f + val style = feature.style with(polyline) { tag = feature.tag - startCap = customCap - endCap = customCap - width = strokeWidth * strokeScale - this.color = feature.colorInt() + if (style.vertexStyle == Feature.VertexStyle.CIRCLE) { + startCap = circleCap + endCap = circleCap + } + width = lineWidth * strokeScale + color = style.color jointType = JointType.ROUND } diff --git a/ground/src/main/res/values/colors.xml b/ground/src/main/res/values/colors.xml index 7346014eeb..3808048e33 100644 --- a/ground/src/main/res/values/colors.xml +++ b/ground/src/main/res/values/colors.xml @@ -22,7 +22,6 @@ #6DDD81 #4B6DDD81 - #55ffffff #FCFDF7 #000000 diff --git a/ground/src/main/res/values/dimens.xml b/ground/src/main/res/values/dimens.xml index 7e9903fb31..42750b2fc9 100644 --- a/ground/src/main/res/values/dimens.xml +++ b/ground/src/main/res/values/dimens.xml @@ -18,7 +18,7 @@ - 4dp + 4dp 1.8 2.5 diff --git a/ground/src/test/java/com/google/android/ground/persistence/local/LocalDataStoreTests.kt b/ground/src/test/java/com/google/android/ground/persistence/local/LocalDataStoreTests.kt index 455ac966e7..84db677cbe 100644 --- a/ground/src/test/java/com/google/android/ground/persistence/local/LocalDataStoreTests.kt +++ b/ground/src/test/java/com/google/android/ground/persistence/local/LocalDataStoreTests.kt @@ -95,8 +95,8 @@ class LocalDataStoreTests : BaseHiltTest() { @Test fun testRemovedJobFromSurvey() = runWithTestDispatcher { - val job1 = Job("job 1", Style(), "job 1 name") - val job2 = Job("job 2", Style(), "job 2 name") + val job1 = Job("job 1", TEST_STYLE, "job 1 name") + val job2 = Job("job 2", TEST_STYLE, "job 2 name") var survey = Survey("foo id", "foo survey", "foo survey description", mapOf(Pair(job1.id, job1))) localSurveyStore.insertOrUpdateSurvey(survey) @@ -125,6 +125,7 @@ class LocalDataStoreTests : BaseHiltTest() { .test() .assertValue { it.geometry == TEST_POINT } } + @Test fun testApplyAndEnqueue_insertsMutation() = runWithTestDispatcher { localUserStore.insertOrUpdateUser(TEST_USER) @@ -371,8 +372,9 @@ class LocalDataStoreTests : BaseHiltTest() { companion object { private val TEST_USER = User("user id", "user@gmail.com", "user 1") private val TEST_TASK = Task("task id", 1, Task.Type.TEXT, "task label", false) + private val TEST_STYLE = Style("#112233") private val TEST_JOB = - Job("job id", Style(), "heading title", mapOf(Pair(TEST_TASK.id, TEST_TASK))) + Job("job id", TEST_STYLE, "heading title", mapOf(Pair(TEST_TASK.id, TEST_TASK))) private val TEST_SURVEY = Survey("survey id", "survey 1", "foo description", mapOf(Pair(TEST_JOB.id, TEST_JOB))) private val TEST_POINT = Point(Coordinates(110.0, -23.1)) diff --git a/ground/src/test/java/com/google/android/ground/persistence/remote/firebase/schema/LoiLocalDataStoreConverterTest.kt b/ground/src/test/java/com/google/android/ground/persistence/remote/firebase/schema/LoiLocalDataStoreConverterTest.kt index b54da83b3a..17aa07ac45 100644 --- a/ground/src/test/java/com/google/android/ground/persistence/remote/firebase/schema/LoiLocalDataStoreConverterTest.kt +++ b/ground/src/test/java/com/google/android/ground/persistence/remote/firebase/schema/LoiLocalDataStoreConverterTest.kt @@ -104,7 +104,7 @@ class LoiLocalDataStoreConverterTest { private fun setUpTestSurvey(jobId: String, vararg tasks: Task) { val taskMap = tasks.associateBy { it.id } - val job = Job(jobId, Style(), "JOB_NAME", taskMap) + val job = Job(jobId, TEST_STYLE, "JOB_NAME", taskMap) survey = Survey("", "", "", mapOf(Pair(job.id, job))) } @@ -124,6 +124,7 @@ class LoiLocalDataStoreConverterTest { toLoi(survey, loiDocumentSnapshot) companion object { + private val TEST_STYLE = Style("#112233") private val AUDIT_INFO_1_NESTED_OBJECT = AuditInfoNestedObject( UserNestedObject("user1", null, null), diff --git a/ground/src/test/java/com/google/android/ground/persistence/remote/firebase/schema/SubmissionLocalDataStoreConverterTest.kt b/ground/src/test/java/com/google/android/ground/persistence/remote/firebase/schema/SubmissionLocalDataStoreConverterTest.kt index e079c91303..a8564f19ea 100644 --- a/ground/src/test/java/com/google/android/ground/persistence/remote/firebase/schema/SubmissionLocalDataStoreConverterTest.kt +++ b/ground/src/test/java/com/google/android/ground/persistence/remote/firebase/schema/SubmissionLocalDataStoreConverterTest.kt @@ -280,7 +280,7 @@ class SubmissionLocalDataStoreConverterTest { private fun setUpTestSurvey(jobId: String, loiId: String, vararg tasks: Task) { val taskMap = tasks.associateBy { it.id } - job = Job(jobId, Style(), "JOB_NAME", taskMap) + job = Job(jobId, TEST_STYLE, "JOB_NAME", taskMap) locationOfInterest = FakeData.LOCATION_OF_INTEREST.copy(id = loiId, surveyId = TEST_SURVEY_ID, job = job) } @@ -312,5 +312,6 @@ class SubmissionLocalDataStoreConverterTest { ) private const val SUBMISSION_ID = "submission123" private const val TEST_SURVEY_ID = "survey001" + private val TEST_STYLE = Style("#112233") } } diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/BaseTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/BaseTaskFragmentTest.kt index 1b3c5f2d74..dfd7b0f8d5 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/BaseTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/BaseTaskFragmentTest.kt @@ -28,6 +28,7 @@ import app.cash.turbine.test import com.google.android.ground.BaseHiltTest import com.google.android.ground.R import com.google.android.ground.launchFragmentWithNavController +import com.google.android.ground.model.job.Job import com.google.android.ground.model.submission.TaskData import com.google.android.ground.model.task.Task import com.google.android.ground.ui.common.ViewModelFactory @@ -103,9 +104,9 @@ abstract class BaseTaskFragmentTest, VM : AbstractT onView(withText(buttonText)).check(matches(isDisplayed())).check(matches(not(isEnabled()))) } - protected inline fun setupTaskFragment(task: Task) { + protected inline fun setupTaskFragment(job: Job, task: Task) { viewModel = viewModelFactory.create(DataCollectionViewModel.getViewModelClass(task.type)) as VM - viewModel.initialize(task, null) + viewModel.initialize(job, task, null) whenever(dataCollectionViewModel.getTaskViewModel(task.index)).thenReturn(viewModel) launchFragmentWithNavController( diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/date/DateTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/date/DateTaskFragmentTest.kt index bd82dd4785..25d72efe91 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/date/DateTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/date/DateTaskFragmentTest.kt @@ -23,6 +23,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isEnabled import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import com.google.android.ground.R +import com.google.android.ground.model.job.Job import com.google.android.ground.model.task.Task import com.google.android.ground.ui.common.ViewModelFactory import com.google.android.ground.ui.datacollection.DataCollectionViewModel @@ -49,16 +50,18 @@ class DateTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) hasTaskViewWithHeader(task) } @Test fun testResponse_defaultIsEmpty() { - setupTaskFragment(task) + setupTaskFragment(job, task) onView(withId(R.id.user_response_text)) .check(matches(withText(""))) @@ -71,7 +74,7 @@ class DateTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) assertThat(fragment.getDatePickerDialog()).isNull() onView(withId(R.id.user_response_text)).perform(click()) @@ -80,14 +83,14 @@ class DateTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) hasButtons(ButtonAction.CONTINUE, ButtonAction.SKIP) } @Test fun testActionButtons_whenTaskIsOptional() { - setupTaskFragment(task.copy(isRequired = false)) + setupTaskFragment(job, task.copy(isRequired = false)) buttonIsDisabled("Continue") buttonIsEnabled("Skip") @@ -95,7 +98,7 @@ class DateTaskFragmentTest : BaseTaskFragmentTest(task.copy(isRequired = true)) + setupTaskFragment(job, task.copy(isRequired = true)) buttonIsDisabled("Continue") buttonIsHidden("Skip") diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskFragmentTest.kt index 1a4ab2097f..6e21bc8bc0 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskFragmentTest.kt @@ -21,6 +21,7 @@ import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.matcher.ViewMatchers.withText import com.google.android.ground.model.geometry.Coordinates import com.google.android.ground.model.geometry.Point +import com.google.android.ground.model.job.Job import com.google.android.ground.model.submission.LocationTaskData import com.google.android.ground.model.task.Task import com.google.android.ground.ui.common.ViewModelFactory @@ -55,10 +56,11 @@ class CaptureLocationTaskFragmentTest : label = "Task for capturing current location", isRequired = false ) + private val job = Job(id = "job1") @Test fun testHeader() { - setupTaskFragment(task) + setupTaskFragment(job, task) hasTaskViewWithoutHeader("Capture location") } @@ -66,7 +68,7 @@ class CaptureLocationTaskFragmentTest : @Test fun testDropPin() = runWithTestDispatcher { val location = setupLocation() - setupTaskFragment(task) + setupTaskFragment(job, task) viewModel.updateLocation(location) onView(withText("Capture")).perform(click()) @@ -79,7 +81,7 @@ class CaptureLocationTaskFragmentTest : @Test fun testInfoCard_noTaskData() { - setupTaskFragment(task) + setupTaskFragment(job, task) infoCardHidden() } @@ -87,7 +89,7 @@ class CaptureLocationTaskFragmentTest : @Test fun testUndo() = runWithTestDispatcher { val location = setupLocation() - setupTaskFragment(task) + setupTaskFragment(job, task) viewModel.updateLocation(location) onView(withText("Capture")).perform(click()) @@ -100,7 +102,7 @@ class CaptureLocationTaskFragmentTest : @Test fun testActionButtons() { - setupTaskFragment(task) + setupTaskFragment(job, task) hasButtons( ButtonAction.CONTINUE, @@ -112,7 +114,7 @@ class CaptureLocationTaskFragmentTest : @Test fun testActionButtons_whenTaskIsOptional() { - setupTaskFragment(task.copy(isRequired = false)) + setupTaskFragment(job, task.copy(isRequired = false)) buttonIsHidden("Continue") buttonIsEnabled("Skip") @@ -122,7 +124,7 @@ class CaptureLocationTaskFragmentTest : @Test fun testActionButtons_whenTaskIsRequired() { - setupTaskFragment(task.copy(isRequired = true)) + setupTaskFragment(job, task.copy(isRequired = true)) buttonIsHidden("Continue") buttonIsHidden("Skip") diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragmentTest.kt index bf81a15dee..97a0853fd3 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragmentTest.kt @@ -22,6 +22,7 @@ import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.* import com.google.android.ground.R +import com.google.android.ground.model.job.Job import com.google.android.ground.model.submission.MultipleChoiceTaskData import com.google.android.ground.model.task.MultipleChoice import com.google.android.ground.model.task.Option @@ -66,6 +67,7 @@ class MultipleChoiceTaskFragmentTest : isRequired = false, multipleChoice = MultipleChoice(persistentListOf(), MultipleChoice.Cardinality.SELECT_ONE) ) + private val job = Job(id = "job1") private val options = persistentListOf( @@ -76,13 +78,13 @@ class MultipleChoiceTaskFragmentTest : @Test fun taskFails_whenMultipleChoiceIsNull() { assertThrows(NullPointerException::class.java) { - setupTaskFragment(task.copy(multipleChoice = null)) + setupTaskFragment(job, task.copy(multipleChoice = null)) } } @Test fun testHeader() { - setupTaskFragment(task) + setupTaskFragment(job, task) hasTaskViewWithHeader(task) } @@ -90,6 +92,7 @@ class MultipleChoiceTaskFragmentTest : @Test fun testMultipleChoice_whenSelectOne() { setupTaskFragment( + job, task.copy(multipleChoice = MultipleChoice(options, MultipleChoice.Cardinality.SELECT_ONE)) ) @@ -101,7 +104,7 @@ class MultipleChoiceTaskFragmentTest : @Test fun testMultipleChoice_whenSelectOne_click() = runWithTestDispatcher { val multipleChoice = MultipleChoice(options, MultipleChoice.Cardinality.SELECT_ONE) - setupTaskFragment(task.copy(multipleChoice = multipleChoice)) + setupTaskFragment(job, task.copy(multipleChoice = multipleChoice)) onView(withText("Option 1")).perform(click()) onView(withText("Option 2")).perform(click()) @@ -113,6 +116,7 @@ class MultipleChoiceTaskFragmentTest : @Test fun testMultipleChoice_whenSelectMultiple() { setupTaskFragment( + job, task.copy( multipleChoice = MultipleChoice(options, MultipleChoice.Cardinality.SELECT_MULTIPLE) ) @@ -126,7 +130,7 @@ class MultipleChoiceTaskFragmentTest : @Test fun testMultipleChoice_whenSelectMultiple_click() = runWithTestDispatcher { val multipleChoice = MultipleChoice(options, MultipleChoice.Cardinality.SELECT_MULTIPLE) - setupTaskFragment(task.copy(multipleChoice = multipleChoice)) + setupTaskFragment(job, task.copy(multipleChoice = multipleChoice)) onView(withText("Option 1")).perform(click()) onView(withText("Option 2")).perform(click()) @@ -137,14 +141,14 @@ class MultipleChoiceTaskFragmentTest : @Test fun testActionButtons() { - setupTaskFragment(task) + setupTaskFragment(job, task) hasButtons(ButtonAction.CONTINUE, ButtonAction.SKIP) } @Test fun testActionButtons_whenTaskIsOptional() { - setupTaskFragment(task.copy(isRequired = false)) + setupTaskFragment(job, task.copy(isRequired = false)) buttonIsDisabled("Continue") buttonIsEnabled("Skip") @@ -153,7 +157,7 @@ class MultipleChoiceTaskFragmentTest : @Test fun testActionButtons_dataEntered_skipButtonTapped_confirmationDialogIsShown() { val multipleChoice = MultipleChoice(options, MultipleChoice.Cardinality.SELECT_ONE) - setupTaskFragment(task.copy(multipleChoice = multipleChoice)) + setupTaskFragment(job, task.copy(multipleChoice = multipleChoice)) onView(withText("Option 1")).perform(click()) @@ -164,7 +168,7 @@ class MultipleChoiceTaskFragmentTest : @Test fun testActionButtons_noDataEntered_skipButtonTapped_confirmationDialogIsNotShown() { val multipleChoice = MultipleChoice(options, MultipleChoice.Cardinality.SELECT_ONE) - setupTaskFragment(task.copy(multipleChoice = multipleChoice)) + setupTaskFragment(job, task.copy(multipleChoice = multipleChoice)) onView(withText("Skip")).perform(click()) assertThat(ShadowAlertDialog.getShownDialogs().isEmpty()).isTrue() @@ -172,7 +176,7 @@ class MultipleChoiceTaskFragmentTest : @Test fun testActionButtons_whenTaskIsRequired() { - setupTaskFragment(task.copy(isRequired = true)) + setupTaskFragment(job, task.copy(isRequired = true)) buttonIsDisabled("Continue") buttonIsHidden("Skip") diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/number/NumberTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/number/NumberTaskFragmentTest.kt index 2a60a7c88e..0a845b4893 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/number/NumberTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/number/NumberTaskFragmentTest.kt @@ -25,6 +25,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withInputType import androidx.test.espresso.matcher.ViewMatchers.withText import com.google.android.ground.CustomViewActions.forceTypeText import com.google.android.ground.R +import com.google.android.ground.model.job.Job import com.google.android.ground.model.submission.NumberTaskData import com.google.android.ground.model.task.Task import com.google.android.ground.ui.common.ViewModelFactory @@ -57,17 +58,18 @@ class NumberTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) hasTaskViewWithHeader(task) } @Test fun testResponse_defaultIsEmpty() = runWithTestDispatcher { - setupTaskFragment(task) + setupTaskFragment(job, task) onView(withId(R.id.user_response_text)) .check(matches(withText(""))) @@ -80,7 +82,7 @@ class NumberTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) onView(withId(R.id.user_response_text)) .check(matches(withInputType(InputType.TYPE_CLASS_NUMBER))) @@ -92,14 +94,14 @@ class NumberTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) hasButtons(ButtonAction.CONTINUE, ButtonAction.SKIP) } @Test fun testActionButtons_whenTaskIsOptional() { - setupTaskFragment(task.copy(isRequired = false)) + setupTaskFragment(job, task.copy(isRequired = false)) buttonIsDisabled("Continue") buttonIsEnabled("Skip") @@ -107,7 +109,7 @@ class NumberTaskFragmentTest : BaseTaskFragmentTest(task.copy(isRequired = true)) + setupTaskFragment(job, task.copy(isRequired = true)) buttonIsDisabled("Continue") buttonIsHidden("Skip") diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskFragmentTest.kt index 6990b8e36b..b87f9e5076 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/point/DropAPinTaskFragmentTest.kt @@ -21,6 +21,8 @@ import androidx.test.espresso.assertion.ViewAssertions.* import androidx.test.espresso.matcher.ViewMatchers.* import com.google.android.ground.model.geometry.Coordinates import com.google.android.ground.model.geometry.Point +import com.google.android.ground.model.job.Job +import com.google.android.ground.model.job.Style import com.google.android.ground.model.submission.GeometryData import com.google.android.ground.model.task.Task import com.google.android.ground.ui.common.ViewModelFactory @@ -55,10 +57,11 @@ class DropAPinTaskFragmentTest : label = "Task for dropping a pin", isRequired = false ) + private val job = Job("job", Style("#112233")) @Test fun testHeader() { - setupTaskFragment(task) + setupTaskFragment(job, task) hasTaskViewWithoutHeader("Drop a pin") } @@ -66,7 +69,7 @@ class DropAPinTaskFragmentTest : @Test fun testDropPin() = runWithTestDispatcher { val testPosition = CameraPosition(Coordinates(10.0, 20.0)) - setupTaskFragment(task) + setupTaskFragment(job, task) viewModel.updateCameraPosition(testPosition) onView(withText("Drop pin")).perform(click()) @@ -79,7 +82,7 @@ class DropAPinTaskFragmentTest : @Test fun testInfoCard_noTaskData() { - setupTaskFragment(task) + setupTaskFragment(job, task) infoCardHidden() } @@ -87,7 +90,7 @@ class DropAPinTaskFragmentTest : @Test fun testUndo() = runWithTestDispatcher { val testPosition = CameraPosition(Coordinates(10.0, 20.0)) - setupTaskFragment(task) + setupTaskFragment(job, task) viewModel.updateCameraPosition(testPosition) onView(withText("Drop pin")).perform(click()) @@ -100,14 +103,14 @@ class DropAPinTaskFragmentTest : @Test fun testActionButtons() { - setupTaskFragment(task) + setupTaskFragment(job, task) hasButtons(ButtonAction.CONTINUE, ButtonAction.SKIP, ButtonAction.UNDO, ButtonAction.DROP_PIN) } @Test fun testActionButtons_whenTaskIsOptional() { - setupTaskFragment(task.copy(isRequired = false)) + setupTaskFragment(job, task.copy(isRequired = false)) buttonIsHidden("Continue") buttonIsEnabled("Skip") @@ -117,7 +120,7 @@ class DropAPinTaskFragmentTest : @Test fun testActionButtons_whenTaskIsRequired() { - setupTaskFragment(task.copy(isRequired = true)) + setupTaskFragment(job, task.copy(isRequired = true)) buttonIsHidden("Continue") buttonIsHidden("Skip") diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/point/LatLngConverterTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/point/LatLngConverterTest.kt index 555f38c91e..67f5694fb1 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/point/LatLngConverterTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/point/LatLngConverterTest.kt @@ -17,7 +17,7 @@ package com.google.android.ground.ui.datacollection.tasks.point import com.google.android.ground.model.geometry.Coordinates -import com.google.android.ground.ui.datacollection.tasks.point.LatLngConverter.processCoordinates +import com.google.android.ground.ui.datacollection.tasks.point.LatLngConverter.formatCoordinates import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -28,23 +28,23 @@ class LatLngConverterTest { @Test fun testProcessCoordinates_ne() { - assertThat(processCoordinates(Coordinates(10.555, 10.555))) + assertThat(formatCoordinates(Coordinates(10.555, 10.555))) .isEqualTo("10°33'18\" N 10°33'18\" E") } @Test fun testProcessCoordinates_se() { - assertThat(processCoordinates(Coordinates(-10.555, 10.555))) + assertThat(formatCoordinates(Coordinates(-10.555, 10.555))) .isEqualTo("10°33'18\" S 10°33'18\" E") } @Test fun testProcessCoordinates_nw() { - assertThat(processCoordinates(Coordinates(10.555, -10.555))) + assertThat(formatCoordinates(Coordinates(10.555, -10.555))) .isEqualTo("10°33'18\" N 10°33'18\" W") } @Test fun testProcessCoordinates_sw() { - assertThat(processCoordinates(Coordinates(-10.555, -10.555))) + assertThat(formatCoordinates(Coordinates(-10.555, -10.555))) .isEqualTo("10°33'18\" S 10°33'18\" W") } } diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingTaskFragmentTest.kt index b0e5d4a1df..4f71c4e3e4 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/polygon/PolygonDrawingTaskFragmentTest.kt @@ -21,6 +21,8 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import com.google.android.ground.model.geometry.Coordinates import com.google.android.ground.model.geometry.LinearRing import com.google.android.ground.model.geometry.Polygon +import com.google.android.ground.model.job.Job +import com.google.android.ground.model.job.Style import com.google.android.ground.model.submission.GeometryData import com.google.android.ground.model.task.Task import com.google.android.ground.ui.common.ViewModelFactory @@ -54,24 +56,25 @@ class PolygonDrawingTaskFragmentTest : label = "Task for drawing a polygon", isRequired = false ) + private val job = Job("job", Style("#112233")) @Test fun testHeader() { - setupTaskFragment(task) + setupTaskFragment(job, task) hasTaskViewWithoutHeader("Draw an area") } @Test fun testInfoCard_noTaskData() { - setupTaskFragment(task) + setupTaskFragment(job, task) infoCardHidden() } @Test fun testActionButtons() { - setupTaskFragment(task) + setupTaskFragment(job, task) hasButtons( ButtonAction.CONTINUE, @@ -84,7 +87,7 @@ class PolygonDrawingTaskFragmentTest : @Test fun testActionButtons_whenTaskIsOptional() { - setupTaskFragment(task.copy(isRequired = false)) + setupTaskFragment(job, task.copy(isRequired = false)) buttonIsHidden("Continue") buttonIsEnabled("Skip") @@ -95,7 +98,7 @@ class PolygonDrawingTaskFragmentTest : @Test fun testActionButtons_whenTaskIsRequired() { - setupTaskFragment(task.copy(isRequired = true)) + setupTaskFragment(job, task.copy(isRequired = true)) buttonIsHidden("Continue") buttonIsHidden("Skip") @@ -106,7 +109,7 @@ class PolygonDrawingTaskFragmentTest : @Test fun testDrawPolygon() = runWithTestDispatcher { - setupTaskFragment(task.copy(isRequired = true)) + setupTaskFragment(job, task.copy(isRequired = true)) updateLastVertexAndAddPoint(COORDINATE_1) updateLastVertexAndAddPoint(COORDINATE_2) diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/text/TextTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/text/TextTaskFragmentTest.kt index 4f9e7a0052..537cc0b7ed 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/text/TextTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/text/TextTaskFragmentTest.kt @@ -26,6 +26,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withInputType import androidx.test.espresso.matcher.ViewMatchers.withText import com.google.android.ground.* +import com.google.android.ground.model.job.Job import com.google.android.ground.model.submission.TextTaskData import com.google.android.ground.model.task.Task import com.google.android.ground.ui.common.ViewModelFactory @@ -51,17 +52,18 @@ class TextTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) hasTaskViewWithHeader(task) } @Test fun testResponse_defaultIsEmpty() = runWithTestDispatcher { - setupTaskFragment(task) + setupTaskFragment(job, task) onView(withId(R.id.user_response_text)) .check(matches(withText(""))) @@ -74,7 +76,7 @@ class TextTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) onView(withId(R.id.user_response_text)) .check(matches(withInputType(InputType.TYPE_CLASS_TEXT))) @@ -86,14 +88,14 @@ class TextTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) hasButtons(ButtonAction.CONTINUE, ButtonAction.SKIP) } @Test fun testActionButtons_whenTaskIsOptional() { - setupTaskFragment(task.copy(isRequired = false)) + setupTaskFragment(job, task.copy(isRequired = false)) buttonIsDisabled("Continue") buttonIsEnabled("Skip") @@ -101,7 +103,7 @@ class TextTaskFragmentTest : BaseTaskFragmentTest(task.copy(isRequired = true)) + setupTaskFragment(job, task.copy(isRequired = true)) buttonIsDisabled("Continue") buttonIsHidden("Skip") diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/time/TimeTaskFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/time/TimeTaskFragmentTest.kt index b0f415e950..89fcd3dda3 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/time/TimeTaskFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/tasks/time/TimeTaskFragmentTest.kt @@ -20,6 +20,7 @@ import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers import com.google.android.ground.R +import com.google.android.ground.model.job.Job import com.google.android.ground.model.task.Task import com.google.android.ground.ui.common.ViewModelFactory import com.google.android.ground.ui.datacollection.DataCollectionViewModel @@ -46,17 +47,18 @@ class TimeTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) hasTaskViewWithHeader(task) } @Test fun testResponse_defaultIsEmpty() { - setupTaskFragment(task) + setupTaskFragment(job, task) Espresso.onView(ViewMatchers.withId(R.id.user_response_text)) .check(ViewAssertions.matches(ViewMatchers.withText(""))) @@ -69,7 +71,7 @@ class TimeTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) Truth.assertThat(fragment.getTimePickerDialog()).isNull() Espresso.onView(ViewMatchers.withId(R.id.user_response_text)).perform(ViewActions.click()) @@ -78,14 +80,14 @@ class TimeTaskFragmentTest : BaseTaskFragmentTest(task) + setupTaskFragment(job, task) hasButtons(ButtonAction.CONTINUE, ButtonAction.SKIP) } @Test fun testActionButtons_whenTaskIsOptional() { - setupTaskFragment(task.copy(isRequired = false)) + setupTaskFragment(job, task.copy(isRequired = false)) buttonIsDisabled("Continue") buttonIsEnabled("Skip") @@ -93,7 +95,7 @@ class TimeTaskFragmentTest : BaseTaskFragmentTest(task.copy(isRequired = true)) + setupTaskFragment(job, task.copy(isRequired = true)) buttonIsDisabled("Continue") buttonIsHidden("Skip") diff --git a/sharedTest/src/main/kotlin/com/sharedtest/FakeData.kt b/sharedTest/src/main/kotlin/com/sharedtest/FakeData.kt index a7dc628724..f97b251369 100644 --- a/sharedTest/src/main/kotlin/com/sharedtest/FakeData.kt +++ b/sharedTest/src/main/kotlin/com/sharedtest/FakeData.kt @@ -39,7 +39,7 @@ object FakeData { val TERMS_OF_SERVICE: TermsOfService = TermsOfService("TERMS_OF_SERVICE", "Fake Terms of Service text") - val JOB = Job(name = "Job", id = "JOB", style = Style()) + val JOB = Job(name = "Job", id = "JOB", style = Style("#000")) val USER = User("user_id", "user@gmail.com", "User") @@ -72,6 +72,7 @@ object FakeData { id = LOCATION_OF_INTEREST.id, type = FeatureType.LOCATION_OF_INTEREST.ordinal, geometry = LOCATION_OF_INTEREST.geometry, + style = Feature.Style(0), clusterable = true )