Skip to content

Commit

Permalink
Add loading state to FormUriActivity
Browse files Browse the repository at this point in the history
  • Loading branch information
seadowg committed Dec 13, 2023
1 parent df10699 commit 1ae3580
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 80 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
package org.odk.collect.android.external

import android.content.ContentResolver
import android.content.Intent
import android.content.res.Resources
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.odk.collect.analytics.Analytics
import org.odk.collect.android.R
import org.odk.collect.android.activities.FormFillingActivity
import org.odk.collect.android.analytics.AnalyticsEvents
import org.odk.collect.android.injection.DaggerUtils
Expand Down Expand Up @@ -55,35 +65,122 @@ class FormUriActivity : ComponentActivity() {
finish()
}

private val formUriViewModel by viewModels<FormUriViewModel> {
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
return FormUriViewModel(
intent.data,
scheduler,
projectsRepository,
projectsDataService,
contentResolver,
formsRepositoryProvider,
instanceRepositoryProvider,
resources
) as T
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DaggerUtils.getComponent(this).inject(this)
setContentView(R.layout.circular_progress_indicator)

formUriViewModel.error.observe(this) {
if (it == null) {
if (savedInstanceState != null) {
if (!savedInstanceState.getBoolean(FORM_FILLING_ALREADY_STARTED)) {
startForm()
}
} else {
startForm()
}
} else {
displayErrorDialog(it)
}
}
}

private fun startForm() {
formFillingAlreadyStarted = true
openForm.launch(
Intent(this, FormFillingActivity::class.java).apply {
action = intent.action
data = intent.data
intent.extras?.let { sourceExtras -> putExtras(sourceExtras) }
if (!canFormBeEdited()) {
putExtra(
ApplicationConstants.BundleKeys.FORM_MODE,
ApplicationConstants.FormModes.VIEW_SENT
)
}
}
)
}

private fun displayErrorDialog(message: String) {
MaterialAlertDialogBuilder(this)
.setMessage(message)
.setPositiveButton(string.ok) { _, _ -> finish() }
.create()
.show()
}

private fun canFormBeEdited(): Boolean {
val uri = intent.data!!
val uriMimeType = contentResolver.getType(uri)

val formEditingEnabled = if (uriMimeType == InstancesContract.CONTENT_ITEM_TYPE) {
val instance = instanceRepositoryProvider.get().get(ContentUriHelper.getIdFromUri(uri))
instance!!.canBeEdited(settingsProvider)
} else {
true
}

return formEditingEnabled
}

override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(FORM_FILLING_ALREADY_STARTED, formFillingAlreadyStarted)
super.onSaveInstanceState(outState)
}

companion object {
private const val FORM_FILLING_ALREADY_STARTED = "FORM_FILLING_ALREADY_STARTED"
}
}

