Skip to content

Commit

Permalink
Merge pull request #6134 from seadowg/forms-anr
Browse files Browse the repository at this point in the history
Fix potential ANR when saving form
  • Loading branch information
seadowg authored May 16, 2024
2 parents 8ddfa8b + eb8891c commit 4fa233e
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 79 deletions.
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

0 comments on commit 4fa233e

Please sign in to comment.