diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 767e4a9940..ce76f22c22 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -7,7 +7,6 @@ - diff --git a/build.gradle b/build.gradle index 4732550734..59161873f1 100644 --- a/build.gradle +++ b/build.gradle @@ -20,8 +20,8 @@ buildscript { ext { // As of Gradle 8.5, Kotlin plugin only tested up to 1.9.20. kotlinVersion = "1.9.20" - hiltVersion = "2.50" - navigationVersion = "2.5.3" + hiltVersion = "2.51" + navigationVersion = "2.7.7" roomVersion = "2.6.1" } repositories { @@ -35,15 +35,15 @@ buildscript { } dependencies { classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion" - classpath 'com.android.tools.build:gradle:8.2.0' - classpath 'com.google.gms:google-services:4.3.15' + classpath 'com.android.tools.build:gradle:8.2.2' + classpath 'com.google.gms:google-services:4.4.1' classpath "com.pascalwelsch.gitversioner:gitversioner:0.5.0" // Performance Monitoring plugin: https://firebase.google.com/docs/perf-mon classpath 'com.google.firebase:perf-plugin:1.4.2' // Crashlytics plugin - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.5' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' // Kotlin classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" @@ -55,7 +55,7 @@ buildscript { } plugins { - id "com.github.ben-manes.versions" version "0.36.0" + id "com.github.ben-manes.versions" version "0.51.0" id "org.jetbrains.kotlin.android" version "$kotlinVersion" apply false id "org.jetbrains.kotlin.plugin.serialization" version "$kotlinVersion" id "com.ncorti.ktfmt.gradle" version "0.17.0" @@ -111,6 +111,9 @@ gitVersioner { baseBranch "master" } +// https://github.com/ben-manes/gradle-versions-plugin/issues/746 +apply plugin: 'jvm-ecosystem' + def isNonStable = { String version -> def stableKeyword = ['RELEASE', 'FINAL', 'GA'].any { it -> version.toUpperCase().contains(it) } def regex = /^[0-9,.v-]+(-r)?$/ @@ -138,7 +141,5 @@ ext { androidCompileSdk = 34 androidMinSdk = 24 androidTargetSdk = 33 - - gmsMapsVersion = '18.1.0' jvmToolchainVersion = 17 } \ No newline at end of file diff --git a/cloudbuild.yaml b/cloudbuild.yaml index 32fff1970f..a3fabddf5e 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -117,8 +117,9 @@ steps: ./gradlew -PdisablePreDex testDevStagingUnitTest --no-daemon 2> unit-test-logs.txt || echo "fail" > build-status.txt cat unit-test-logs.txt - # TODO: Add a check for _PUSH_TO_MASTER - ./gradlew jacocoTestStagingUnitTestReport --no-daemon + if [[ "${_PUSH_TO_MASTER}" ]]; then + ./gradlew jacocoTestStagingUnitTestReport --no-daemon + fi - name: 'gcr.io/$PROJECT_ID/android:34' id: &authenticate_gcloud 'Authorize gcloud' @@ -219,10 +220,11 @@ steps: args: - '-c' - | - # TODO: Add a check for _PUSH_TO_MASTER - curl -Os https://uploader.codecov.io/latest/linux/codecov - chmod +x codecov - ./codecov -t ${_CODECOV_TOKEN} + if [[ "${_PUSH_TO_MASTER}" ]]; then + curl -Os https://uploader.codecov.io/latest/linux/codecov + chmod +x codecov + ./codecov -t ${_CODECOV_TOKEN} + fi - name: 'gcr.io/$PROJECT_ID/android:base' id: &compress_cache 'Compress gradle build cache' diff --git a/ground/build.gradle b/ground/build.gradle index 076873a622..80cc6f1bad 100644 --- a/ground/build.gradle +++ b/ground/build.gradle @@ -27,14 +27,12 @@ apply from: '../config/lint/lint.gradle' apply from: '../config/jacoco/jacoco.gradle' project.ext { - autoDisposeVersion = "1.4.0" - autoValueVersion = "1.9" + autoValueVersion = "1.10.4" fragmentVersion = "1.5.7" - hiltJetpackVersion = "1.0.0" - lifecycleVersion = "2.6.1" - workVersion = "2.8.1" - mockitoVersion = "4.5.1" - mockitoKotlinVersion = "4.1.0" + hiltJetpackVersion = "1.2.0" + lifecycleVersion = "2.7.0" + workVersion = "2.9.0" + mockitoVersion = "5.11.0" coroutinesVersion = "1.6.4" } @@ -167,13 +165,13 @@ dependencies { androidTestImplementation project(':sharedTest') implementation 'androidx.multidex:multidex:2.0.1' - implementation 'androidx.preference:preference-ktx:1.2.0' + implementation 'androidx.preference:preference-ktx:1.2.1' // Kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib:$project.kotlinVersion" - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.0-RC" - implementation "org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.4.0-RC" - implementation "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.6.3" + implementation "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7" // Kotlin Coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion" @@ -188,25 +186,25 @@ dependencies { implementation 'androidx.cardview:cardview:1.0.0' implementation 'com.google.android.material:material:1.11.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.compose.ui:ui:1.6.1' - implementation 'androidx.compose.compiler:compiler:1.5.9' + implementation 'androidx.compose.ui:ui:1.6.2' + implementation 'androidx.compose.compiler:compiler:1.5.10' implementation 'androidx.compose.material3:material3-android:1.2.0' // Google Play Services. - implementation 'com.google.android.gms:play-services-auth:20.6.0' - implementation "com.google.android.gms:play-services-maps:$rootProject.gmsMapsVersion" - implementation 'com.google.android.gms:play-services-location:21.0.1' + implementation 'com.google.android.gms:play-services-auth:21.0.0' + implementation 'com.google.android.gms:play-services-maps:18.2.0' + implementation 'com.google.android.gms:play-services-location:21.1.0' - implementation "com.google.maps.android:android-maps-utils:2.3.0" + implementation "com.google.maps.android:android-maps-utils:3.8.2" // GeoJSON support - implementation 'com.google.code.gson:gson:2.10' + implementation 'com.google.code.gson:gson:2.10.1' // Test Json testImplementation 'org.json:json:20180813' // Firebase and related libraries. - implementation platform('com.google.firebase:firebase-bom:32.4.1') + implementation platform('com.google.firebase:firebase-bom:32.7.3') implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.firebase:firebase-firestore' implementation 'com.google.firebase:firebase-functions-ktx' @@ -269,15 +267,12 @@ dependencies { implementation "androidx.work:work-runtime-ktx:$workVersion" testImplementation "androidx.work:work-testing:$workVersion" - implementation "com.uber.autodispose:autodispose-android:$project.autoDisposeVersion" - implementation "com.uber.autodispose:autodispose-android-archcomponents:$project.autoDisposeVersion" - // Testing testImplementation 'junit:junit:4.13.2' androidTestImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' - testImplementation 'com.google.truth:truth:1.1.3' - androidTestImplementation 'com.google.truth:truth:1.1.3' + testImplementation 'com.google.truth:truth:1.4.2' + androidTestImplementation 'com.google.truth:truth:1.4.2' testImplementation 'androidx.test:core:1.5.0' testImplementation 'org.robolectric:robolectric:4.11.1' testImplementation 'android.arch.core:core-testing:1.1.1' @@ -290,12 +285,12 @@ dependencies { testImplementation 'app.cash.turbine:turbine:0.12.3' // Mockito - testImplementation "org.mockito:mockito-inline:$mockitoVersion" + testImplementation 'org.mockito:mockito-inline:4.5.1' testImplementation "org.mockito:mockito-core:$mockitoVersion" testImplementation "org.mockito:mockito-android:$mockitoVersion" androidTestImplementation "org.mockito:mockito-core:$mockitoVersion" androidTestImplementation "org.mockito:mockito-android:$mockitoVersion" - testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" + testImplementation 'org.mockito.kotlin:mockito-kotlin:4.1.0' // Espresso testImplementation 'androidx.test.espresso:espresso-contrib:3.5.1' diff --git a/ground/src/main/java/com/google/android/ground/Config.kt b/ground/src/main/java/com/google/android/ground/Config.kt index 10f45bb180..28ad6845bc 100644 --- a/ground/src/main/java/com/google/android/ground/Config.kt +++ b/ground/src/main/java/com/google/android/ground/Config.kt @@ -55,10 +55,11 @@ object Config { const val MAX_MEDIA_UPLOAD_RETRY_COUNT = 5 // TODO(#1730): Make sub-paths configurable and stop hardcoding here. + const val DEFAULT_MOG_TILE_LOCATION = "/offline-imagery/default" const val DEFAULT_MOG_MIN_ZOOM = 8 const val DEFAULT_MOG_MAX_ZOOM = 14 - fun getMogSources(path: String = "/offline-imagery/default/") = + fun getMogSources(path: String) = listOf( MogSource( 0 ..< DEFAULT_MOG_MIN_ZOOM, diff --git a/ground/src/main/java/com/google/android/ground/GroundApplication.kt b/ground/src/main/java/com/google/android/ground/GroundApplication.kt index d71fbed7cd..23fba3231d 100644 --- a/ground/src/main/java/com/google/android/ground/GroundApplication.kt +++ b/ground/src/main/java/com/google/android/ground/GroundApplication.kt @@ -34,6 +34,9 @@ class GroundApplication : MultiDexApplication(), Configuration.Provider { @Inject lateinit var crashReportingTree: CrashReportingTree @Inject lateinit var workerFactory: HiltWorkerFactory + override val workManagerConfiguration: Configuration + get() = Configuration.Builder().setWorkerFactory(workerFactory).build() + private fun isReleaseBuild(): Boolean = BuildConfig.BUILD_TYPE.contentEquals("release") override fun onCreate() { @@ -47,9 +50,6 @@ class GroundApplication : MultiDexApplication(), Configuration.Provider { } } - override fun getWorkManagerConfiguration(): Configuration = - Configuration.Builder().setWorkerFactory(workerFactory).build() - private fun setStrictMode() { // NOTE: Enabling strict thread policy causes Maps SDK to lag on pan and zoom. Enable // only as needed when debugging. diff --git a/ground/src/main/java/com/google/android/ground/model/Survey.kt b/ground/src/main/java/com/google/android/ground/model/Survey.kt index 95c2d7b433..a6b433d876 100644 --- a/ground/src/main/java/com/google/android/ground/model/Survey.kt +++ b/ground/src/main/java/com/google/android/ground/model/Survey.kt @@ -24,6 +24,7 @@ data class Survey( val title: String, val description: String, val jobMap: Map, + // TODO(#1730): Remove tileSources from survey. val tileSources: List = listOf(), val acl: Map = mapOf() ) { diff --git a/ground/src/main/java/com/google/android/ground/model/locationofinterest/LocationOfInterest.kt b/ground/src/main/java/com/google/android/ground/model/locationofinterest/LocationOfInterest.kt index 438c23eab1..e4c5912edb 100644 --- a/ground/src/main/java/com/google/android/ground/model/locationofinterest/LocationOfInterest.kt +++ b/ground/src/main/java/com/google/android/ground/model/locationofinterest/LocationOfInterest.kt @@ -16,7 +16,7 @@ package com.google.android.ground.model.locationofinterest import com.google.android.ground.model.AuditInfo -import com.google.android.ground.model.geometry.* +import com.google.android.ground.model.geometry.Geometry import com.google.android.ground.model.job.Job import com.google.android.ground.model.mutation.LocationOfInterestMutation import com.google.android.ground.model.mutation.Mutation @@ -75,6 +75,8 @@ data class LocationOfInterest( submissionCount = submissionCount, ownerEmail = ownerEmail, isOpportunistic = isOpportunistic, - properties = properties + properties = properties, ) + + fun getProperty(key: String): String = properties[key]?.toString() ?: "" } diff --git a/ground/src/main/java/com/google/android/ground/repository/OfflineAreaRepository.kt b/ground/src/main/java/com/google/android/ground/repository/OfflineAreaRepository.kt index d0ee6fa105..b8c180f834 100644 --- a/ground/src/main/java/com/google/android/ground/repository/OfflineAreaRepository.kt +++ b/ground/src/main/java/com/google/android/ground/repository/OfflineAreaRepository.kt @@ -108,8 +108,8 @@ constructor( private suspend fun getLocalTileSourcePath(): String = File(fileUtil.getFilesDir(), "tiles").path fun getOfflineTileSourcesFlow() = - surveyRepository.activeSurveyFlow.combine(getOfflineAreaBounds()) { survey, bounds -> - applyBounds(survey?.tileSources, bounds) + surveyRepository.activeSurveyFlow.combine(getOfflineAreaBounds()) { _, bounds -> + applyBounds(getDefaultTileSources(), bounds) } private suspend fun applyBounds( @@ -143,15 +143,14 @@ constructor( return MogClient(mogCollection, remoteStorageManager) } - private fun getMogSources(): List = Config.getMogSources(getFirstTileSourceUrl()) + private fun getMogSources(): List = + Config.getMogSources(getDefaultTileSources().first().url) - /** - * Returns the URL of the first tile source in the current survey, or throws an error if no survey - * is active or if no tile sources are defined. - */ - private fun getFirstTileSourceUrl() = - surveyRepository.activeSurvey?.tileSources?.firstOrNull()?.url - ?: error("Survey has no tile sources") + /** Returns the default configured tile sources. */ + fun getDefaultTileSources(): List = + listOf( + TileSource(url = Config.DEFAULT_MOG_TILE_LOCATION, type = TileSource.Type.MOG_COLLECTION) + ) suspend fun hasHiResImagery(bounds: Bounds): Boolean { val client = getMogClient() diff --git a/ground/src/main/java/com/google/android/ground/ui/common/LocationOfInterestHelper.kt b/ground/src/main/java/com/google/android/ground/ui/common/LocationOfInterestHelper.kt index 50c5e24be3..ed2176d6f4 100644 --- a/ground/src/main/java/com/google/android/ground/ui/common/LocationOfInterestHelper.kt +++ b/ground/src/main/java/com/google/android/ground/ui/common/LocationOfInterestHelper.kt @@ -17,46 +17,48 @@ package com.google.android.ground.ui.common import android.content.res.Resources import com.google.android.ground.R -import com.google.android.ground.model.AuditInfo -import com.google.android.ground.model.User -import com.google.android.ground.model.geometry.LineString -import com.google.android.ground.model.geometry.LinearRing +import com.google.android.ground.model.geometry.Geometry 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.model.locationofinterest.LocationOfInterest -import java.util.Optional import javax.inject.Inject -/** Common logic for formatting attributes of [LocationOfInterest] for display to the user. */ +/** Helper class for creating user-visible text. */ class LocationOfInterestHelper @Inject internal constructor(private val resources: Resources) { - fun getCreatedBy(locationOfInterest: Optional): String = - getUserName(locationOfInterest).map { resources.getString(R.string.added_by, it) }.orElse("") - // TODO(#793): Allow user-defined LOI names for other LOI types. - fun getLabel(locationOfInterest: Optional): String = - locationOfInterest.map(::getLabel).orElse("") + fun getDisplayLoiName(loi: LocationOfInterest): String { + val loiId = loi.customId.ifEmpty { loi.getProperty("id") } + val loiName = loi.getProperty("name") - fun getLabel(loi: LocationOfInterest): String { - // TODO(#2046): Reuse logic from card util to display LOI label - val caption = loi.customId?.trim { it <= ' ' } ?: "" - return caption.ifEmpty { getLocationOfInterestType(loi) } + return when { + loiName.isNotEmpty() && loiId.isNotEmpty() -> "$loiName ($loiId)" + loiName.isNotEmpty() -> loiName + loiId.isNotEmpty() -> "${loi.geometry.toType()} ($loiId)" + else -> loi.geometry.toDefaultName() + } } - private fun getLocationOfInterestType(locationOfInterest: LocationOfInterest): String = - when (locationOfInterest.geometry) { - is Polygon -> "Polygon" - is Point -> "Point" - is LineString -> "LineString" - is LinearRing -> "LinearRing" - is MultiPolygon -> "MultiPolygon" + fun getJobName(loi: LocationOfInterest): String? = loi.job.name + + /** Returns a user-visible string representing the type of the geometry. */ + private fun Geometry.toType(): String = + when (this) { + is Point -> resources.getString(R.string.point) + is Polygon, + is MultiPolygon -> resources.getString(R.string.area) + else -> throw IllegalArgumentException("Unsupported geometry type $this") } - fun getSubtitle(locationOfInterest: Optional): String = - locationOfInterest - .map { resources.getString(R.string.layer_label_format, it.job.name) } - .orElse("") + /** Returns a default user-visible name for the geometry. */ + private fun Geometry.toDefaultName(): String = + when (this) { + is Point -> resources.getString(R.string.unnamed_point) + is Polygon, + is MultiPolygon -> resources.getString(R.string.unnamed_area) + else -> throw IllegalArgumentException("Unsupported geometry type $this") + } - private fun getUserName(locationOfInterest: Optional): Optional = - locationOfInterest.map(LocationOfInterest::created).map(AuditInfo::user).map(User::displayName) + fun getSubtitle(loi: LocationOfInterest): String = + resources.getString(R.string.layer_label_format, loi.job.name) } diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionFragment.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionFragment.kt index 18c3e1b1c2..1ae570df6b 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionFragment.kt @@ -17,11 +17,14 @@ package com.google.android.ground.ui.datacollection import android.animation.ValueAnimator import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import androidx.constraintlayout.widget.Guideline +import androidx.core.view.WindowInsetsCompat import androidx.hilt.navigation.fragment.hiltNavGraphViewModels import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.lifecycle.lifecycleScope @@ -40,6 +43,7 @@ import kotlinx.coroutines.launch @AndroidEntryPoint class DataCollectionFragment : AbstractFragment(), BackPressListener { @Inject lateinit var navigator: Navigator + @Inject lateinit var viewPagerAdapterFactory: DataCollectionViewPagerAdapterFactory private val viewModel: DataCollectionViewModel by hiltNavGraphViewModels(R.id.data_collection) @@ -76,20 +80,34 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener { viewPager.registerOnPageChangeCallback( object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - super.onPageSelected(position) - - val buttonContainer = view.findViewById(R.id.action_buttons) ?: return - val anchorLocation = IntArray(2) - buttonContainer.getLocationInWindow(anchorLocation) - val guidelineTop = - anchorLocation[1] - buttonContainer.rootWindowInsets.systemWindowInsetTop - guideline.setGuidelineBegin(guidelineTop) + override fun onPageScrollStateChanged(state: Int) { + super.onPageScrollStateChanged(state) + if (state == ViewPager2.SCROLL_STATE_IDLE) { + Handler(Looper.getMainLooper()) + .postDelayed( + { + // Reset the progress bar position after a delay to wait for the keyboard to + // close. + setProgressBarPosition(view) + }, + 100 + ) + } } } ) } + private fun setProgressBarPosition(view: View) { + val buttonContainer = view.findViewById(R.id.action_buttons) ?: return + val anchorLocation = IntArray(2) + buttonContainer.getLocationInWindow(anchorLocation) + val windowInsets = WindowInsetsCompat.toWindowInsetsCompat(buttonContainer.rootWindowInsets) + val guidelineTop = + anchorLocation[1] - windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).top + guideline.setGuidelineBegin(guidelineTop) + } + private fun loadTasks(tasks: List) { val currentAdapter = viewPager.adapter as? DataCollectionViewPagerAdapter if (currentAdapter == null || currentAdapter.tasks != tasks) { 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 dd5f8e1331..cdefe5e5c8 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 @@ -99,7 +99,7 @@ internal constructor( else flow { val loi = locationOfInterestRepository.getOfflineLoi(surveyId, loiId) - val label = locationOfInterestHelper.getLabel(loi) + val label = locationOfInterestHelper.getDisplayLoiName(loi) emit(label) }) .stateIn(viewModelScope, SharingStarted.Lazily, "") diff --git a/ground/src/main/java/com/google/android/ground/ui/home/HomeScreenViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/home/HomeScreenViewModel.kt index 61824315d1..bcc45a612f 100644 --- a/ground/src/main/java/com/google/android/ground/ui/home/HomeScreenViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/home/HomeScreenViewModel.kt @@ -16,9 +16,8 @@ package com.google.android.ground.ui.home import androidx.lifecycle.LiveData -import androidx.lifecycle.asLiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import com.google.android.ground.repository.SurveyRepository import com.google.android.ground.ui.common.AbstractViewModel import com.google.android.ground.ui.common.Navigator import com.google.android.ground.ui.common.SharedViewModel @@ -26,7 +25,6 @@ import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @SharedViewModel @@ -34,14 +32,13 @@ class HomeScreenViewModel @Inject internal constructor( private val navigator: Navigator, - private val surveyRepository: SurveyRepository, ) : AbstractViewModel() { private val _openDrawerRequests: MutableSharedFlow = MutableSharedFlow() val openDrawerRequestsFlow: SharedFlow = _openDrawerRequests.asSharedFlow() - val showOfflineAreaMenuItem: LiveData = - surveyRepository.activeSurveyFlow.map { it?.tileSources?.isNotEmpty() ?: false }.asLiveData() + // TODO(#1730): Allow tile source configuration from a non-survey accessible source. + val showOfflineAreaMenuItem: LiveData = MutableLiveData(true) fun openNavDrawer() { viewModelScope.launch { _openDrawerRequests.emit(Unit) } diff --git a/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt b/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt index 8f63a0ac4e..bc89ef9e2e 100644 --- a/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt @@ -40,7 +40,7 @@ import com.google.android.ground.ui.common.BaseMapViewModel import com.google.android.ground.ui.common.EphemeralPopups import com.google.android.ground.ui.home.HomeScreenFragmentDirections import com.google.android.ground.ui.home.HomeScreenViewModel -import com.google.android.ground.ui.home.mapcontainer.cards.LoiCardUtil +import com.google.android.ground.ui.home.mapcontainer.HomeScreenMapContainerViewModel.SurveyProperties import com.google.android.ground.ui.home.mapcontainer.cards.MapCardAdapter import com.google.android.ground.ui.home.mapcontainer.cards.MapCardUiData import com.google.android.ground.ui.map.MapFragment @@ -51,6 +51,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import timber.log.Timber /** Main app view, displaying the map and related controls (center cross-hairs, add button, etc). */ @AndroidEntryPoint @@ -106,8 +107,10 @@ class HomeScreenMapContainerFragment : AbstractMapContainerFragment() { /** Updates the given [TextView] with the submission count for the given [LocationOfInterest]. */ private fun updateSubmissionCount(loi: LocationOfInterest, view: TextView) { externalScope.launch { - val submissionCount = submissionRepository.getTotalSubmissionCount(loi) - val submissionText = LoiCardUtil.getSubmissionsText(submissionCount) + val count = submissionRepository.getTotalSubmissionCount(loi) + val submissionText = + if (count == 0) resources.getString(R.string.no_submissions) + else resources.getQuantityString(R.plurals.submission_count, count, count) withContext(mainDispatcher) { view.text = submissionText } } } @@ -117,6 +120,7 @@ class HomeScreenMapContainerFragment : AbstractMapContainerFragment() { container: ViewGroup?, savedInstanceState: Bundle?, ): View { + super.onCreateView(inflater, container, savedInstanceState) binding = BasemapLayoutBinding.inflate(inflater, container, false) binding.fragment = this binding.viewModel = mapContainerViewModel @@ -128,7 +132,7 @@ class HomeScreenMapContainerFragment : AbstractMapContainerFragment() { super.onViewCreated(view, savedInstanceState) setupMenuFab() setupBottomLoiCards() - lifecycleScope.launch { showDataCollectionHint() } + viewLifecycleOwner.lifecycleScope.launch { showDataCollectionHint() } } /** @@ -138,23 +142,23 @@ class HomeScreenMapContainerFragment : AbstractMapContainerFragment() { * This method should only be called after view creation. */ private suspend fun showDataCollectionHint() { - check(this::mapContainerViewModel.isInitialized) { - "showDataCollectionHint called before mapContainerViewModel was initialized" + if (!this::mapContainerViewModel.isInitialized) { + return Timber.w("showDataCollectionHint() called before mapContainerViewModel initialized") } - check(this::binding.isInitialized) { - "showDataCollectionHint called before binding was initialized" - } - mapContainerViewModel.surveyUpdateFlow.collect { - val messageId = - when { - it.addLoiPermitted -> R.string.suggest_data_collection_hint - it.readOnly -> R.string.read_only_data_collection_hint - else -> R.string.predefined_data_collection_hint - } - ephemeralPopups - .InfoPopup() - .show(binding.root, messageId, EphemeralPopups.PopupDuration.INDEFINITE) + mapContainerViewModel.surveyUpdateFlow.collect(this::onSurveyUpdate) + } + + private fun onSurveyUpdate(surveyProperties: SurveyProperties) { + if (!this::binding.isInitialized) { + return Timber.w("showDataCollectionHint() called before binding initialized") } + val messageId = + when { + surveyProperties.addLoiPermitted -> R.string.suggest_data_collection_hint + surveyProperties.readOnly -> R.string.read_only_data_collection_hint + else -> R.string.predefined_data_collection_hint + } + ephemeralPopups.InfoPopup().show(binding.root, messageId, EphemeralPopups.PopupDuration.SHORT) } private fun setupMenuFab() { 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 af42275831..6b997ca07d 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 @@ -90,9 +90,9 @@ internal constructor( * determine if and how behavior should change based on differing survey properties. */ val surveyUpdateFlow: Flow = - activeSurvey.filterNotNull().map { - val lois = loiRepository.getLocationsOfInterests(it).first() - SurveyProperties(it.jobs.any { it.canDataCollectorsAddLois }, lois.isEmpty()) + activeSurvey.filterNotNull().map { survey -> + val lois = loiRepository.getLocationsOfInterests(survey).first() + SurveyProperties(survey.jobs.any { job -> job.canDataCollectorsAddLois }, lois.isEmpty()) } /** Set of [Feature] to render on the map. */ diff --git a/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/cards/LoiCardUtil.kt b/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/cards/LoiCardUtil.kt deleted file mode 100644 index 1122c65bdd..0000000000 --- a/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/cards/LoiCardUtil.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2023 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 com.google.android.ground.ui.home.mapcontainer.cards - -import android.content.Context -import com.google.android.ground.R -import com.google.android.ground.model.geometry.Geometry -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.model.locationofinterest.LocationOfInterest -import com.google.android.ground.util.isNotNullOrEmpty - -/** Helper class for creating user-visible text. */ -object LoiCardUtil { - - fun getDisplayLoiName(context: Context, loi: LocationOfInterest): String { - val loiId = - if (loi.customId.isNotNullOrEmpty()) loi.customId else loi.properties?.get("id")?.toString() - val geometry = loi.geometry - val name: String? = loi.properties?.get("name")?.toString() - return if (name.isNotNullOrEmpty() && loiId.isNotNullOrEmpty()) { - "$name ($loiId)" - } else if (name.isNotNullOrEmpty()) { - "$name" - } else if (loiId.isNotNullOrEmpty()) { - "${geometry.toType(context)} ($loiId)" - } else { - geometry.toDefaultName(context) - } - } - - fun getJobName(loi: LocationOfInterest): String? = loi.job.name - - fun getSubmissionsText(count: Int): String = - when (count) { - 0 -> "No submissions" - 1 -> "$count submission" - else -> "$count submissions" - } - - /** Returns a user-visible string representing the type of the geometry. */ - private fun Geometry.toType(context: Context): String = - when (this) { - is Point -> context.getString(R.string.point) - is Polygon, - is MultiPolygon -> context.getString(R.string.area) - else -> throw IllegalArgumentException("Unsupported geometry type $this") - } - - /** Returns a default user-visible name for the geometry. */ - private fun Geometry.toDefaultName(context: Context): String = - when (this) { - is Point -> context.getString(R.string.unnamed_point) - is Polygon, - is MultiPolygon -> context.getString(R.string.unnamed_area) - else -> throw IllegalArgumentException("Unsupported geometry type $this") - } -} diff --git a/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/cards/MapCardAdapter.kt b/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/cards/MapCardAdapter.kt index b30bf3e693..f5d8245985 100644 --- a/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/cards/MapCardAdapter.kt +++ b/ground/src/main/java/com/google/android/ground/ui/home/mapcontainer/cards/MapCardAdapter.kt @@ -26,6 +26,7 @@ import com.google.android.ground.databinding.AddLoiCardItemBinding import com.google.android.ground.databinding.LoiCardItemBinding import com.google.android.ground.model.job.Job import com.google.android.ground.model.locationofinterest.LocationOfInterest +import com.google.android.ground.ui.common.LocationOfInterestHelper /** * An implementation of [RecyclerView.Adapter] that associates [LocationOfInterest] data with the @@ -138,10 +139,12 @@ class MapCardAdapter( private val canUserSubmitData: Boolean, private val updateSubmissionCount: (loi: LocationOfInterest, view: TextView) -> Unit, ) : CardViewHolder(binding.root) { + private val loiHelper = LocationOfInterestHelper(itemView.resources) + fun bind(loi: LocationOfInterest) { with(binding) { - loiName.text = LoiCardUtil.getDisplayLoiName(binding.wrapperView.context, loi) - jobName.text = LoiCardUtil.getJobName(loi) + loiName.text = loiHelper.getDisplayLoiName(loi) + jobName.text = loiHelper.getJobName(loi) collectData.visibility = if (canUserSubmitData && loi.job.hasTasks()) View.VISIBLE else View.GONE updateSubmissionCount(loi, submissions) diff --git a/ground/src/main/java/com/google/android/ground/ui/map/gms/features/FeatureClusterItem.kt b/ground/src/main/java/com/google/android/ground/ui/map/gms/features/FeatureClusterItem.kt index db3081c53c..ef934351f8 100644 --- a/ground/src/main/java/com/google/android/ground/ui/map/gms/features/FeatureClusterItem.kt +++ b/ground/src/main/java/com/google/android/ground/ui/map/gms/features/FeatureClusterItem.kt @@ -27,4 +27,6 @@ data class FeatureClusterItem(val feature: Feature) : ClusterItem { override fun getTitle(): String? = null override fun getSnippet(): String? = null + + override fun getZIndex(): Float? = null } diff --git a/ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/OfflineAreaSelectorViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/OfflineAreaSelectorViewModel.kt index f04c45287b..8ee7e1d172 100644 --- a/ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/OfflineAreaSelectorViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/offlineareas/selector/OfflineAreaSelectorViewModel.kt @@ -90,7 +90,7 @@ internal constructor( ) init { - remoteTileSources = surveyRepository.activeSurvey!!.tileSources + remoteTileSources = offlineAreaRepository.getDefaultTileSources() } fun onDownloadClick() { diff --git a/ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncStatusViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncStatusViewModel.kt index 462531fbe5..7bace3b758 100644 --- a/ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncStatusViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/syncstatus/SyncStatusViewModel.kt @@ -25,7 +25,6 @@ import com.google.android.ground.repository.SurveyRepository import com.google.android.ground.repository.UserRepository import com.google.android.ground.ui.common.AbstractViewModel import com.google.android.ground.ui.common.LocationOfInterestHelper -import java.util.Optional import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -71,8 +70,8 @@ internal constructor( return MutationDetail( user = user.displayName, mutation = mutation, - loiLabel = locationOfInterestHelper.getLabel(loi), - loiSubtitle = locationOfInterestHelper.getSubtitle(Optional.of(loi)), + loiLabel = locationOfInterestHelper.getDisplayLoiName(loi), + loiSubtitle = locationOfInterestHelper.getSubtitle(loi), ) } } diff --git a/ground/src/main/res/layout/offline_areas_list_item.xml b/ground/src/main/res/layout/offline_areas_list_item.xml index 995778f669..253d88affd 100644 --- a/ground/src/main/res/layout/offline_areas_list_item.xml +++ b/ground/src/main/res/layout/offline_areas_list_item.xml @@ -39,9 +39,11 @@ android:id="@+id/offline_area_list_item_icon" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginEnd="16dp" android:alpha="1.0" app:alpha="1.0" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/offline_area_list_item_name" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" android:contentDescription="@string/offline_area_list_item_icon" @@ -52,11 +54,12 @@ diff --git a/ground/src/main/res/values-es/strings.xml b/ground/src/main/res/values-es/strings.xml index 29d13419ea..fca9f42f9b 100644 --- a/ground/src/main/res/values-es/strings.xml +++ b/ground/src/main/res/values-es/strings.xml @@ -50,7 +50,6 @@ Área del mapa base sin conexión Eliminar Área sin nombre - Añadido por %s Seleccionar área No se descargaron mapas base. diff --git a/ground/src/main/res/values-pt/strings.xml b/ground/src/main/res/values-pt/strings.xml index dbc24ce174..29c72643cc 100644 --- a/ground/src/main/res/values-pt/strings.xml +++ b/ground/src/main/res/values-pt/strings.xml @@ -61,7 +61,6 @@ Visualizador offline de Títulos de Base de Mapas Remover Area sem nome - Adicionado por %s Selecionar área Nenhum mapa carregado. diff --git a/ground/src/main/res/values/strings.xml b/ground/src/main/res/values/strings.xml index 3b350aea48..7cab93092c 100644 --- a/ground/src/main/res/values/strings.xml +++ b/ground/src/main/res/values/strings.xml @@ -54,7 +54,6 @@ Settings Downloaded area Remove from device - Added by %s Select area No imagery downloaded for offline use. Tap “Select and download” to get started. @@ -139,4 +138,9 @@ Zoom in to start collecting data Survey is read-only Zoom in to a data collection site to collect data + No submissions + + %d submission + %d submissions + diff --git a/ground/src/test/java/com/google/android/ground/ui/common/LocationOfInterestHelperTest.kt b/ground/src/test/java/com/google/android/ground/ui/common/LocationOfInterestHelperTest.kt index 39f39d6134..01aed46a68 100644 --- a/ground/src/test/java/com/google/android/ground/ui/common/LocationOfInterestHelperTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/common/LocationOfInterestHelperTest.kt @@ -16,12 +16,9 @@ package com.google.android.ground.ui.common import com.google.android.ground.BaseHiltTest -import com.google.android.ground.model.AuditInfo -import com.google.android.ground.model.locationofinterest.LocationOfInterest import com.google.common.truth.Truth.assertThat import com.sharedtest.FakeData import dagger.hilt.android.testing.HiltAndroidTest -import java.util.Optional import javax.inject.Inject import org.junit.Test import org.junit.runner.RunWith @@ -34,59 +31,88 @@ class LocationOfInterestHelperTest : BaseHiltTest() { @Inject lateinit var loiHelper: LocationOfInterestHelper @Test - fun testGetCreatedBy() { - val user = FakeData.USER.copy(displayName = TEST_USER_NAME) - val loi = FakeData.LOCATION_OF_INTEREST.copy(created = AuditInfo(user)) - assertCreatedBy(loi, "Added by $TEST_USER_NAME") + fun testGetSubtitle() { + val loi = FakeData.LOCATION_OF_INTEREST.copy(job = FakeData.JOB.copy(name = TEST_JOB_NAME)) + assertThat(loiHelper.getSubtitle(loi)).isEqualTo("Job: $TEST_JOB_NAME") } @Test - fun testGetCreatedBy_whenLoiIsNull() { - assertCreatedBy(null, "") + fun testLoiNameWithPoint_whenCustomIdAndPropertiesAreNull() { + assertThat(loiHelper.getDisplayLoiName(TEST_LOI.copy(customId = "", properties = mapOf()))) + .isEqualTo("Unnamed point") } @Test - fun testGetLabel_whenLoiIsNull() { - assertLabel(null, "") + fun testLoiNameWithPolygon_whenCustomIdAndPropertiesAreNull() { + assertThat(loiHelper.getDisplayLoiName(TEST_AREA.copy(customId = "", properties = mapOf()))) + .isEqualTo("Unnamed area") } @Test - fun testGetLabel_whenCaptionIsEmptyAndLoiIsPoint() { - val loi = FakeData.LOCATION_OF_INTEREST.copy("") - assertLabel(loi, "Point") + fun testLoiName_whenCustomIdIsAvailable() { + assertThat(loiHelper.getDisplayLoiName(TEST_LOI.copy(customId = "some value"))) + .isEqualTo("Point (some value)") } @Test - fun testGetLabel_whenCaptionIsEmptyAndLoiIsPolygon() { - val loi = FakeData.AREA_OF_INTEREST.copy("") - assertLabel(loi, "Polygon") + fun testArea_whenCustomIdIsAvailable() { + assertThat(loiHelper.getDisplayLoiName(TEST_AREA.copy(customId = "some value"))) + .isEqualTo("Area (some value)") } @Test - fun testGetSubtitle() { - val loi = FakeData.LOCATION_OF_INTEREST.copy(job = FakeData.JOB.copy(name = TEST_JOB_NAME)) - assertSubtitle(loi, "Job: $TEST_JOB_NAME") + fun testArea_whenCustomIdIsNotAvailable_usesPropertiesId() { + assertThat( + loiHelper.getDisplayLoiName( + TEST_AREA.copy(customId = "", properties = mapOf("id" to "property id")) + ) + ) + .isEqualTo("Area (property id)") + } + + @Test + fun testLoiName_whenPropertiesNameIsAvailable() { + assertThat( + loiHelper.getDisplayLoiName(TEST_LOI.copy(properties = mapOf("name" to "custom name"))) + ) + .isEqualTo("custom name") } @Test - fun testGetSubtitle_whenLoiIsEmpty() { - assertSubtitle(null, "") + fun testLoiName_whenCustomIdAndPropertiesNameIsAvailable() { + assertThat( + loiHelper.getDisplayLoiName( + TEST_LOI.copy(customId = "some value", properties = mapOf("name" to "custom name")) + ) + ) + .isEqualTo("custom name (some value)") } - private fun assertCreatedBy(loi: LocationOfInterest?, expectedCreatedBy: String) { - assertThat(loiHelper.getCreatedBy(Optional.ofNullable(loi))).isEqualTo(expectedCreatedBy) + @Test + fun testLoiName_whenPropertiesDoesNotContainName() { + assertThat( + loiHelper.getDisplayLoiName( + TEST_LOI.copy(customId = "", properties = mapOf("not" to "a name field")) + ) + ) + .isEqualTo("Unnamed point") } - private fun assertLabel(loi: LocationOfInterest?, expectedLabel: String) { - assertThat(loiHelper.getLabel(Optional.ofNullable(loi))).isEqualTo(expectedLabel) + @Test + fun testLoiJobName_whenNameIsNull() { + val job = TEST_LOI.job.copy(name = null) + assertThat(loiHelper.getJobName(TEST_LOI.copy(job = job))).isNull() } - private fun assertSubtitle(loi: LocationOfInterest?, expectedSubtitle: String) { - assertThat(loiHelper.getSubtitle(Optional.ofNullable(loi))).isEqualTo(expectedSubtitle) + @Test + fun testLoiJobName_whenNameIsAvailable() { + val job = TEST_LOI.job.copy(name = "job name") + assertThat(loiHelper.getJobName(TEST_LOI.copy(job = job))).isEqualTo("job name") } companion object { - private const val TEST_USER_NAME = "some user name" + private val TEST_LOI = FakeData.LOCATION_OF_INTEREST.copy() + private val TEST_AREA = FakeData.AREA_OF_INTEREST.copy() private const val TEST_JOB_NAME = "some job name" } } diff --git a/ground/src/test/java/com/google/android/ground/ui/datacollection/DataCollectionFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/datacollection/DataCollectionFragmentTest.kt index 9e4e0275ec..9070b087cf 100644 --- a/ground/src/test/java/com/google/android/ground/ui/datacollection/DataCollectionFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/datacollection/DataCollectionFragmentTest.kt @@ -68,8 +68,7 @@ class DataCollectionFragmentTest : BaseHiltTest() { setupSubmission() setupFragment() - // TODO(#2046): Update this test to match new LOI label format - onView(withText("Point")).check(matches(isDisplayed())) + onView(withText("Unnamed point")).check(matches(isDisplayed())) onView(withText(JOB.name)).check(matches(isDisplayed())) } diff --git a/ground/src/test/java/com/google/android/ground/ui/home/HomeScreenFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/home/HomeScreenFragmentTest.kt index 59cf986b8c..a666429177 100644 --- a/ground/src/test/java/com/google/android/ground/ui/home/HomeScreenFragmentTest.kt +++ b/ground/src/test/java/com/google/android/ground/ui/home/HomeScreenFragmentTest.kt @@ -42,7 +42,6 @@ import dagger.hilt.android.testing.HiltAndroidTest import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle -import org.hamcrest.CoreMatchers.not import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -131,30 +130,11 @@ class HomeScreenFragmentTest : AbstractHomeScreenFragmentTest() { mapOf(Pair(FakeData.USER.email, "data-collector")) ) - private val surveyWithTileSources: Survey = - surveyWithoutBasemap.copy( - tileSources = - listOf( - TileSource("http://google.com", TileSource.Type.MOG_COLLECTION), - ), - id = "SURVEY_WITH_TILE_SOURCES" - ) - @Test - fun offlineMapImageryMenuIsDisabledWhenActiveSurveyHasNoBasemap() = runWithTestDispatcher { + fun `offline map imagery menu is always enabled`() = runWithTestDispatcher { surveyRepository.selectedSurveyId = surveyWithoutBasemap.id advanceUntilIdle() - openDrawer() - onView(withId(R.id.nav_offline_areas)).check(matches(not(isEnabled()))) - } - - @Test - fun offlineMapImageryMenuIsEnabledWhenActiveSurveyHasBasemap() = runWithTestDispatcher { - localSurveyStore.insertOrUpdateSurvey(surveyWithTileSources) - surveyRepository.selectedSurveyId = surveyWithTileSources.id - advanceUntilIdle() - openDrawer() onView(withId(R.id.nav_offline_areas)).check(matches(isEnabled())) } @@ -172,6 +152,7 @@ class NavigationDrawerItemClickTest( ) : AbstractHomeScreenFragmentTest() { @Inject lateinit var navigator: Navigator + @Inject lateinit var surveyRepository: SurveyRepository @Test @@ -219,19 +200,12 @@ class NavigationDrawerItemClickTest( true, "Clicking 'sync status' should navigate to fragment" ), - arrayOf( - "Offline map imagery", - TEST_SURVEY_WITHOUT_OFFLINE_TILES, - null, - false, - "Clicking 'offline map imagery' when survey doesn't have offline tiles should do nothing" - ), arrayOf( "Offline map imagery", TEST_SURVEY_WITH_OFFLINE_TILES, HomeScreenFragmentDirections.showOfflineAreas(), true, - "Clicking 'offline map imagery' when survey has offline tiles should navigate to fragment" + "Clicking 'offline map imagery' should navigate to fragment" ), arrayOf( "Settings", diff --git a/ground/src/test/java/com/google/android/ground/ui/home/mapcontainer/LoiCardUtilTest.kt b/ground/src/test/java/com/google/android/ground/ui/home/mapcontainer/LoiCardUtilTest.kt deleted file mode 100644 index c3f4f13895..0000000000 --- a/ground/src/test/java/com/google/android/ground/ui/home/mapcontainer/LoiCardUtilTest.kt +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2023 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 com.google.android.ground.ui.home.mapcontainer - -import android.content.Context -import androidx.test.core.app.ApplicationProvider -import com.google.android.ground.BaseHiltTest -import com.google.android.ground.ui.home.mapcontainer.cards.LoiCardUtil.getDisplayLoiName -import com.google.android.ground.ui.home.mapcontainer.cards.LoiCardUtil.getJobName -import com.google.android.ground.ui.home.mapcontainer.cards.LoiCardUtil.getSubmissionsText -import com.google.common.truth.Truth.assertThat -import com.sharedtest.FakeData -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@HiltAndroidTest -@RunWith(RobolectricTestRunner::class) -class LoiCardUtilTest : BaseHiltTest() { - - private val context: Context = ApplicationProvider.getApplicationContext() - - @Test - fun testLoiNameWithPoint_whenCustomIdAndPropertiesAreNull() { - assertThat(getDisplayLoiName(context, TEST_LOI.copy(customId = "", properties = mapOf()))) - .isEqualTo("Unnamed point") - } - - @Test - fun testLoiNameWithPolygon_whenCustomIdAndPropertiesAreNull() { - assertThat(getDisplayLoiName(context, TEST_AREA.copy(customId = "", properties = mapOf()))) - .isEqualTo("Unnamed area") - } - - @Test - fun testLoiName_whenCustomIdIsAvailable() { - assertThat(getDisplayLoiName(context, TEST_LOI.copy(customId = "some value"))) - .isEqualTo("Point (some value)") - } - - @Test - fun testArea_whenCustomIdIsAvailable() { - assertThat(getDisplayLoiName(context, TEST_AREA.copy(customId = "some value"))) - .isEqualTo("Area (some value)") - } - - @Test - fun testArea_whenCustomIdIsNotAvailable_usesPropertiesId() { - assertThat( - getDisplayLoiName( - context, - TEST_AREA.copy(customId = "", properties = mapOf("id" to "property id")) - ) - ) - .isEqualTo("Area (property id)") - } - - @Test - fun testLoiName_whenPropertiesNameIsAvailable() { - assertThat( - getDisplayLoiName(context, TEST_LOI.copy(properties = mapOf("name" to "custom name"))) - ) - .isEqualTo("custom name") - } - - @Test - fun testLoiName_whenCustomIdAndPropertiesNameIsAvailable() { - assertThat( - getDisplayLoiName( - context, - TEST_LOI.copy(customId = "some value", properties = mapOf("name" to "custom name")) - ) - ) - .isEqualTo("custom name (some value)") - } - - @Test - fun testLoiName_whenPropertiesDoesNotContainName() { - assertThat( - getDisplayLoiName( - context, - TEST_LOI.copy(customId = "", properties = mapOf("not" to "a name field")) - ) - ) - .isEqualTo("Unnamed point") - } - - @Test - fun testLoiJobName_whenNameIsNull() { - val job = TEST_LOI.job.copy(name = null) - assertThat(getJobName(TEST_LOI.copy(job = job))).isNull() - } - - @Test - fun testLoiJobName_whenNameIsAvailable() { - val job = TEST_LOI.job.copy(name = "job name") - assertThat(getJobName(TEST_LOI.copy(job = job))).isEqualTo("job name") - } - - @Test - fun testSubmissionsText_whenZero() { - assertThat(getSubmissionsText(0)).isEqualTo("No submissions") - } - - @Test - fun testSubmissionsText_whenOne() { - assertThat(getSubmissionsText(1)).isEqualTo("1 submission") - } - - @Test - fun testSubmissionsText_whenTwo() { - assertThat(getSubmissionsText(2)).isEqualTo("2 submissions") - } - - companion object { - private val TEST_LOI = FakeData.LOCATION_OF_INTEREST.copy() - private val TEST_AREA = FakeData.AREA_OF_INTEREST.copy() - } -} diff --git a/sharedTest/src/main/kotlin/com/sharedtest/persistence/sync/FakeWorkManager.kt b/sharedTest/src/main/kotlin/com/sharedtest/persistence/sync/FakeWorkManager.kt index 36cb2f75ca..d0b9497078 100644 --- a/sharedTest/src/main/kotlin/com/sharedtest/persistence/sync/FakeWorkManager.kt +++ b/sharedTest/src/main/kotlin/com/sharedtest/persistence/sync/FakeWorkManager.kt @@ -18,14 +18,25 @@ package com.sharedtest.persistence.sync import android.annotation.SuppressLint import android.app.PendingIntent import androidx.lifecycle.LiveData -import androidx.work.* +import androidx.work.Configuration +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.Operation +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkContinuation +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkQuery +import androidx.work.WorkRequest import com.google.common.util.concurrent.ListenableFuture -import java.util.* +import java.util.UUID +import kotlinx.coroutines.flow.Flow @SuppressLint("RestrictedApi") class FakeWorkManager : WorkManager() { override fun getConfiguration(): Configuration { - TODO("Not yet implemented") + throw NotImplementedError() } override fun enqueue(requests: List): Operation { @@ -96,6 +107,10 @@ class FakeWorkManager : WorkManager() { throw NotImplementedError() } + override fun getWorkInfoByIdFlow(id: UUID): Flow { + throw NotImplementedError() + } + override fun getWorkInfoById(id: UUID): ListenableFuture { throw NotImplementedError() } @@ -104,6 +119,10 @@ class FakeWorkManager : WorkManager() { throw NotImplementedError() } + override fun getWorkInfosByTagFlow(tag: String): Flow> { + throw NotImplementedError() + } + override fun getWorkInfosByTag(tag: String): ListenableFuture> { throw NotImplementedError() } @@ -112,6 +131,10 @@ class FakeWorkManager : WorkManager() { throw NotImplementedError() } + override fun getWorkInfosForUniqueWorkFlow(uniqueWorkName: String): Flow> { + throw NotImplementedError() + } + override fun getWorkInfosForUniqueWork(uniqueWorkName: String): ListenableFuture> { throw NotImplementedError() } @@ -120,11 +143,15 @@ class FakeWorkManager : WorkManager() { throw NotImplementedError() } + override fun getWorkInfosFlow(workQuery: WorkQuery): Flow> { + throw NotImplementedError() + } + override fun getWorkInfos(workQuery: WorkQuery): ListenableFuture> { throw NotImplementedError() } override fun updateWork(request: WorkRequest): ListenableFuture { - TODO("Not yet implemented") + throw NotImplementedError() } }