From e8facbd13640046eb97d2c7a61242624f3c26264 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 11 Oct 2023 10:33:11 +0100 Subject: [PATCH 01/18] Don't allow encrypted forms to be bulk finalized --- .../formmanagement/BulkFinalizationTest.kt | 19 ++++++++++++++ .../formmanagement/InstancesDataService.kt | 26 +++++++++---------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt index 3be869aba4c..ff28efbd796 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt @@ -108,6 +108,25 @@ class BulkFinalizationTest { .assertNumberOfFinalizedForms(0) } + @Test + fun doesNotFinalizeInstancesFromEncryptedForms() { + rule.startAtMainMenu() + .copyForm("encrypted.xml") + .startBlankForm("encrypted") + .swipeToEndScreen() + .clickSaveAsDraft() + + .clickDrafts(1) + .clickOptionsIcon(string.finalize_all_forms) + .clickOnString(string.finalize_all_forms) + .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_failure, 1) + .assertText("encrypted") + .pressBack(MainMenuPage()) + + .assertNumberOfEditableForms(1) + .assertNumberOfFinalizedForms(0) + } + @Test fun doesNotFinalizeAlreadyFinalizedInstances() { rule.startAtMainMenu() diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt index 65f16d8a504..074bfe8ebf9 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt @@ -75,22 +75,22 @@ class InstancesDataService( val formController = FormEntryUseCases.loadDraft(form, instance, formEntryController) val cacheDir = storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE) - val newFailCount = - if (FormEntryUseCases.getSavePoint(formController, File(cacheDir)) == null) { - val finalizedInstance = FormEntryUseCases.finalizeDraft( - formController, - instancesRepository, - entitiesRepository - ) + val savePoint = FormEntryUseCases.getSavePoint(formController, File(cacheDir)) + val newFailCount = if (savePoint == null && form.basE64RSAPublicKey == null) { + val finalizedInstance = FormEntryUseCases.finalizeDraft( + formController, + instancesRepository, + entitiesRepository + ) - if (finalizedInstance == null) { - failCount + 1 - } else { - failCount - } - } else { + if (finalizedInstance == null) { failCount + 1 + } else { + failCount } + } else { + failCount + 1 + } Collect.getInstance().externalDataManager?.close() newFailCount From 82488df0b693d94b10509d64d0e903ba3dc10070 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 11 Oct 2023 10:56:48 +0100 Subject: [PATCH 02/18] Show unsupported message when trying to bulk finalize unsupported instances --- .../formmanagement/BulkFinalizationTest.kt | 4 +-- .../activities/InstanceChooserList.java | 27 ++++++++++++------- .../formmanagement/InstancesDataService.kt | 18 +++++++------ .../drafts/BulkFinalizationViewModel.kt | 5 ++-- strings/src/main/res/values/strings.xml | 3 +++ 5 files changed, 35 insertions(+), 22 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt index ff28efbd796..7e7c344c15a 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt @@ -100,7 +100,7 @@ class BulkFinalizationTest { .clickDrafts() .clickOptionsIcon(string.finalize_all_forms) .clickOnString(string.finalize_all_forms) - .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_failure, 1) + .checkIsSnackbarWithMessageDisplayed(string.bulk_finalize_unsupported, 0) .assertText("One Question") .pressBack(MainMenuPage()) @@ -119,7 +119,7 @@ class BulkFinalizationTest { .clickDrafts(1) .clickOptionsIcon(string.finalize_all_forms) .clickOnString(string.finalize_all_forms) - .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_failure, 1) + .checkIsSnackbarWithMessageDisplayed(string.bulk_finalize_unsupported, 0) .assertText("encrypted") .pressBack(MainMenuPage()) diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java index 2688964074f..e79130ec357 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java @@ -48,6 +48,7 @@ import org.odk.collect.android.external.FormUriActivity; import org.odk.collect.android.external.InstancesContract; import org.odk.collect.android.formlists.sorting.FormListSortingOption; +import org.odk.collect.android.formmanagement.FinalizeAllResult; import org.odk.collect.android.formmanagement.InstancesDataService; import org.odk.collect.android.formmanagement.drafts.BulkFinalizationViewModel; import org.odk.collect.android.formmanagement.drafts.DraftsMenuProvider; @@ -70,8 +71,6 @@ import javax.inject.Inject; -import kotlin.Pair; - /** * Responsible for displaying all the valid instances in the instance directory. * @@ -164,29 +163,37 @@ public void onCreate(Bundle savedInstanceState) { bulkFinalizationViewModel.getFinalizedForms().observe(this, finalizedForms -> { if (!finalizedForms.isConsumed()) { - Pair pair = finalizedForms.getValue(); - if (pair.getSecond().equals(0)) { + FinalizeAllResult result = finalizedForms.getValue(); + if (result.getUnsupportedInstances()) { + SnackbarUtils.showLongSnackbar( + this.findViewById(android.R.id.content), + getResources().getString( + string.bulk_finalize_unsupported, + result.getSuccessCount() + ) + ); + } else if (result.getFailureCount() == 0) { SnackbarUtils.showLongSnackbar( this.findViewById(android.R.id.content), getResources().getQuantityString( plurals.bulk_finalize_success, - pair.getFirst(), - pair.getFirst() + result.getSuccessCount(), + result.getSuccessCount() ) ); - } else if (pair.getFirst().equals(pair.getSecond())) { + } else if (result.getSuccessCount() == 0) { SnackbarUtils.showLongSnackbar( this.findViewById(android.R.id.content), getResources().getQuantityString( plurals.bulk_finalize_failure, - pair.getFirst(), - pair.getFirst() + result.getFailureCount(), + result.getFailureCount() ) ); } else { SnackbarUtils.showLongSnackbar( this.findViewById(android.R.id.content), - getString(string.bulk_finalize_partial_success, pair.getFirst() - pair.getSecond(), pair.getSecond()) + getString(string.bulk_finalize_partial_success, result.getSuccessCount(), result.getFailureCount()) ); } diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt index 074bfe8ebf9..9e9073ff661 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt @@ -49,7 +49,7 @@ class InstancesDataService( onUpdate() } - fun finalizeAllDrafts(): Pair { + fun finalizeAllDrafts(): FinalizeAllResult { val instancesRepository = instancesRepositoryProvider.get() val formsRepository = formsRepositoryProvider.get() val entitiesRepository = entitiesRepositoryProvider.get() @@ -61,7 +61,7 @@ class InstancesDataService( Instance.STATUS_VALID ) - val totalFailed = instances.fold(0) { failCount, instance -> + val result = instances.fold(FinalizeAllResult(0, 0, false)) { result, instance -> val (formDef, form) = FormEntryUseCases.loadFormDef( instance, formsRepository, @@ -76,7 +76,7 @@ class InstancesDataService( val cacheDir = storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE) val savePoint = FormEntryUseCases.getSavePoint(formController, File(cacheDir)) - val newFailCount = if (savePoint == null && form.basE64RSAPublicKey == null) { + val newResult = if (savePoint == null && form.basE64RSAPublicKey == null) { val finalizedInstance = FormEntryUseCases.finalizeDraft( formController, instancesRepository, @@ -84,20 +84,20 @@ class InstancesDataService( ) if (finalizedInstance == null) { - failCount + 1 + result.copy(failureCount = result.failureCount + 1) } else { - failCount + result } } else { - failCount + 1 + result.copy(failureCount = result.failureCount + 1, unsupportedInstances = true) } Collect.getInstance().externalDataManager?.close() - newFailCount + newResult } update() - return Pair(instances.size, totalFailed) + return result.copy(successCount = instances.size - result.failureCount) } companion object { @@ -106,3 +106,5 @@ class InstancesDataService( private const val SENT_COUNT_KEY = "instancesSentCount" } } + +data class FinalizeAllResult(val successCount: Int, val failureCount: Int, val unsupportedInstances: Boolean) diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt index 72f1b4ae644..afac2a322e8 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt @@ -2,6 +2,7 @@ package org.odk.collect.android.formmanagement.drafts import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import org.odk.collect.android.formmanagement.FinalizeAllResult import org.odk.collect.android.formmanagement.InstancesDataService import org.odk.collect.androidshared.data.Consumable import org.odk.collect.androidshared.livedata.MutableNonNullLiveData @@ -12,8 +13,8 @@ class BulkFinalizationViewModel( private val scheduler: Scheduler, private val instancesDataService: InstancesDataService ) { - private val _finalizedForms = MutableLiveData>>() - val finalizedForms: LiveData>> = _finalizedForms + private val _finalizedForms = MutableLiveData>() + val finalizedForms: LiveData> = _finalizedForms private val _isFinalizing = MutableNonNullLiveData(false) val isFinalizing: NonNullLiveData = _isFinalizing diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index ae779fe2905..c61df0ab42f 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -1243,4 +1243,7 @@ Complete + + + %d forms finalized. Some forms need to be finalized manually. From 06c1c288852edb3183356152a49eccbcaa36f416 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 11 Oct 2023 14:13:37 +0100 Subject: [PATCH 03/18] Pull repeated work out of loop --- .../collect/android/formmanagement/InstancesDataService.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt index 9e9073ff661..ae8045ce949 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt @@ -54,6 +54,7 @@ class InstancesDataService( val formsRepository = formsRepositoryProvider.get() val entitiesRepository = entitiesRepositoryProvider.get() val projectRootDir = File(storagePathProvider.getProjectRootDirPath()) + val cacheDir = storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE) val instances = instancesRepository.getAllByStatus( Instance.STATUS_INCOMPLETE, @@ -74,9 +75,9 @@ class InstancesDataService( CollectFormEntryControllerFactory().create(formDef, formMediaDir) val formController = FormEntryUseCases.loadDraft(form, instance, formEntryController) - val cacheDir = storagePathProvider.getOdkDirPath(StorageSubdirectory.CACHE) val savePoint = FormEntryUseCases.getSavePoint(formController, File(cacheDir)) - val newResult = if (savePoint == null && form.basE64RSAPublicKey == null) { + val needsEncrypted = form.basE64RSAPublicKey == null + val newResult = if (savePoint == null && needsEncrypted) { val finalizedInstance = FormEntryUseCases.finalizeDraft( formController, instancesRepository, From 057f49155290c7d02c14b9c1c8265a3ef6086823 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 11 Oct 2023 14:39:06 +0100 Subject: [PATCH 04/18] Extract common component for showing snackbars based on LiveData --- .../collect/androidshared/ui/SnackbarUtils.kt | 32 ++++++++++-- .../activities/InstanceChooserList.java | 48 ++--------------- .../FinalizeAllSnackbarPresenter.java | 51 +++++++++++++++++++ 3 files changed, 84 insertions(+), 47 deletions(-) create mode 100644 collect_app/src/main/java/org/odk/collect/android/instancemanagement/FinalizeAllSnackbarPresenter.java diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/ui/SnackbarUtils.kt b/androidshared/src/main/java/org/odk/collect/androidshared/ui/SnackbarUtils.kt index d68bc42885a..f228695874c 100644 --- a/androidshared/src/main/java/org/odk/collect/androidshared/ui/SnackbarUtils.kt +++ b/androidshared/src/main/java/org/odk/collect/androidshared/ui/SnackbarUtils.kt @@ -19,8 +19,10 @@ import android.widget.Button import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView +import androidx.lifecycle.Observer import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar +import org.odk.collect.androidshared.data.Consumable /** * Convenience wrapper around Android's [Snackbar] API. @@ -40,6 +42,11 @@ object SnackbarUtils { return showSnackbar(parentView, message, 3500, anchorView, action, displayDismissButton) } + @JvmStatic + fun showLongSnackbar(parentView: View, snackbarDetails: SnackbarDetails) { + showLongSnackbar(parentView, snackbarDetails.text, action = snackbarDetails.action) + } + @JvmStatic @JvmOverloads fun showLongSnackbar( @@ -74,7 +81,8 @@ object SnackbarUtils { lastSnackbar?.dismiss() lastSnackbar = Snackbar.make(parentView, message.trim(), duration).apply { - val textView = view.findViewById(com.google.android.material.R.id.snackbar_text) + val textView = + view.findViewById(com.google.android.material.R.id.snackbar_text) textView.isSingleLine = false if (anchorView?.visibility != View.GONE) { @@ -88,7 +96,8 @@ object SnackbarUtils { setOnClickListener { dismiss() } - contentDescription = context.getString(org.odk.collect.strings.R.string.close_snackbar) + contentDescription = + context.getString(org.odk.collect.strings.R.string.close_snackbar) } val params = LinearLayout.LayoutParams( @@ -116,8 +125,23 @@ object SnackbarUtils { lastSnackbar?.show() } - data class Action( + data class SnackbarDetails @JvmOverloads constructor( val text: String, - val listener: () -> Unit + val action: Action? = null ) + + data class Action(val text: String, val listener: () -> Unit) + + abstract class SnackbarPresenterObserver(private val parentView: View) : + Observer> { + + abstract fun getSnackbarDetails(value: T): SnackbarDetails + + override fun onChanged(consumable: Consumable) { + if (!consumable.isConsumed()) { + showLongSnackbar(parentView, getSnackbarDetails(consumable.value)) + consumable.consume() + } + } + } } diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java index e79130ec357..daf5f5dd0db 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java @@ -48,24 +48,21 @@ import org.odk.collect.android.external.FormUriActivity; import org.odk.collect.android.external.InstancesContract; import org.odk.collect.android.formlists.sorting.FormListSortingOption; -import org.odk.collect.android.formmanagement.FinalizeAllResult; import org.odk.collect.android.formmanagement.InstancesDataService; import org.odk.collect.android.formmanagement.drafts.BulkFinalizationViewModel; import org.odk.collect.android.formmanagement.drafts.DraftsMenuProvider; import org.odk.collect.android.injection.DaggerUtils; +import org.odk.collect.android.instancemanagement.FinalizeAllSnackbarPresenter; import org.odk.collect.android.projects.CurrentProjectProvider; import org.odk.collect.android.utilities.ApplicationConstants; import org.odk.collect.android.utilities.FormsRepositoryProvider; import org.odk.collect.android.utilities.InstancesRepositoryProvider; -import org.odk.collect.androidshared.ui.SnackbarUtils; import org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard; import org.odk.collect.async.Scheduler; import org.odk.collect.forms.Form; import org.odk.collect.forms.instances.Instance; import org.odk.collect.material.MaterialProgressDialogFragment; import org.odk.collect.settings.SettingsProvider; -import org.odk.collect.strings.R.plurals; -import org.odk.collect.strings.R.string; import java.util.Arrays; @@ -161,45 +158,10 @@ public void onCreate(Bundle savedInstanceState) { return dialog; }); - bulkFinalizationViewModel.getFinalizedForms().observe(this, finalizedForms -> { - if (!finalizedForms.isConsumed()) { - FinalizeAllResult result = finalizedForms.getValue(); - if (result.getUnsupportedInstances()) { - SnackbarUtils.showLongSnackbar( - this.findViewById(android.R.id.content), - getResources().getString( - string.bulk_finalize_unsupported, - result.getSuccessCount() - ) - ); - } else if (result.getFailureCount() == 0) { - SnackbarUtils.showLongSnackbar( - this.findViewById(android.R.id.content), - getResources().getQuantityString( - plurals.bulk_finalize_success, - result.getSuccessCount(), - result.getSuccessCount() - ) - ); - } else if (result.getSuccessCount() == 0) { - SnackbarUtils.showLongSnackbar( - this.findViewById(android.R.id.content), - getResources().getQuantityString( - plurals.bulk_finalize_failure, - result.getFailureCount(), - result.getFailureCount() - ) - ); - } else { - SnackbarUtils.showLongSnackbar( - this.findViewById(android.R.id.content), - getString(string.bulk_finalize_partial_success, result.getSuccessCount(), result.getFailureCount()) - ); - } - - finalizedForms.consume(); - } - }); + bulkFinalizationViewModel.getFinalizedForms().observe( + this, + new FinalizeAllSnackbarPresenter(this.findViewById(android.R.id.content), this) + ); } private void init() { diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/FinalizeAllSnackbarPresenter.java b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/FinalizeAllSnackbarPresenter.java new file mode 100644 index 00000000000..c49863f1d32 --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/FinalizeAllSnackbarPresenter.java @@ -0,0 +1,51 @@ +package org.odk.collect.android.instancemanagement; + +import android.content.Context; +import android.view.View; + +import androidx.annotation.NonNull; + +import org.odk.collect.android.formmanagement.FinalizeAllResult; +import org.odk.collect.androidshared.ui.SnackbarUtils; + +public class FinalizeAllSnackbarPresenter extends SnackbarUtils.SnackbarPresenterObserver { + private final Context context; + + public FinalizeAllSnackbarPresenter(@NonNull View parentView, Context context) { + super(parentView); + this.context = context; + } + + @NonNull + @Override + public SnackbarUtils.SnackbarDetails getSnackbarDetails(FinalizeAllResult result) { + if (result.getUnsupportedInstances()) { + return new SnackbarUtils.SnackbarDetails( + context.getString( + org.odk.collect.strings.R.string.bulk_finalize_unsupported, + result.getSuccessCount() + ) + ); + } else if (result.getFailureCount() == 0) { + return new SnackbarUtils.SnackbarDetails( + context.getResources().getQuantityString( + org.odk.collect.strings.R.plurals.bulk_finalize_success, + result.getSuccessCount(), + result.getSuccessCount() + ) + ); + } else if (result.getSuccessCount() == 0) { + return new SnackbarUtils.SnackbarDetails( + context.getResources().getQuantityString( + org.odk.collect.strings.R.plurals.bulk_finalize_failure, + result.getFailureCount(), + result.getFailureCount() + ) + ); + } else { + return new SnackbarUtils.SnackbarDetails( + context.getString(org.odk.collect.strings.R.string.bulk_finalize_partial_success, result.getSuccessCount(), result.getFailureCount()) + ); + } + } +} From 8c39832944370785f4287caedd51f3e7eab9a000 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 13 Oct 2023 10:08:33 +0100 Subject: [PATCH 05/18] Auto send forms after bulk finalization --- .../formmanagement/BulkFinalizationTest.kt | 54 ++++++++++++++----- .../formmanagement/InstancesDataService.kt | 6 +++ .../injection/config/AppDependencyModule.java | 4 +- 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt index 7e7c344c15a..b73fa56358c 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt @@ -1,10 +1,13 @@ package org.odk.collect.android.feature.formmanagement import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain import org.junit.runner.RunWith +import org.odk.collect.android.support.TestDependencies import org.odk.collect.android.support.pages.FormEntryPage.QuestionAndAnswer import org.odk.collect.android.support.pages.MainMenuPage import org.odk.collect.android.support.pages.SaveOrDiscardFormDialog @@ -16,15 +19,16 @@ import org.odk.collect.strings.R.string @RunWith(AndroidJUnit4::class) class BulkFinalizationTest { - val rule = CollectTestRule() + val testDependencies = TestDependencies() + val rule = CollectTestRule(useDemoProject = false) @get:Rule - val chain: RuleChain = TestRuleChain.chain().around(rule) + val chain: RuleChain = TestRuleChain.chain(testDependencies).around(rule) @Test fun canBulkFinalizeDrafts() { - rule.startAtMainMenu() - .copyForm("one-question.xml") + rule.withProject("http://example.com") + .copyForm("one-question.xml", "example.com") .startBlankForm("One Question") .fillOutAndSave(QuestionAndAnswer("what is your age", "97")) .startBlankForm("One Question") @@ -42,8 +46,8 @@ class BulkFinalizationTest { @Test fun whenThereAreDraftsWithConstraintViolations_marksFormsAsHavingErrors() { - rule.startAtMainMenu() - .copyForm("two-question-required.xml") + rule.withProject("http://example.com") + .copyForm("two-question-required.xml", "example.com") .startBlankForm("Two Question Required") .fillOut(QuestionAndAnswer("What is your name?", "Dan")) .pressBack(SaveOrDiscardFormDialog(MainMenuPage())) @@ -68,8 +72,8 @@ class BulkFinalizationTest { @Test fun whenADraftPreviouslyHadConstraintViolations_marksFormsAsHavingErrors() { - rule.startAtMainMenu() - .copyForm("two-question-required.xml") + rule.withProject("http://example.com") + .copyForm("two-question-required.xml", "example.com") .startBlankForm("Two Question Required") .fillOut(QuestionAndAnswer("What is your name?", "Dan")) .pressBack(SaveOrDiscardFormDialog(MainMenuPage())) @@ -87,8 +91,8 @@ class BulkFinalizationTest { @Test fun doesNotFinalizeInstancesWithSavePoints() { - rule.startAtMainMenu() - .copyForm("one-question.xml") + rule.withProject("http://example.com") + .copyForm("one-question.xml", "example.com") .startBlankForm("One Question") .swipeToEndScreen() .clickSaveAsDraft() @@ -110,8 +114,8 @@ class BulkFinalizationTest { @Test fun doesNotFinalizeInstancesFromEncryptedForms() { - rule.startAtMainMenu() - .copyForm("encrypted.xml") + rule.withProject("http://example.com") + .copyForm("encrypted.xml", "example.com") .startBlankForm("encrypted") .swipeToEndScreen() .clickSaveAsDraft() @@ -129,8 +133,8 @@ class BulkFinalizationTest { @Test fun doesNotFinalizeAlreadyFinalizedInstances() { - rule.startAtMainMenu() - .copyForm("one-question.xml") + rule.withProject("http://example.com") + .copyForm("one-question.xml", "example.com") .startBlankForm("One Question") .fillOutAndSave(QuestionAndAnswer("what is your age", "97")) .startBlankForm("One Question") @@ -145,4 +149,26 @@ class BulkFinalizationTest { .assertNumberOfFinalizedForms(2) } + + @Test + fun whenAutoSendIsEnabled_draftsAreSentAfterFinalizing() { + val mainMenuPage = rule.withProject(testDependencies.server.url) + .enableAutoSend() + + .copyForm("one-question.xml", testDependencies.server.hostName) + .startBlankForm("One Question") + .fillOutAndSave(QuestionAndAnswer("what is your age", "97")) + + .clickDrafts(1) + .clickOptionsIcon(string.finalize_all_forms) + .clickOnString(string.finalize_all_forms) + .pressBack(MainMenuPage()) + + testDependencies.scheduler.runDeferredTasks() + + mainMenuPage.clickViewSentForm(1) + .assertText("One Question") + + assertThat(testDependencies.server.submissions.size, equalTo(1)) + } } diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt index ae8045ce949..712fb4a9a82 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt @@ -2,8 +2,10 @@ package org.odk.collect.android.formmanagement import androidx.lifecycle.LiveData import org.odk.collect.android.application.Collect +import org.odk.collect.android.backgroundwork.InstanceSubmitScheduler import org.odk.collect.android.entities.EntitiesRepositoryProvider import org.odk.collect.android.formentry.FormEntryUseCases +import org.odk.collect.android.projects.CurrentProjectProvider import org.odk.collect.android.storage.StoragePathProvider import org.odk.collect.android.storage.StorageSubdirectory import org.odk.collect.android.utilities.ExternalizableFormDefCache @@ -19,6 +21,8 @@ class InstancesDataService( private val instancesRepositoryProvider: InstancesRepositoryProvider, private val entitiesRepositoryProvider: EntitiesRepositoryProvider, private val storagePathProvider: StoragePathProvider, + private val instanceSubmitScheduler: InstanceSubmitScheduler, + private val currentProjectProvider: CurrentProjectProvider, private val onUpdate: () -> Unit ) { val editableCount: LiveData = appState.getLive(EDITABLE_COUNT_KEY, 0) @@ -98,6 +102,8 @@ class InstancesDataService( } update() + instanceSubmitScheduler.scheduleSubmit(currentProjectProvider.getCurrentProject().uuid) + return result.copy(successCount = instances.size - result.failureCount) } diff --git a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java index e2048698b43..ed58a7696ca 100644 --- a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java +++ b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java @@ -459,7 +459,7 @@ public UUIDGenerator providesUUIDGenerator() { } @Provides - public InstancesDataService providesInstancesDataService(Application application, InstancesRepositoryProvider instancesRepositoryProvider, CurrentProjectProvider currentProjectProvider, FormsRepositoryProvider formsRepositoryProvider, EntitiesRepositoryProvider entitiesRepositoryProvider, StoragePathProvider storagePathProvider) { + public InstancesDataService providesInstancesDataService(Application application, InstancesRepositoryProvider instancesRepositoryProvider, CurrentProjectProvider currentProjectProvider, FormsRepositoryProvider formsRepositoryProvider, EntitiesRepositoryProvider entitiesRepositoryProvider, StoragePathProvider storagePathProvider, InstanceSubmitScheduler instanceSubmitScheduler) { Function0 onUpdate = () -> { application.getContentResolver().notifyChange( InstancesContract.getUri(currentProjectProvider.getCurrentProject().getUuid()), @@ -469,7 +469,7 @@ public InstancesDataService providesInstancesDataService(Application application return null; }; - return new InstancesDataService(getState(application), formsRepositoryProvider, instancesRepositoryProvider, entitiesRepositoryProvider, storagePathProvider, onUpdate); + return new InstancesDataService(getState(application), formsRepositoryProvider, instancesRepositoryProvider, entitiesRepositoryProvider, storagePathProvider, instanceSubmitScheduler, currentProjectProvider, onUpdate); } @Provides From 0ef1f5d57a7a368003a570d72a34389047921c96 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 13 Oct 2023 15:39:35 +0100 Subject: [PATCH 06/18] Add dialog to confirm bulk finalization --- .../formmanagement/BulkFinalizationTest.kt | 26 +++++++++++++++++++ .../activities/InstanceChooserList.java | 2 +- .../drafts/DraftsMenuProvider.kt | 17 ++++++++++-- strings/src/main/res/values/strings.xml | 3 +++ 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt index b73fa56358c..c7fddc0c9ea 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt @@ -8,6 +8,7 @@ import org.junit.Test import org.junit.rules.RuleChain import org.junit.runner.RunWith import org.odk.collect.android.support.TestDependencies +import org.odk.collect.android.support.pages.EditSavedFormPage import org.odk.collect.android.support.pages.FormEntryPage.QuestionAndAnswer import org.odk.collect.android.support.pages.MainMenuPage import org.odk.collect.android.support.pages.SaveOrDiscardFormDialog @@ -37,6 +38,7 @@ class BulkFinalizationTest { .clickDrafts(2) .clickOptionsIcon(string.finalize_all_forms) .clickOnString(string.finalize_all_forms) + .clickOnButtonInDialog(string.finalize, EditSavedFormPage()) .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_success, 2) .assertTextDoesNotExist("One Question") .pressBack(MainMenuPage()) @@ -62,6 +64,7 @@ class BulkFinalizationTest { .clickDrafts(2) .clickOptionsIcon(string.finalize_all_forms) .clickOnString(string.finalize_all_forms) + .clickOnButtonInDialog(string.finalize, EditSavedFormPage()) .checkIsSnackbarWithMessageDisplayed(string.bulk_finalize_partial_success, 1, 1) .assertText("Two Question Required") .pressBack(MainMenuPage()) @@ -82,10 +85,12 @@ class BulkFinalizationTest { .clickDrafts(1) .clickOptionsIcon(string.finalize_all_forms) .clickOnString(string.finalize_all_forms) + .clickOnButtonInDialog(string.finalize, EditSavedFormPage()) .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_failure, 1) .clickOptionsIcon(string.finalize_all_forms) .clickOnString(string.finalize_all_forms) + .clickOnButtonInDialog(string.finalize, EditSavedFormPage()) .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_failure, 1) } @@ -104,6 +109,7 @@ class BulkFinalizationTest { .clickDrafts() .clickOptionsIcon(string.finalize_all_forms) .clickOnString(string.finalize_all_forms) + .clickOnButtonInDialog(string.finalize, EditSavedFormPage()) .checkIsSnackbarWithMessageDisplayed(string.bulk_finalize_unsupported, 0) .assertText("One Question") .pressBack(MainMenuPage()) @@ -123,6 +129,7 @@ class BulkFinalizationTest { .clickDrafts(1) .clickOptionsIcon(string.finalize_all_forms) .clickOnString(string.finalize_all_forms) + .clickOnButtonInDialog(string.finalize, EditSavedFormPage()) .checkIsSnackbarWithMessageDisplayed(string.bulk_finalize_unsupported, 0) .assertText("encrypted") .pressBack(MainMenuPage()) @@ -143,6 +150,7 @@ class BulkFinalizationTest { .clickDrafts(1) .clickOptionsIcon(string.finalize_all_forms) .clickOnString(string.finalize_all_forms) + .clickOnButtonInDialog(string.finalize, EditSavedFormPage()) .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_success, 1) .assertTextDoesNotExist("One Question") .pressBack(MainMenuPage()) @@ -162,6 +170,7 @@ class BulkFinalizationTest { .clickDrafts(1) .clickOptionsIcon(string.finalize_all_forms) .clickOnString(string.finalize_all_forms) + .clickOnButtonInDialog(string.finalize, EditSavedFormPage()) .pressBack(MainMenuPage()) testDependencies.scheduler.runDeferredTasks() @@ -171,4 +180,21 @@ class BulkFinalizationTest { assertThat(testDependencies.server.submissions.size, equalTo(1)) } + + @Test + fun canCancel() { + rule.withProject("http://example.com") + .copyForm("one-question.xml", "example.com") + .startBlankForm("One Question") + .fillOutAndSave(QuestionAndAnswer("what is your age", "97")) + + .clickDrafts(1) + .clickOptionsIcon(string.finalize_all_forms) + .clickOnString(string.finalize_all_forms) + .clickOnButtonInDialog(string.cancel, EditSavedFormPage()) + .assertText("One Question") + .pressBack(MainMenuPage()) + + .assertNumberOfEditableForms(1) + } } diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java index daf5f5dd0db..b4e0e3e75ac 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java @@ -149,7 +149,7 @@ public void onCreate(Bundle savedInstanceState) { instancesDataService ); - DraftsMenuProvider draftsMenuProvider = new DraftsMenuProvider(bulkFinalizationViewModel); + DraftsMenuProvider draftsMenuProvider = new DraftsMenuProvider(this, bulkFinalizationViewModel); addMenuProvider(draftsMenuProvider); MaterialProgressDialogFragment.showOn(this, bulkFinalizationViewModel.isFinalizing(), getSupportFragmentManager(), () -> { diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt index 92187a510b0..325e73f17b8 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt @@ -1,19 +1,32 @@ package org.odk.collect.android.formmanagement.drafts +import android.content.Context import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import androidx.core.view.MenuProvider +import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.odk.collect.android.R +import org.odk.collect.strings.R.string -class DraftsMenuProvider(private val bulkFinalizationViewModel: BulkFinalizationViewModel) : MenuProvider { +class DraftsMenuProvider( + private val context: Context, + private val bulkFinalizationViewModel: BulkFinalizationViewModel +) : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.drafts, menu) } override fun onMenuItemSelected(menuItem: MenuItem): Boolean { if (menuItem.itemId == R.id.bulk_finalize) { - bulkFinalizationViewModel.finalizeAllDrafts() + MaterialAlertDialogBuilder(context) + .setMessage(string.bulk_finalize_explanation) + .setPositiveButton(string.finalize) { _, _ -> + bulkFinalizationViewModel.finalizeAllDrafts() + } + .setNegativeButton(string.cancel, null) + .show() + return true } diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index c61df0ab42f..29971b75cb3 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -1246,4 +1246,7 @@ %d forms finalized. Some forms need to be finalized manually. + + + Once you finalize all forms, they will be in "Ready to send" and you will not be able to make edits. Any forms that have errors will not be finalized.\n\nYou will not be able to undo this action. From 046f14bf33c8ac9b352a4d655ee6a5c121914b42 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 13 Oct 2023 16:00:16 +0100 Subject: [PATCH 07/18] Confirm how many drafts are being finalized --- .../formmanagement/BulkFinalizationTest.kt | 42 ++++++++----------- .../BulkFinalizationConfirmationDialogPage.kt | 30 +++++++++++++ .../support/pages/EditSavedFormPage.java | 24 +++++++---- .../drafts/BulkFinalizationViewModel.kt | 2 + .../drafts/DraftsMenuProvider.kt | 9 ++++ strings/src/main/res/values/strings.xml | 6 +++ 6 files changed, 80 insertions(+), 33 deletions(-) create mode 100644 collect_app/src/androidTest/java/org/odk/collect/android/support/pages/BulkFinalizationConfirmationDialogPage.kt diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt index c7fddc0c9ea..b6cd96c7a2b 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt @@ -36,9 +36,8 @@ class BulkFinalizationTest { .fillOutAndSave(QuestionAndAnswer("what is your age", "98")) .clickDrafts(2) - .clickOptionsIcon(string.finalize_all_forms) - .clickOnString(string.finalize_all_forms) - .clickOnButtonInDialog(string.finalize, EditSavedFormPage()) + .clickFinalizeAll(2) + .clickFinalize() .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_success, 2) .assertTextDoesNotExist("One Question") .pressBack(MainMenuPage()) @@ -62,9 +61,8 @@ class BulkFinalizationTest { ) .clickDrafts(2) - .clickOptionsIcon(string.finalize_all_forms) - .clickOnString(string.finalize_all_forms) - .clickOnButtonInDialog(string.finalize, EditSavedFormPage()) + .clickFinalizeAll(2) + .clickFinalize() .checkIsSnackbarWithMessageDisplayed(string.bulk_finalize_partial_success, 1, 1) .assertText("Two Question Required") .pressBack(MainMenuPage()) @@ -83,9 +81,8 @@ class BulkFinalizationTest { .clickSaveChanges() .clickDrafts(1) - .clickOptionsIcon(string.finalize_all_forms) - .clickOnString(string.finalize_all_forms) - .clickOnButtonInDialog(string.finalize, EditSavedFormPage()) + .clickFinalizeAll(1) + .clickFinalize() .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_failure, 1) .clickOptionsIcon(string.finalize_all_forms) @@ -106,10 +103,9 @@ class BulkFinalizationTest { .clickOnForm("One Question") .killAndReopenApp(MainMenuPage()) - .clickDrafts() - .clickOptionsIcon(string.finalize_all_forms) - .clickOnString(string.finalize_all_forms) - .clickOnButtonInDialog(string.finalize, EditSavedFormPage()) + .clickDrafts(1) + .clickFinalizeAll(1) + .clickFinalize() .checkIsSnackbarWithMessageDisplayed(string.bulk_finalize_unsupported, 0) .assertText("One Question") .pressBack(MainMenuPage()) @@ -127,9 +123,8 @@ class BulkFinalizationTest { .clickSaveAsDraft() .clickDrafts(1) - .clickOptionsIcon(string.finalize_all_forms) - .clickOnString(string.finalize_all_forms) - .clickOnButtonInDialog(string.finalize, EditSavedFormPage()) + .clickFinalizeAll(1) + .clickFinalize() .checkIsSnackbarWithMessageDisplayed(string.bulk_finalize_unsupported, 0) .assertText("encrypted") .pressBack(MainMenuPage()) @@ -148,9 +143,8 @@ class BulkFinalizationTest { .fillOutAndFinalize(QuestionAndAnswer("what is your age", "98")) .clickDrafts(1) - .clickOptionsIcon(string.finalize_all_forms) - .clickOnString(string.finalize_all_forms) - .clickOnButtonInDialog(string.finalize, EditSavedFormPage()) + .clickFinalizeAll(1) + .clickFinalize() .checkIsSnackbarWithQuantityDisplayed(plurals.bulk_finalize_success, 1) .assertTextDoesNotExist("One Question") .pressBack(MainMenuPage()) @@ -168,9 +162,8 @@ class BulkFinalizationTest { .fillOutAndSave(QuestionAndAnswer("what is your age", "97")) .clickDrafts(1) - .clickOptionsIcon(string.finalize_all_forms) - .clickOnString(string.finalize_all_forms) - .clickOnButtonInDialog(string.finalize, EditSavedFormPage()) + .clickFinalizeAll(1) + .clickFinalize() .pressBack(MainMenuPage()) testDependencies.scheduler.runDeferredTasks() @@ -189,9 +182,8 @@ class BulkFinalizationTest { .fillOutAndSave(QuestionAndAnswer("what is your age", "97")) .clickDrafts(1) - .clickOptionsIcon(string.finalize_all_forms) - .clickOnString(string.finalize_all_forms) - .clickOnButtonInDialog(string.cancel, EditSavedFormPage()) + .clickFinalizeAll(1) + .clickCancel() .assertText("One Question") .pressBack(MainMenuPage()) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/BulkFinalizationConfirmationDialogPage.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/BulkFinalizationConfirmationDialogPage.kt new file mode 100644 index 00000000000..5b2f1590c6d --- /dev/null +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/BulkFinalizationConfirmationDialogPage.kt @@ -0,0 +1,30 @@ +package org.odk.collect.android.support.pages + +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withText +import org.odk.collect.strings.R +import org.odk.collect.strings.R.plurals +import org.odk.collect.strings.localization.getLocalizedQuantityString + +class BulkFinalizationConfirmationDialogPage(private val count: Int) : Page() { + override fun assertOnPage(): BulkFinalizationConfirmationDialogPage { + val title = ApplicationProvider.getApplicationContext() + .getLocalizedQuantityString(plurals.bulk_finalize_confirmation, count, count) + + onView(withText(title)).inRoot(isDialog()).check(matches(isDisplayed())) + return this + } + + fun clickFinalize(): EditSavedFormPage { + return this.clickOnButtonInDialog(R.string.finalize, EditSavedFormPage()) + } + + fun clickCancel(): EditSavedFormPage { + return this.clickOnButtonInDialog(R.string.cancel, EditSavedFormPage()) + } +} diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/EditSavedFormPage.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/EditSavedFormPage.java index 1d1e1e74c91..e87daa58d6e 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/EditSavedFormPage.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/EditSavedFormPage.java @@ -16,13 +16,6 @@ package org.odk.collect.android.support.pages; -import android.widget.RelativeLayout; - -import androidx.appcompat.widget.Toolbar; - -import org.odk.collect.android.R; -import org.odk.collect.android.adapters.InstanceListCursorAdapter; - import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.replaceText; @@ -37,11 +30,19 @@ import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.Matchers.not; +import android.widget.RelativeLayout; + +import androidx.appcompat.widget.Toolbar; + +import org.odk.collect.android.R; +import org.odk.collect.android.adapters.InstanceListCursorAdapter; +import org.odk.collect.strings.R.string; + public class EditSavedFormPage extends Page { @Override public EditSavedFormPage assertOnPage() { - assertText(org.odk.collect.strings.R.string.review_data); + assertText(string.review_data); return this; } @@ -93,4 +94,11 @@ public EditSavedFormPage searchInBar(String query) { onView(withId(androidx.appcompat.R.id.search_src_text)).perform(replaceText(query)); return this; } + + public BulkFinalizationConfirmationDialogPage clickFinalizeAll(int count) { + this.clickOptionsIcon(string.finalize_all_forms) + .clickOnString(string.finalize_all_forms); + + return new BulkFinalizationConfirmationDialogPage(count).assertOnPage(); + } } diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt index afac2a322e8..60f18e7f0fd 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt @@ -19,6 +19,8 @@ class BulkFinalizationViewModel( private val _isFinalizing = MutableNonNullLiveData(false) val isFinalizing: NonNullLiveData = _isFinalizing + val draftsCount = instancesDataService.editableCount.value + fun finalizeAllDrafts() { _isFinalizing.value = true diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt index 325e73f17b8..60114d95932 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt @@ -7,6 +7,7 @@ import android.view.MenuItem import androidx.core.view.MenuProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.odk.collect.android.R +import org.odk.collect.strings.R.plurals import org.odk.collect.strings.R.string class DraftsMenuProvider( @@ -19,7 +20,15 @@ class DraftsMenuProvider( override fun onMenuItemSelected(menuItem: MenuItem): Boolean { if (menuItem.itemId == R.id.bulk_finalize) { + val draftsCount = bulkFinalizationViewModel.draftsCount!! + val dialogTitle = context.resources.getQuantityString( + plurals.bulk_finalize_confirmation, + draftsCount, + draftsCount + ) + MaterialAlertDialogBuilder(context) + .setTitle(dialogTitle) .setMessage(string.bulk_finalize_explanation) .setPositiveButton(string.finalize) { _, _ -> bulkFinalizationViewModel.finalizeAllDrafts() diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index 29971b75cb3..d2eac3c8c5e 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -1249,4 +1249,10 @@ Once you finalize all forms, they will be in "Ready to send" and you will not be able to make edits. Any forms that have errors will not be finalized.\n\nYou will not be able to undo this action. + + + + Do you want to finalize %d form? + Do you want to finalize %d forms? + From 85bb5c27880737d160cebcbf22a5ff6fba24eb41 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 13 Oct 2023 16:45:10 +0100 Subject: [PATCH 08/18] Account for cases where drafts are not loaded or aren't present --- .../drafts/BulkFinalizationViewModel.kt | 2 +- .../drafts/DraftsMenuProvider.kt | 26 +++++-- .../drafts/DraftsMenuProviderTest.kt | 78 +++++++++++++++++++ 3 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 collect_app/src/test/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProviderTest.kt diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt index 60f18e7f0fd..fd440caa216 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt @@ -19,7 +19,7 @@ class BulkFinalizationViewModel( private val _isFinalizing = MutableNonNullLiveData(false) val isFinalizing: NonNullLiveData = _isFinalizing - val draftsCount = instancesDataService.editableCount.value + val draftsCount = instancesDataService.editableCount fun finalizeAllDrafts() { _isFinalizing.value = true diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt index 60114d95932..4e4a51624e2 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt @@ -1,9 +1,9 @@ package org.odk.collect.android.formmanagement.drafts -import android.content.Context import android.view.Menu import android.view.MenuInflater import android.view.MenuItem +import androidx.activity.ComponentActivity import androidx.core.view.MenuProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.odk.collect.android.R @@ -11,23 +11,39 @@ import org.odk.collect.strings.R.plurals import org.odk.collect.strings.R.string class DraftsMenuProvider( - private val context: Context, + private val activity: ComponentActivity, private val bulkFinalizationViewModel: BulkFinalizationViewModel ) : MenuProvider { + + private var draftsCount: Int? = null + + init { + bulkFinalizationViewModel.draftsCount.observe(activity) { + draftsCount = it + activity.invalidateOptionsMenu() + } + } + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.drafts, menu) } + override fun onPrepareMenu(menu: Menu) { + if (draftsCount == null || draftsCount == 0) { + menu.findItem(R.id.bulk_finalize).isVisible = false + } + } + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { if (menuItem.itemId == R.id.bulk_finalize) { - val draftsCount = bulkFinalizationViewModel.draftsCount!! - val dialogTitle = context.resources.getQuantityString( + val draftsCount = bulkFinalizationViewModel.draftsCount.value!! + val dialogTitle = activity.resources.getQuantityString( plurals.bulk_finalize_confirmation, draftsCount, draftsCount ) - MaterialAlertDialogBuilder(context) + MaterialAlertDialogBuilder(activity) .setTitle(dialogTitle) .setMessage(string.bulk_finalize_explanation) .setPositiveButton(string.finalize) { _, _ -> diff --git a/collect_app/src/test/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProviderTest.kt b/collect_app/src/test/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProviderTest.kt new file mode 100644 index 00000000000..9233aece59b --- /dev/null +++ b/collect_app/src/test/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProviderTest.kt @@ -0,0 +1,78 @@ +package org.odk.collect.android.formmanagement.drafts + +import androidx.appcompat.view.SupportMenuInflater +import androidx.appcompat.view.menu.MenuBuilder +import androidx.core.view.MenuProvider +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.MutableLiveData +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.odk.collect.android.R +import org.odk.collect.android.support.CollectHelpers + +@RunWith(AndroidJUnit4::class) +class DraftsMenuProviderTest { + + private val activity = CollectHelpers.createThemedActivity(MenuProviderTestActivity::class.java) + private val menuInflater = SupportMenuInflater(activity) + private val menu = MenuBuilder(activity) + + private val draftsCountLiveData: MutableLiveData = MutableLiveData(null) + private val bulkFinalizationViewModel = mock { + on { draftsCount } doReturn draftsCountLiveData + } + + private val draftsMenuProvider = DraftsMenuProvider(activity, bulkFinalizationViewModel).also { + it.onCreateMenu(menu, menuInflater) + } + + @Test + fun whenDraftCountHasNotLoaded_doesNotShowFinalizeAll() { + draftsCountLiveData.value = null + draftsMenuProvider.onPrepareMenu(menu) + assertThat(menu.findItem(R.id.bulk_finalize).isVisible, equalTo(false)) + } + + @Test + fun whenDraftCountIsZero_doesNotShowFinalizeAll() { + draftsCountLiveData.value = 0 + draftsMenuProvider.onPrepareMenu(menu) + assertThat(menu.findItem(R.id.bulk_finalize).isVisible, equalTo(false)) + } + + @Test + fun whenDraftsCountIsNonZero_showsFinalizeAll() { + draftsCountLiveData.value = 1 + draftsMenuProvider.onPrepareMenu(menu) + assertThat(menu.findItem(R.id.bulk_finalize).isVisible, equalTo(true)) + } + + @Test + fun whenDraftsCountUpdates_invalidatesMenu() { + assertThat(activity.invalidateCount, equalTo(1)) + + draftsCountLiveData.value = 11 + assertThat(activity.invalidateCount, equalTo(2)) + } +} + +private class MenuProviderTestActivity : FragmentActivity() { + + var invalidateCount = 0 + private set + + override fun addMenuProvider(provider: MenuProvider) { + super.addMenuProvider(provider) + invalidateCount = 0 // Reset the count after Activity creation + } + + override fun invalidateOptionsMenu() { + super.invalidateOptionsMenu() + invalidateCount++ + } +} From 73a4956559af9053904f02517f62a7a49c4fd08c Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 13 Oct 2023 17:02:53 +0100 Subject: [PATCH 09/18] Remove ViewModel interactions from MenuProvider --- .../activities/InstanceChooserList.java | 10 ++-- .../drafts/DraftsMenuProvider.kt | 46 ++++++++----------- .../drafts/DraftsMenuProviderTest.kt | 43 ++--------------- 3 files changed, 32 insertions(+), 67 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java index b4e0e3e75ac..b81aa66d312 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java @@ -149,15 +149,19 @@ public void onCreate(Bundle savedInstanceState) { instancesDataService ); - DraftsMenuProvider draftsMenuProvider = new DraftsMenuProvider(this, bulkFinalizationViewModel); - addMenuProvider(draftsMenuProvider); - MaterialProgressDialogFragment.showOn(this, bulkFinalizationViewModel.isFinalizing(), getSupportFragmentManager(), () -> { MaterialProgressDialogFragment dialog = new MaterialProgressDialogFragment(); dialog.setMessage("Finalizing drafts..."); return dialog; }); + DraftsMenuProvider draftsMenuProvider = new DraftsMenuProvider(this, bulkFinalizationViewModel::finalizeAllDrafts); + addMenuProvider(draftsMenuProvider); + bulkFinalizationViewModel.getDraftsCount().observe(this, draftsCount -> { + draftsMenuProvider.setDraftsCount(draftsCount); + invalidateMenu(); + }); + bulkFinalizationViewModel.getFinalizedForms().observe( this, new FinalizeAllSnackbarPresenter(this.findViewById(android.R.id.content), this) diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt index 4e4a51624e2..e09f4f928a9 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProvider.kt @@ -1,9 +1,9 @@ package org.odk.collect.android.formmanagement.drafts +import android.content.Context import android.view.Menu import android.view.MenuInflater import android.view.MenuItem -import androidx.activity.ComponentActivity import androidx.core.view.MenuProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.odk.collect.android.R @@ -11,18 +11,11 @@ import org.odk.collect.strings.R.plurals import org.odk.collect.strings.R.string class DraftsMenuProvider( - private val activity: ComponentActivity, - private val bulkFinalizationViewModel: BulkFinalizationViewModel + private val context: Context, + private val onFinalizeAll: Runnable ) : MenuProvider { - private var draftsCount: Int? = null - - init { - bulkFinalizationViewModel.draftsCount.observe(activity) { - draftsCount = it - activity.invalidateOptionsMenu() - } - } + var draftsCount: Int? = null override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.drafts, menu) @@ -36,21 +29,22 @@ class DraftsMenuProvider( override fun onMenuItemSelected(menuItem: MenuItem): Boolean { if (menuItem.itemId == R.id.bulk_finalize) { - val draftsCount = bulkFinalizationViewModel.draftsCount.value!! - val dialogTitle = activity.resources.getQuantityString( - plurals.bulk_finalize_confirmation, - draftsCount, - draftsCount - ) - - MaterialAlertDialogBuilder(activity) - .setTitle(dialogTitle) - .setMessage(string.bulk_finalize_explanation) - .setPositiveButton(string.finalize) { _, _ -> - bulkFinalizationViewModel.finalizeAllDrafts() - } - .setNegativeButton(string.cancel, null) - .show() + draftsCount?.also { + val dialogTitle = context.resources.getQuantityString( + plurals.bulk_finalize_confirmation, + it, + it + ) + + MaterialAlertDialogBuilder(context) + .setTitle(dialogTitle) + .setMessage(string.bulk_finalize_explanation) + .setPositiveButton(string.finalize) { _, _ -> + onFinalizeAll.run() + } + .setNegativeButton(string.cancel, null) + .show() + } return true } diff --git a/collect_app/src/test/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProviderTest.kt b/collect_app/src/test/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProviderTest.kt index 9233aece59b..8693bf8342d 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProviderTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/formmanagement/drafts/DraftsMenuProviderTest.kt @@ -2,15 +2,12 @@ package org.odk.collect.android.formmanagement.drafts import androidx.appcompat.view.SupportMenuInflater import androidx.appcompat.view.menu.MenuBuilder -import androidx.core.view.MenuProvider import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.MutableLiveData import androidx.test.ext.junit.runners.AndroidJUnit4 import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo import org.junit.Test import org.junit.runner.RunWith -import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.odk.collect.android.R import org.odk.collect.android.support.CollectHelpers @@ -18,61 +15,31 @@ import org.odk.collect.android.support.CollectHelpers @RunWith(AndroidJUnit4::class) class DraftsMenuProviderTest { - private val activity = CollectHelpers.createThemedActivity(MenuProviderTestActivity::class.java) + private val activity = CollectHelpers.createThemedActivity(FragmentActivity::class.java) private val menuInflater = SupportMenuInflater(activity) private val menu = MenuBuilder(activity) - - private val draftsCountLiveData: MutableLiveData = MutableLiveData(null) - private val bulkFinalizationViewModel = mock { - on { draftsCount } doReturn draftsCountLiveData - } - - private val draftsMenuProvider = DraftsMenuProvider(activity, bulkFinalizationViewModel).also { + private val draftsMenuProvider = DraftsMenuProvider(activity, mock()).also { it.onCreateMenu(menu, menuInflater) } @Test fun whenDraftCountHasNotLoaded_doesNotShowFinalizeAll() { - draftsCountLiveData.value = null + draftsMenuProvider.draftsCount = null draftsMenuProvider.onPrepareMenu(menu) assertThat(menu.findItem(R.id.bulk_finalize).isVisible, equalTo(false)) } @Test fun whenDraftCountIsZero_doesNotShowFinalizeAll() { - draftsCountLiveData.value = 0 + draftsMenuProvider.draftsCount = 0 draftsMenuProvider.onPrepareMenu(menu) assertThat(menu.findItem(R.id.bulk_finalize).isVisible, equalTo(false)) } @Test fun whenDraftsCountIsNonZero_showsFinalizeAll() { - draftsCountLiveData.value = 1 + draftsMenuProvider.draftsCount = 1 draftsMenuProvider.onPrepareMenu(menu) assertThat(menu.findItem(R.id.bulk_finalize).isVisible, equalTo(true)) } - - @Test - fun whenDraftsCountUpdates_invalidatesMenu() { - assertThat(activity.invalidateCount, equalTo(1)) - - draftsCountLiveData.value = 11 - assertThat(activity.invalidateCount, equalTo(2)) - } -} - -private class MenuProviderTestActivity : FragmentActivity() { - - var invalidateCount = 0 - private set - - override fun addMenuProvider(provider: MenuProvider) { - super.addMenuProvider(provider) - invalidateCount = 0 // Reset the count after Activity creation - } - - override fun invalidateOptionsMenu() { - super.invalidateOptionsMenu() - invalidateCount++ - } } From edbb2afd28755b581e6cbdac9bed94d4a90d102f Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 13 Oct 2023 17:05:27 +0100 Subject: [PATCH 10/18] Make sure to pass lifecycle with menu provider --- .../org/odk/collect/android/activities/InstanceChooserList.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java index b81aa66d312..033f5349043 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java @@ -156,7 +156,7 @@ public void onCreate(Bundle savedInstanceState) { }); DraftsMenuProvider draftsMenuProvider = new DraftsMenuProvider(this, bulkFinalizationViewModel::finalizeAllDrafts); - addMenuProvider(draftsMenuProvider); + addMenuProvider(draftsMenuProvider, this); bulkFinalizationViewModel.getDraftsCount().observe(this, draftsCount -> { draftsMenuProvider.setDraftsCount(draftsCount); invalidateMenu(); From e8097625290c2065a860ea8db8b121076867dee8 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Sat, 14 Oct 2023 12:47:42 +0100 Subject: [PATCH 11/18] Add setting to disable bulk finalize --- .../formmanagement/BulkFinalizationTest.kt | 21 +++++++++++++++ .../odk/collect/android/support/pages/Page.kt | 16 +++++++++-- .../activities/InstanceChooserList.java | 27 ++++++++++--------- .../drafts/BulkFinalizationViewModel.kt | 7 ++++- .../res/xml/form_entry_access_preferences.xml | 9 ++++++- .../settings/keys/ProtectedProjectKeys.kt | 4 ++- strings/src/main/res/values/strings.xml | 3 +++ 7 files changed, 70 insertions(+), 17 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt index b6cd96c7a2b..7d81458291b 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt @@ -8,9 +8,11 @@ import org.junit.Test import org.junit.rules.RuleChain import org.junit.runner.RunWith import org.odk.collect.android.support.TestDependencies +import org.odk.collect.android.support.pages.AccessControlPage import org.odk.collect.android.support.pages.EditSavedFormPage import org.odk.collect.android.support.pages.FormEntryPage.QuestionAndAnswer import org.odk.collect.android.support.pages.MainMenuPage +import org.odk.collect.android.support.pages.ProjectSettingsPage import org.odk.collect.android.support.pages.SaveOrDiscardFormDialog import org.odk.collect.android.support.rules.CollectTestRule import org.odk.collect.android.support.rules.TestRuleChain @@ -189,4 +191,23 @@ class BulkFinalizationTest { .assertNumberOfEditableForms(1) } + + @Test + fun canBeDisabled() { + rule.withProject("http://example.com") + .openProjectSettingsDialog() + .clickSettings() + .clickAccessControl() + .clickFormEntrySettings() + .clickOnString(string.finalize_all_forms) + .pressBack(AccessControlPage()) + .pressBack(ProjectSettingsPage()) + .pressBack(MainMenuPage()) + + .copyForm("one-question.xml", "example.com") + .startBlankForm("One Question") + .fillOutAndSave(QuestionAndAnswer("what is your age", "1892")) + .clickDrafts() + .assertNoOptionsMenu() + } } diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt index 138f5dd2ac3..ab717024059 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt @@ -2,6 +2,7 @@ package org.odk.collect.android.support.pages import android.app.Application import android.content.pm.ActivityInfo +import android.view.View import androidx.annotation.StringRes import androidx.recyclerview.widget.RecyclerView import androidx.test.core.app.ApplicationProvider @@ -34,6 +35,8 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector import org.hamcrest.CoreMatchers.not +import org.hamcrest.Matcher +import org.hamcrest.Matchers import org.hamcrest.Matchers.allOf import org.hamcrest.core.StringContains.containsString import org.hamcrest.core.StringEndsWith.endsWith @@ -42,7 +45,6 @@ import org.odk.collect.android.BuildConfig import org.odk.collect.android.R import org.odk.collect.android.application.Collect import org.odk.collect.android.storage.StoragePathProvider -import org.odk.collect.android.support.ActivityHelpers import org.odk.collect.android.support.CollectHelpers import org.odk.collect.android.support.WaitFor.wait250ms import org.odk.collect.android.support.WaitFor.waitFor @@ -448,7 +450,7 @@ abstract class Page> { fun clickOptionsIcon(expectedOptionString: String): T { tryAgainOnFail({ - Espresso.openActionBarOverflowOrOptionsMenu(ActivityHelpers.getActivity()) + onView(OVERFLOW_BUTTON_MATCHER).perform(click()) assertText(expectedOptionString) }) @@ -474,6 +476,11 @@ abstract class Page> { return destination!!.assertOnPage() } + fun assertNoOptionsMenu(): T { + onView(OVERFLOW_BUTTON_MATCHER).check(doesNotExist()) + return this as T + } + companion object { private fun rotateToLandscape(): ViewAction { return RotateAction(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) @@ -482,5 +489,10 @@ abstract class Page> { private fun rotateToPortrait(): ViewAction { return RotateAction(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) } + + private val OVERFLOW_BUTTON_MATCHER: Matcher = Matchers.anyOf( + allOf(isDisplayed(), withContentDescription("More options")), + allOf(isDisplayed(), withClassName(Matchers.endsWith("OverflowMenuButton"))) + ) } } diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java index 033f5349043..b61cd2df921 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/InstanceChooserList.java @@ -146,7 +146,8 @@ public void onCreate(Bundle savedInstanceState) { BulkFinalizationViewModel bulkFinalizationViewModel = new BulkFinalizationViewModel( scheduler, - instancesDataService + instancesDataService, + settingsProvider ); MaterialProgressDialogFragment.showOn(this, bulkFinalizationViewModel.isFinalizing(), getSupportFragmentManager(), () -> { @@ -155,17 +156,19 @@ public void onCreate(Bundle savedInstanceState) { return dialog; }); - DraftsMenuProvider draftsMenuProvider = new DraftsMenuProvider(this, bulkFinalizationViewModel::finalizeAllDrafts); - addMenuProvider(draftsMenuProvider, this); - bulkFinalizationViewModel.getDraftsCount().observe(this, draftsCount -> { - draftsMenuProvider.setDraftsCount(draftsCount); - invalidateMenu(); - }); - - bulkFinalizationViewModel.getFinalizedForms().observe( - this, - new FinalizeAllSnackbarPresenter(this.findViewById(android.R.id.content), this) - ); + if (bulkFinalizationViewModel.isEnabled()) { + DraftsMenuProvider draftsMenuProvider = new DraftsMenuProvider(this, bulkFinalizationViewModel::finalizeAllDrafts); + addMenuProvider(draftsMenuProvider, this); + bulkFinalizationViewModel.getDraftsCount().observe(this, draftsCount -> { + draftsMenuProvider.setDraftsCount(draftsCount); + invalidateMenu(); + }); + + bulkFinalizationViewModel.getFinalizedForms().observe( + this, + new FinalizeAllSnackbarPresenter(this.findViewById(android.R.id.content), this) + ); + } } private void init() { diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt index fd440caa216..7e96f787d12 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/drafts/BulkFinalizationViewModel.kt @@ -8,10 +8,13 @@ import org.odk.collect.androidshared.data.Consumable import org.odk.collect.androidshared.livedata.MutableNonNullLiveData import org.odk.collect.androidshared.livedata.NonNullLiveData import org.odk.collect.async.Scheduler +import org.odk.collect.settings.SettingsProvider +import org.odk.collect.settings.keys.ProtectedProjectKeys class BulkFinalizationViewModel( private val scheduler: Scheduler, - private val instancesDataService: InstancesDataService + private val instancesDataService: InstancesDataService, + private val settingsProvider: SettingsProvider ) { private val _finalizedForms = MutableLiveData>() val finalizedForms: LiveData> = _finalizedForms @@ -20,6 +23,8 @@ class BulkFinalizationViewModel( val isFinalizing: NonNullLiveData = _isFinalizing val draftsCount = instancesDataService.editableCount + val isEnabled = + settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_BULK_FINALIZE) fun finalizeAllDrafts() { _isFinalizing.value = true diff --git a/collect_app/src/main/res/xml/form_entry_access_preferences.xml b/collect_app/src/main/res/xml/form_entry_access_preferences.xml index 7a69e689d5d..b1305c83d67 100644 --- a/collect_app/src/main/res/xml/form_entry_access_preferences.xml +++ b/collect_app/src/main/res/xml/form_entry_access_preferences.xml @@ -44,4 +44,11 @@ android:title="@string/finalize" app:iconSpaceReserved="false" /> - \ No newline at end of file + + + + + diff --git a/settings/src/main/java/org/odk/collect/settings/keys/ProtectedProjectKeys.kt b/settings/src/main/java/org/odk/collect/settings/keys/ProtectedProjectKeys.kt index f746c401d28..dc29614c2ae 100644 --- a/settings/src/main/java/org/odk/collect/settings/keys/ProtectedProjectKeys.kt +++ b/settings/src/main/java/org/odk/collect/settings/keys/ProtectedProjectKeys.kt @@ -43,6 +43,7 @@ object ProtectedProjectKeys { const val KEY_FINALIZE = "finalize" const val ALLOW_OTHER_WAYS_OF_EDITING_FORM = "allow_other_ways_of_editing_form" + const val KEY_BULK_FINALIZE = "bulk_finalize" fun allKeys() = listOf( KEY_ADMIN_PW, @@ -82,6 +83,7 @@ object ProtectedProjectKeys { KEY_SAVE_MID, KEY_SAVE_AS_DRAFT, KEY_FINALIZE, - ALLOW_OTHER_WAYS_OF_EDITING_FORM + ALLOW_OTHER_WAYS_OF_EDITING_FORM, + KEY_BULK_FINALIZE ) } diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index d2eac3c8c5e..9eb93a4146e 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -1255,4 +1255,7 @@ Do you want to finalize %d form? Do you want to finalize %d forms? + + + Uncheck to hide from Drafts From f137a4ef9c64db3836f1d31c0a5e19aeaedb5f4a Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Sat, 14 Oct 2023 13:20:28 +0100 Subject: [PATCH 12/18] Disable bulk finalize for configurations that previously had finalize in form entry disabled --- .../android/formentry/FormEndViewModel.kt | 2 +- .../FormEntryAccessPreferencesFragment.kt | 16 ++++----- .../res/xml/form_entry_access_preferences.xml | 2 +- .../settings/ODKAppSettingsMigrator.java | 16 ++++++--- .../settings/keys/ProtectedProjectKeys.kt | 4 +-- .../settings/ODKAppSettingsMigratorTest.java | 35 +++++++++++++++---- 6 files changed, 51 insertions(+), 24 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/FormEndViewModel.kt b/collect_app/src/main/java/org/odk/collect/android/formentry/FormEndViewModel.kt index 1d53b51da5f..4e1442c8400 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/FormEndViewModel.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/FormEndViewModel.kt @@ -18,7 +18,7 @@ class FormEndViewModel( } fun isFinalizeEnabled(): Boolean { - return settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_FINALIZE) + return settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY) } fun shouldFormBeSentAutomatically(): Boolean { diff --git a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/FormEntryAccessPreferencesFragment.kt b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/FormEntryAccessPreferencesFragment.kt index a49dac92a41..66fa6deea35 100644 --- a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/FormEntryAccessPreferencesFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/FormEntryAccessPreferencesFragment.kt @@ -43,15 +43,15 @@ class FormEntryAccessPreferencesFragment : BaseAdminPreferencesFragment() { settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM) findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isEnabled = - settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM) && findPreference(ProtectedProjectKeys.KEY_FINALIZE).isChecked + settingsProvider.getProtectedSettings().getBoolean(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM) && findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isChecked findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference, newValue: Any? -> - findPreference(ProtectedProjectKeys.KEY_FINALIZE).isEnabled = newValue as Boolean + findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled = newValue as Boolean true } - findPreference(ProtectedProjectKeys.KEY_FINALIZE).isEnabled = findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isChecked - findPreference(ProtectedProjectKeys.KEY_FINALIZE).onPreferenceChangeListener = + findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled = findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isChecked + findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference, newValue: Any? -> findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isEnabled = newValue as Boolean true @@ -62,18 +62,18 @@ class FormEntryAccessPreferencesFragment : BaseAdminPreferencesFragment() { settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM, false) settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_EDIT_SAVED, false) settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false) - settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_FINALIZE, true) + settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true) settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_JUMP_TO, false) settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_CONSTRAINT_BEHAVIOR, ProjectKeys.CONSTRAINT_BEHAVIOR_ON_SWIPE) findPreference(ProtectedProjectKeys.KEY_JUMP_TO).isEnabled = false findPreference(ProtectedProjectKeys.KEY_SAVE_MID).isEnabled = false findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isEnabled = false - findPreference(ProtectedProjectKeys.KEY_FINALIZE).isEnabled = false + findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled = false findPreference(ProtectedProjectKeys.KEY_JUMP_TO).isChecked = false findPreference(ProtectedProjectKeys.KEY_SAVE_MID).isChecked = false findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isChecked = false - findPreference(ProtectedProjectKeys.KEY_FINALIZE).isChecked = true + findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isChecked = true } private fun onMovingBackwardsEnabled() { @@ -81,7 +81,7 @@ class FormEntryAccessPreferencesFragment : BaseAdminPreferencesFragment() { findPreference(ProtectedProjectKeys.KEY_JUMP_TO).isEnabled = true findPreference(ProtectedProjectKeys.KEY_SAVE_MID).isEnabled = true findPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isEnabled = true - findPreference(ProtectedProjectKeys.KEY_FINALIZE).isEnabled = true + findPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled = true } private fun findPreference(key: String): CheckBoxPreference { diff --git a/collect_app/src/main/res/xml/form_entry_access_preferences.xml b/collect_app/src/main/res/xml/form_entry_access_preferences.xml index b1305c83d67..7850d2dc673 100644 --- a/collect_app/src/main/res/xml/form_entry_access_preferences.xml +++ b/collect_app/src/main/res/xml/form_entry_access_preferences.xml @@ -40,7 +40,7 @@ android:title="@string/save_as_draft" app:iconSpaceReserved="false" /> diff --git a/settings/src/main/java/org/odk/collect/settings/ODKAppSettingsMigrator.java b/settings/src/main/java/org/odk/collect/settings/ODKAppSettingsMigrator.java index 60f0579af3a..ccd0ec4bed5 100644 --- a/settings/src/main/java/org/odk/collect/settings/ODKAppSettingsMigrator.java +++ b/settings/src/main/java/org/odk/collect/settings/ODKAppSettingsMigrator.java @@ -155,17 +155,17 @@ public List getProtectedMigrations() { .withValues(false, false) .toPairs( ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, true, - ProtectedProjectKeys.KEY_FINALIZE, false + ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, false ) .withValues(false, true) .toPairs( ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false, - ProtectedProjectKeys.KEY_FINALIZE, true + ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true ) .withValues(false, null) .toPairs( ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false, - ProtectedProjectKeys.KEY_FINALIZE, true + ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true ), removeKey("mark_as_finalized"), removeKey("default_completed"), @@ -173,8 +173,14 @@ public List getProtectedMigrations() { .withValues(false) .toPairs( ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false, - ProtectedProjectKeys.KEY_FINALIZE, true - ) + ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true + ), + updateKeys("finalize").withValues(false) + .toPairs( + ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, false, + ProtectedProjectKeys.KEY_BULK_FINALIZE, false + ), + removeKey("finalize") ); } } diff --git a/settings/src/main/java/org/odk/collect/settings/keys/ProtectedProjectKeys.kt b/settings/src/main/java/org/odk/collect/settings/keys/ProtectedProjectKeys.kt index dc29614c2ae..ae373d02f17 100644 --- a/settings/src/main/java/org/odk/collect/settings/keys/ProtectedProjectKeys.kt +++ b/settings/src/main/java/org/odk/collect/settings/keys/ProtectedProjectKeys.kt @@ -40,7 +40,7 @@ object ProtectedProjectKeys { const val KEY_JUMP_TO = "jump_to" const val KEY_SAVE_MID = "save_mid" const val KEY_SAVE_AS_DRAFT = "save_as_draft" - const val KEY_FINALIZE = "finalize" + const val KEY_FINALIZE_IN_FORM_ENTRY = "finalize_in_form_entry" const val ALLOW_OTHER_WAYS_OF_EDITING_FORM = "allow_other_ways_of_editing_form" const val KEY_BULK_FINALIZE = "bulk_finalize" @@ -82,7 +82,7 @@ object ProtectedProjectKeys { KEY_JUMP_TO, KEY_SAVE_MID, KEY_SAVE_AS_DRAFT, - KEY_FINALIZE, + KEY_FINALIZE_IN_FORM_ENTRY, ALLOW_OTHER_WAYS_OF_EDITING_FORM, KEY_BULK_FINALIZE ) diff --git a/settings/src/test/java/org/odk/collect/settings/ODKAppSettingsMigratorTest.java b/settings/src/test/java/org/odk/collect/settings/ODKAppSettingsMigratorTest.java index ff949f415f4..45c07635971 100644 --- a/settings/src/test/java/org/odk/collect/settings/ODKAppSettingsMigratorTest.java +++ b/settings/src/test/java/org/odk/collect/settings/ODKAppSettingsMigratorTest.java @@ -260,7 +260,7 @@ public void when_markAsFinalized_wasDisabled_and_defaultCompleted_wasDisabled_th runMigrations(); - assertSettings(protectedSettings, ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, true, ProtectedProjectKeys.KEY_FINALIZE, false); + assertSettings(protectedSettings, ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, true, ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, false); assertThat(protectedSettings.contains("mark_as_finalized"), equalTo(false)); assertThat(protectedSettings.contains("default_completed"), equalTo(false)); @@ -273,7 +273,7 @@ public void when_markAsFinalized_wasDisabled_and_defaultCompleted_wasEnabled_the runMigrations(); - assertSettings(protectedSettings, ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false, ProtectedProjectKeys.KEY_FINALIZE, true); + assertSettings(protectedSettings, ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false, ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true); assertThat(protectedSettings.contains("mark_as_finalized"), equalTo(false)); assertThat(protectedSettings.contains("default_completed"), equalTo(false)); @@ -285,7 +285,7 @@ public void when_markAsFinalized_wasDisabled_and_defaultCompleted_wasNotSet_then runMigrations(); - assertSettings(protectedSettings, ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false, ProtectedProjectKeys.KEY_FINALIZE, true); + assertSettings(protectedSettings, ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false, ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true); assertThat(protectedSettings.contains("mark_as_finalized"), equalTo(false)); assertThat(protectedSettings.contains("default_completed"), equalTo(false)); @@ -322,7 +322,7 @@ public void when_AllowOtherWaysOfEditingFormIsDisabled_thenSaveAsDraftShouldBeDi initSettings(protectedSettings, ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM, false, ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, true, - ProtectedProjectKeys.KEY_FINALIZE, false + ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, false ); runMigrations(); @@ -330,7 +330,7 @@ public void when_AllowOtherWaysOfEditingFormIsDisabled_thenSaveAsDraftShouldBeDi assertThat(protectedSettings.contains(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM), equalTo(true)); assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM), equalTo(false)); assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT), equalTo(false)); - assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_FINALIZE), equalTo(true)); + assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY), equalTo(true)); } @Test @@ -338,7 +338,7 @@ public void when_AllowOtherWaysOfEditingFormIsEnabled_thenDoNotUpdateSaveAsDraft initSettings(protectedSettings, ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM, true, ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, true, - ProtectedProjectKeys.KEY_FINALIZE, false + ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, false ); runMigrations(); @@ -346,7 +346,28 @@ public void when_AllowOtherWaysOfEditingFormIsEnabled_thenDoNotUpdateSaveAsDraft assertThat(protectedSettings.contains(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM), equalTo(true)); assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.ALLOW_OTHER_WAYS_OF_EDITING_FORM), equalTo(true)); assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT), equalTo(true)); - assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_FINALIZE), equalTo(false)); + assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY), equalTo(false)); + } + + @Test + public void migratesFinalizeInFormEntryToNewKey() { + initSettings(protectedSettings, "finalize", false); + + runMigrations(); + + assertThat(protectedSettings.contains("finalize"), equalTo(false)); + assertThat(protectedSettings.contains(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY), equalTo(true)); + assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY), equalTo(false)); + } + + @Test + public void whenFinalizeInFormEntryWasDisabledWithOldKey_disablesBulkFinalize() { + initSettings(protectedSettings, "finalize", false); + + runMigrations(); + + assertThat(protectedSettings.contains(ProtectedProjectKeys.KEY_BULK_FINALIZE), equalTo(true)); + assertThat(protectedSettings.getBoolean(ProtectedProjectKeys.KEY_BULK_FINALIZE), equalTo(false)); } private void runMigrations() { From b06aaad26fb2d119f27eae55e7bd1fbe2fbdb93b Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Sat, 14 Oct 2023 13:42:38 +0100 Subject: [PATCH 13/18] Fix references in tests --- .../android/formentry/FormEndViewModelTest.kt | 4 ++-- .../FormEntryAccessPreferencesFragmentTest.kt | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/collect_app/src/test/java/org/odk/collect/android/formentry/FormEndViewModelTest.kt b/collect_app/src/test/java/org/odk/collect/android/formentry/FormEndViewModelTest.kt index ea8db74968f..aaedf4bef07 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formentry/FormEndViewModelTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/formentry/FormEndViewModelTest.kt @@ -35,13 +35,13 @@ class FormEndViewModelTest { @Test fun `when 'Finalize' is enabled, isFinalizeEnabled should return true`() { - settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_FINALIZE, true) + settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true) assertThat(formEndViewModel.isFinalizeEnabled(), equalTo(true)) } @Test fun `when 'Finalize' is disabled, isFinalizeEnabled should return false`() { - settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_FINALIZE, false) + settingsProvider.getProtectedSettings().save(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, false) assertThat(formEndViewModel.isFinalizeEnabled(), equalTo(false)) } diff --git a/collect_app/src/test/java/org/odk/collect/android/preferences/screens/FormEntryAccessPreferencesFragmentTest.kt b/collect_app/src/test/java/org/odk/collect/android/preferences/screens/FormEntryAccessPreferencesFragmentTest.kt index 834657fbe90..634cdd1f99e 100644 --- a/collect_app/src/test/java/org/odk/collect/android/preferences/screens/FormEntryAccessPreferencesFragmentTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/preferences/screens/FormEntryAccessPreferencesFragmentTest.kt @@ -32,7 +32,7 @@ class FormEntryAccessPreferencesFragmentTest { @Test fun `when the 'Save as draft' option is unchecked, the 'Finalize' option can't be changed`() { adminSettings.save(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, false) - adminSettings.save(ProtectedProjectKeys.KEY_FINALIZE, true) + adminSettings.save(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, true) val scenario = launcherRule.launch(FormEntryAccessPreferencesFragment::class.java) scenario.onFragment { fragment: FormEntryAccessPreferencesFragment -> @@ -42,7 +42,7 @@ class FormEntryAccessPreferencesFragmentTest { ) assertThat( - fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE).isEnabled, + fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled, equalTo(false) ) } @@ -51,7 +51,7 @@ class FormEntryAccessPreferencesFragmentTest { @Test fun `when the 'Finalize' option is unchecked, the 'Save as draft' option can't be changed`() { adminSettings.save(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT, true) - adminSettings.save(ProtectedProjectKeys.KEY_FINALIZE, false) + adminSettings.save(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY, false) val scenario = launcherRule.launch(FormEntryAccessPreferencesFragment::class.java) scenario.onFragment { fragment: FormEntryAccessPreferencesFragment -> @@ -61,7 +61,7 @@ class FormEntryAccessPreferencesFragmentTest { ) assertThat( - fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE).isEnabled, + fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled, equalTo(true) ) } @@ -72,18 +72,18 @@ class FormEntryAccessPreferencesFragmentTest { val scenario = launcherRule.launch(FormEntryAccessPreferencesFragment::class.java) scenario.onFragment { fragment: FormEntryAccessPreferencesFragment -> assertThat( - fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE).isEnabled, + fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled, equalTo(true) ) fragment.getPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).performClick() assertThat( - fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE).isEnabled, + fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isEnabled, equalTo(false) ) assertThat( - fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE).isChecked, + fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).isChecked, equalTo(true) ) } @@ -98,7 +98,7 @@ class FormEntryAccessPreferencesFragmentTest { equalTo(true) ) - fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE).performClick() + fragment.getPreference(ProtectedProjectKeys.KEY_FINALIZE_IN_FORM_ENTRY).performClick() assertThat( fragment.getPreference(ProtectedProjectKeys.KEY_SAVE_AS_DRAFT).isEnabled, From ad3fc152a4d7b76dbe8dbd6ebf3e70f2298c01c9 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 16 Oct 2023 09:45:55 +0100 Subject: [PATCH 14/18] Make sure old keys are still importable --- ...OriginalJsonSchemaSettingsValidatorTest.kt | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/settings/src/test/java/org/odk/collect/settings/validation/OriginalJsonSchemaSettingsValidatorTest.kt b/settings/src/test/java/org/odk/collect/settings/validation/OriginalJsonSchemaSettingsValidatorTest.kt index bb2f7e2b9db..40a48fb1320 100644 --- a/settings/src/test/java/org/odk/collect/settings/validation/OriginalJsonSchemaSettingsValidatorTest.kt +++ b/settings/src/test/java/org/odk/collect/settings/validation/OriginalJsonSchemaSettingsValidatorTest.kt @@ -7,8 +7,8 @@ import org.junit.Test class OriginalJsonSchemaSettingsValidatorTest { /* - * 'default_completed' and 'mark_as_finalized' were replaced by new settings in v2023.2 but - * we need the schema to still recognize the old fields so that we can migrate them correctly. + * Some settings end up replaced by new settings in but we need the schema to still + * recognize the old fields so that we can migrate them correctly. */ @Test fun `isValueSupported returns true for fields we no longer use`() { @@ -16,14 +16,17 @@ class OriginalJsonSchemaSettingsValidatorTest { javaClass.getResourceAsStream("/client-settings.schema.json")!! } - assertThat( - validator.isValueSupported("general", "default_completed", "true"), - equalTo(true) - ) - - assertThat( - validator.isValueSupported("admin", "mark_as_finalized", "true"), - equalTo(true) - ) + removedKeys.forEach { + assertThat( + validator.isValueSupported(it.first, it.second, "true"), + equalTo(true) + ) + } } + + private val removedKeys = listOf( + Pair("admin", "mark_as_finalized"), + Pair("general", "default_completed"), + Pair("admin", "finalize") + ) } From a7cfcf150af7837e2ffd372d3ebd5c9ad4235ed7 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 16 Oct 2023 10:28:36 +0100 Subject: [PATCH 15/18] Upate schema --- settings/src/main/resources/client-settings.schema.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/settings/src/main/resources/client-settings.schema.json b/settings/src/main/resources/client-settings.schema.json index d1fb73ab25e..60c74a923ab 100644 --- a/settings/src/main/resources/client-settings.schema.json +++ b/settings/src/main/resources/client-settings.schema.json @@ -321,6 +321,13 @@ "type": "boolean" }, "finalize": { + "type": "boolean", + "deprecated": true + }, + "finalize_in_form_entry": { + "type": "boolean" + }, + "bulk_finalize": { "type": "boolean" } } From 869fde35d2f4e7362e89031425b9239047cc53e2 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 16 Oct 2023 10:45:16 +0100 Subject: [PATCH 16/18] Add analytics for unsupported drafts --- .../collect/android/analytics/AnalyticsEvents.kt | 6 ++++++ .../android/formmanagement/InstancesDataService.kt | 14 ++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt index 9b8b078eba1..447b2238009 100644 --- a/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt +++ b/collect_app/src/main/java/org/odk/collect/android/analytics/AnalyticsEvents.kt @@ -168,4 +168,10 @@ object AnalyticsEvents { * Tracks how often a form is finalized using a `ref` attribute on the `submission` element */ const val PARTIAL_FORM_FINALIZED = "PartialFormFinalized" + + /** + * Tracks how often drafts that can't be bulk finalized are attempted to be + */ + const val BULK_FINALIZE_ENCRYPTED_FORM = "BulkFinalizeEncryptedForm" + const val BULK_FINALIZE_SAVE_POINT = "BulkFinalizeSavePoint" } diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt index 712fb4a9a82..5bf869b8713 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/InstancesDataService.kt @@ -1,6 +1,8 @@ package org.odk.collect.android.formmanagement import androidx.lifecycle.LiveData +import org.odk.collect.analytics.Analytics +import org.odk.collect.android.analytics.AnalyticsEvents import org.odk.collect.android.application.Collect import org.odk.collect.android.backgroundwork.InstanceSubmitScheduler import org.odk.collect.android.entities.EntitiesRepositoryProvider @@ -80,8 +82,14 @@ class InstancesDataService( val formController = FormEntryUseCases.loadDraft(form, instance, formEntryController) val savePoint = FormEntryUseCases.getSavePoint(formController, File(cacheDir)) - val needsEncrypted = form.basE64RSAPublicKey == null - val newResult = if (savePoint == null && needsEncrypted) { + val needsEncrypted = form.basE64RSAPublicKey != null + val newResult = if (savePoint != null) { + Analytics.log(AnalyticsEvents.BULK_FINALIZE_SAVE_POINT) + result.copy(failureCount = result.failureCount + 1, unsupportedInstances = true) + } else if (needsEncrypted) { + Analytics.log(AnalyticsEvents.BULK_FINALIZE_ENCRYPTED_FORM) + result.copy(failureCount = result.failureCount + 1, unsupportedInstances = true) + } else { val finalizedInstance = FormEntryUseCases.finalizeDraft( formController, instancesRepository, @@ -93,8 +101,6 @@ class InstancesDataService( } else { result } - } else { - result.copy(failureCount = result.failureCount + 1, unsupportedInstances = true) } Collect.getInstance().externalDataManager?.close() From b9202f2134f00df1eb3e322e4b16f495d2141d75 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 18 Oct 2023 12:58:43 +0100 Subject: [PATCH 17/18] Convert presenter to Kotlin --- .../FinalizeAllSnackbarPresenter.java | 51 ------------------- .../FinalizeAllSnackbarPresenter.kt | 47 +++++++++++++++++ 2 files changed, 47 insertions(+), 51 deletions(-) delete mode 100644 collect_app/src/main/java/org/odk/collect/android/instancemanagement/FinalizeAllSnackbarPresenter.java create mode 100644 collect_app/src/main/java/org/odk/collect/android/instancemanagement/FinalizeAllSnackbarPresenter.kt diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/FinalizeAllSnackbarPresenter.java b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/FinalizeAllSnackbarPresenter.java deleted file mode 100644 index c49863f1d32..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/FinalizeAllSnackbarPresenter.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.odk.collect.android.instancemanagement; - -import android.content.Context; -import android.view.View; - -import androidx.annotation.NonNull; - -import org.odk.collect.android.formmanagement.FinalizeAllResult; -import org.odk.collect.androidshared.ui.SnackbarUtils; - -public class FinalizeAllSnackbarPresenter extends SnackbarUtils.SnackbarPresenterObserver { - private final Context context; - - public FinalizeAllSnackbarPresenter(@NonNull View parentView, Context context) { - super(parentView); - this.context = context; - } - - @NonNull - @Override - public SnackbarUtils.SnackbarDetails getSnackbarDetails(FinalizeAllResult result) { - if (result.getUnsupportedInstances()) { - return new SnackbarUtils.SnackbarDetails( - context.getString( - org.odk.collect.strings.R.string.bulk_finalize_unsupported, - result.getSuccessCount() - ) - ); - } else if (result.getFailureCount() == 0) { - return new SnackbarUtils.SnackbarDetails( - context.getResources().getQuantityString( - org.odk.collect.strings.R.plurals.bulk_finalize_success, - result.getSuccessCount(), - result.getSuccessCount() - ) - ); - } else if (result.getSuccessCount() == 0) { - return new SnackbarUtils.SnackbarDetails( - context.getResources().getQuantityString( - org.odk.collect.strings.R.plurals.bulk_finalize_failure, - result.getFailureCount(), - result.getFailureCount() - ) - ); - } else { - return new SnackbarUtils.SnackbarDetails( - context.getString(org.odk.collect.strings.R.string.bulk_finalize_partial_success, result.getSuccessCount(), result.getFailureCount()) - ); - } - } -} diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/FinalizeAllSnackbarPresenter.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/FinalizeAllSnackbarPresenter.kt new file mode 100644 index 00000000000..371a88af47e --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/FinalizeAllSnackbarPresenter.kt @@ -0,0 +1,47 @@ +package org.odk.collect.android.instancemanagement + +import android.content.Context +import android.view.View +import org.odk.collect.android.formmanagement.FinalizeAllResult +import org.odk.collect.androidshared.ui.SnackbarUtils.SnackbarDetails +import org.odk.collect.androidshared.ui.SnackbarUtils.SnackbarPresenterObserver +import org.odk.collect.strings.R + +class FinalizeAllSnackbarPresenter(parentView: View, private val context: Context) : + SnackbarPresenterObserver(parentView) { + + override fun getSnackbarDetails(value: FinalizeAllResult): SnackbarDetails { + return if (value.unsupportedInstances) { + SnackbarDetails( + context.getString( + R.string.bulk_finalize_unsupported, + value.successCount + ) + ) + } else if (value.failureCount == 0) { + SnackbarDetails( + context.resources.getQuantityString( + R.plurals.bulk_finalize_success, + value.successCount, + value.successCount + ) + ) + } else if (value.successCount == 0) { + SnackbarDetails( + context.resources.getQuantityString( + R.plurals.bulk_finalize_failure, + value.failureCount, + value.failureCount + ) + ) + } else { + SnackbarDetails( + context.getString( + R.string.bulk_finalize_partial_success, + value.successCount, + value.failureCount + ) + ) + } + } +} From 5db872d2c7ad6d07e3487f93933579e97efb6db5 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 18 Oct 2023 14:56:55 +0100 Subject: [PATCH 18/18] Add styling to section header --- .../res/xml/form_entry_access_preferences.xml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/collect_app/src/main/res/xml/form_entry_access_preferences.xml b/collect_app/src/main/res/xml/form_entry_access_preferences.xml index 7850d2dc673..248e68eadac 100644 --- a/collect_app/src/main/res/xml/form_entry_access_preferences.xml +++ b/collect_app/src/main/res/xml/form_entry_access_preferences.xml @@ -8,10 +8,11 @@ android:title="@string/moving_backwards_title" app:iconSpaceReserved="false" /> - + app:iconSpaceReserved="false"> - + app:iconSpaceReserved="false"> - +