Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix potential ANR when saving form #6134

Merged
merged 4 commits into from
May 16, 2024
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
@@ -1,5 +1,8 @@
package org.odk.collect.androidshared.data

import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData

/**
* Useful for values that are read multiple times but only used
* once (like an error that shows a dialog once).
Expand All @@ -16,3 +19,12 @@ data class Consumable<T>(val value: T) {
consumed = true
}
}

fun <T> LiveData<Consumable<T>>.consume(lifecycleOwner: LifecycleOwner, consumer: (T) -> Unit) {
observe(lifecycleOwner) { consumable ->
if (!consumable.isConsumed()) {
consumable.consume()
consumer(consumable.value)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package org.odk.collect.android.mainmenu

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
Expand All @@ -28,6 +27,7 @@ import org.odk.collect.android.projects.ProjectIconView
import org.odk.collect.android.projects.ProjectSettingsDialog
import org.odk.collect.android.utilities.ActionRegister
import org.odk.collect.android.utilities.ApplicationConstants
import org.odk.collect.androidshared.data.consume
import org.odk.collect.androidshared.ui.DialogFragmentUtils
import org.odk.collect.androidshared.ui.SnackbarUtils
import org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard
Expand All @@ -45,8 +45,9 @@ class MainMenuFragment(
private lateinit var permissionsViewModel: RequestPermissionsViewModel

private val formEntryFlowLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
displayFormSavedSnackbar(it.data?.data)
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val uri = result.data?.data
mainMenuViewModel.setSavedForm(uri)
}

override fun onAttach(context: Context) {
Expand Down Expand Up @@ -85,6 +86,24 @@ class MainMenuFragment(
this.parentFragmentManager
)
}

mainMenuViewModel.savedForm.consume(viewLifecycleOwner) { value ->
SnackbarUtils.showLongSnackbar(
requireView(),
getString(value.message),
action = value.action?.let { action ->
SnackbarUtils.Action(getString(action)) {
formEntryFlowLauncher.launch(
FormFillingIntentFactory.editInstanceIntent(
requireContext(),
value.uri
)
)
}
},
displayDismissButton = true
)
}
}

override fun onResume() {
Expand Down Expand Up @@ -243,30 +262,4 @@ class MainMenuFragment(
binding.googleDriveDeprecationBanner.root.visibility = View.GONE
}
}

private fun displayFormSavedSnackbar(uri: Uri?) {
if (uri == null) {
return
}

val formSavedSnackbarDetails = mainMenuViewModel.getFormSavedSnackbarDetails(uri)

formSavedSnackbarDetails?.let {
SnackbarUtils.showLongSnackbar(
requireView(),
getString(it.first),
action = it.second?.let { action ->
SnackbarUtils.Action(getString(action)) {
formEntryFlowLauncher.launch(
FormFillingIntentFactory.editInstanceIntent(
requireContext(),
uri
)
)
}
},
displayDismissButton = true
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package org.odk.collect.android.mainmenu
import android.app.Application
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
import org.odk.collect.android.instancemanagement.InstanceDiskSynchronizer
import org.odk.collect.android.instancemanagement.InstancesDataService
import org.odk.collect.android.instancemanagement.autosend.AutoSendSettingsProvider
Expand All @@ -15,6 +17,7 @@ import org.odk.collect.android.utilities.ContentUriHelper
import org.odk.collect.android.utilities.FormsRepositoryProvider
import org.odk.collect.android.utilities.InstancesRepositoryProvider
import org.odk.collect.android.version.VersionInformation
import org.odk.collect.androidshared.data.Consumable
import org.odk.collect.async.Scheduler
import org.odk.collect.forms.instances.Instance
import org.odk.collect.settings.SettingsProvider
Expand Down Expand Up @@ -42,7 +45,10 @@ class MainMenuViewModel(
var commitDescription = ""
if (versionInformation.commitCount != null) {
commitDescription =
appendToCommitDescription(commitDescription, versionInformation.commitCount.toString())
appendToCommitDescription(
commitDescription,
versionInformation.commitCount.toString()
)
}
if (versionInformation.commitSHA != null) {
commitDescription =
Expand All @@ -58,29 +64,38 @@ class MainMenuViewModel(
}
}

private val _savedForm = MutableLiveData<SavedForm>()
val savedForm: LiveData<Consumable<SavedForm>> = _savedForm.map { Consumable(it) }

fun shouldEditSavedFormButtonBeVisible(): Boolean {
return settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_EDIT_SAVED)
return settingsProvider.getProtectedSettings()
.getBoolean(ProtectedProjectKeys.KEY_EDIT_SAVED)
}

fun shouldSendFinalizedFormButtonBeVisible(): Boolean {
return settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_SEND_FINALIZED)
return settingsProvider.getProtectedSettings()
.getBoolean(ProtectedProjectKeys.KEY_SEND_FINALIZED)
}

fun shouldViewSentFormButtonBeVisible(): Boolean {
return settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_VIEW_SENT)
return settingsProvider.getProtectedSettings()
.getBoolean(ProtectedProjectKeys.KEY_VIEW_SENT)
}

fun shouldGetBlankFormButtonBeVisible(): Boolean {
val buttonEnabled = settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_GET_BLANK)
val buttonEnabled =
settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_GET_BLANK)
return !isMatchExactlyEnabled() && buttonEnabled
}

fun shouldDeleteSavedFormButtonBeVisible(): Boolean {
return settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_DELETE_SAVED)
return settingsProvider.getProtectedSettings()
.getBoolean(ProtectedProjectKeys.KEY_DELETE_SAVED)
}

private fun isMatchExactlyEnabled(): Boolean {
return settingsProvider.getUnprotectedSettings().getFormUpdateMode(application) == FormUpdateMode.MATCH_EXACTLY
return settingsProvider.getUnprotectedSettings()
.getFormUpdateMode(application) == FormUpdateMode.MATCH_EXACTLY
}

private fun appendToCommitDescription(commitDescription: String, part: String): String {
Expand Down Expand Up @@ -108,13 +123,27 @@ class MainMenuViewModel(
val sentInstancesCount: LiveData<Int>
get() = instancesDataService.sentCount

fun getFormSavedSnackbarDetails(uri: Uri): Pair<Int, Int?>? {
fun setSavedForm(uri: Uri?) {
if (uri == null) {
return
}

scheduler.immediate {
val details = getFormSavedSnackbarDetails(uri)
if (details != null) {
_savedForm.postValue(SavedForm(uri, details.first, details.second))
}
}
}

private fun getFormSavedSnackbarDetails(uri: Uri): Pair<Int, Int?>? {
val instance = instancesRepositoryProvider.get().get(ContentUriHelper.getIdFromUri(uri))
return if (instance != null) {
val message = if (instance.isDraft()) {
org.odk.collect.strings.R.string.form_saved_as_draft
} else if (instance.status == Instance.STATUS_COMPLETE || instance.status == Instance.STATUS_SUBMISSION_FAILED) {
val form = formsRepositoryProvider.get().getAllByFormIdAndVersion(instance.formId, instance.formVersion).first()
val form = formsRepositoryProvider.get()
.getAllByFormIdAndVersion(instance.formId, instance.formVersion).first()
if (form.shouldFormBeSentAutomatically(autoSendSettingsProvider.isAutoSendEnabledInSettings())) {
org.odk.collect.strings.R.string.form_sending
} else {
Expand All @@ -139,4 +168,6 @@ class MainMenuViewModel(
null
}
}

data class SavedForm(val uri: Uri, val message: Int, val action: Int?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class MainMenuActivityTest {
on { sendableInstancesCount } doReturn MutableLiveData(0)
on { sentInstancesCount } doReturn MutableLiveData(0)
on { editableInstancesCount } doReturn MutableLiveData(0)
on { savedForm } doReturn MutableLiveData()
}

private val currentProjectViewModel = mock<CurrentProjectViewModel> {
Expand Down
Loading