diff --git a/app/src/main/java/org/groundplatform/android/data/local/room/converter/SubmissionDataConverter.kt b/app/src/main/java/org/groundplatform/android/data/local/room/converter/SubmissionDataConverter.kt index 91cf4c37b8..f755fda267 100644 --- a/app/src/main/java/org/groundplatform/android/data/local/room/converter/SubmissionDataConverter.kt +++ b/app/src/main/java/org/groundplatform/android/data/local/room/converter/SubmissionDataConverter.kt @@ -50,19 +50,28 @@ object SubmissionDataConverter { val jsonObject = JSONObject(jsonString) val keys = jsonObject.keys() while (keys.hasNext()) { - try { - val taskId = keys.next() - val task = job.getTask(taskId) - ValueJsonConverter.toResponse(task, jsonObject[taskId])?.let { map[taskId] = it } - } catch (e: LocalDataConsistencyException) { - Timber.d("Bad submission data in local db: ${e.message}") - } catch (e: Job.TaskNotFoundException) { - Timber.d(e, "Ignoring data for unknown task") - } + val taskId = keys.next() + parseTaskData(job, jsonObject, taskId)?.let { (id, taskData) -> map[id] = taskData } } } catch (e: JSONException) { Timber.e(e, "Error parsing JSON string") } return SubmissionData(map.toPersistentMap()) } + + private fun parseTaskData( + job: Job, + jsonObject: JSONObject, + taskId: String, + ): Pair? = + try { + val task = job.getTask(taskId) + ValueJsonConverter.toResponse(task, jsonObject[taskId])?.let { Pair(taskId, it) } + } catch (e: LocalDataConsistencyException) { + Timber.d("Bad submission data in local db: ${e.message}") + null + } catch (e: Job.TaskNotFoundException) { + Timber.d(e, "Ignoring data for unknown task") + null + } } diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt index 1e4804444b..201ac97b22 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataCollectionViewModel.kt @@ -285,46 +285,50 @@ internal constructor( moveToTask(withReady { taskSequenceHandler.getPreviousTask(it.currentTaskId) }) } - fun onNextClicked(taskId: String) = withReady { uiState -> - val taskViewModel = getTaskViewModel(taskId) ?: return@withReady - validateOrShow(taskViewModel) { - val task = taskViewModel.task - val value = taskViewModel.taskTaskData.value - updateDataAndInvalidateTasks(task, value) - - if (!taskSequenceHandler.isLastPosition(task.id)) { - moveToNextTask() - } else { - clearDraft() - externalScope.launch(ioDispatcher) { - val submittedLoiId = saveChanges(uiState, getDeltas()) - val loiReport = - getLoiReportUseCase.invoke( - loiName = getTypedLoiNameOrEmpty(), - loiId = submittedLoiId, - surveyId = uiState.surveyId, - ) - _uiState.value = DataCollectionUiState.TaskSubmitted(loiReport) + fun onNextClicked(taskId: String) { + val taskViewModel = getTaskViewModel(taskId) ?: return + withReady { uiState -> + validateOrShow(taskViewModel) { + val task = taskViewModel.task + val value = taskViewModel.taskTaskData.value + updateDataAndInvalidateTasks(task, value) + + if (!taskSequenceHandler.isLastPosition(task.id)) { + moveToNextTask() + } else { + clearDraft() + externalScope.launch(ioDispatcher) { + val submittedLoiId = saveChanges(uiState, getDeltas()) + val loiReport = + getLoiReportUseCase.invoke( + loiName = getTypedLoiNameOrEmpty(), + loiId = submittedLoiId, + surveyId = uiState.surveyId, + ) + _uiState.value = DataCollectionUiState.TaskSubmitted(loiReport) + } } } } } - fun onPreviousClicked(taskId: String) = withReady { _ -> - val taskViewModel = getTaskViewModel(taskId) ?: return@withReady - val task = taskViewModel.task - val taskValue = taskViewModel.taskTaskData.value + fun onPreviousClicked(taskId: String) { + val taskViewModel = getTaskViewModel(taskId) ?: return + withReady { _ -> + val task = taskViewModel.task + val taskValue = taskViewModel.taskTaskData.value - val validationError = - if (taskValue?.isNotNullOrEmpty() == true) taskViewModel.validate() else null + val validationError = + if (taskValue?.isNotNullOrEmpty() == true) taskViewModel.validate() else null - if (validationError != null) { - viewModelScope.launch { - _uiEffects.send(DataCollectionUiEffect.ShowValidationError(validationError)) + if (validationError != null) { + viewModelScope.launch { + _uiEffects.send(DataCollectionUiEffect.ShowValidationError(validationError)) + } + } else { + updateDataAndInvalidateTasks(task, taskValue) + moveToPreviousTask() } - } else { - updateDataAndInvalidateTasks(task, taskValue) - moveToPreviousTask() } } diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt index 03adbf731b..a9f71514d3 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt @@ -17,7 +17,6 @@ package org.groundplatform.android.ui.home import android.content.ActivityNotFoundException import android.content.Intent -import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -25,6 +24,7 @@ import android.view.ViewGroup import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.core.net.toUri import androidx.core.view.GravityCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope @@ -206,12 +206,12 @@ class HomeScreenFragment : AbstractFragment(), BackPressListener { internal fun openPlayStore(packageName: String, startActivity: (Intent) -> Unit) { try { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName"))) + startActivity(Intent(Intent.ACTION_VIEW, "market://details?id=$packageName".toUri())) } catch (_: ActivityNotFoundException) { startActivity( Intent( Intent.ACTION_VIEW, - Uri.parse("https://play.google.com/store/apps/details?id=$packageName"), + "https://play.google.com/store/apps/details?id=$packageName".toUri(), ) ) } diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt index 2302b11773..1aac304972 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt @@ -17,7 +17,6 @@ package org.groundplatform.android.ui.home import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -51,8 +50,6 @@ import org.groundplatform.domain.repository.UserRepositoryInterface data class HomeDrawerState(val user: User, val survey: Survey?, val appVersion: String) -private const val AWAITING_PHOTO_CAPTURE_KEY = "awaiting_photo_capture" - @SharedViewModel class HomeScreenViewModel @Inject @@ -66,7 +63,6 @@ internal constructor( val userRepository: UserRepositoryInterface, ) : AbstractViewModel() { - private val savedStateHandle: SavedStateHandle = SavedStateHandle() private val _openDrawerRequests: MutableSharedFlow = MutableSharedFlow() val openDrawerRequestsFlow: SharedFlow = _openDrawerRequests.asSharedFlow() @@ -83,17 +79,8 @@ internal constructor( // Issue URL: https://github.com/google/ground-android/issues/1730 val showOfflineAreaMenuItem: LiveData = MutableLiveData(true) - /* Indicates the application is being restored after a photo capture. - * - * We need to persist this state here to control [HomeScreenFragement] UI treatments when returning - * from a photo capture task—we do it this way because saving instance state bundles across fragments - * does not prove simple. - * */ - var awaitingPhotoCapture: Boolean - get() = savedStateHandle[AWAITING_PHOTO_CAPTURE_KEY] ?: false - set(newValue) { - savedStateHandle[AWAITING_PHOTO_CAPTURE_KEY] = newValue - } + // Indicates whether the application is being restored after a photo capture. + var awaitingPhotoCapture: Boolean = false init { viewModelScope.launch { kickLocalMutationSyncWorkers() } @@ -127,12 +114,10 @@ internal constructor( } /** Attempts to return draft submission for the currently active active survey. */ - suspend fun getDraftSubmission(): DraftSubmission? { + suspend fun getDraftSubmission(): DraftSubmission? = // TODO: Check whether the previous user id matches with current user or not. // Issue URL: https://github.com/google/ground-android/issues/2903 - val survey = surveyRepository.activeSurveyFlow.first() ?: return null - return submissionRepository.getDraftSubmission(survey) - } + surveyRepository.activeSurveyFlow.first()?.let { submissionRepository.getDraftSubmission(it) } fun openNavDrawer() { viewModelScope.launch { _openDrawerRequests.emit(Unit) } diff --git a/app/src/main/java/org/groundplatform/android/ui/main/MainActivity.kt b/app/src/main/java/org/groundplatform/android/ui/main/MainActivity.kt index 3d8d9b19b4..452d7fb099 100644 --- a/app/src/main/java/org/groundplatform/android/ui/main/MainActivity.kt +++ b/app/src/main/java/org/groundplatform/android/ui/main/MainActivity.kt @@ -18,11 +18,11 @@ package org.groundplatform.android.ui.main import android.app.AlertDialog import android.content.ActivityNotFoundException import android.content.Intent -import android.net.Uri import android.os.Bundle import androidx.activity.OnBackPressedCallback import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatDelegate +import androidx.core.net.toUri import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -210,15 +210,13 @@ class MainActivity : AbstractActivity() { .setPositiveButton(R.string.dialog_button_update) { _, _ -> val appPackageName = packageName try { - startActivity( - Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$appPackageName")) - ) + startActivity(Intent(Intent.ACTION_VIEW, "market://details?id=$appPackageName".toUri())) } catch (e: ActivityNotFoundException) { Timber.e("Not able to open play store: $e") startActivity( Intent( Intent.ACTION_VIEW, - Uri.parse("https://play.google.com/store/apps/details?id=$appPackageName"), + "https://play.google.com/store/apps/details?id=$appPackageName".toUri(), ) ) } diff --git a/app/src/main/java/org/groundplatform/android/ui/main/MainViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/main/MainViewModel.kt index a9e06300fa..bbc8624b65 100644 --- a/app/src/main/java/org/groundplatform/android/ui/main/MainViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/main/MainViewModel.kt @@ -144,7 +144,7 @@ constructor( fun isAppUpdateAvailable(currentVersion: String = BuildConfig.VERSION_NAME): Boolean { val forceUpdate = remoteConfig.getBoolean("force_update") - val latestVersion = remoteConfig.getString("min_app_version") ?: "" + val latestVersion = remoteConfig.getString("min_app_version") return forceUpdate && latestVersion.isNotBlank() && diff --git a/app/src/main/java/org/groundplatform/android/ui/map/gms/GoogleMapsFragment.kt b/app/src/main/java/org/groundplatform/android/ui/map/gms/GoogleMapsFragment.kt index 56ede322fa..fe610a5815 100644 --- a/app/src/main/java/org/groundplatform/android/ui/map/gms/GoogleMapsFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/map/gms/GoogleMapsFragment.kt @@ -152,9 +152,10 @@ class GoogleMapsFragment : SupportMapFragment(), MapFragment { ) { containerFragment.replaceFragment(containerId, this) getMapAsync { googleMap: GoogleMap -> - if (view == null) return@getMapAsync - onMapReady(googleMap) - onMapReadyCallback(this) + if (view != null) { + onMapReady(googleMap) + onMapReadyCallback(this) + } } } diff --git a/app/src/main/java/org/groundplatform/android/ui/map/gms/mog/MogTileExt.kt b/app/src/main/java/org/groundplatform/android/ui/map/gms/mog/MogTileExt.kt index 8ecc5453c6..1616e2dd82 100644 --- a/app/src/main/java/org/groundplatform/android/ui/map/gms/mog/MogTileExt.kt +++ b/app/src/main/java/org/groundplatform/android/ui/map/gms/mog/MogTileExt.kt @@ -17,6 +17,7 @@ package org.groundplatform.android.ui.map.gms.mog import android.graphics.Color +import androidx.core.graphics.get import com.google.android.gms.maps.model.Tile import org.groundplatform.android.util.image.TileImageTransformer import org.groundplatform.domain.model.imagery.MogTile @@ -32,6 +33,6 @@ fun MogTile.getProcessedImageData(): ByteArray { val noData = metadata.noDataValue ?: return buildJfifFile() val noDataColor = Color.rgb(noData, noData, noData) return TileImageTransformer.setTransparentIf(buildJfifFile()) { bitmap, x, y -> - bitmap.getPixel(x, y) == noDataColor + bitmap[x, y] == noDataColor } } diff --git a/app/src/main/java/org/groundplatform/android/ui/signin/SignInScreen.kt b/app/src/main/java/org/groundplatform/android/ui/signin/SignInScreen.kt index 83e07f956a..c4eee64751 100644 --- a/app/src/main/java/org/groundplatform/android/ui/signin/SignInScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/signin/SignInScreen.kt @@ -49,7 +49,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.android.gms.auth.api.signin.GoogleSignInStatusCodes.SIGN_IN_CANCELLED import com.google.android.gms.common.api.ApiException diff --git a/app/src/main/java/org/groundplatform/android/ui/util/ColorUtil.kt b/app/src/main/java/org/groundplatform/android/ui/util/ColorUtil.kt index 05fa18aae8..236902ef81 100644 --- a/app/src/main/java/org/groundplatform/android/ui/util/ColorUtil.kt +++ b/app/src/main/java/org/groundplatform/android/ui/util/ColorUtil.kt @@ -16,12 +16,13 @@ package org.groundplatform.android.ui.util import android.graphics.Color +import androidx.core.graphics.toColorInt import org.groundplatform.domain.model.job.Job import timber.log.Timber fun Job.getDefaultColor(): Int = try { - Color.parseColor(style?.color ?: "") + (style?.color ?: "").toColorInt() } catch (t: Throwable) { Timber.w(t, "Invalid or missing color ${style?.color} in job $id") Color.BLACK diff --git a/app/src/main/java/org/groundplatform/android/util/image/TileImageTransformer.kt b/app/src/main/java/org/groundplatform/android/util/image/TileImageTransformer.kt index 8c2a98c94a..c880c1167b 100644 --- a/app/src/main/java/org/groundplatform/android/util/image/TileImageTransformer.kt +++ b/app/src/main/java/org/groundplatform/android/util/image/TileImageTransformer.kt @@ -20,6 +20,7 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Color import android.os.Build +import androidx.core.graphics.set import java.io.ByteArrayOutputStream /** Utility for transforming map tile images. */ @@ -40,7 +41,7 @@ object TileImageTransformer { for (y in 0 until bitmap.height) { for (x in 0 until bitmap.width) { if (isTransparent(bitmap, x, y)) { - bitmap.setPixel(x, y, Color.TRANSPARENT) + bitmap[x, y] = Color.TRANSPARENT } } } diff --git a/app/src/main/res/layout/home_screen_frag.xml b/app/src/main/res/layout/home_screen_frag.xml index fb27f2b6ac..6ce4fa0deb 100644 --- a/app/src/main/res/layout/home_screen_frag.xml +++ b/app/src/main/res/layout/home_screen_frag.xml @@ -23,6 +23,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clickable="true" + android:focusable="true" android:focusableInTouchMode="true"> diff --git a/app/src/test/java/org/groundplatform/android/TestFirebaseModule.kt b/app/src/test/java/org/groundplatform/android/TestFirebaseModule.kt index 12173cc2e1..cf748997a1 100644 --- a/app/src/test/java/org/groundplatform/android/TestFirebaseModule.kt +++ b/app/src/test/java/org/groundplatform/android/TestFirebaseModule.kt @@ -29,5 +29,7 @@ object TestFirebaseModule { @Provides fun provideFirebaseRemoteConfig(): FirebaseRemoteConfig = - Mockito.mock(FirebaseRemoteConfig::class.java) + Mockito.mock(FirebaseRemoteConfig::class.java).apply { + Mockito.`when`(getString(Mockito.anyString())).thenReturn("") + } } diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index e5004c672e..936bc8ded7 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -3,12 +3,6 @@ CyclomaticComplexMethod:ValueJsonConverter.kt$ValueJsonConverter$fun toResponse(task: Task, obj: Any): TaskData? - LabeledExpression:DataCollectionViewModel.kt$DataCollectionViewModel$@withReady - LabeledExpression:GoogleMapsFragment.kt$GoogleMapsFragment$@getMapAsync - LargeClass:DataCollectionFragmentTest.kt$DataCollectionFragmentTest : BaseHiltTest - LongMethod:DataCollectionTaskFragment.kt$DataCollectionTaskFragment$override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ) LongMethod:ValueJsonConverter.kt$ValueJsonConverter$fun toResponse(task: Task, obj: Any): TaskData? - NestedBlockDepth:SubmissionDataConverter.kt$SubmissionDataConverter$@JvmStatic fun fromString(job: Job, jsonString: String?): SubmissionData - ReturnCount:HomeScreenViewModel.kt$HomeScreenViewModel$suspend fun getDraftSubmission(): DraftSubmission? diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 596ad02e9d..0941be57a4 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -140,6 +140,7 @@ complexity: ignoredLabels: [ ] LargeClass: active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] threshold: 600 LongMethod: active: true diff --git a/config/lint/lint.xml b/config/lint/lint.xml index bcbfea816b..3c01b1a8d1 100644 --- a/config/lint/lint.xml +++ b/config/lint/lint.xml @@ -53,6 +53,7 @@ +