private class FormUriViewModel(
private val uri: Uri?,
scheduler: Scheduler,
private val projectsRepository: ProjectsRepository,
private val projectsDataService: ProjectsDataService,
private val contentResolver: ContentResolver,
private val formsRepositoryProvider: FormsRepositoryProvider,
private val instancesRepositoryProvider: InstancesRepositoryProvider,
private val resources: Resources
) : ViewModel() {

private val _error = MutableLiveData<String?>()
val error: LiveData<String?> = _error

init {
scheduler.immediate(
background = {
assertProjectListNotEmpty() ?: assertCurrentProjectUsed() ?: assertValidUri()
?: assertFormExists() ?: assertFormNotEncrypted()
},
foreground = { error ->
if (error != null) {
displayErrorDialog(error)
} else {
if (savedInstanceState != null) {
if (!savedInstanceState.getBoolean(FORM_FILLING_ALREADY_STARTED)) {
startForm()
}
} else {
startForm()
}
}
foreground = {
_error.value = it
}
)
}

private fun assertProjectListNotEmpty(): String? {
val projects = projectsRepository.getAll()
return if (projects.isEmpty()) {
getString(string.app_not_configured)
resources.getString(string.app_not_configured)
} else {
null
}
Expand All @@ -92,18 +189,18 @@ class FormUriActivity : ComponentActivity() {
private fun assertCurrentProjectUsed(): String? {
val projects = projectsRepository.getAll()
val firstProject = projects.first()
val uriProjectId = intent.data?.getQueryParameter("projectId")
val uriProjectId = uri?.getQueryParameter("projectId")
val projectId = uriProjectId ?: firstProject.uuid

return if (projectId != projectsDataService.getCurrentProject().uuid) {
getString(string.wrong_project_selected_for_form)
resources.getString(string.wrong_project_selected_for_form)
} else {
null
}
}

private fun assertValidUri(): String? {
val isUriValid = intent.data?.let {
val isUriValid = uri?.let {
val uriMimeType = contentResolver.getType(it)
if (uriMimeType == null) {
false
Expand All @@ -113,50 +210,50 @@ class FormUriActivity : ComponentActivity() {
} ?: false

return if (!isUriValid) {
getString(string.unrecognized_uri)
resources.getString(string.unrecognized_uri)
} else {
null
}
}

private fun assertFormExists(): String? {
val uri = intent.data!!
val uriMimeType = contentResolver.getType(uri)
val uriMimeType = contentResolver.getType(uri!!)

return if (uriMimeType == FormsContract.CONTENT_ITEM_TYPE) {
val formExists = formsRepositoryProvider.get().get(ContentUriHelper.getIdFromUri(uri))?.let {
File(it.formFilePath).exists()
} ?: false
val formExists =
formsRepositoryProvider.get().get(ContentUriHelper.getIdFromUri(uri))?.let {
File(it.formFilePath).exists()
} ?: false

if (formExists) {
null
} else {
getString(string.bad_uri)
resources.getString(string.bad_uri)
}
} else {
val instance = instanceRepositoryProvider.get().get(ContentUriHelper.getIdFromUri(uri))
val instance = instancesRepositoryProvider.get().get(ContentUriHelper.getIdFromUri(uri))
if (instance == null) {
getString(string.bad_uri)
resources.getString(string.bad_uri)
} else if (!File(instance.instanceFilePath).exists()) {
Analytics.log(AnalyticsEvents.OPEN_DELETED_INSTANCE)
InstanceDeleter(
instanceRepositoryProvider.get(),
instancesRepositoryProvider.get(),
formsRepositoryProvider.get()
).delete(instance.dbId)
getString(string.instance_deleted_message)
resources.getString(string.instance_deleted_message)
} else {
val candidateForms = formsRepositoryProvider.get()
.getAllByFormIdAndVersion(instance.formId, instance.formVersion)
if (candidateForms.isEmpty()) {
val version = if (instance.formVersion == null) {
""
} else {
"\n${getString(string.version)} ${instance.formVersion}"
"\n${resources.getString(string.version)} ${instance.formVersion}"
}

getString(string.parent_form_not_present, "${instance.formId}$version")
resources.getString(string.parent_form_not_present, "${instance.formId}$version")
} else if (candidateForms.size > 1) {
getString(string.survey_multiple_forms_error)
resources.getString(string.survey_multiple_forms_error)
} else {
null
}
Expand All @@ -165,63 +262,17 @@ class FormUriActivity : ComponentActivity() {
}

private fun assertFormNotEncrypted(): String? {
val uri = intent.data!!
val uriMimeType = contentResolver.getType(uri)
val uriMimeType = contentResolver.getType(uri!!)

return if (uriMimeType == InstancesContract.CONTENT_ITEM_TYPE) {
val instance = instanceRepositoryProvider.get().get(ContentUriHelper.getIdFromUri(uri))
val instance = instancesRepositoryProvider.get().get(ContentUriHelper.getIdFromUri(uri))
if (instance!!.canEditWhenComplete()) {
null
} else {
getString(string.encrypted_form)
resources.getString(string.encrypted_form)
}
} else {
null
}
}

private fun startForm() {
formFillingAlreadyStarted = true
openForm.launch(
Intent(this, FormFillingActivity::class.java).apply {
action = intent.action
data = intent.data
intent.extras?.let { sourceExtras -> putExtras(sourceExtras) }
if (!canFormBeEdited()) {
putExtra(ApplicationConstants.BundleKeys.FORM_MODE, ApplicationConstants.FormModes.VIEW_SENT)
}
}
)
}

private fun displayErrorDialog(message: String) {
MaterialAlertDialogBuilder(this)
.setMessage(message)
.setPositiveButton(string.ok) { _, _ -> finish() }
.create()
.show()
}

private fun canFormBeEdited(): Boolean {
val uri = intent.data!!
val uriMimeType = contentResolver.getType(uri)

val formEditingEnabled = if (uriMimeType == InstancesContract.CONTENT_ITEM_TYPE) {
val instance = instanceRepositoryProvider.get().get(ContentUriHelper.getIdFromUri(uri))
instance!!.canBeEdited(settingsProvider)
} else {
true
}

return formEditingEnabled
}

override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(FORM_FILLING_ALREADY_STARTED, formFillingAlreadyStarted)
super.onSaveInstanceState(outState)
}

companion object {
private const val FORM_FILLING_ALREADY_STARTED = "FORM_FILLING_ALREADY_STARTED"
}
}
16 changes: 16 additions & 0 deletions collect_app/src/main/res/layout/circular_progress_indicator.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">

<com.google.android.material.progressindicator.CircularProgressIndicator
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"/>

</androidx.constraintlayout.widget.ConstraintLayout>
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,7 @@ class FormUriActivityTest {
}

@Test
fun `Form filling should not be started again after recreating the activity or getting back to it`() {
fun `Form filling should not be started again after recreating the activity`() {
val project = Project.Saved("123", "First project", "A", "#cccccc")
projectsRepository.save(project)
whenever(projectsDataService.getCurrentProject()).thenReturn(project)
Expand All @@ -630,6 +630,28 @@ class FormUriActivityTest {
Intents.intended(hasComponent(FormFillingActivity::class.java.name), Intents.times(1))
}

@Test
fun `Form filling should be started again after recreating the activity before starting`() {
val project = Project.Saved("123", "First project", "A", "#cccccc")
projectsRepository.save(project)
whenever(projectsDataService.getCurrentProject()).thenReturn(project)

val form = formsRepository.save(
FormUtils.buildForm(
"1",
"1",
TempFiles.createTempDir().absolutePath
).build()
)

val scenario =
launcherRule.launch<FormUriActivity>(getBlankFormIntent(project.uuid, form.dbId))
scenario.recreate()
fakeScheduler.flush()

Intents.intended(hasComponent(FormFillingActivity::class.java.name), Intents.times(1))
}

@Test
fun `When there is project id specified in uri that represents a blank form and it matches current project id then start form filling`() {
val project = Project.Saved("123", "First project", "A", "#cccccc")
Expand Down

0 comments on commit 1ae3580

Please sign in to comment.