From fc3d35e6897495398679d8afba5f42d04648817a Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 3 Oct 2023 14:00:43 +0100 Subject: [PATCH 1/7] Genericize deprectation banner --- .../android/mainmenu/MainMenuActivity.kt | 20 +++++++++++-------- ...tion_banner.xml => deprecation_banner.xml} | 16 +++++++-------- .../src/main/res/layout/form_entry_end.xml | 6 +++--- collect_app/src/main/res/layout/main_menu.xml | 8 ++++---- 4 files changed, 27 insertions(+), 23 deletions(-) rename collect_app/src/main/res/layout/{google_drive_deprecation_banner.xml => deprecation_banner.xml} (81%) diff --git a/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt b/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt index a6d7848dc92..40ef49ed0a2 100644 --- a/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt +++ b/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt @@ -41,6 +41,7 @@ import org.odk.collect.permissions.PermissionsProvider import org.odk.collect.projects.Project.Saved import org.odk.collect.settings.SettingsProvider import org.odk.collect.settings.keys.ProjectKeys +import org.odk.collect.strings.R.string import org.odk.collect.strings.localization.LocalizedActivity import javax.inject.Inject @@ -130,7 +131,7 @@ class MainMenuActivity : LocalizedActivity() { currentProjectViewModel.refresh() mainMenuViewModel.refreshInstances() setButtonsVisibility() - manageGoogleDriveDeprecationBanner() + setupDeprecationBanner() } private fun setButtonsVisibility() { @@ -151,7 +152,7 @@ class MainMenuActivity : LocalizedActivity() { (projectsMenuItem.actionView as ProjectIconView).apply { project = currentProjectViewModel.currentProject.value setOnClickListener { onOptionsItemSelected(projectsMenuItem) } - contentDescription = getString(org.odk.collect.strings.R.string.projects) + contentDescription = getString(string.projects) } return super.onPrepareOptionsMenu(menu) } @@ -275,7 +276,7 @@ class MainMenuActivity : LocalizedActivity() { private fun initAppName() { binding.appName.text = String.format( "%s %s", - getString(org.odk.collect.strings.R.string.collect_app_name), + getString(string.collect_app_name), mainMenuViewModel.version ) @@ -287,18 +288,21 @@ class MainMenuActivity : LocalizedActivity() { } } - private fun manageGoogleDriveDeprecationBanner() { + private fun setupDeprecationBanner() { val unprotectedSettings = settingsProvider.getUnprotectedSettings() val protocol = unprotectedSettings.getString(ProjectKeys.KEY_PROTOCOL) - if (ProjectKeys.PROTOCOL_GOOGLE_SHEETS == protocol) { - binding.googleDriveDeprecationBanner.root.visibility = View.VISIBLE - binding.googleDriveDeprecationBanner.learnMoreButton.setOnClickListener { + val usingGoogleDrive = ProjectKeys.PROTOCOL_GOOGLE_SHEETS == protocol + + if (usingGoogleDrive) { + binding.deprecationBanner.root.visibility = View.VISIBLE + binding.deprecationBanner.message.setText(string.google_drive_deprecation_message) + binding.deprecationBanner.learnMoreButton.setOnClickListener { val intent = Intent(this, WebViewActivity::class.java) intent.putExtra("url", "https://forum.getodk.org/t/40097") startActivity(intent) } } else { - binding.googleDriveDeprecationBanner.root.visibility = View.GONE + binding.deprecationBanner.root.visibility = View.GONE } } diff --git a/collect_app/src/main/res/layout/google_drive_deprecation_banner.xml b/collect_app/src/main/res/layout/deprecation_banner.xml similarity index 81% rename from collect_app/src/main/res/layout/google_drive_deprecation_banner.xml rename to collect_app/src/main/res/layout/deprecation_banner.xml index c8970c74775..28f876ad7c7 100644 --- a/collect_app/src/main/res/layout/google_drive_deprecation_banner.xml +++ b/collect_app/src/main/res/layout/deprecation_banner.xml @@ -5,18 +5,18 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="gone" - app:shapeAppearance="?shapeAppearanceLargeComponent" app:cardBackgroundColor="?colorSurfaceContainerHighest" + app:shapeAppearance="?shapeAppearanceLargeComponent" tools:visibility="visible"> + android:paddingHorizontal="@dimen/margin_standard" + android:paddingTop="@dimen/margin_standard"> + app:layout_constraintStart_toEndOf="@id/deprecation_icon" + app:layout_constraintTop_toTopOf="@id/deprecation_icon" + tools:text="@string/google_drive_deprecation_message" /> - \ No newline at end of file + diff --git a/collect_app/src/main/res/layout/form_entry_end.xml b/collect_app/src/main/res/layout/form_entry_end.xml index 12e261dcee9..abf42fb0c5b 100644 --- a/collect_app/src/main/res/layout/form_entry_end.xml +++ b/collect_app/src/main/res/layout/form_entry_end.xml @@ -55,7 +55,7 @@ the specific language governing permissions and limitations under the License. android:padding="@dimen/margin"> diff --git a/collect_app/src/main/res/layout/main_menu.xml b/collect_app/src/main/res/layout/main_menu.xml index 59b587be405..a78f7862c5e 100644 --- a/collect_app/src/main/res/layout/main_menu.xml +++ b/collect_app/src/main/res/layout/main_menu.xml @@ -35,8 +35,8 @@ @@ -49,7 +49,7 @@ app:layout_constraintWidth_max="@dimen/max_content_width" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/google_drive_deprecation_banner" /> + app:layout_constraintTop_toBottomOf="@id/deprecation_banner" /> - \ No newline at end of file + From 6468094b5de846a399617b5d17bfb514153b1923 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 3 Oct 2023 15:21:59 +0100 Subject: [PATCH 2/7] Show banner after editing finalized form --- .../SendFinalizedFormTest.kt | 26 ++++++++++++++++--- .../activities/FormEntryViewModelFactory.kt | 7 +++-- .../activities/FormFillingActivity.java | 4 ++- .../activities/FormHierarchyActivity.java | 4 ++- .../formentry/saving/FormSaveViewModel.java | 9 ++++++- .../android/mainmenu/MainMenuActivity.kt | 11 ++++++++ .../audit/FormSaveViewModelTest.java | 8 +++--- 7 files changed, 56 insertions(+), 13 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/SendFinalizedFormTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/SendFinalizedFormTest.kt index 34305547f32..ed973ee8e37 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/SendFinalizedFormTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/SendFinalizedFormTest.kt @@ -1,10 +1,15 @@ package org.odk.collect.android.feature.instancemanagement +import androidx.test.espresso.intent.Intents.intended +import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent +import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.CoreMatchers.allOf import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain import org.junit.runner.RunWith +import org.odk.collect.android.activities.WebViewActivity import org.odk.collect.android.support.CollectHelpers.addGDProject import org.odk.collect.android.support.TestDependencies import org.odk.collect.android.support.pages.FormEntryPage.QuestionAndAnswer @@ -17,6 +22,7 @@ import org.odk.collect.android.support.rules.CollectTestRule import org.odk.collect.android.support.rules.TestRuleChain.chain import org.odk.collect.androidtest.RecordedIntentsRule import org.odk.collect.projects.Project.New +import org.odk.collect.strings.R.string @RunWith(AndroidJUnit4::class) class SendFinalizedFormTest { @@ -43,7 +49,19 @@ class SendFinalizedFormTest { .answerQuestion("what is your age", "53") .swipeToEndScreen() .clickFinalize() - .checkIsSnackbarWithMessageDisplayed(org.odk.collect.strings.R.string.form_saved) + .checkIsSnackbarWithMessageDisplayed(string.form_saved) + + // Check deprecation banner is shown + .assertText(string.edit_finalized_form_warning) + .clickOnString(string.learn_more_button_text) + .also { + intended( + allOf( + hasComponent(WebViewActivity::class.java.name), + hasExtra("url", "https://forum.getodk.org/t/42007") + ) + ) + }.pressBack(MainMenuPage()) .clickSendFinalizedForm(1) .clickOnFormToEdit("One Question") @@ -64,7 +82,7 @@ class SendFinalizedFormTest { .answerQuestion("what is your age", "53") .swipeToEndScreen() .clickSaveAsDraft() - .checkIsSnackbarWithMessageDisplayed(org.odk.collect.strings.R.string.form_saved_as_draft) + .checkIsSnackbarWithMessageDisplayed(string.form_saved_as_draft) .clickEditSavedForm(1) .clickOnForm("One Question") @@ -124,7 +142,7 @@ class SendFinalizedFormTest { .clickViewSentForm(1) .clickOnForm("One Question") .assertText("123") - .assertText(org.odk.collect.strings.R.string.exit) + .assertText(string.exit) } @Test @@ -154,7 +172,7 @@ class SendFinalizedFormTest { .openProjectSettingsDialog() .clickSettings() .clickFormManagement() - .scrollToRecyclerViewItemAndClickText(org.odk.collect.strings.R.string.delete_after_send) + .scrollToRecyclerViewItemAndClickText(string.delete_after_send) .pressBack(ProjectSettingsPage()) .pressBack(MainMenuPage()) .copyForm("one-question.xml", testDependencies.server.hostName) diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/FormEntryViewModelFactory.kt b/collect_app/src/main/java/org/odk/collect/android/activities/FormEntryViewModelFactory.kt index d8405e0ba16..2964407dfa0 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/FormEntryViewModelFactory.kt +++ b/collect_app/src/main/java/org/odk/collect/android/activities/FormEntryViewModelFactory.kt @@ -23,6 +23,7 @@ import org.odk.collect.android.projects.CurrentProjectProvider import org.odk.collect.android.utilities.ApplicationConstants import org.odk.collect.android.utilities.InstancesRepositoryProvider import org.odk.collect.android.utilities.MediaUtils +import org.odk.collect.androidshared.data.AppState import org.odk.collect.async.Scheduler import org.odk.collect.audiorecorder.recording.AudioRecorder import org.odk.collect.location.LocationClient @@ -46,7 +47,8 @@ class FormEntryViewModelFactory( private val fusedLocationClient: LocationClient, private val permissionsProvider: PermissionsProvider, private val autoSendSettingsProvider: AutoSendSettingsProvider, - private val instancesRepositoryProvider: InstancesRepositoryProvider + private val instancesRepositoryProvider: InstancesRepositoryProvider, + private val appState: AppState ) : AbstractSavedStateViewModelFactory(owner, null) { override fun create( @@ -75,7 +77,8 @@ class FormEntryViewModelFactory( currentProjectProvider, formSessionRepository.get(sessionId), entitiesRepositoryProvider.get(projectId), - instancesRepositoryProvider.get(projectId) + instancesRepositoryProvider.get(projectId), + appState ) } diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/FormFillingActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/FormFillingActivity.java index cc7c0c043fe..864590b3c53 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/FormFillingActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/FormFillingActivity.java @@ -25,6 +25,7 @@ import static org.odk.collect.android.utilities.AnimationUtils.areAnimationsEnabled; import static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes; import static org.odk.collect.android.utilities.DialogUtils.getDialog; +import static org.odk.collect.androidshared.data.AppStateKt.getState; import static org.odk.collect.androidshared.ui.DialogFragmentUtils.showIfNotShowing; import static org.odk.collect.androidshared.ui.ToastUtils.showLongToast; import static org.odk.collect.androidshared.ui.ToastUtils.showShortToast; @@ -430,7 +431,8 @@ public void onCreate(Bundle savedInstanceState) { fusedLocatonClient, permissionsProvider, autoSendSettingsProvider, - instancesRepositoryProvider + instancesRepositoryProvider, + getState(getApplication()) ); this.getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder() diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/FormHierarchyActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/FormHierarchyActivity.java index 4e03dac95c5..85fb0a3ffc1 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/FormHierarchyActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/FormHierarchyActivity.java @@ -15,6 +15,7 @@ package org.odk.collect.android.activities; import static org.odk.collect.android.javarosawrapper.FormIndexUtils.getPreviousLevel; +import static org.odk.collect.androidshared.data.AppStateKt.getState; import android.content.DialogInterface; import android.os.Bundle; @@ -196,7 +197,8 @@ public void onCreate(Bundle savedInstanceState) { fusedLocationClient, permissionsProvider, autoSendSettingsProvider, - instancesRepositoryProvider + instancesRepositoryProvider, + getState(getApplication()) ); this.getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder() diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java b/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java index 9ae649be985..4a7ef86915b 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java @@ -29,6 +29,7 @@ import org.odk.collect.android.utilities.FileUtils; import org.odk.collect.android.utilities.MediaUtils; import org.odk.collect.android.utilities.QuestionMediaManager; +import org.odk.collect.androidshared.data.AppState; import org.odk.collect.androidshared.livedata.LiveDataUtils; import org.odk.collect.async.Scheduler; import org.odk.collect.audiorecorder.recording.AudioRecorder; @@ -84,8 +85,9 @@ public class FormSaveViewModel extends ViewModel implements MaterialProgressDial private final EntitiesRepository entitiesRepository; private final InstancesRepository instancesRepository; private Instance instance; + private AppState appState; - public FormSaveViewModel(SavedStateHandle stateHandle, Supplier clock, FormSaver formSaver, MediaUtils mediaUtils, Scheduler scheduler, AudioRecorder audioRecorder, CurrentProjectProvider currentProjectProvider, LiveData formSession, EntitiesRepository entitiesRepository, InstancesRepository instancesRepository) { + public FormSaveViewModel(SavedStateHandle stateHandle, Supplier clock, FormSaver formSaver, MediaUtils mediaUtils, Scheduler scheduler, AudioRecorder audioRecorder, CurrentProjectProvider currentProjectProvider, LiveData formSession, EntitiesRepository entitiesRepository, InstancesRepository instancesRepository, AppState appState) { this.stateHandle = stateHandle; this.clock = clock; this.formSaver = formSaver; @@ -95,6 +97,7 @@ public FormSaveViewModel(SavedStateHandle stateHandle, Supplier clock, For this.currentProjectProvider = currentProjectProvider; this.entitiesRepository = entitiesRepository; this.instancesRepository = instancesRepository; + this.appState = appState; if (stateHandle.get(ORIGINAL_FILES) != null) { originalFiles = stateHandle.get(ORIGINAL_FILES); @@ -110,6 +113,10 @@ public FormSaveViewModel(SavedStateHandle stateHandle, Supplier clock, For } public void saveForm(Uri instanceContentURI, boolean shouldFinalize, String updatedSaveName, boolean viewExiting) { + if (instance != null && instance.getStatus().equals(Instance.STATUS_COMPLETE)) { + appState.set("editedFinalizedForm", true); + } + if (isSaving() || formController == null) { return; } diff --git a/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt b/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt index 40ef49ed0a2..7a79c799fae 100644 --- a/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt +++ b/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt @@ -32,6 +32,7 @@ import org.odk.collect.android.projects.ProjectSettingsDialog import org.odk.collect.android.utilities.ApplicationConstants import org.odk.collect.android.utilities.PlayServicesChecker import org.odk.collect.android.utilities.ThemeUtils +import org.odk.collect.androidshared.data.getState import org.odk.collect.androidshared.ui.DialogFragmentUtils.showIfNotShowing import org.odk.collect.androidshared.ui.FragmentFactoryBuilder import org.odk.collect.androidshared.ui.SnackbarUtils @@ -292,6 +293,8 @@ class MainMenuActivity : LocalizedActivity() { val unprotectedSettings = settingsProvider.getUnprotectedSettings() val protocol = unprotectedSettings.getString(ProjectKeys.KEY_PROTOCOL) val usingGoogleDrive = ProjectKeys.PROTOCOL_GOOGLE_SHEETS == protocol + val editedFinalizedForm = + application.getState().get("editedFinalizedForm") ?: false if (usingGoogleDrive) { binding.deprecationBanner.root.visibility = View.VISIBLE @@ -301,6 +304,14 @@ class MainMenuActivity : LocalizedActivity() { intent.putExtra("url", "https://forum.getodk.org/t/40097") startActivity(intent) } + } else if (editedFinalizedForm) { + binding.deprecationBanner.root.visibility = View.VISIBLE + binding.deprecationBanner.message.setText(string.edit_finalized_form_warning) + binding.deprecationBanner.learnMoreButton.setOnClickListener { + val intent = Intent(this, WebViewActivity::class.java) + intent.putExtra("url", "https://forum.getodk.org/t/42007") + startActivity(intent) + } } else { binding.deprecationBanner.root.visibility = View.GONE } diff --git a/collect_app/src/test/java/org/odk/collect/android/formentry/audit/FormSaveViewModelTest.java b/collect_app/src/test/java/org/odk/collect/android/formentry/audit/FormSaveViewModelTest.java index 190d4f35788..594b32b0f52 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formentry/audit/FormSaveViewModelTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/formentry/audit/FormSaveViewModelTest.java @@ -105,7 +105,7 @@ public void setup() { when(currentProjectProvider.getCurrentProject()).thenReturn(Project.Companion.getDEMO_PROJECT()); formSession = new MutableLiveData<>(new FormSession(formController, form)); - viewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, audioRecorder, currentProjectProvider, formSession, entitiesRepository, instancesRepository); + viewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, audioRecorder, currentProjectProvider, formSession, entitiesRepository, instancesRepository, appState); CollectHelpers.createDemoProject(); // Needed to deal with `new StoragePathProvider()` calls in `FormSaveViewModel` } @@ -477,7 +477,7 @@ public void deleteAnswerFile_whenAnswerFileHasAlreadyBeenDeleted_actuallyDeletes public void deleteAnswerFile_whenAnswerFileHasAlreadyBeenDeleted_onRecreatingViewModel_actuallyDeletesNewFile() { viewModel.deleteAnswerFile("index", "blah1"); - FormSaveViewModel restoredViewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), currentProjectProvider, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository); + FormSaveViewModel restoredViewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), currentProjectProvider, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository, appState); restoredViewModel.deleteAnswerFile("index", "blah2"); verify(mediaUtils).deleteMediaFile("blah2"); @@ -499,7 +499,7 @@ public void replaceAnswerFile_whenAnswerFileHasAlreadyBeenReplaced_deletesPrevio public void replaceAnswerFile_whenAnswerFileHasAlreadyBeenReplaced_afterRecreatingViewModel_deletesPreviousReplacement() { viewModel.replaceAnswerFile("index", "blah1"); - FormSaveViewModel restoredViewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), currentProjectProvider, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository); + FormSaveViewModel restoredViewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), currentProjectProvider, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository, appState); restoredViewModel.replaceAnswerFile("index", "blah2"); verify(mediaUtils).deleteMediaFile("blah1"); @@ -573,7 +573,7 @@ public void isSavingFileAnswerFile_isTrueWhenWhileIsSaving() throws Exception { @Test public void ignoreChanges_whenFormControllerNotSet_doesNothing() { - FormSaveViewModel viewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), currentProjectProvider, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository); + FormSaveViewModel viewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), currentProjectProvider, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository, appState); viewModel.ignoreChanges(); // Checks nothing explodes } From da66779587e322950d7f5c119df49e2874ee9eb5 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 3 Oct 2023 16:18:50 +0100 Subject: [PATCH 3/7] Extract constant for key --- .../collect/android/formentry/saving/FormSaveViewModel.java | 3 ++- .../org/odk/collect/android/mainmenu/MainMenuActivity.kt | 3 ++- .../odk/collect/android/utilities/ApplicationConstants.java | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java b/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java index 4a7ef86915b..4cbecea38df 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java @@ -2,6 +2,7 @@ import static org.odk.collect.android.tasks.SaveFormToDisk.SAVED; import static org.odk.collect.android.tasks.SaveFormToDisk.SAVED_AND_EXIT; +import static org.odk.collect.android.utilities.ApplicationConstants.AppStateKeys.EDITED_FINALIZED_FORM; import static org.odk.collect.shared.strings.StringUtils.isBlank; import android.net.Uri; @@ -114,7 +115,7 @@ public FormSaveViewModel(SavedStateHandle stateHandle, Supplier clock, For public void saveForm(Uri instanceContentURI, boolean shouldFinalize, String updatedSaveName, boolean viewExiting) { if (instance != null && instance.getStatus().equals(Instance.STATUS_COMPLETE)) { - appState.set("editedFinalizedForm", true); + appState.set(EDITED_FINALIZED_FORM, true); } if (isSaving() || formController == null) { diff --git a/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt b/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt index 7a79c799fae..147a32c574f 100644 --- a/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt +++ b/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt @@ -30,6 +30,7 @@ import org.odk.collect.android.injection.DaggerUtils import org.odk.collect.android.projects.ProjectIconView import org.odk.collect.android.projects.ProjectSettingsDialog import org.odk.collect.android.utilities.ApplicationConstants +import org.odk.collect.android.utilities.ApplicationConstants.AppStateKeys.EDITED_FINALIZED_FORM import org.odk.collect.android.utilities.PlayServicesChecker import org.odk.collect.android.utilities.ThemeUtils import org.odk.collect.androidshared.data.getState @@ -294,7 +295,7 @@ class MainMenuActivity : LocalizedActivity() { val protocol = unprotectedSettings.getString(ProjectKeys.KEY_PROTOCOL) val usingGoogleDrive = ProjectKeys.PROTOCOL_GOOGLE_SHEETS == protocol val editedFinalizedForm = - application.getState().get("editedFinalizedForm") ?: false + application.getState().get(EDITED_FINALIZED_FORM) ?: false if (usingGoogleDrive) { binding.deprecationBanner.root.visibility = View.VISIBLE diff --git a/collect_app/src/main/java/org/odk/collect/android/utilities/ApplicationConstants.java b/collect_app/src/main/java/org/odk/collect/android/utilities/ApplicationConstants.java index 9b39589fd18..53205d40e13 100644 --- a/collect_app/src/main/java/org/odk/collect/android/utilities/ApplicationConstants.java +++ b/collect_app/src/main/java/org/odk/collect/android/utilities/ApplicationConstants.java @@ -88,4 +88,9 @@ public abstract static class Namespaces { public static final String XML_OPENROSA_NAMESPACE = "http://openrosa.org/xforms"; public static final String XML_OPENDATAKIT_NAMESPACE = "http://www.opendatakit.org/xforms"; } + + public abstract static class AppStateKeys { + + public static final String EDITED_FINALIZED_FORM = "editedFinalizedForm"; + } } From bcd363017d779af70f90f18595456a9054d1a595 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 3 Oct 2023 17:48:46 +0100 Subject: [PATCH 4/7] Move analytics event --- .../odk/collect/android/activities/InstanceChooserList.java | 4 +--- .../collect/android/formentry/saving/FormSaveViewModel.java | 5 ++++- 2 files changed, 5 insertions(+), 4 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 d0f5b278f48..2688964074f 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 @@ -63,8 +63,8 @@ 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.string; import org.odk.collect.strings.R.plurals; +import org.odk.collect.strings.R.string; import java.util.Arrays; @@ -264,8 +264,6 @@ private void logFormEdit(Cursor cursor) { if (status.equals(Instance.STATUS_INCOMPLETE)) { AnalyticsUtils.logFormEvent(AnalyticsEvents.EDIT_NON_FINALIZED_FORM, formId, formTitle); - } else if (status.equals(Instance.STATUS_COMPLETE)) { - AnalyticsUtils.logFormEvent(AnalyticsEvents.EDIT_FINALIZED_FORM, formId, formTitle); } } diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java b/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java index 4cbecea38df..0452f871606 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java @@ -17,6 +17,8 @@ import org.apache.commons.io.IOUtils; import org.javarosa.form.api.FormEntryController; +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.dao.helpers.InstancesDaoHelper; import org.odk.collect.android.externaldata.ExternalDataManager; @@ -86,7 +88,7 @@ public class FormSaveViewModel extends ViewModel implements MaterialProgressDial private final EntitiesRepository entitiesRepository; private final InstancesRepository instancesRepository; private Instance instance; - private AppState appState; + private final AppState appState; public FormSaveViewModel(SavedStateHandle stateHandle, Supplier clock, FormSaver formSaver, MediaUtils mediaUtils, Scheduler scheduler, AudioRecorder audioRecorder, CurrentProjectProvider currentProjectProvider, LiveData formSession, EntitiesRepository entitiesRepository, InstancesRepository instancesRepository, AppState appState) { this.stateHandle = stateHandle; @@ -116,6 +118,7 @@ public FormSaveViewModel(SavedStateHandle stateHandle, Supplier clock, For public void saveForm(Uri instanceContentURI, boolean shouldFinalize, String updatedSaveName, boolean viewExiting) { if (instance != null && instance.getStatus().equals(Instance.STATUS_COMPLETE)) { appState.set(EDITED_FINALIZED_FORM, true); + Analytics.log(AnalyticsEvents.EDIT_FINALIZED_FORM, "form"); } if (isSaving() || formController == null) { From e8cdc895d4760a31652bd32a1e5584308404bb65 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 3 Oct 2023 18:28:55 +0100 Subject: [PATCH 5/7] Update test --- .../audit/FormSaveViewModelTest.java | 21 ++++++++++--------- .../odk/collect/formstest/InstanceFixtures.kt | 2 ++ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/collect_app/src/test/java/org/odk/collect/android/formentry/audit/FormSaveViewModelTest.java b/collect_app/src/test/java/org/odk/collect/android/formentry/audit/FormSaveViewModelTest.java index 594b32b0f52..88ced7ff79b 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formentry/audit/FormSaveViewModelTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/formentry/audit/FormSaveViewModelTest.java @@ -46,12 +46,14 @@ import org.odk.collect.android.tasks.SaveFormToDisk; import org.odk.collect.android.tasks.SaveToDiskResult; import org.odk.collect.android.utilities.MediaUtils; +import org.odk.collect.androidshared.data.AppState; import org.odk.collect.audiorecorder.recording.AudioRecorder; import org.odk.collect.entities.EntitiesRepository; import org.odk.collect.forms.Form; import org.odk.collect.forms.instances.Instance; import org.odk.collect.forms.instances.InstancesRepository; import org.odk.collect.formstest.InMemInstancesRepository; +import org.odk.collect.formstest.InstanceFixtures; import org.odk.collect.projects.Project; import org.odk.collect.shared.TempFiles; import org.odk.collect.testshared.FakeScheduler; @@ -105,7 +107,7 @@ public void setup() { when(currentProjectProvider.getCurrentProject()).thenReturn(Project.Companion.getDEMO_PROJECT()); formSession = new MutableLiveData<>(new FormSession(formController, form)); - viewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, audioRecorder, currentProjectProvider, formSession, entitiesRepository, instancesRepository, appState); + viewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, audioRecorder, currentProjectProvider, formSession, entitiesRepository, instancesRepository, new AppState()); CollectHelpers.createDemoProject(); // Needed to deal with `new StoragePathProvider()` calls in `FormSaveViewModel` } @@ -477,7 +479,7 @@ public void deleteAnswerFile_whenAnswerFileHasAlreadyBeenDeleted_actuallyDeletes public void deleteAnswerFile_whenAnswerFileHasAlreadyBeenDeleted_onRecreatingViewModel_actuallyDeletesNewFile() { viewModel.deleteAnswerFile("index", "blah1"); - FormSaveViewModel restoredViewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), currentProjectProvider, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository, appState); + FormSaveViewModel restoredViewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), currentProjectProvider, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository, new AppState()); restoredViewModel.deleteAnswerFile("index", "blah2"); verify(mediaUtils).deleteMediaFile("blah2"); @@ -499,7 +501,7 @@ public void replaceAnswerFile_whenAnswerFileHasAlreadyBeenReplaced_deletesPrevio public void replaceAnswerFile_whenAnswerFileHasAlreadyBeenReplaced_afterRecreatingViewModel_deletesPreviousReplacement() { viewModel.replaceAnswerFile("index", "blah1"); - FormSaveViewModel restoredViewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), currentProjectProvider, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository, appState); + FormSaveViewModel restoredViewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), currentProjectProvider, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository, new AppState()); restoredViewModel.replaceAnswerFile("index", "blah2"); verify(mediaUtils).deleteMediaFile("blah1"); @@ -573,7 +575,7 @@ public void isSavingFileAnswerFile_isTrueWhenWhileIsSaving() throws Exception { @Test public void ignoreChanges_whenFormControllerNotSet_doesNothing() { - FormSaveViewModel viewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), currentProjectProvider, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository, appState); + FormSaveViewModel viewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), currentProjectProvider, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository, new AppState()); viewModel.ignoreChanges(); // Checks nothing explodes } @@ -625,9 +627,10 @@ public static class FakeFormSaver implements FormSaver { public int numberOfTimesCalled; - public final Instance instance = new Instance.Builder() - .lastStatusChangeDate(123L) - .build(); + public final Instance instance = InstanceFixtures.instance( + Instance.STATUS_INCOMPLETE, + 123L + ); @Override public SaveToDiskResult save(Uri instanceContentURI, FormController formController, MediaUtils mediaUtils, boolean shouldFinalize, @@ -636,9 +639,7 @@ public SaveToDiskResult save(Uri instanceContentURI, FormController formControll numberOfTimesCalled++; if (saveToDiskResult.getSaveResult() == SaveFormToDisk.SAVED) { - saveToDiskResult.setInstance(new Instance.Builder() - .lastStatusChangeDate(123L) - .build()); + saveToDiskResult.setInstance(instance); } return saveToDiskResult; diff --git a/formstest/src/main/java/org/odk/collect/formstest/InstanceFixtures.kt b/formstest/src/main/java/org/odk/collect/formstest/InstanceFixtures.kt index c712d242a68..c65a613a704 100644 --- a/formstest/src/main/java/org/odk/collect/formstest/InstanceFixtures.kt +++ b/formstest/src/main/java/org/odk/collect/formstest/InstanceFixtures.kt @@ -5,6 +5,8 @@ import org.odk.collect.shared.TempFiles object InstanceFixtures { + @JvmStatic + @JvmOverloads fun instance(status: String = Instance.STATUS_INCOMPLETE, lastStatusChangeDate: Long = 0): Instance { val instancesDir = TempFiles.createTempDir() return InstanceUtils.buildInstance("formId", "version", instancesDir.absolutePath) From 7a0f0bda1f9c74ee58b6ea16a447508b6602137d Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 3 Oct 2023 19:18:10 +0100 Subject: [PATCH 6/7] Update copy --- .../feature/instancemanagement/SendFinalizedFormTest.kt | 2 +- .../java/org/odk/collect/android/mainmenu/MainMenuActivity.kt | 2 +- strings/src/main/res/values/strings.xml | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/SendFinalizedFormTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/SendFinalizedFormTest.kt index ed973ee8e37..7d6d89f216c 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/SendFinalizedFormTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/SendFinalizedFormTest.kt @@ -52,7 +52,7 @@ class SendFinalizedFormTest { .checkIsSnackbarWithMessageDisplayed(string.form_saved) // Check deprecation banner is shown - .assertText(string.edit_finalized_form_warning) + .assertText(string.edit_finalized_form_deprecation_message) .clickOnString(string.learn_more_button_text) .also { intended( diff --git a/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt b/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt index 147a32c574f..a2a06902341 100644 --- a/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt +++ b/collect_app/src/main/java/org/odk/collect/android/mainmenu/MainMenuActivity.kt @@ -307,7 +307,7 @@ class MainMenuActivity : LocalizedActivity() { } } else if (editedFinalizedForm) { binding.deprecationBanner.root.visibility = View.VISIBLE - binding.deprecationBanner.message.setText(string.edit_finalized_form_warning) + binding.deprecationBanner.message.setText(string.edit_finalized_form_deprecation_message) binding.deprecationBanner.learnMoreButton.setOnClickListener { val intent = Intent(this, WebViewActivity::class.java) intent.putExtra("url", "https://forum.getodk.org/t/42007") diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index 09339886f24..5961f40eb0f 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -1217,6 +1217,9 @@ In later releases, you will not be able to edit finalized forms. Save forms as draft to edit them later.\n\nYou can check for errors in a draft form by tapping the three dots (⋮) and then Check for errors. + + New update: Finalized forms will no longer be editable. + Finalize all forms From 2665ddc21b4e141e41358151c63650bd61b6e273 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Tue, 3 Oct 2023 20:52:41 +0100 Subject: [PATCH 7/7] Make both warnings match --- strings/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/strings/src/main/res/values/strings.xml b/strings/src/main/res/values/strings.xml index 5961f40eb0f..efe5b0ad6b4 100644 --- a/strings/src/main/res/values/strings.xml +++ b/strings/src/main/res/values/strings.xml @@ -1215,10 +1215,10 @@ View Close snackbar - In later releases, you will not be able to edit finalized forms. Save forms as draft to edit them later.\n\nYou can check for errors in a draft form by tapping the three dots (⋮) and then Check for errors. + In later versions, finalized forms will no longer be editable. Save forms as draft to edit them later.\n\nYou can check for errors in a draft form by tapping the three dots (⋮) and then Check for errors. - New update: Finalized forms will no longer be editable. + In later versions, finalized forms will no longer be editable. Finalize all forms