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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, TaskData>? =
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ 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
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
Expand Down Expand Up @@ -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(),
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -66,7 +63,6 @@ internal constructor(
val userRepository: UserRepositoryInterface,
) : AbstractViewModel() {

private val savedStateHandle: SavedStateHandle = SavedStateHandle()
private val _openDrawerRequests: MutableSharedFlow<Unit> = MutableSharedFlow()
val openDrawerRequestsFlow: SharedFlow<Unit> = _openDrawerRequests.asSharedFlow()

Expand All @@ -83,17 +79,8 @@ internal constructor(
// Issue URL: https://github.com/google/ground-android/issues/1730
val showOfflineAreaMenuItem: LiveData<Boolean> = 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() }
Expand Down Expand Up @@ -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) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(),
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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
}
}
}
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/layout/home_screen_frag.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true">

<androidx.coordinatorlayout.widget.CoordinatorLayout
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/layout/map_task_frag.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:baselineAligned="false"
android:orientation="horizontal"
android:padding="8dp">

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("")
}
}
6 changes: 0 additions & 6 deletions config/detekt/baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,6 @@
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>CyclomaticComplexMethod:ValueJsonConverter.kt$ValueJsonConverter$fun toResponse(task: Task, obj: Any): TaskData?</ID>
<ID>LabeledExpression:DataCollectionViewModel.kt$DataCollectionViewModel$@withReady</ID>
<ID>LabeledExpression:GoogleMapsFragment.kt$GoogleMapsFragment$@getMapAsync</ID>
<ID>LargeClass:DataCollectionFragmentTest.kt$DataCollectionFragmentTest : BaseHiltTest</ID>
<ID>LongMethod:DataCollectionTaskFragment.kt$DataCollectionTaskFragment$override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, )</ID>
<ID>LongMethod:ValueJsonConverter.kt$ValueJsonConverter$fun toResponse(task: Task, obj: Any): TaskData?</ID>
<ID>NestedBlockDepth:SubmissionDataConverter.kt$SubmissionDataConverter$@JvmStatic fun fromString(job: Job, jsonString: String?): SubmissionData</ID>
<ID>ReturnCount:HomeScreenViewModel.kt$HomeScreenViewModel$suspend fun getDraftSubmission(): DraftSubmission?</ID>
</CurrentIssues>
</SmellBaseline>
1 change: 1 addition & 0 deletions config/detekt/detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ complexity:
ignoredLabels: [ ]
LargeClass:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]
threshold: 600
LongMethod:
active: true
Expand Down
1 change: 1 addition & 0 deletions config/lint/lint.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
<issue id="Typos" severity="error" />
<issue id="Untranslatable" severity="error" />
<issue id="UnusedResources" severity="error">
<!-- display_700 font family is defined for design system typography parity but not yet directly referenced in UI layouts -->
<ignore path="res/font/display_700.xml" />
</issue>
<issue id="UselessLeaf" severity="error" />
Expand Down
Loading