From 88dd6b41ca5a15862b2979b1ef5f777796ae629f Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 6 Aug 2024 21:10:01 +0530 Subject: [PATCH 01/63] Safeguard sendBroadcast calls to not crash --- .../org/commcare/CommCareNoficationManager.java | 4 +++- .../commcare/services/CommCareSessionService.java | 7 +++++-- .../commcare/sync/ExternalDataUpdateHelper.java | 15 ++++++++++++--- .../sync/FirebaseMessagingDataSyncer.java | 3 +-- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/app/src/org/commcare/CommCareNoficationManager.java b/app/src/org/commcare/CommCareNoficationManager.java index 6180f9e0e2..8020299d6b 100644 --- a/app/src/org/commcare/CommCareNoficationManager.java +++ b/app/src/org/commcare/CommCareNoficationManager.java @@ -27,6 +27,8 @@ import static android.content.Context.NOTIFICATION_SERVICE; +import static org.commcare.sync.ExternalDataUpdateHelper.sendBroadcastFailSafe; + /** * Handles displaying and clearing pinned notifications for CommCare */ @@ -112,7 +114,7 @@ public void clearNotifications(String category) { public ArrayList purgeNotifications() { synchronized (pendingMessages) { - context.sendBroadcast(new Intent(ACTION_PURGE_NOTIFICATIONS)); + sendBroadcastFailSafe(context, new Intent(ACTION_PURGE_NOTIFICATIONS), null); ArrayList cloned = (ArrayList)pendingMessages.clone(); clearNotifications(null); return cloned; diff --git a/app/src/org/commcare/services/CommCareSessionService.java b/app/src/org/commcare/services/CommCareSessionService.java index 1ed06a1e51..e689dc050a 100644 --- a/app/src/org/commcare/services/CommCareSessionService.java +++ b/app/src/org/commcare/services/CommCareSessionService.java @@ -1,5 +1,7 @@ package org.commcare.services; +import static org.commcare.sync.ExternalDataUpdateHelper.sendBroadcastFailSafe; + import android.annotation.SuppressLint; import android.app.Notification; import android.app.NotificationManager; @@ -29,6 +31,7 @@ import org.commcare.models.database.user.UserSandboxUtils; import org.commcare.models.encryption.CipherPool; import org.commcare.preferences.HiddenPreferences; +import org.commcare.sync.ExternalDataUpdateHelper; import org.commcare.sync.FormSubmissionHelper; import org.commcare.tasks.DataSubmissionListener; import org.commcare.util.LogTypes; @@ -309,7 +312,7 @@ public void startSession(User user, UserKeyRecord record) { //Let anyone who is listening know! Intent i = new Intent("org.commcare.dalvik.api.action.session.login"); - this.sendBroadcast(i); + sendBroadcastFailSafe(this, i, null); } this.user = user; @@ -469,7 +472,7 @@ public void closeServiceResources() { // Let anyone who is listening know! Intent i = new Intent("org.commcare.dalvik.api.action.session.logout"); - this.sendBroadcast(i); + sendBroadcastFailSafe(this, i, null); Logger.log(LogTypes.TYPE_MAINTENANCE, "Logging out service login"); diff --git a/app/src/org/commcare/sync/ExternalDataUpdateHelper.java b/app/src/org/commcare/sync/ExternalDataUpdateHelper.java index c7c1f2bc3d..3d8c97f601 100644 --- a/app/src/org/commcare/sync/ExternalDataUpdateHelper.java +++ b/app/src/org/commcare/sync/ExternalDataUpdateHelper.java @@ -7,6 +7,7 @@ import android.content.Intent; import org.commcare.CommCareApplication; +import org.javarosa.core.services.Logger; import java.util.ArrayList; @@ -32,12 +33,12 @@ public static void broadcastDataUpdate(Context c, if (CommCareApplication.instance().getSession().isActive()) { i.putExtra("cc-logged-in-user-id", CommCareApplication.instance().getCurrentUserId()); } - c.sendBroadcast(i, COMMCARE_CASE_READ_PERMISSION); + sendBroadcastFailSafe(c, i, COMMCARE_CASE_READ_PERMISSION); // send explicit broadcast to CommCare Reminders App i.setComponent(new ComponentName("org.commcare.dalvik.reminders", "org.commcare.dalvik.reminders.CommCareReceiver")); - c.sendBroadcast(i); + sendBroadcastFailSafe(c, i, null); // Broadcast to CommCare, there is the option to handle the permission required by the // broadcast above @@ -48,6 +49,14 @@ public static void broadcastDataUpdate(Context c, private static void broadcastDataUpdateToCommCare(Context c){ Intent i = new Intent(COMMCARE_DATA_UPDATE_ACTION); i.setPackage(c.getPackageName()); - c.sendBroadcast(i); + sendBroadcastFailSafe(c, i, null); + } + + public static void sendBroadcastFailSafe(Context context, Intent intent, @Nullable String receiverPermission) { + try { + context.sendBroadcast(intent, receiverPermission); + } catch (Exception e) { + Logger.exception("Exception when sending a broadcast with intent " + intent, e); + } } } diff --git a/app/src/org/commcare/sync/FirebaseMessagingDataSyncer.java b/app/src/org/commcare/sync/FirebaseMessagingDataSyncer.java index db7c3ef34c..03392b3bd5 100644 --- a/app/src/org/commcare/sync/FirebaseMessagingDataSyncer.java +++ b/app/src/org/commcare/sync/FirebaseMessagingDataSyncer.java @@ -194,8 +194,7 @@ private void informUserAboutPendingSync(FCMMessageData fcmMessageData) { Bundle b = new Bundle(); b.putSerializable(FCM_MESSAGE_DATA, FirebaseMessagingUtil.serializeFCMMessageData(fcmMessageData)); intent.putExtra(FCM_MESSAGE_DATA_KEY, b); - - context.sendBroadcast(intent); + ExternalDataUpdateHelper.sendBroadcastFailSafe(context, intent, null); } @Override From 67df8556a7e2e1ae77a2dfa9218a0e9e13089708 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 1 Aug 2024 16:47:51 +0200 Subject: [PATCH 02/63] Add android lifecycle process dependency --- app/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/app/build.gradle b/app/build.gradle index 36ad2d888a..afd2d4d746 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -105,6 +105,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version" // Markdown implementation "io.noties.markwon:core:$markwon_version" From 647d8bf9f65468a8d9abea55cf140c72cf503248 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 1 Aug 2024 17:11:22 +0200 Subject: [PATCH 03/63] Make CommCareApplication a lifecycle-aware component --- app/src/org/commcare/CommCareApplication.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/src/org/commcare/CommCareApplication.java b/app/src/org/commcare/CommCareApplication.java index 8feecb8960..7d81a38909 100644 --- a/app/src/org/commcare/CommCareApplication.java +++ b/app/src/org/commcare/CommCareApplication.java @@ -17,6 +17,10 @@ import android.util.Log; import androidx.annotation.NonNull; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleEventObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.ProcessLifecycleOwner; import androidx.multidex.MultiDexApplication; import androidx.preference.PreferenceManager; import androidx.work.BackoffPolicy; @@ -143,7 +147,7 @@ import okhttp3.MultipartBody; import okhttp3.RequestBody; -public class CommCareApplication extends MultiDexApplication { +public class CommCareApplication extends MultiDexApplication implements LifecycleEventObserver { private static final String TAG = CommCareApplication.class.getSimpleName(); @@ -1224,4 +1228,17 @@ private void setRxJavaGlobalHandler() { Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), throwable); }); } + + @Override + public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) { + Logger.log("commcare-state-changed", "Lifecycle.Event : " + event.name()); + switch (event.name()) { + case "ON_START": + case "ON_RESUME": + case "ON_STOP": + break; + case "ON_PAUSE": + break; + } + } } From 76fc7e407364636b5030405251e056c9fd9ca108 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 1 Aug 2024 17:12:10 +0200 Subject: [PATCH 04/63] Attach lifecycle observer --- app/src/org/commcare/CommCareApplication.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/org/commcare/CommCareApplication.java b/app/src/org/commcare/CommCareApplication.java index 7d81a38909..0840612148 100644 --- a/app/src/org/commcare/CommCareApplication.java +++ b/app/src/org/commcare/CommCareApplication.java @@ -260,6 +260,8 @@ public void onCreate() { customiseOkHttp(); setRxJavaGlobalHandler(); + + ProcessLifecycleOwner.get().getLifecycle().addObserver(this); } protected void loadSqliteLibs() { From e93dd1ef60c4c3088c9f5bb3119b3bfde31e94ca Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 5 Aug 2024 12:57:12 +0530 Subject: [PATCH 05/63] use a flag exit to exit the form --- .../org/commcare/activities/FormEntryActivity.java | 14 ++++++++------ .../org/commcare/interfaces/FormSaveCallback.java | 2 +- .../org/commcare/interfaces/FormSavedListener.java | 2 +- .../commcare/services/CommCareSessionService.java | 4 ++-- app/src/org/commcare/tasks/SaveToDiskTask.java | 6 ++---- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index f127f1b6a2..49bc93fbea 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -245,7 +245,7 @@ public void onCreateSessionSafe(Bundle savedInstanceState) { } @Override - public void formSaveCallback(Runnable listener) { + public void formSaveCallback(boolean exit, Runnable listener) { // note that we have started saving the form customFormSaveCallback = listener; @@ -253,7 +253,7 @@ public void formSaveCallback(Runnable listener) { CommCareApplication.instance().getCurrentSessionWrapper().setCurrentStateAsInterrupted(); // Start saving form; will trigger expireUserSession() on completion - saveIncompleteFormToDisk(); + saveIncompleteFormToDisk(exit); } private void handleLocationErrorAction() { @@ -766,8 +766,8 @@ private void saveCompletedFormToDisk(String updatedSaveName) { saveDataToDisk(FormEntryConstants.EXIT, true, updatedSaveName, false); } - private void saveIncompleteFormToDisk() { - saveDataToDisk(FormEntryConstants.EXIT, false, null, true); + private void saveIncompleteFormToDisk(boolean exit) { + saveDataToDisk(exit, false, null, true); } /** @@ -1208,7 +1208,7 @@ private void registerSessionFormSaveCallback() { * continue closing the session/logging out. */ @Override - public void savingComplete(SaveToDiskTask.SaveStatus saveStatus, String errorMessage) { + public void savingComplete(SaveToDiskTask.SaveStatus saveStatus, String errorMessage, boolean exit) { // Did we just save a form because the key session // (CommCareSessionService) is ending? if (customFormSaveCallback != null) { @@ -1216,7 +1216,9 @@ public void savingComplete(SaveToDiskTask.SaveStatus saveStatus, String errorMes customFormSaveCallback = null; toCall.run(); - returnAsInterrupted(); + if (exit) { + returnAsInterrupted(); + } } else if (saveStatus != null) { String toastMessage = ""; switch (saveStatus) { diff --git a/app/src/org/commcare/interfaces/FormSaveCallback.java b/app/src/org/commcare/interfaces/FormSaveCallback.java index f404af1692..91c9047a18 100644 --- a/app/src/org/commcare/interfaces/FormSaveCallback.java +++ b/app/src/org/commcare/interfaces/FormSaveCallback.java @@ -8,5 +8,5 @@ public interface FormSaveCallback { * Starts a task to save the current form being edited. Will be expected to call the provided * listener when saving is complete and the current session state is no longer volatile */ - void formSaveCallback(Runnable callback); + void formSaveCallback(boolean exit, Runnable callback); } diff --git a/app/src/org/commcare/interfaces/FormSavedListener.java b/app/src/org/commcare/interfaces/FormSavedListener.java index d5ee318387..095f365c7c 100644 --- a/app/src/org/commcare/interfaces/FormSavedListener.java +++ b/app/src/org/commcare/interfaces/FormSavedListener.java @@ -10,5 +10,5 @@ public interface FormSavedListener { /** * Callback to be run after a form has been saved. */ - void savingComplete(SaveToDiskTask.SaveStatus formSaveStatus, String errorMessage); + void savingComplete(SaveToDiskTask.SaveStatus formSaveStatus, String errorMessage, boolean exit); } diff --git a/app/src/org/commcare/services/CommCareSessionService.java b/app/src/org/commcare/services/CommCareSessionService.java index e689dc050a..f87ec91213 100644 --- a/app/src/org/commcare/services/CommCareSessionService.java +++ b/app/src/org/commcare/services/CommCareSessionService.java @@ -409,7 +409,7 @@ private void saveFormAndCloseSession() { // save form progress, if any synchronized (lock) { if (formSaver != null) { - formSaver.formSaveCallback(() -> { + formSaver.formSaveCallback(true, () -> { CommCareApplication.instance().expireUserSession(); }); } else { @@ -428,7 +428,7 @@ public void proceedWithSavedSessionIfNeeded(Runnable callback) { if (formSaver != null) { Toast.makeText(CommCareApplication.instance(), "Suspending existing form entry session...", Toast.LENGTH_LONG).show(); - formSaver.formSaveCallback(callback); + formSaver.formSaveCallback(true, callback); formSaver = null; return; } diff --git a/app/src/org/commcare/tasks/SaveToDiskTask.java b/app/src/org/commcare/tasks/SaveToDiskTask.java index 64091c4a58..3f99d0f0e0 100644 --- a/app/src/org/commcare/tasks/SaveToDiskTask.java +++ b/app/src/org/commcare/tasks/SaveToDiskTask.java @@ -1,7 +1,5 @@ package org.commcare.tasks; -import android.util.Log; - import org.commcare.CommCareApplication; import org.commcare.activities.FormEntryActivity; import org.commcare.activities.components.ImageCaptureProcessing; @@ -272,9 +270,9 @@ protected void onPostExecute(ResultAndError result) { synchronized (this) { if (mSavedListener != null) { if (result == null) { - mSavedListener.savingComplete(SaveStatus.SAVE_ERROR, "Unknown Error"); + mSavedListener.savingComplete(SaveStatus.SAVE_ERROR, "Unknown Error", exitAfterSave); } else { - mSavedListener.savingComplete(result.data, result.errorMessage); + mSavedListener.savingComplete(result.data, result.errorMessage, exitAfterSave); } } } From cea8cbfccd985cf7d21568a15ee68b0600b1944d Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 5 Aug 2024 16:46:26 +0530 Subject: [PATCH 06/63] Save form as incomplete when going to background --- app/src/org/commcare/activities/FormEntryActivity.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 49bc93fbea..e7df3f13e1 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -242,13 +242,17 @@ public void onCreateSessionSafe(Bundle savedInstanceState) { uiController.refreshView(); } TextToSpeechConverter.INSTANCE.setListener(mTTSCallback); + HiddenPreferences.clearInterruptedSSD(); } @Override public void formSaveCallback(boolean exit, Runnable listener) { // note that we have started saving the form customFormSaveCallback = listener; + interruptAndSaveForm(exit); + } + private void interruptAndSaveForm(boolean exit) { // Set flag that will allow us to restore this form when we log back in CommCareApplication.instance().getCurrentSessionWrapper().setCurrentStateAsInterrupted(); @@ -912,6 +916,12 @@ protected void onPause() { unregisterReceiver(pendingSyncAlertBroadcastReceiver); } + @Override + protected void onStop() { + super.onStop(); + interruptAndSaveForm(false); + } + private void saveInlineVideoState() { if (uiController.questionsView != null) { for (int i = 0; i < uiController.questionsView.getWidgets().size(); i++) { From ec96345df5da1b8e4e554439836e6d119f9cf398 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 6 Aug 2024 17:05:47 +0530 Subject: [PATCH 07/63] Safeguard save on pause with checks --- .../org/commcare/activities/FormEntryActivity.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index e7df3f13e1..28b5bfff04 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -148,6 +148,7 @@ public class FormEntryActivity extends SaveSessionCommCareActivity Date: Thu, 8 Aug 2024 13:34:07 +0530 Subject: [PATCH 08/63] Put the auto-save behind a custom dev preference --- app/res/xml/preferences_developer.xml | 7 +++++++ app/src/org/commcare/activities/FormEntryActivity.java | 7 ++++--- app/src/org/commcare/preferences/DeveloperPreferences.java | 7 +++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/res/xml/preferences_developer.xml b/app/res/xml/preferences_developer.xml index 8c99523e95..b9a1603c92 100644 --- a/app/res/xml/preferences_developer.xml +++ b/app/res/xml/preferences_developer.xml @@ -173,4 +173,11 @@ android:entryValues="@array/pref_enabled_vals" android:key="cc-enable-certificate-transparency" android:title="Certificate Transparency"/> + diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 28b5bfff04..742cd8507b 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -57,6 +57,7 @@ import org.commcare.models.AndroidSessionWrapper; import org.commcare.models.FormRecordProcessor; import org.commcare.models.database.SqlStorage; +import org.commcare.preferences.DeveloperPreferences; import org.commcare.preferences.HiddenPreferences; import org.commcare.services.FCMMessageData; import org.commcare.services.PendingSyncAlertBroadcastReceiver; @@ -926,9 +927,9 @@ protected void onStop() { } private boolean shouldSaveFormOnStop() { - // if the form has loaded and another widget workflow is not in progress and we ourselves have not - // called exit as part of user workflow - return formHasLoaded() && !triggeredExit; + // if feature enabled and the form has loaded and another widget workflow is not in progress and we + // ourselves have not called exit as part of user workflow + return DeveloperPreferences.isAutoSaveFormOnPause() && formHasLoaded() && !triggeredExit; } private void saveInlineVideoState() { diff --git a/app/src/org/commcare/preferences/DeveloperPreferences.java b/app/src/org/commcare/preferences/DeveloperPreferences.java index 75e4fc4ddb..080cc48ead 100644 --- a/app/src/org/commcare/preferences/DeveloperPreferences.java +++ b/app/src/org/commcare/preferences/DeveloperPreferences.java @@ -77,6 +77,8 @@ public class DeveloperPreferences extends CommCarePreferenceFragment { public final static String ALTERNATE_QUESTION_LAYOUT_ENABLED = "cc-alternate-question-text-format"; public final static String OFFER_PIN_FOR_LOGIN = "cc-offer-pin-for-login"; + public final static String AUTO_SAVE_FORM_ON_PAUSE = "cc-auto-form-save-on-pause"; + private static final Set WHITELISTED_DEVELOPER_PREF_KEYS = new HashSet<>(); static { @@ -85,6 +87,7 @@ public class DeveloperPreferences extends CommCarePreferenceFragment { WHITELISTED_DEVELOPER_PREF_KEYS.add(AUTO_PURGE_ENABLED); WHITELISTED_DEVELOPER_PREF_KEYS.add(ALTERNATE_QUESTION_LAYOUT_ENABLED); WHITELISTED_DEVELOPER_PREF_KEYS.add(ENABLE_CERTIFICATE_TRANSPARENCY); + WHITELISTED_DEVELOPER_PREF_KEYS.add(AUTO_SAVE_FORM_ON_PAUSE); } /** @@ -428,6 +431,10 @@ public static boolean useExpressionCachingInForms() { return doesPropertyMatch(USE_EXPRESSION_CACHING_IN_FORMS, PrefValues.NO, PrefValues.YES); } + public static boolean isAutoSaveFormOnPause() { + return doesPropertyMatch(AUTO_SAVE_FORM_ON_PAUSE, PrefValues.NO, PrefValues.YES); + } + private void hideOrShowDangerousSettings() { Preference[] onScreenPrefs = getOnScreenPrefs(); if (!GlobalPrivilegesManager.isAdvancedSettingsAccessEnabled() && !BuildConfig.DEBUG) { From 62bb3114d6150c3b96406809a781dced9dae9525 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 7 Aug 2024 13:44:49 +0200 Subject: [PATCH 09/63] Return serialized form index --- .../commcare/logic/AndroidFormController.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/org/commcare/logic/AndroidFormController.java b/app/src/org/commcare/logic/AndroidFormController.java index 79892da3f0..15f7395bc2 100644 --- a/app/src/org/commcare/logic/AndroidFormController.java +++ b/app/src/org/commcare/logic/AndroidFormController.java @@ -1,18 +1,16 @@ package org.commcare.logic; +import android.util.Base64; import androidx.annotation.NonNull; import org.commcare.google.services.analytics.FormAnalyticsHelper; -import org.commcare.utils.FileUtil; +import org.commcare.utils.SerializationUtil; import org.commcare.views.widgets.WidgetFactory; import org.javarosa.core.model.FormDef; import org.javarosa.core.model.FormIndex; import org.javarosa.form.api.FormController; import org.javarosa.form.api.FormEntryController; -import java.io.File; -import java.util.Date; - /** * Wrapper around FormController to handle Android-specific form entry actions */ @@ -88,4 +86,14 @@ public FormAnalyticsHelper getFormAnalyticsHelper() { public FormDef getFormDef() { return mFormEntryController.getModel().getForm(); } + + // TODO: we should cache this + public String getSerializedFormIndex() { + try{ + byte[] serializedFormIndex = SerializationUtil.serialize(getFormIndex()); + return Base64.encodeToString(serializedFormIndex, Base64.DEFAULT); + } catch (Exception e){ + return null; + } + } } From e00dcea639dc5e8d074d91ed68791b62e79dd50e Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 7 Aug 2024 13:49:01 +0200 Subject: [PATCH 10/63] Save current form index in app preferences --- app/src/org/commcare/activities/FormEntryActivity.java | 1 + app/src/org/commcare/preferences/HiddenPreferences.java | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 742cd8507b..817361b17b 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -258,6 +258,7 @@ private void interruptAndSaveForm(boolean exit) { // Set flag that will allow us to restore this form when we log back in CommCareApplication.instance().getCurrentSessionWrapper().setCurrentStateAsInterrupted(); + HiddenPreferences.setInterruptedFormIndex(mFormController.getSerializedFormIndex()); // Start saving form; will trigger expireUserSession() on completion saveIncompleteFormToDisk(exit); } diff --git a/app/src/org/commcare/preferences/HiddenPreferences.java b/app/src/org/commcare/preferences/HiddenPreferences.java index b638f93a0b..021a64e066 100644 --- a/app/src/org/commcare/preferences/HiddenPreferences.java +++ b/app/src/org/commcare/preferences/HiddenPreferences.java @@ -112,6 +112,7 @@ public class HiddenPreferences { * be used to remove it form the user domain name to match how the domain represented in the backend */ public static final String USER_DOMAIN_SERVER_URL_SUFFIX = ".commcarehq.org"; + private static final String INTERRUPTED_FORM_INDEX = "interrupted-form-index"; /** * @return How many seconds should a user session remain open before expiring? @@ -629,4 +630,11 @@ public static void setPendingSyncDialogDisabled(boolean dialogDisabled) { public static boolean isBackgroundSyncEnabled() { return DeveloperPreferences.doesPropertyMatch(ENABLE_BACKGROUND_SYNC, PrefValues.NO, PrefValues.YES); } + + public static void setInterruptedFormIndex(String serializedFormIndex) { + String currentUserId = CommCareApplication.instance().getCurrentUserId(); + CommCareApplication.instance().getCurrentApp().getAppPreferences().edit() + .putString(INTERRUPTED_FORM_INDEX + currentUserId, serializedFormIndex) + .apply(); + } } From 2b301ea1a20872c722c1702ea684a89bf6b6d1b1 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 8 Aug 2024 01:00:50 +0200 Subject: [PATCH 11/63] Save serialized form index when saving session state --- app/src/org/commcare/activities/FormEntryActivity.java | 4 ++-- app/src/org/commcare/models/AndroidSessionWrapper.java | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 817361b17b..fc346bcd99 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -256,9 +256,9 @@ public void formSaveCallback(boolean exit, Runnable listener) { private void interruptAndSaveForm(boolean exit) { // Set flag that will allow us to restore this form when we log back in - CommCareApplication.instance().getCurrentSessionWrapper().setCurrentStateAsInterrupted(); + CommCareApplication.instance().getCurrentSessionWrapper() + .setCurrentStateAsInterrupted(mFormController.getSerializedFormIndex()); - HiddenPreferences.setInterruptedFormIndex(mFormController.getSerializedFormIndex()); // Start saving form; will trigger expireUserSession() on completion saveIncompleteFormToDisk(exit); } diff --git a/app/src/org/commcare/models/AndroidSessionWrapper.java b/app/src/org/commcare/models/AndroidSessionWrapper.java index 308ece8254..8a71b456ba 100755 --- a/app/src/org/commcare/models/AndroidSessionWrapper.java +++ b/app/src/org/commcare/models/AndroidSessionWrapper.java @@ -5,7 +5,6 @@ import org.commcare.CommCareApplication; import org.commcare.android.database.user.models.FormRecord; import org.commcare.android.database.user.models.SessionStateDescriptor; -import org.commcare.core.interfaces.RemoteInstanceFetcher; import org.commcare.models.database.AndroidSandbox; import org.commcare.models.database.SqlStorage; import org.commcare.modern.session.SessionWrapper; @@ -20,7 +19,6 @@ import org.commcare.suite.model.Entry; import org.commcare.suite.model.FormEntry; import org.commcare.suite.model.SessionDatum; -import org.commcare.suite.model.StackFrameStep; import org.commcare.suite.model.StackOperation; import org.commcare.util.CommCarePlatform; import org.commcare.utils.AndroidInstanceInitializer; @@ -184,12 +182,16 @@ private static boolean ssdHasValidFormRecordId(int ssdId, formRecordStorage.getMetaDataFieldForRecord(correspondingFormRecordId, FormRecord.META_STATUS)); } - public void setCurrentStateAsInterrupted() { + public void setCurrentStateAsInterrupted(String serializedFormIndex) { if (sessionStateRecordId != -1) { SqlStorage sessionStorage = CommCareApplication.instance().getUserStorage(SessionStateDescriptor.class); SessionStateDescriptor current = sessionStorage.read(sessionStateRecordId); HiddenPreferences.setInterruptedSSD(current.getID()); + + if (serializedFormIndex != null) { + HiddenPreferences.setInterruptedFormIndex(serializedFormIndex); + } } } From 10bd885f1b946ab762b9db005c02fda268537b4b Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 8 Aug 2024 01:03:42 +0200 Subject: [PATCH 12/63] Add SSD record Id to serialized form index --- .../commcare/models/AndroidSessionWrapper.java | 3 ++- .../commcare/preferences/HiddenPreferences.java | 7 +++++-- app/src/org/commcare/utils/StringUtils.java | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/src/org/commcare/models/AndroidSessionWrapper.java b/app/src/org/commcare/models/AndroidSessionWrapper.java index 8a71b456ba..c398347c2a 100755 --- a/app/src/org/commcare/models/AndroidSessionWrapper.java +++ b/app/src/org/commcare/models/AndroidSessionWrapper.java @@ -9,6 +9,7 @@ import org.commcare.models.database.SqlStorage; import org.commcare.modern.session.SessionWrapper; import org.commcare.modern.session.SessionWrapperInterface; +import org.commcare.modern.util.Pair; import org.commcare.preferences.HiddenPreferences; import org.commcare.session.CommCareSession; import org.commcare.session.SessionDescriptorUtil; @@ -190,7 +191,7 @@ public void setCurrentStateAsInterrupted(String serializedFormIndex) { HiddenPreferences.setInterruptedSSD(current.getID()); if (serializedFormIndex != null) { - HiddenPreferences.setInterruptedFormIndex(serializedFormIndex); + HiddenPreferences.setInterruptedFormIndex(new Pair<>(current.getID(), serializedFormIndex)); } } } diff --git a/app/src/org/commcare/preferences/HiddenPreferences.java b/app/src/org/commcare/preferences/HiddenPreferences.java index 021a64e066..485b4ec400 100644 --- a/app/src/org/commcare/preferences/HiddenPreferences.java +++ b/app/src/org/commcare/preferences/HiddenPreferences.java @@ -6,11 +6,13 @@ import org.commcare.CommCareApplication; import org.commcare.activities.GeoPointActivity; import org.commcare.android.logging.ReportingUtils; +import org.commcare.modern.util.Pair; import org.commcare.services.FCMMessageData; import org.commcare.utils.AndroidCommCarePlatform; import org.commcare.utils.FirebaseMessagingUtil; import org.commcare.utils.GeoUtils; import org.commcare.utils.MapLayer; +import org.commcare.utils.StringUtils; import java.util.Date; import java.util.concurrent.TimeUnit; @@ -631,10 +633,11 @@ public static boolean isBackgroundSyncEnabled() { return DeveloperPreferences.doesPropertyMatch(ENABLE_BACKGROUND_SYNC, PrefValues.NO, PrefValues.YES); } - public static void setInterruptedFormIndex(String serializedFormIndex) { + public static void setInterruptedFormIndex(Pair ssIdAndSerializedFormIndexPair) { String currentUserId = CommCareApplication.instance().getCurrentUserId(); CommCareApplication.instance().getCurrentApp().getAppPreferences().edit() - .putString(INTERRUPTED_FORM_INDEX + currentUserId, serializedFormIndex) + .putString(INTERRUPTED_FORM_INDEX + currentUserId, + StringUtils.convertPairToJsonString(ssIdAndSerializedFormIndexPair)) .apply(); } } diff --git a/app/src/org/commcare/utils/StringUtils.java b/app/src/org/commcare/utils/StringUtils.java index bacc412eca..68e2c225a5 100755 --- a/app/src/org/commcare/utils/StringUtils.java +++ b/app/src/org/commcare/utils/StringUtils.java @@ -5,9 +5,14 @@ import android.os.Build; import android.text.Spannable; +import com.google.gson.Gson; +import com.google.gson.JsonIOException; + +import org.commcare.modern.util.Pair; import org.javarosa.core.services.locale.Localization; import org.javarosa.core.util.NoLocalizedTextException; +import java.io.Serializable; import java.text.Normalizer; import java.util.regex.Pattern; @@ -55,4 +60,15 @@ public static Spannable getStringSpannableRobust(Context c, int resId, String ar } return MarkupUtil.styleSpannable(c, ret); } + + public static String convertPairToJsonString(Pair pair){ + Gson gson = new Gson(); + try{ + String jsonString = gson.toJson(pair); + return jsonString; + } catch(JsonIOException e){ + // default to null + return null; + } + } } From 34cf2eec5a2b7bc2ebd8c89509dea9b8cf36d255 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 8 Aug 2024 15:22:23 +0530 Subject: [PATCH 13/63] Fix form Storage test --- .../org/commcare/android/tests/processing/FormStorageTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/unit-tests/src/org/commcare/android/tests/processing/FormStorageTest.java b/app/unit-tests/src/org/commcare/android/tests/processing/FormStorageTest.java index 3283f13401..7612a2e8c1 100644 --- a/app/unit-tests/src/org/commcare/android/tests/processing/FormStorageTest.java +++ b/app/unit-tests/src/org/commcare/android/tests/processing/FormStorageTest.java @@ -357,6 +357,9 @@ public class FormStorageTest { , "org.commcare.suite.model.QueryGroup" , "org.commcare.android.database.global.models.ConnectKeyRecord" , "org.commcare.android.database.global.models.ConnectKeyRecordV6" + + // Added in 2.55 + , "org.javarosa.core.model.FormIndex" ); From 1522bfb29f8ae78688e7d38034aebdbb3641f79f Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 7 Aug 2024 14:38:41 +0200 Subject: [PATCH 14/63] Retrieve serialized form index --- app/src/org/commcare/activities/FormEntryActivity.java | 6 +++++- app/src/org/commcare/preferences/HiddenPreferences.java | 6 ++++++ app/src/org/commcare/tasks/FormLoaderTask.java | 6 +++--- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index fc346bcd99..71b2d11893 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -1009,6 +1009,7 @@ private void restorePriorStates() { private void loadForm() { mFormController = null; instanceState.setFormRecordPath(null); + String serializedFormIndex = null; Intent intent = getIntent(); if (intent != null) { @@ -1025,6 +1026,9 @@ private void loadForm() { instanceState); formId = instanceAndStatus.first; instanceIsReadOnly = instanceAndStatus.second; + + // only retrieve a potentially stored form index when loading an existing form record + serializedFormIndex = HiddenPreferences.getInterruptedFormIndex(); } else if (intent.hasExtra(KEY_FORM_DEF_ID)) { formId = intent.getIntExtra(KEY_FORM_DEF_ID, -1); instanceState.setFormDefPath(FormFileSystemHelpers.getFormDefPath(formDefStorage, formId)); @@ -1041,7 +1045,7 @@ private void loadForm() { } mFormLoaderTask = new FormLoaderTask(symetricKey, instanceIsReadOnly, - formEntryRestoreSession.isRecording(), FormEntryInstanceState.mFormRecordPath, this) { + formEntryRestoreSession.isRecording(), FormEntryInstanceState.mFormRecordPath, this, serializedFormIndex) { @Override protected void deliverResult(FormEntryActivity receiver, FECWrapper wrapperResult) { receiver.handleFormLoadCompletion(wrapperResult.getController()); diff --git a/app/src/org/commcare/preferences/HiddenPreferences.java b/app/src/org/commcare/preferences/HiddenPreferences.java index 485b4ec400..1bcebd1c4e 100644 --- a/app/src/org/commcare/preferences/HiddenPreferences.java +++ b/app/src/org/commcare/preferences/HiddenPreferences.java @@ -640,4 +640,10 @@ public static void setInterruptedFormIndex(Pair ssIdAndSerializ StringUtils.convertPairToJsonString(ssIdAndSerializedFormIndexPair)) .apply(); } + + public static String getInterruptedFormIndex() { + String currentUserId = CommCareApplication.instance().getCurrentUserId(); + return CommCareApplication.instance().getCurrentApp().getAppPreferences() + .getString(INTERRUPTED_FORM_INDEX + currentUserId, null); + } } diff --git a/app/src/org/commcare/tasks/FormLoaderTask.java b/app/src/org/commcare/tasks/FormLoaderTask.java index 694250d600..a537f9832c 100644 --- a/app/src/org/commcare/tasks/FormLoaderTask.java +++ b/app/src/org/commcare/tasks/FormLoaderTask.java @@ -1,7 +1,6 @@ package org.commcare.tasks; import android.content.Context; -import android.os.Environment; import android.util.Log; import org.commcare.CommCareApplication; @@ -14,7 +13,6 @@ import org.commcare.logging.UserCausedRuntimeException; import org.commcare.logging.XPathErrorLogger; import org.commcare.logic.AndroidFormController; -import org.commcare.logic.FileReferenceFactory; import org.commcare.models.encryption.EncryptionIO; import org.commcare.preferences.DeveloperPreferences; import org.commcare.tasks.templates.CommCareTask; @@ -64,6 +62,7 @@ public abstract class FormLoaderTask extends CommCareTask extends CommCareTask Date: Wed, 7 Aug 2024 14:44:03 +0200 Subject: [PATCH 15/63] Deserialize and initialize form index to return to --- .../commcare/logic/AndroidFormController.java | 18 +++++++++++++++++- app/src/org/commcare/tasks/FormLoaderTask.java | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/src/org/commcare/logic/AndroidFormController.java b/app/src/org/commcare/logic/AndroidFormController.java index 15f7395bc2..e037235769 100644 --- a/app/src/org/commcare/logic/AndroidFormController.java +++ b/app/src/org/commcare/logic/AndroidFormController.java @@ -4,10 +4,12 @@ import androidx.annotation.NonNull; import org.commcare.google.services.analytics.FormAnalyticsHelper; +import org.commcare.util.LogTypes; import org.commcare.utils.SerializationUtil; import org.commcare.views.widgets.WidgetFactory; import org.javarosa.core.model.FormDef; import org.javarosa.core.model.FormIndex; +import org.javarosa.core.services.Logger; import org.javarosa.form.api.FormController; import org.javarosa.form.api.FormEntryController; @@ -24,9 +26,10 @@ public class AndroidFormController extends FormController implements PendingCall private FormAnalyticsHelper formAnalyticsHelper; - public AndroidFormController(FormEntryController fec, boolean readOnly) { + public AndroidFormController(FormEntryController fec, boolean readOnly, String serializedFormIndex) { super(fec, readOnly); formAnalyticsHelper = new FormAnalyticsHelper(); + formIndexToReturnTo = deserializeFormIndex(serializedFormIndex); } @Override @@ -96,4 +99,17 @@ public String getSerializedFormIndex() { return null; } } + + private FormIndex deserializeFormIndex(String serializedFormIndex) { + if (serializedFormIndex != null) { + try{ + byte[] decodedFormIndex = Base64.decode(serializedFormIndex, Base64.DEFAULT); + return SerializationUtil.deserialize(decodedFormIndex, FormIndex.class); + } catch(Exception e) { + Logger.log(LogTypes.TYPE_FORM_ENTRY, + "Deserialization of last form index failed, " + e.getMessage()); + } + } + return null; + } } diff --git a/app/src/org/commcare/tasks/FormLoaderTask.java b/app/src/org/commcare/tasks/FormLoaderTask.java index a537f9832c..dc730eb809 100644 --- a/app/src/org/commcare/tasks/FormLoaderTask.java +++ b/app/src/org/commcare/tasks/FormLoaderTask.java @@ -137,7 +137,7 @@ protected FECWrapper doTaskBackground(Integer... formDefId) { setupFormMedia(formDefRecord.getMediaPath()); - AndroidFormController formController = new AndroidFormController(fec, mReadOnly); + AndroidFormController formController = new AndroidFormController(fec, mReadOnly, serializedLastFormIndex); data = new FECWrapper(formController); return data; From 51f268b61817ffe1ff6a96c67f505c7590486e8b Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 7 Aug 2024 14:45:58 +0200 Subject: [PATCH 16/63] Jump to last form index --- app/src/org/commcare/activities/FormEntryActivity.java | 5 +++++ app/src/org/commcare/preferences/HiddenPreferences.java | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 71b2d11893..5e6c8133f6 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -1117,12 +1117,17 @@ private void handleFormLoadCompletion(AndroidFormController fc) { formEntryRestoreSession.replaySession(this); + // jump to form index, no action if null + mFormController.returnToStoredIndex(); + uiController.refreshView(); FormNavigationUI.updateNavigationCues(this, mFormController, uiController.questionsView); if (isRestartAfterSessionExpiration) { Toast.makeText(this, Localization.get("form.entry.restart.after.expiration"), Toast.LENGTH_LONG).show(); } + + HiddenPreferences.clearInterruptedFormIndex(); } private void handleXpathErrorBroadcast() { diff --git a/app/src/org/commcare/preferences/HiddenPreferences.java b/app/src/org/commcare/preferences/HiddenPreferences.java index 1bcebd1c4e..cb26ffa1e6 100644 --- a/app/src/org/commcare/preferences/HiddenPreferences.java +++ b/app/src/org/commcare/preferences/HiddenPreferences.java @@ -646,4 +646,11 @@ public static String getInterruptedFormIndex() { return CommCareApplication.instance().getCurrentApp().getAppPreferences() .getString(INTERRUPTED_FORM_INDEX + currentUserId, null); } + + public static void clearInterruptedFormIndex() { + String currentUserId = CommCareApplication.instance().getCurrentUserId(); + CommCareApplication.instance().getCurrentApp().getAppPreferences().edit() + .remove(INTERRUPTED_FORM_INDEX + currentUserId) + .apply(); + } } From 86d35ba3a11340d3fe985d0e4534d864f247adbd Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 8 Aug 2024 01:45:06 +0200 Subject: [PATCH 17/63] Verify serialized form index based on SSD Id --- .../activities/FormEntryActivity.java | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 5e6c8133f6..3adcf0c7ef 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -1028,7 +1028,8 @@ private void loadForm() { instanceIsReadOnly = instanceAndStatus.second; // only retrieve a potentially stored form index when loading an existing form record - serializedFormIndex = HiddenPreferences.getInterruptedFormIndex(); + AndroidSessionWrapper asw = CommCareApplication.instance().getCurrentSessionWrapper(); + serializedFormIndex = retrieveAndValidateSerializedFormIndex(asw.getSessionDescriptorId()); } else if (intent.hasExtra(KEY_FORM_DEF_ID)) { formId = intent.getIntExtra(KEY_FORM_DEF_ID, -1); instanceState.setFormDefPath(FormFileSystemHelpers.getFormDefPath(formDefStorage, formId)); @@ -1088,6 +1089,31 @@ protected void deliverError(FormEntryActivity receiver, Exception e) { } } + private String retrieveAndValidateSerializedFormIndex(int sessionDescriptorId) { + String interruptedFormIndex = HiddenPreferences.getInterruptedFormIndex(); + if (interruptedFormIndex == null || interruptedFormIndex.isEmpty()){ + return null; + } + + int spaceIndex = interruptedFormIndex.indexOf(" "); + if (spaceIndex != -1) { + try { + int ssdId = Integer.parseInt(interruptedFormIndex.substring(0, spaceIndex)); + String serializedFormIndex = + interruptedFormIndex.substring( + spaceIndex + 1, interruptedFormIndex.length()); + if (ssdId == sessionDescriptorId) { + return serializedFormIndex; + } + } catch (Exception e) { + // something went wrong + } + } + // data format is invalid, so better to clear the data + HiddenPreferences.clearInterruptedFormIndex(); + return null; + } + private void handleFormLoadCompletion(AndroidFormController fc) { if (PollSensorAction.XPATH_ERROR_ACTION.equals(locationRecieverErrorAction)) { handleXpathErrorBroadcast(); From 13e057ac003f3a9f0b36dd5a99c3f05f8363ea74 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 8 Aug 2024 10:03:29 +0200 Subject: [PATCH 18/63] Retrieve form index in a Pair format --- .../activities/FormEntryActivity.java | 23 ++++++------------ .../preferences/HiddenPreferences.java | 6 +++-- app/src/org/commcare/utils/StringUtils.java | 24 +++++++++++++------ 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 3adcf0c7ef..993d95a129 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -1090,24 +1090,15 @@ protected void deliverError(FormEntryActivity receiver, Exception e) { } private String retrieveAndValidateSerializedFormIndex(int sessionDescriptorId) { - String interruptedFormIndex = HiddenPreferences.getInterruptedFormIndex(); - if (interruptedFormIndex == null || interruptedFormIndex.isEmpty()){ + org.commcare.modern.util.Pair interruptedFormIndex = HiddenPreferences.getInterruptedFormIndex(); + if (interruptedFormIndex == null + || interruptedFormIndex.first == null + || interruptedFormIndex.second == null + || interruptedFormIndex.second.isEmpty()){ return null; } - - int spaceIndex = interruptedFormIndex.indexOf(" "); - if (spaceIndex != -1) { - try { - int ssdId = Integer.parseInt(interruptedFormIndex.substring(0, spaceIndex)); - String serializedFormIndex = - interruptedFormIndex.substring( - spaceIndex + 1, interruptedFormIndex.length()); - if (ssdId == sessionDescriptorId) { - return serializedFormIndex; - } - } catch (Exception e) { - // something went wrong - } + if (interruptedFormIndex.first == (double)sessionDescriptorId) { + return interruptedFormIndex.second; } // data format is invalid, so better to clear the data HiddenPreferences.clearInterruptedFormIndex(); diff --git a/app/src/org/commcare/preferences/HiddenPreferences.java b/app/src/org/commcare/preferences/HiddenPreferences.java index cb26ffa1e6..0aba6cb2f3 100644 --- a/app/src/org/commcare/preferences/HiddenPreferences.java +++ b/app/src/org/commcare/preferences/HiddenPreferences.java @@ -641,10 +641,12 @@ public static void setInterruptedFormIndex(Pair ssIdAndSerializ .apply(); } - public static String getInterruptedFormIndex() { + // This was changed to Double due to the way Gson handles numeric values + public static Pair getInterruptedFormIndex() { String currentUserId = CommCareApplication.instance().getCurrentUserId(); - return CommCareApplication.instance().getCurrentApp().getAppPreferences() + String ssIdAndSerializedFormIndexJsonForm = CommCareApplication.instance().getCurrentApp().getAppPreferences() .getString(INTERRUPTED_FORM_INDEX + currentUserId, null); + return (Pair)StringUtils.convertJsonStringToPair(ssIdAndSerializedFormIndexJsonForm); } public static void clearInterruptedFormIndex() { diff --git a/app/src/org/commcare/utils/StringUtils.java b/app/src/org/commcare/utils/StringUtils.java index 68e2c225a5..11aa911a96 100755 --- a/app/src/org/commcare/utils/StringUtils.java +++ b/app/src/org/commcare/utils/StringUtils.java @@ -1,10 +1,10 @@ package org.commcare.utils; -import android.annotation.SuppressLint; import android.content.Context; -import android.os.Build; import android.text.Spannable; +import androidx.annotation.NonNull; + import com.google.gson.Gson; import com.google.gson.JsonIOException; @@ -13,11 +13,6 @@ import org.javarosa.core.util.NoLocalizedTextException; import java.io.Serializable; -import java.text.Normalizer; -import java.util.regex.Pattern; - -import androidx.annotation.NonNull; -import androidx.collection.LruCache; /** * @author ctsims @@ -71,4 +66,19 @@ public static String convertPairToJsonString(Pair convertJsonStringToPair( + String stringInJsonFormat) { + Gson gson = new Gson(); + if (stringInJsonFormat == null) { + return null; + } + + try{ + return gson.fromJson(stringInJsonFormat, Pair.class); + } catch(JsonIOException e){ + // default to null + return null; + } + } } From 8ee1d1387109a51948d1c2d134be10ca96e00883 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 8 Aug 2024 10:16:51 +0200 Subject: [PATCH 19/63] Deserialize FormIndex when retrieving from app preferences --- .../activities/FormEntryActivity.java | 25 +++++++++++++++---- .../commcare/logic/AndroidFormController.java | 20 +++------------ .../org/commcare/tasks/FormLoaderTask.java | 9 ++++--- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 993d95a129..3ccc92b13e 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -14,6 +14,7 @@ import android.os.Build; import android.os.Bundle; import android.speech.tts.TextToSpeech; +import android.util.Base64; import android.util.Log; import android.util.Pair; import android.view.ContextMenu; @@ -70,6 +71,7 @@ import org.commcare.utils.CompoundIntentList; import org.commcare.utils.FileUtil; import org.commcare.utils.GeoUtils; +import org.commcare.utils.SerializationUtil; import org.commcare.utils.SessionUnavailableException; import org.commcare.utils.StringUtils; import org.commcare.views.QuestionsView; @@ -1009,7 +1011,7 @@ private void restorePriorStates() { private void loadForm() { mFormController = null; instanceState.setFormRecordPath(null); - String serializedFormIndex = null; + FormIndex lastFormIndex = null; Intent intent = getIntent(); if (intent != null) { @@ -1029,7 +1031,7 @@ private void loadForm() { // only retrieve a potentially stored form index when loading an existing form record AndroidSessionWrapper asw = CommCareApplication.instance().getCurrentSessionWrapper(); - serializedFormIndex = retrieveAndValidateSerializedFormIndex(asw.getSessionDescriptorId()); + lastFormIndex = retrieveAndValidateFormIndex(asw.getSessionDescriptorId()); } else if (intent.hasExtra(KEY_FORM_DEF_ID)) { formId = intent.getIntExtra(KEY_FORM_DEF_ID, -1); instanceState.setFormDefPath(FormFileSystemHelpers.getFormDefPath(formDefStorage, formId)); @@ -1046,7 +1048,7 @@ private void loadForm() { } mFormLoaderTask = new FormLoaderTask(symetricKey, instanceIsReadOnly, - formEntryRestoreSession.isRecording(), FormEntryInstanceState.mFormRecordPath, this, serializedFormIndex) { + formEntryRestoreSession.isRecording(), FormEntryInstanceState.mFormRecordPath, this, lastFormIndex) { @Override protected void deliverResult(FormEntryActivity receiver, FECWrapper wrapperResult) { receiver.handleFormLoadCompletion(wrapperResult.getController()); @@ -1089,7 +1091,7 @@ protected void deliverError(FormEntryActivity receiver, Exception e) { } } - private String retrieveAndValidateSerializedFormIndex(int sessionDescriptorId) { + private FormIndex retrieveAndValidateFormIndex(int sessionDescriptorId) { org.commcare.modern.util.Pair interruptedFormIndex = HiddenPreferences.getInterruptedFormIndex(); if (interruptedFormIndex == null || interruptedFormIndex.first == null @@ -1098,13 +1100,26 @@ private String retrieveAndValidateSerializedFormIndex(int sessionDescriptorId) { return null; } if (interruptedFormIndex.first == (double)sessionDescriptorId) { - return interruptedFormIndex.second; + return deserializeFormIndex(interruptedFormIndex.second); } // data format is invalid, so better to clear the data HiddenPreferences.clearInterruptedFormIndex(); return null; } + private FormIndex deserializeFormIndex(String serializedFormIndex) { + if (serializedFormIndex != null) { + try{ + byte[] decodedFormIndex = Base64.decode(serializedFormIndex, Base64.DEFAULT); + return SerializationUtil.deserialize(decodedFormIndex, FormIndex.class); + } catch(Exception e) { + Logger.log(LogTypes.TYPE_FORM_ENTRY, + "Deserialization of last form index failed, " + e.getMessage()); + } + } + return null; + } + private void handleFormLoadCompletion(AndroidFormController fc) { if (PollSensorAction.XPATH_ERROR_ACTION.equals(locationRecieverErrorAction)) { handleXpathErrorBroadcast(); diff --git a/app/src/org/commcare/logic/AndroidFormController.java b/app/src/org/commcare/logic/AndroidFormController.java index e037235769..e0a6de5469 100644 --- a/app/src/org/commcare/logic/AndroidFormController.java +++ b/app/src/org/commcare/logic/AndroidFormController.java @@ -1,15 +1,14 @@ package org.commcare.logic; import android.util.Base64; + import androidx.annotation.NonNull; import org.commcare.google.services.analytics.FormAnalyticsHelper; -import org.commcare.util.LogTypes; import org.commcare.utils.SerializationUtil; import org.commcare.views.widgets.WidgetFactory; import org.javarosa.core.model.FormDef; import org.javarosa.core.model.FormIndex; -import org.javarosa.core.services.Logger; import org.javarosa.form.api.FormController; import org.javarosa.form.api.FormEntryController; @@ -26,10 +25,10 @@ public class AndroidFormController extends FormController implements PendingCall private FormAnalyticsHelper formAnalyticsHelper; - public AndroidFormController(FormEntryController fec, boolean readOnly, String serializedFormIndex) { + public AndroidFormController(FormEntryController fec, boolean readOnly, FormIndex formIndex) { super(fec, readOnly); formAnalyticsHelper = new FormAnalyticsHelper(); - formIndexToReturnTo = deserializeFormIndex(serializedFormIndex); + formIndexToReturnTo = formIndex; } @Override @@ -99,17 +98,4 @@ public String getSerializedFormIndex() { return null; } } - - private FormIndex deserializeFormIndex(String serializedFormIndex) { - if (serializedFormIndex != null) { - try{ - byte[] decodedFormIndex = Base64.decode(serializedFormIndex, Base64.DEFAULT); - return SerializationUtil.deserialize(decodedFormIndex, FormIndex.class); - } catch(Exception e) { - Logger.log(LogTypes.TYPE_FORM_ENTRY, - "Deserialization of last form index failed, " + e.getMessage()); - } - } - return null; - } } diff --git a/app/src/org/commcare/tasks/FormLoaderTask.java b/app/src/org/commcare/tasks/FormLoaderTask.java index dc730eb809..6869ce3eb0 100644 --- a/app/src/org/commcare/tasks/FormLoaderTask.java +++ b/app/src/org/commcare/tasks/FormLoaderTask.java @@ -21,6 +21,7 @@ import org.commcare.utils.GlobalConstants; import org.javarosa.core.io.StreamsUtil; import org.javarosa.core.model.FormDef; +import org.javarosa.core.model.FormIndex; import org.javarosa.core.model.instance.InstanceInitializationFactory; import org.javarosa.core.model.instance.TreeElement; import org.javarosa.core.model.instance.TreeReference; @@ -62,7 +63,7 @@ public abstract class FormLoaderTask extends CommCareTask extends CommCareTask Date: Thu, 8 Aug 2024 10:25:16 +0200 Subject: [PATCH 20/63] Log exception --- app/src/org/commcare/activities/FormEntryActivity.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 3ccc92b13e..bade744e97 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -1113,8 +1113,7 @@ private FormIndex deserializeFormIndex(String serializedFormIndex) { byte[] decodedFormIndex = Base64.decode(serializedFormIndex, Base64.DEFAULT); return SerializationUtil.deserialize(decodedFormIndex, FormIndex.class); } catch(Exception e) { - Logger.log(LogTypes.TYPE_FORM_ENTRY, - "Deserialization of last form index failed, " + e.getMessage()); + Logger.exception("Deserialization of last form index failed ", e); } } return null; From 3215e2535265dd58102d7191ebc378e22037e3d6 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 8 Aug 2024 11:51:31 +0200 Subject: [PATCH 21/63] Log form index serialization exceptions --- app/src/org/commcare/logic/AndroidFormController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/org/commcare/logic/AndroidFormController.java b/app/src/org/commcare/logic/AndroidFormController.java index e0a6de5469..1db51c7531 100644 --- a/app/src/org/commcare/logic/AndroidFormController.java +++ b/app/src/org/commcare/logic/AndroidFormController.java @@ -9,6 +9,7 @@ import org.commcare.views.widgets.WidgetFactory; import org.javarosa.core.model.FormDef; import org.javarosa.core.model.FormIndex; +import org.javarosa.core.services.Logger; import org.javarosa.form.api.FormController; import org.javarosa.form.api.FormEntryController; @@ -95,6 +96,7 @@ public String getSerializedFormIndex() { byte[] serializedFormIndex = SerializationUtil.serialize(getFormIndex()); return Base64.encodeToString(serializedFormIndex, Base64.DEFAULT); } catch (Exception e){ + Logger.exception("Serialization of last form index failed ", e); return null; } } From 9e396226391897cab4b27c7956d3aa318fd27edf Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 8 Aug 2024 11:55:01 +0200 Subject: [PATCH 22/63] Log JSON <-> Pair conversion errors --- app/src/org/commcare/utils/StringUtils.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/org/commcare/utils/StringUtils.java b/app/src/org/commcare/utils/StringUtils.java index 11aa911a96..1dd967238f 100755 --- a/app/src/org/commcare/utils/StringUtils.java +++ b/app/src/org/commcare/utils/StringUtils.java @@ -9,6 +9,7 @@ import com.google.gson.JsonIOException; import org.commcare.modern.util.Pair; +import org.javarosa.core.services.Logger; import org.javarosa.core.services.locale.Localization; import org.javarosa.core.util.NoLocalizedTextException; @@ -62,7 +63,7 @@ public static String convertPairToJsonString(Pair Date: Thu, 8 Aug 2024 15:59:12 +0530 Subject: [PATCH 23/63] Use a Model to save interrupted form state --- .../activities/FormEntryActivity.java | 30 +++++------ .../models/AndroidSessionWrapper.java | 10 ++-- .../models/database/InterruptedFormState.java | 51 +++++++++++++++++++ .../preferences/HiddenPreferences.java | 40 ++++++++++----- .../org/commcare/utils/SerializationUtil.java | 10 ++++ app/src/org/commcare/utils/StringUtils.java | 26 ---------- 6 files changed, 106 insertions(+), 61 deletions(-) create mode 100644 app/src/org/commcare/models/database/InterruptedFormState.java diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index bade744e97..e14ec5884e 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -57,6 +57,7 @@ import org.commcare.logic.AndroidFormController; import org.commcare.models.AndroidSessionWrapper; import org.commcare.models.FormRecordProcessor; +import org.commcare.models.database.InterruptedFormState; import org.commcare.models.database.SqlStorage; import org.commcare.preferences.DeveloperPreferences; import org.commcare.preferences.HiddenPreferences; @@ -257,12 +258,14 @@ public void formSaveCallback(boolean exit, Runnable listener) { } private void interruptAndSaveForm(boolean exit) { - // Set flag that will allow us to restore this form when we log back in - CommCareApplication.instance().getCurrentSessionWrapper() - .setCurrentStateAsInterrupted(mFormController.getSerializedFormIndex()); + if (mFormController != null) { + // Set flag that will allow us to restore this form when we log back in + CommCareApplication.instance().getCurrentSessionWrapper().setCurrentStateAsInterrupted( + mFormController.getFormIndex()); - // Start saving form; will trigger expireUserSession() on completion - saveIncompleteFormToDisk(exit); + // Start saving form; will trigger expireUserSession() on completion + saveIncompleteFormToDisk(exit); + } } private void handleLocationErrorAction() { @@ -1092,18 +1095,13 @@ protected void deliverError(FormEntryActivity receiver, Exception e) { } private FormIndex retrieveAndValidateFormIndex(int sessionDescriptorId) { - org.commcare.modern.util.Pair interruptedFormIndex = HiddenPreferences.getInterruptedFormIndex(); - if (interruptedFormIndex == null - || interruptedFormIndex.first == null - || interruptedFormIndex.second == null - || interruptedFormIndex.second.isEmpty()){ - return null; - } - if (interruptedFormIndex.first == (double)sessionDescriptorId) { - return deserializeFormIndex(interruptedFormIndex.second); + InterruptedFormState interruptedFormState = + HiddenPreferences.getInterruptedFormState(); + if (interruptedFormState!= null && interruptedFormState.getSessionStateDescriptorId() == sessionDescriptorId) { + return interruptedFormState.getFormIndex(); } // data format is invalid, so better to clear the data - HiddenPreferences.clearInterruptedFormIndex(); + HiddenPreferences.clearInterruptedFormState(); return null; } @@ -1158,7 +1156,7 @@ private void handleFormLoadCompletion(AndroidFormController fc) { Localization.get("form.entry.restart.after.expiration"), Toast.LENGTH_LONG).show(); } - HiddenPreferences.clearInterruptedFormIndex(); + HiddenPreferences.clearInterruptedFormState(); } private void handleXpathErrorBroadcast() { diff --git a/app/src/org/commcare/models/AndroidSessionWrapper.java b/app/src/org/commcare/models/AndroidSessionWrapper.java index c398347c2a..b2cd353db8 100755 --- a/app/src/org/commcare/models/AndroidSessionWrapper.java +++ b/app/src/org/commcare/models/AndroidSessionWrapper.java @@ -6,6 +6,7 @@ import org.commcare.android.database.user.models.FormRecord; import org.commcare.android.database.user.models.SessionStateDescriptor; import org.commcare.models.database.AndroidSandbox; +import org.commcare.models.database.InterruptedFormState; import org.commcare.models.database.SqlStorage; import org.commcare.modern.session.SessionWrapper; import org.commcare.modern.session.SessionWrapperInterface; @@ -25,6 +26,7 @@ import org.commcare.utils.AndroidInstanceInitializer; import org.commcare.utils.CommCareUtil; import org.commcare.utils.CrashUtil; +import org.javarosa.core.model.FormIndex; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.xpath.analysis.InstanceNameAccumulatingAnalyzer; import org.javarosa.xpath.analysis.XPathAnalyzable; @@ -183,16 +185,14 @@ private static boolean ssdHasValidFormRecordId(int ssdId, formRecordStorage.getMetaDataFieldForRecord(correspondingFormRecordId, FormRecord.META_STATUS)); } - public void setCurrentStateAsInterrupted(String serializedFormIndex) { + public void setCurrentStateAsInterrupted(FormIndex formIndex) { if (sessionStateRecordId != -1) { SqlStorage sessionStorage = CommCareApplication.instance().getUserStorage(SessionStateDescriptor.class); SessionStateDescriptor current = sessionStorage.read(sessionStateRecordId); + InterruptedFormState interruptedFormState = new InterruptedFormState(current.getID(), formIndex); HiddenPreferences.setInterruptedSSD(current.getID()); - - if (serializedFormIndex != null) { - HiddenPreferences.setInterruptedFormIndex(new Pair<>(current.getID(), serializedFormIndex)); - } + HiddenPreferences.setInterruptedFormState(interruptedFormState); } } diff --git a/app/src/org/commcare/models/database/InterruptedFormState.java b/app/src/org/commcare/models/database/InterruptedFormState.java new file mode 100644 index 0000000000..ce72df9729 --- /dev/null +++ b/app/src/org/commcare/models/database/InterruptedFormState.java @@ -0,0 +1,51 @@ +package org.commcare.models.database; + +import org.javarosa.core.model.FormIndex; +import org.javarosa.core.util.externalizable.DeserializationException; +import org.javarosa.core.util.externalizable.ExtUtil; +import org.javarosa.core.util.externalizable.Externalizable; +import org.javarosa.core.util.externalizable.PrototypeFactory; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * Model to store info about an interrupted form + */ +public class InterruptedFormState implements Externalizable { + + int sessionStateDescriptorId; + FormIndex formIndex; + + public InterruptedFormState(int sessionStateDescriptorId, FormIndex formIndex) { + this.sessionStateDescriptorId = sessionStateDescriptorId; + this.formIndex = formIndex; + } + + public InterruptedFormState() { + // serialization only + } + + + @Override + public void readExternal(DataInputStream in, PrototypeFactory pf) + throws IOException, DeserializationException { + sessionStateDescriptorId = ExtUtil.readInt(in); + formIndex = (FormIndex)ExtUtil.read(in, FormIndex.class, pf); + } + + @Override + public void writeExternal(DataOutputStream out) throws IOException { + ExtUtil.writeNumeric(out, sessionStateDescriptorId); + ExtUtil.write(out, formIndex); + } + + public int getSessionStateDescriptorId() { + return sessionStateDescriptorId; + } + + public FormIndex getFormIndex() { + return formIndex; + } +} diff --git a/app/src/org/commcare/preferences/HiddenPreferences.java b/app/src/org/commcare/preferences/HiddenPreferences.java index 0aba6cb2f3..f4ec173ed6 100644 --- a/app/src/org/commcare/preferences/HiddenPreferences.java +++ b/app/src/org/commcare/preferences/HiddenPreferences.java @@ -6,13 +6,14 @@ import org.commcare.CommCareApplication; import org.commcare.activities.GeoPointActivity; import org.commcare.android.logging.ReportingUtils; -import org.commcare.modern.util.Pair; +import org.commcare.models.database.InterruptedFormState; import org.commcare.services.FCMMessageData; import org.commcare.utils.AndroidCommCarePlatform; import org.commcare.utils.FirebaseMessagingUtil; import org.commcare.utils.GeoUtils; import org.commcare.utils.MapLayer; -import org.commcare.utils.StringUtils; +import org.commcare.utils.SerializationUtil; +import org.javarosa.core.services.Logger; import java.util.Date; import java.util.concurrent.TimeUnit; @@ -633,23 +634,34 @@ public static boolean isBackgroundSyncEnabled() { return DeveloperPreferences.doesPropertyMatch(ENABLE_BACKGROUND_SYNC, PrefValues.NO, PrefValues.YES); } - public static void setInterruptedFormIndex(Pair ssIdAndSerializedFormIndexPair) { - String currentUserId = CommCareApplication.instance().getCurrentUserId(); - CommCareApplication.instance().getCurrentApp().getAppPreferences().edit() - .putString(INTERRUPTED_FORM_INDEX + currentUserId, - StringUtils.convertPairToJsonString(ssIdAndSerializedFormIndexPair)) - .apply(); + public static void setInterruptedFormState(InterruptedFormState interruptedFormState) { + try { + String currentUserId = CommCareApplication.instance().getCurrentUserId(); + CommCareApplication.instance().getCurrentApp().getAppPreferences().edit() + .putString(INTERRUPTED_FORM_INDEX + currentUserId, SerializationUtil.serializeToString(interruptedFormState)) + .apply(); + } catch (Exception e) { + Logger.exception("Error while trying to save interrupted form state into prefs", e); + } } // This was changed to Double due to the way Gson handles numeric values - public static Pair getInterruptedFormIndex() { - String currentUserId = CommCareApplication.instance().getCurrentUserId(); - String ssIdAndSerializedFormIndexJsonForm = CommCareApplication.instance().getCurrentApp().getAppPreferences() - .getString(INTERRUPTED_FORM_INDEX + currentUserId, null); - return (Pair)StringUtils.convertJsonStringToPair(ssIdAndSerializedFormIndexJsonForm); + public static InterruptedFormState getInterruptedFormState() { + try { + String currentUserId = CommCareApplication.instance().getCurrentUserId(); + String interruptedFormStateStr = CommCareApplication.instance().getCurrentApp().getAppPreferences() + .getString(INTERRUPTED_FORM_INDEX + currentUserId, null); + if (interruptedFormStateStr != null) { + return SerializationUtil.deserializeFromString(interruptedFormStateStr, + InterruptedFormState.class); + } + } catch (Exception e) { + Logger.exception("Error while trying to load interrupted form state from prefs", e); + } + return null; } - public static void clearInterruptedFormIndex() { + public static void clearInterruptedFormState() { String currentUserId = CommCareApplication.instance().getCurrentUserId(); CommCareApplication.instance().getCurrentApp().getAppPreferences().edit() .remove(INTERRUPTED_FORM_INDEX + currentUserId) diff --git a/app/src/org/commcare/utils/SerializationUtil.java b/app/src/org/commcare/utils/SerializationUtil.java index b5d5c97155..a62561ecb5 100644 --- a/app/src/org/commcare/utils/SerializationUtil.java +++ b/app/src/org/commcare/utils/SerializationUtil.java @@ -2,6 +2,7 @@ import android.content.Intent; import android.os.Bundle; +import android.util.Base64; import org.commcare.CommCareApplication; import org.javarosa.core.util.externalizable.DeserializationException; @@ -44,6 +45,15 @@ public static T deserialize(byte[] bytes, Class ty public static void serializeToIntent(Intent i, String name, Externalizable data) { i.putExtra(name, serialize(data)); } + + public static String serializeToString(Externalizable data) { + return Base64.encodeToString(serialize(data), Base64.DEFAULT); + } + + public static T deserializeFromString(String data, Class type) { + byte[] decodedData = Base64.decode(data, Base64.DEFAULT); + return deserialize(decodedData, type); + } public static T deserializeFromIntent(Intent i, String name, Class type) { if(!i.hasExtra(name)) { return null;} diff --git a/app/src/org/commcare/utils/StringUtils.java b/app/src/org/commcare/utils/StringUtils.java index 1dd967238f..6a21bb8751 100755 --- a/app/src/org/commcare/utils/StringUtils.java +++ b/app/src/org/commcare/utils/StringUtils.java @@ -56,30 +56,4 @@ public static Spannable getStringSpannableRobust(Context c, int resId, String ar } return MarkupUtil.styleSpannable(c, ret); } - - public static String convertPairToJsonString(Pair pair){ - Gson gson = new Gson(); - try{ - String jsonString = gson.toJson(pair); - return jsonString; - } catch(JsonIOException e){ - Logger.exception("Conversion from Pair to JSON failed ", e); - return null; - } - } - - public static Pair convertJsonStringToPair( - String stringInJsonFormat) { - Gson gson = new Gson(); - if (stringInJsonFormat == null) { - return null; - } - - try{ - return gson.fromJson(stringInJsonFormat, Pair.class); - } catch(JsonIOException e){ - Logger.exception("Conversion from JSON to Pair failed ", e); - return null; - } - } } From a45d5cf481238a2592d8877ab6e7709012044909 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 8 Aug 2024 16:03:57 +0530 Subject: [PATCH 24/63] Fix test --- .../org/commcare/android/tests/processing/FormStorageTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/unit-tests/src/org/commcare/android/tests/processing/FormStorageTest.java b/app/unit-tests/src/org/commcare/android/tests/processing/FormStorageTest.java index 7612a2e8c1..4a1bc4fc5a 100644 --- a/app/unit-tests/src/org/commcare/android/tests/processing/FormStorageTest.java +++ b/app/unit-tests/src/org/commcare/android/tests/processing/FormStorageTest.java @@ -360,6 +360,7 @@ public class FormStorageTest { // Added in 2.55 , "org.javarosa.core.model.FormIndex" + , "org.commcare.models.database.InterruptedFormState" ); From 550d26df030389557765b43dc5ae314178d5976b Mon Sep 17 00:00:00 2001 From: Clayton Sims Date: Thu, 8 Aug 2024 11:46:52 -0500 Subject: [PATCH 25/63] Move autosave preference from developer options to user settings --- app/res/xml/main_preferences.xml | 7 +++++++ app/res/xml/preferences_developer.xml | 7 ------- app/src/org/commcare/activities/FormEntryActivity.java | 3 ++- .../org/commcare/preferences/DeveloperPreferences.java | 7 ------- .../preferences/MainConfigurablePreferences.java | 9 +++++++++ 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/app/res/xml/main_preferences.xml b/app/res/xml/main_preferences.xml index 82de52108e..11985e64df 100644 --- a/app/res/xml/main_preferences.xml +++ b/app/res/xml/main_preferences.xml @@ -54,6 +54,13 @@ android:entryValues="@array/pref_enabled_vals" android:key="cc-enable-tts" android:title="Enable Text To Speech"/> + - diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index e14ec5884e..fc260ef95a 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -61,6 +61,7 @@ import org.commcare.models.database.SqlStorage; import org.commcare.preferences.DeveloperPreferences; import org.commcare.preferences.HiddenPreferences; +import org.commcare.preferences.MainConfigurablePreferences; import org.commcare.services.FCMMessageData; import org.commcare.services.PendingSyncAlertBroadcastReceiver; import org.commcare.tasks.FormLoaderTask; @@ -935,7 +936,7 @@ protected void onStop() { private boolean shouldSaveFormOnStop() { // if feature enabled and the form has loaded and another widget workflow is not in progress and we // ourselves have not called exit as part of user workflow - return DeveloperPreferences.isAutoSaveFormOnPause() && formHasLoaded() && !triggeredExit; + return MainConfigurablePreferences.isAutoSaveFormOnPause() && formHasLoaded() && !triggeredExit; } private void saveInlineVideoState() { diff --git a/app/src/org/commcare/preferences/DeveloperPreferences.java b/app/src/org/commcare/preferences/DeveloperPreferences.java index 080cc48ead..75e4fc4ddb 100644 --- a/app/src/org/commcare/preferences/DeveloperPreferences.java +++ b/app/src/org/commcare/preferences/DeveloperPreferences.java @@ -77,8 +77,6 @@ public class DeveloperPreferences extends CommCarePreferenceFragment { public final static String ALTERNATE_QUESTION_LAYOUT_ENABLED = "cc-alternate-question-text-format"; public final static String OFFER_PIN_FOR_LOGIN = "cc-offer-pin-for-login"; - public final static String AUTO_SAVE_FORM_ON_PAUSE = "cc-auto-form-save-on-pause"; - private static final Set WHITELISTED_DEVELOPER_PREF_KEYS = new HashSet<>(); static { @@ -87,7 +85,6 @@ public class DeveloperPreferences extends CommCarePreferenceFragment { WHITELISTED_DEVELOPER_PREF_KEYS.add(AUTO_PURGE_ENABLED); WHITELISTED_DEVELOPER_PREF_KEYS.add(ALTERNATE_QUESTION_LAYOUT_ENABLED); WHITELISTED_DEVELOPER_PREF_KEYS.add(ENABLE_CERTIFICATE_TRANSPARENCY); - WHITELISTED_DEVELOPER_PREF_KEYS.add(AUTO_SAVE_FORM_ON_PAUSE); } /** @@ -431,10 +428,6 @@ public static boolean useExpressionCachingInForms() { return doesPropertyMatch(USE_EXPRESSION_CACHING_IN_FORMS, PrefValues.NO, PrefValues.YES); } - public static boolean isAutoSaveFormOnPause() { - return doesPropertyMatch(AUTO_SAVE_FORM_ON_PAUSE, PrefValues.NO, PrefValues.YES); - } - private void hideOrShowDangerousSettings() { Preference[] onScreenPrefs = getOnScreenPrefs(); if (!GlobalPrivilegesManager.isAdvancedSettingsAccessEnabled() && !BuildConfig.DEBUG) { diff --git a/app/src/org/commcare/preferences/MainConfigurablePreferences.java b/app/src/org/commcare/preferences/MainConfigurablePreferences.java index aecdf0f9b8..2f6f671436 100755 --- a/app/src/org/commcare/preferences/MainConfigurablePreferences.java +++ b/app/src/org/commcare/preferences/MainConfigurablePreferences.java @@ -35,6 +35,7 @@ public class MainConfigurablePreferences public final static String ANALYTICS_ENABLED = "cc-analytics-enabled"; public final static String INTENT_CALLOUT_FOR_SCANNER = "cc-intent-callout-for-scanner"; public final static String ENABLE_TEXT_TO_SPEECH = "cc-enable-tts"; + public final static String AUTO_SAVE_FORM_ON_PAUSE = "cc-auto-form-save-on-pause"; // Fake settings that really act as buttons to open a new activity or choice dialog private final static String DEVELOPER_SETTINGS = "developer-settings-button"; @@ -182,6 +183,14 @@ public static boolean useIntentCalloutForScanner() { return app.getAppPreferences().getString(INTENT_CALLOUT_FOR_SCANNER, PrefValues.NO).equals(PrefValues.YES); } + public static boolean isAutoSaveFormOnPause() { + CommCareApp app = CommCareApplication.instance().getCurrentApp(); + if (app == null) { + return false; + } + return app.getAppPreferences().getString(AUTO_SAVE_FORM_ON_PAUSE, PrefValues.NO).equals(PrefValues.YES); + } + public static boolean isTTSEnabled() { CommCareApp app = CommCareApplication.instance().getCurrentApp(); if (app == null) { From 4fd4cee17af11200e0af168d624c79f017c163c7 Mon Sep 17 00:00:00 2001 From: Clayton Sims Date: Thu, 8 Aug 2024 16:07:26 -0500 Subject: [PATCH 26/63] Add logging when session recovery is identified --- app/src/org/commcare/activities/FormEntryActivity.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index fc260ef95a..52d17a23b9 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -1036,6 +1036,9 @@ private void loadForm() { // only retrieve a potentially stored form index when loading an existing form record AndroidSessionWrapper asw = CommCareApplication.instance().getCurrentSessionWrapper(); lastFormIndex = retrieveAndValidateFormIndex(asw.getSessionDescriptorId()); + if (lastFormIndex != null) { + Logger.log(LogTypes.TYPE_FORM_ENTRY, "Recovering form entry session"); + } } else if (intent.hasExtra(KEY_FORM_DEF_ID)) { formId = intent.getIntExtra(KEY_FORM_DEF_ID, -1); instanceState.setFormDefPath(FormFileSystemHelpers.getFormDefPath(formDefStorage, formId)); From 50ef0b301808173ed763ae364f668b4bb1eda034 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 9 Aug 2024 12:05:25 +0530 Subject: [PATCH 27/63] Adds logging on user session expiry --- app/src/org/commcare/services/CommCareSessionService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/org/commcare/services/CommCareSessionService.java b/app/src/org/commcare/services/CommCareSessionService.java index f87ec91213..205a81342a 100644 --- a/app/src/org/commcare/services/CommCareSessionService.java +++ b/app/src/org/commcare/services/CommCareSessionService.java @@ -364,6 +364,7 @@ private void timeToExpireSession() { return; } try { + Logger.log(LogTypes.TYPE_USER, "Expiring user session forcefully due to session timeout"); CommCareApplication.instance().expireUserSession(); } finally { CommCareSessionService.sessionAliveLock.unlock(); @@ -385,6 +386,7 @@ private void timeToExpireSession() { } try { + Logger.log(LogTypes.TYPE_USER, "Expiring user session due to session timeout"); saveFormAndCloseSession(); } finally { CommCareSessionService.sessionAliveLock.unlock(); From 20104f4e2177a1fe20c748127e50a913d5c17f1a Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 9 Aug 2024 18:23:25 +0530 Subject: [PATCH 28/63] Clear state on user exiting/saving the form --- app/src/org/commcare/activities/FormEntryActivity.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 52d17a23b9..3415afcc08 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -1380,6 +1380,9 @@ private void finishReturnInstance() { * activity */ private void finishReturnInstance(boolean reportSaved) { + HiddenPreferences.clearInterruptedFormState(); + HiddenPreferences.clearInterruptedSSD(); + String action = getIntent().getAction(); if (Intent.ACTION_PICK.equals(action) || Intent.ACTION_EDIT.equals(action)) { Intent formReturnIntent = new Intent(); From ee4878192ef464a3da018ed6a5369090868abe94 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Mon, 12 Aug 2024 14:00:13 +0530 Subject: [PATCH 29/63] Show button to re-process quarantine form even for local process errored form --- .../commcare/activities/FormRecordListActivity.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/src/org/commcare/activities/FormRecordListActivity.java b/app/src/org/commcare/activities/FormRecordListActivity.java index 38cba1e2ed..51e5348b15 100644 --- a/app/src/org/commcare/activities/FormRecordListActivity.java +++ b/app/src/org/commcare/activities/FormRecordListActivity.java @@ -396,14 +396,8 @@ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuIn if (FormRecord.STATUS_QUARANTINED.equals(value.getStatus())) { menu.add(Menu.NONE, VIEW_QUARANTINE_REASON, VIEW_QUARANTINE_REASON, Localization.get("app.workflow.forms.view.quarantine.reason")); - - if (!FormRecord.QuarantineReason_LOCAL_PROCESSING_ERROR.equals(value.getQuarantineReasonType())) { - // Records that were quarantined due to a local processing error can't attempt - // re-submission, since doing so would send them straight to "Unsent" when they - // haven't even been processed - menu.add(Menu.NONE, RESTORE_RECORD, RESTORE_RECORD, - Localization.get("app.workflow.forms.restore")); - } + menu.add(Menu.NONE, RESTORE_RECORD, RESTORE_RECORD, + Localization.get("app.workflow.forms.restore")); } menu.add(Menu.NONE, SCAN_RECORD, SCAN_RECORD, Localization.get("app.workflow.forms.scan")); From 20e00aa952bab43551f4b55d2a485e048a3b3e6e Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Fri, 9 Aug 2024 11:41:51 +0200 Subject: [PATCH 30/63] Remove unused methods --- .../commcare/activities/FormEntryActivity.java | 15 --------------- .../org/commcare/logic/AndroidFormController.java | 14 -------------- 2 files changed, 29 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 3415afcc08..dcdd05d34f 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -14,7 +14,6 @@ import android.os.Build; import android.os.Bundle; import android.speech.tts.TextToSpeech; -import android.util.Base64; import android.util.Log; import android.util.Pair; import android.view.ContextMenu; @@ -59,7 +58,6 @@ import org.commcare.models.FormRecordProcessor; import org.commcare.models.database.InterruptedFormState; import org.commcare.models.database.SqlStorage; -import org.commcare.preferences.DeveloperPreferences; import org.commcare.preferences.HiddenPreferences; import org.commcare.preferences.MainConfigurablePreferences; import org.commcare.services.FCMMessageData; @@ -73,7 +71,6 @@ import org.commcare.utils.CompoundIntentList; import org.commcare.utils.FileUtil; import org.commcare.utils.GeoUtils; -import org.commcare.utils.SerializationUtil; import org.commcare.utils.SessionUnavailableException; import org.commcare.utils.StringUtils; import org.commcare.views.QuestionsView; @@ -1109,18 +1106,6 @@ private FormIndex retrieveAndValidateFormIndex(int sessionDescriptorId) { return null; } - private FormIndex deserializeFormIndex(String serializedFormIndex) { - if (serializedFormIndex != null) { - try{ - byte[] decodedFormIndex = Base64.decode(serializedFormIndex, Base64.DEFAULT); - return SerializationUtil.deserialize(decodedFormIndex, FormIndex.class); - } catch(Exception e) { - Logger.exception("Deserialization of last form index failed ", e); - } - } - return null; - } - private void handleFormLoadCompletion(AndroidFormController fc) { if (PollSensorAction.XPATH_ERROR_ACTION.equals(locationRecieverErrorAction)) { handleXpathErrorBroadcast(); diff --git a/app/src/org/commcare/logic/AndroidFormController.java b/app/src/org/commcare/logic/AndroidFormController.java index 1db51c7531..57591535a2 100644 --- a/app/src/org/commcare/logic/AndroidFormController.java +++ b/app/src/org/commcare/logic/AndroidFormController.java @@ -1,15 +1,12 @@ package org.commcare.logic; -import android.util.Base64; import androidx.annotation.NonNull; import org.commcare.google.services.analytics.FormAnalyticsHelper; -import org.commcare.utils.SerializationUtil; import org.commcare.views.widgets.WidgetFactory; import org.javarosa.core.model.FormDef; import org.javarosa.core.model.FormIndex; -import org.javarosa.core.services.Logger; import org.javarosa.form.api.FormController; import org.javarosa.form.api.FormEntryController; @@ -89,15 +86,4 @@ public FormAnalyticsHelper getFormAnalyticsHelper() { public FormDef getFormDef() { return mFormEntryController.getModel().getForm(); } - - // TODO: we should cache this - public String getSerializedFormIndex() { - try{ - byte[] serializedFormIndex = SerializationUtil.serialize(getFormIndex()); - return Base64.encodeToString(serializedFormIndex, Base64.DEFAULT); - } catch (Exception e){ - Logger.exception("Serialization of last form index failed ", e); - return null; - } - } } From b34b7cb2964d316c358642279a12f705ad01c81e Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Fri, 9 Aug 2024 11:43:05 +0200 Subject: [PATCH 31/63] Remove comment --- app/src/org/commcare/preferences/HiddenPreferences.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/org/commcare/preferences/HiddenPreferences.java b/app/src/org/commcare/preferences/HiddenPreferences.java index f4ec173ed6..b18b2af379 100644 --- a/app/src/org/commcare/preferences/HiddenPreferences.java +++ b/app/src/org/commcare/preferences/HiddenPreferences.java @@ -645,7 +645,6 @@ public static void setInterruptedFormState(InterruptedFormState interruptedFormS } } - // This was changed to Double due to the way Gson handles numeric values public static InterruptedFormState getInterruptedFormState() { try { String currentUserId = CommCareApplication.instance().getCurrentUserId(); From 64c6f70ec9ace385e9c0c201b910379efba56e1d Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Fri, 9 Aug 2024 11:44:45 +0200 Subject: [PATCH 32/63] Refactor --- app/src/org/commcare/activities/FormEntryActivity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index dcdd05d34f..02eff5482a 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -1107,6 +1107,8 @@ private FormIndex retrieveAndValidateFormIndex(int sessionDescriptorId) { } private void handleFormLoadCompletion(AndroidFormController fc) { + HiddenPreferences.clearInterruptedFormState(); + if (PollSensorAction.XPATH_ERROR_ACTION.equals(locationRecieverErrorAction)) { handleXpathErrorBroadcast(); } @@ -1144,8 +1146,6 @@ private void handleFormLoadCompletion(AndroidFormController fc) { Toast.makeText(this, Localization.get("form.entry.restart.after.expiration"), Toast.LENGTH_LONG).show(); } - - HiddenPreferences.clearInterruptedFormState(); } private void handleXpathErrorBroadcast() { From 7bbf4f30a87558759aff95a5693bcbe9da96d23d Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 5 Sep 2024 14:55:08 +0530 Subject: [PATCH 33/63] Logs while saving and clearing the interrupted state --- app/src/org/commcare/preferences/HiddenPreferences.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/org/commcare/preferences/HiddenPreferences.java b/app/src/org/commcare/preferences/HiddenPreferences.java index b18b2af379..f435f5831e 100644 --- a/app/src/org/commcare/preferences/HiddenPreferences.java +++ b/app/src/org/commcare/preferences/HiddenPreferences.java @@ -8,6 +8,7 @@ import org.commcare.android.logging.ReportingUtils; import org.commcare.models.database.InterruptedFormState; import org.commcare.services.FCMMessageData; +import org.commcare.util.LogTypes; import org.commcare.utils.AndroidCommCarePlatform; import org.commcare.utils.FirebaseMessagingUtil; import org.commcare.utils.GeoUtils; @@ -296,6 +297,7 @@ public static void setPostUpdateSyncNeeded(boolean b) { } public static void setInterruptedSSD(int ssdId) { + Logger.log(LogTypes.TYPE_MAINTENANCE, "Saving interrupted state"); String currentUserId = CommCareApplication.instance().getCurrentUserId(); CommCareApplication.instance().getCurrentApp().getAppPreferences().edit() .putInt(ID_OF_INTERRUPTED_SSD + currentUserId, ssdId).apply(); @@ -308,6 +310,7 @@ public static int getIdOfInterruptedSSD() { } public static void clearInterruptedSSD() { + Logger.log(LogTypes.TYPE_MAINTENANCE, "Clearing interrupted state"); String currentUserId = CommCareApplication.instance().getCurrentUserId(); CommCareApplication.instance().getCurrentApp().getAppPreferences().edit() .putInt(ID_OF_INTERRUPTED_SSD + currentUserId, -1).apply(); From 2e0274fbb9619c3e56db6540ac8891f704b092da Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 5 Sep 2024 14:55:41 +0530 Subject: [PATCH 34/63] Clears interruptes state as soon as we are saving a completed form --- app/src/org/commcare/activities/FormEntryActivity.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 02eff5482a..b5f8a60792 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -839,6 +839,11 @@ private void saveDataToDisk(boolean exit, boolean complete, String updatedSaveNa return; } + if (complete) { + HiddenPreferences.clearInterruptedFormState(); + HiddenPreferences.clearInterruptedSSD(); + } + mSaveToDiskTask = new SaveToDiskTask(getIntent().getIntExtra(KEY_FORM_RECORD_ID, -1), getIntent().getIntExtra(KEY_FORM_DEF_ID, -1), FormEntryInstanceState.mFormRecordPath, From 294848f99ebf98230d10502f1e4a4ac30f3a3243 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 5 Sep 2024 12:49:10 +0530 Subject: [PATCH 35/63] Do not save read only forms again --- app/src/org/commcare/activities/FormEntryActivity.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index b5f8a60792..7fa37ddea2 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -820,6 +820,10 @@ private void saveDataToDisk(boolean exit, boolean complete, String updatedSaveNa return; } + if (mFormController.isFormReadOnly()) { + return; + } + // save current answer; if headless, don't evaluate the constraints // before doing so. boolean wasScreenSaved = From 031978cac6753d0552fde544e19ca82ccd079a14 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Thu, 5 Sep 2024 13:22:31 +0530 Subject: [PATCH 36/63] Reword Finish to Exit for read only forms --- app/assets/locales/android_translatable_strings.txt | 1 + .../commcare/activities/FormEntryActivityUIController.java | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/locales/android_translatable_strings.txt b/app/assets/locales/android_translatable_strings.txt index 588441a279..932d7b29b8 100644 --- a/app/assets/locales/android_translatable_strings.txt +++ b/app/assets/locales/android_translatable_strings.txt @@ -167,6 +167,7 @@ form.entry.incomplete.save.success=Form saved as incomplete form.entry.save.error=Sorry, form save failed. Please contact CommCare Support to look into the issue. form.entry.save.invalid.unicode=Could not save '${0}' text in form. form.entry.finish.button=FINISH +form.entry.exit.button=EXIT form.entry.restart.after.expiration=You were logged out due to session expiration. The form you were in the middle of has been saved and resumed. login.attempt.badcred=Username or password are incorrect. Please try again. diff --git a/app/src/org/commcare/activities/FormEntryActivityUIController.java b/app/src/org/commcare/activities/FormEntryActivityUIController.java index f917a62542..e7388a9753 100644 --- a/app/src/org/commcare/activities/FormEntryActivityUIController.java +++ b/app/src/org/commcare/activities/FormEntryActivityUIController.java @@ -82,6 +82,7 @@ public class FormEntryActivityUIController implements CommCareActivityUIControll private boolean formRelevanciesUpdateInProgress = false; private static final String KEY_LAST_CHANGED_WIDGET = "index-of-last-changed-widget"; + private TextView finishText; enum AnimationType { LEFT, RIGHT, FADE @@ -103,7 +104,7 @@ public void setupUI() { View finishButton = activity.findViewById(R.id.nav_btn_finish); - TextView finishText = finishButton.findViewById(R.id.nav_btn_finish_text); + finishText = finishButton.findViewById(R.id.nav_btn_finish_text); finishText.setText(Localization.get("form.entry.finish.button").toUpperCase()); nextButton.setOnClickListener(v -> { @@ -199,6 +200,10 @@ protected void refreshCurrentView(boolean animateLastView) { QuestionsView current = createView(); showView(current, AnimationType.FADE, animateLastView); } + + if (finishText != null && FormEntryActivity.mFormController.isFormReadOnly()) { + finishText.setText(Localization.get("form.entry.exit.button").toUpperCase()); + } } /** From 658de7d69a50a7cbc1f81651e3ae8af5494c5ea4 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 5 Sep 2024 17:52:23 +0200 Subject: [PATCH 37/63] Add logging around form entry start --- .../commcare/activities/HomeScreenBaseActivity.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/org/commcare/activities/HomeScreenBaseActivity.java b/app/src/org/commcare/activities/HomeScreenBaseActivity.java index e5eb7e1c94..fdac431f72 100644 --- a/app/src/org/commcare/activities/HomeScreenBaseActivity.java +++ b/app/src/org/commcare/activities/HomeScreenBaseActivity.java @@ -415,6 +415,12 @@ private boolean tryRestoringFormFromSessionExpiration() { if (existing != null) { AndroidSessionWrapper state = CommCareApplication.instance().getCurrentSessionWrapper(); state.loadFromStateDescription(existing); + + FormRecord formRecord = state.getFormRecord(); + //TODO: Temporary for GD, to remove + Logger.log(LogTypes.TYPE_FORM_ENTRY, "Restoring form from expired Session |" + + (formRecord.getInstanceID() == null ? "" : formRecord.getInstanceID() + "|") + + formRecord.getFormNamespace()); formEntry(CommCareApplication.instance().getCommCarePlatform() .getFormDefId(state.getSession().getForm()), state.getFormRecord(), null, true); @@ -632,6 +638,11 @@ int record = intent.getIntExtra("FORMRECORDS", -1); AndroidCommCarePlatform platform = CommCareApplication.instance().getCommCarePlatform(); + + //TODO: Temporary for GD, to remove + Logger.log(LogTypes.TYPE_FORM_ENTRY, "Loading an incomplete form |" + + (r.getInstanceID() == null ? "" : r.getInstanceID() + "|") + + r.getFormNamespace()); formEntry(platform.getFormDefId(r.getFormNamespace()), r); return; } From 63c476e7fe9dfea91a4edb75d3e5371431cf342f Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Fri, 13 Sep 2024 10:35:05 +0200 Subject: [PATCH 38/63] Log when CommCare is terminated --- app/src/org/commcare/CommCareApplication.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/org/commcare/CommCareApplication.java b/app/src/org/commcare/CommCareApplication.java index 0840612148..54ee507644 100644 --- a/app/src/org/commcare/CommCareApplication.java +++ b/app/src/org/commcare/CommCareApplication.java @@ -147,6 +147,8 @@ import okhttp3.MultipartBody; import okhttp3.RequestBody; +import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY; + public class CommCareApplication extends MultiDexApplication implements LifecycleEventObserver { private static final String TAG = CommCareApplication.class.getSimpleName(); @@ -1233,13 +1235,9 @@ private void setRxJavaGlobalHandler() { @Override public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) { - Logger.log("commcare-state-changed", "Lifecycle.Event : " + event.name()); - switch (event.name()) { - case "ON_START": - case "ON_RESUME": - case "ON_STOP": - break; - case "ON_PAUSE": + switch (event) { + case ON_DESTROY: + Logger.log(LogTypes.TYPE_MAINTENANCE, "CommCare has been closed"); break; } } From 7b599c0253376770008c6f215bc3370176bfabb0 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 12 Aug 2024 11:43:31 +0200 Subject: [PATCH 39/63] Update model --- .../org/commcare/models/AndroidSessionWrapper.java | 2 +- .../models/database/InterruptedFormState.java | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/org/commcare/models/AndroidSessionWrapper.java b/app/src/org/commcare/models/AndroidSessionWrapper.java index b2cd353db8..8b045acb66 100755 --- a/app/src/org/commcare/models/AndroidSessionWrapper.java +++ b/app/src/org/commcare/models/AndroidSessionWrapper.java @@ -190,7 +190,7 @@ public void setCurrentStateAsInterrupted(FormIndex formIndex) { SqlStorage sessionStorage = CommCareApplication.instance().getUserStorage(SessionStateDescriptor.class); SessionStateDescriptor current = sessionStorage.read(sessionStateRecordId); - InterruptedFormState interruptedFormState = new InterruptedFormState(current.getID(), formIndex); + InterruptedFormState interruptedFormState = new InterruptedFormState(current.getID(), formIndex, current.getFormRecordId()); HiddenPreferences.setInterruptedSSD(current.getID()); HiddenPreferences.setInterruptedFormState(interruptedFormState); } diff --git a/app/src/org/commcare/models/database/InterruptedFormState.java b/app/src/org/commcare/models/database/InterruptedFormState.java index ce72df9729..fdec4975cf 100644 --- a/app/src/org/commcare/models/database/InterruptedFormState.java +++ b/app/src/org/commcare/models/database/InterruptedFormState.java @@ -15,12 +15,14 @@ */ public class InterruptedFormState implements Externalizable { - int sessionStateDescriptorId; - FormIndex formIndex; + private int sessionStateDescriptorId; + private FormIndex formIndex; + private int formRecordId; - public InterruptedFormState(int sessionStateDescriptorId, FormIndex formIndex) { + public InterruptedFormState(int sessionStateDescriptorId, FormIndex formIndex, int formRecordId) { this.sessionStateDescriptorId = sessionStateDescriptorId; this.formIndex = formIndex; + this.formRecordId = formRecordId; } public InterruptedFormState() { @@ -33,12 +35,14 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException { sessionStateDescriptorId = ExtUtil.readInt(in); formIndex = (FormIndex)ExtUtil.read(in, FormIndex.class, pf); + formRecordId = ExtUtil.readInt(in); } @Override public void writeExternal(DataOutputStream out) throws IOException { ExtUtil.writeNumeric(out, sessionStateDescriptorId); ExtUtil.write(out, formIndex); + ExtUtil.writeNumeric(out, formRecordId); } public int getSessionStateDescriptorId() { @@ -48,4 +52,8 @@ public int getSessionStateDescriptorId() { public FormIndex getFormIndex() { return formIndex; } + + public int getFormRecordId() { + return formRecordId; + } } From acbce289c466ddb7d35b40aaeafd41615f2eb166 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 12 Aug 2024 11:45:33 +0200 Subject: [PATCH 40/63] Check form record Id when restoring Form index --- .../commcare/activities/FormEntryActivity.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 7fa37ddea2..12651af724 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -1032,16 +1032,16 @@ private void loadForm() { SqlStorage formDefStorage = CommCareApplication.instance() .getAppStorage(FormDefRecord.class); if (intent.hasExtra(KEY_FORM_RECORD_ID)) { - Pair instanceAndStatus = instanceState.getFormDefIdForRecord( - formDefStorage, - intent.getIntExtra(KEY_FORM_RECORD_ID, -1), - instanceState); + int formRecordId = intent.getIntExtra(KEY_FORM_RECORD_ID, -1); + Pair instanceAndStatus = instanceState.getFormDefIdForRecord(formDefStorage, + formRecordId, instanceState); + formId = instanceAndStatus.first; instanceIsReadOnly = instanceAndStatus.second; // only retrieve a potentially stored form index when loading an existing form record - AndroidSessionWrapper asw = CommCareApplication.instance().getCurrentSessionWrapper(); - lastFormIndex = retrieveAndValidateFormIndex(asw.getSessionDescriptorId()); + lastFormIndex = retrieveAndValidateFormIndex( + CommCareApplication.instance().getCurrentSessionWrapper()); if (lastFormIndex != null) { Logger.log(LogTypes.TYPE_FORM_ENTRY, "Recovering form entry session"); } @@ -1104,10 +1104,12 @@ protected void deliverError(FormEntryActivity receiver, Exception e) { } } - private FormIndex retrieveAndValidateFormIndex(int sessionDescriptorId) { + private FormIndex retrieveAndValidateFormIndex(AndroidSessionWrapper androidSessionWrapper) { InterruptedFormState interruptedFormState = HiddenPreferences.getInterruptedFormState(); - if (interruptedFormState!= null && interruptedFormState.getSessionStateDescriptorId() == sessionDescriptorId) { + if (interruptedFormState!= null + && interruptedFormState.getSessionStateDescriptorId() == androidSessionWrapper.getSessionDescriptorId() + && interruptedFormState.getFormRecordId() == androidSessionWrapper.getFormRecordId()) { return interruptedFormState.getFormIndex(); } // data format is invalid, so better to clear the data From 8cc2dbd22817f8275454337aabcc23fee755fb24 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 16 Sep 2024 19:48:47 +0200 Subject: [PATCH 41/63] Set default state to formRecordId --- app/src/org/commcare/activities/FormEntryActivity.java | 3 ++- app/src/org/commcare/models/database/InterruptedFormState.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 12651af724..927d100636 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -1109,7 +1109,8 @@ private FormIndex retrieveAndValidateFormIndex(AndroidSessionWrapper androidSess HiddenPreferences.getInterruptedFormState(); if (interruptedFormState!= null && interruptedFormState.getSessionStateDescriptorId() == androidSessionWrapper.getSessionDescriptorId() - && interruptedFormState.getFormRecordId() == androidSessionWrapper.getFormRecordId()) { + && (interruptedFormState.getFormRecordId() == -1 + || interruptedFormState.getFormRecordId() == androidSessionWrapper.getFormRecordId())) { return interruptedFormState.getFormIndex(); } // data format is invalid, so better to clear the data diff --git a/app/src/org/commcare/models/database/InterruptedFormState.java b/app/src/org/commcare/models/database/InterruptedFormState.java index fdec4975cf..8849d29ee1 100644 --- a/app/src/org/commcare/models/database/InterruptedFormState.java +++ b/app/src/org/commcare/models/database/InterruptedFormState.java @@ -17,7 +17,7 @@ public class InterruptedFormState implements Externalizable { private int sessionStateDescriptorId; private FormIndex formIndex; - private int formRecordId; + private int formRecordId = -1; public InterruptedFormState(int sessionStateDescriptorId, FormIndex formIndex, int formRecordId) { this.sessionStateDescriptorId = sessionStateDescriptorId; From ad2501da72ec3fdeeedf67a5e810be1b2181e20a Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 16 Sep 2024 21:07:14 +0200 Subject: [PATCH 42/63] Handle update from previous version of InterruptedFormState model --- .../commcare/models/database/InterruptedFormState.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/org/commcare/models/database/InterruptedFormState.java b/app/src/org/commcare/models/database/InterruptedFormState.java index 8849d29ee1..a4dd6fbab6 100644 --- a/app/src/org/commcare/models/database/InterruptedFormState.java +++ b/app/src/org/commcare/models/database/InterruptedFormState.java @@ -8,6 +8,7 @@ import java.io.DataInputStream; import java.io.DataOutputStream; +import java.io.EOFException; import java.io.IOException; /** @@ -35,7 +36,12 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException { sessionStateDescriptorId = ExtUtil.readInt(in); formIndex = (FormIndex)ExtUtil.read(in, FormIndex.class, pf); - formRecordId = ExtUtil.readInt(in); + try{ + formRecordId = ExtUtil.readInt(in); + } catch(EOFException e){ + // this is to catch errors caused by EOF when updating from the previous model which didn't have the + // formRecordId field + } } @Override From 5ad2c02031368431e2880cb5ff12f74389b2fc19 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 19 Sep 2024 00:15:31 +0200 Subject: [PATCH 43/63] Add SAVE_UNRECOVERABLE_ERROR SaveStatus for malformed or corrupted form XML --- app/src/org/commcare/tasks/SaveToDiskTask.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/org/commcare/tasks/SaveToDiskTask.java b/app/src/org/commcare/tasks/SaveToDiskTask.java index 3f99d0f0e0..490f30fd18 100644 --- a/app/src/org/commcare/tasks/SaveToDiskTask.java +++ b/app/src/org/commcare/tasks/SaveToDiskTask.java @@ -58,6 +58,7 @@ public enum SaveStatus { SAVED_COMPLETE, SAVED_INCOMPLETE, SAVE_ERROR, + SAVE_UNRECOVERABLE_ERROR, INVALID_ANSWER, SAVED_AND_EXIT } @@ -126,7 +127,7 @@ protected ResultAndError doTaskBackground(Void... nothing) { // Likely a user level issue, so send error to HQ as a app build error XPathErrorLogger.INSTANCE.logErrorToCurrentApp(cleanedMessage); - return new ResultAndError<>(SaveStatus.SAVE_ERROR, cleanedMessage); + return new ResultAndError<>(SaveStatus.SAVE_UNRECOVERABLE_ERROR, cleanedMessage); } if (mMarkCompleted) { From cdda600023156270aa5447469aafa5b4d215a353 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 19 Sep 2024 00:28:52 +0200 Subject: [PATCH 44/63] Quarantine only malformed or corrupted form XML as local processing error --- .../commcare/activities/FormEntryActivity.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 7fa37ddea2..b87959b42a 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -108,7 +108,9 @@ import androidx.core.app.ActivityCompat; import static org.commcare.android.database.user.models.FormRecord.QuarantineReason_LOCAL_PROCESSING_ERROR; +import static org.commcare.android.database.user.models.FormRecord.QuarantineReason_RECORD_ERROR; import static org.commcare.sync.FirebaseMessagingDataSyncer.PENGING_SYNC_ALERT_ACTION; +import static org.commcare.tasks.SaveToDiskTask.SaveStatus.SAVE_UNRECOVERABLE_ERROR; /** * Displays questions, animates transitions between @@ -1301,13 +1303,15 @@ public void savingComplete(SaveToDiskTask.SaveStatus saveStatus, String errorMes uiController.refreshView(); saveAnswersForCurrentScreen(true); return; - case SAVE_ERROR: + case SAVE_ERROR, SAVE_UNRECOVERABLE_ERROR: if (!CommCareApplication.instance().isConsumerApp()) { new UserfacingErrorHandling<>().createErrorDialog(this, errorMessage, Localization.get("notification.formentry.save_error.title"), FormEntryConstants.EXIT); } - quarantineRecordOnError(errorMessage); + String reasonType = (saveStatus == SAVE_UNRECOVERABLE_ERROR) ? QuarantineReason_LOCAL_PROCESSING_ERROR + : QuarantineReason_RECORD_ERROR ; + quarantineRecordOnError(errorMessage, reasonType); return; } if (!"".equals(toastMessage) && !CommCareApplication.instance().isConsumerApp()) { @@ -1318,17 +1322,13 @@ public void savingComplete(SaveToDiskTask.SaveStatus saveStatus, String errorMes } // clean the form record in case it was saved - private void quarantineRecordOnError(String errorMessage) { + private void quarantineRecordOnError(String errorMessage, String reasonType) { AndroidSessionWrapper currentState = CommCareApplication.instance().getCurrentSessionWrapper(); FormRecord toBeQuarantined = currentState.getFormRecord(); // quarantine in case the form record was saved if (toBeQuarantined != null) { - new FormRecordProcessor(this).quarantineRecord( - toBeQuarantined, - QuarantineReason_LOCAL_PROCESSING_ERROR, - errorMessage - ); + new FormRecordProcessor(this).quarantineRecord(toBeQuarantined, reasonType, errorMessage); } } From 621792e49178c1939836b892ec0f4a0976f9836f Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 19 Sep 2024 00:37:50 +0200 Subject: [PATCH 45/63] Revert "Show button to re-process quarantine form even for local process errored form" This reverts commit ee4878192ef464a3da018ed6a5369090868abe94. --- .../commcare/activities/FormRecordListActivity.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/org/commcare/activities/FormRecordListActivity.java b/app/src/org/commcare/activities/FormRecordListActivity.java index 51e5348b15..38cba1e2ed 100644 --- a/app/src/org/commcare/activities/FormRecordListActivity.java +++ b/app/src/org/commcare/activities/FormRecordListActivity.java @@ -396,8 +396,14 @@ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuIn if (FormRecord.STATUS_QUARANTINED.equals(value.getStatus())) { menu.add(Menu.NONE, VIEW_QUARANTINE_REASON, VIEW_QUARANTINE_REASON, Localization.get("app.workflow.forms.view.quarantine.reason")); - menu.add(Menu.NONE, RESTORE_RECORD, RESTORE_RECORD, - Localization.get("app.workflow.forms.restore")); + + if (!FormRecord.QuarantineReason_LOCAL_PROCESSING_ERROR.equals(value.getQuarantineReasonType())) { + // Records that were quarantined due to a local processing error can't attempt + // re-submission, since doing so would send them straight to "Unsent" when they + // haven't even been processed + menu.add(Menu.NONE, RESTORE_RECORD, RESTORE_RECORD, + Localization.get("app.workflow.forms.restore")); + } } menu.add(Menu.NONE, SCAN_RECORD, SCAN_RECORD, Localization.get("app.workflow.forms.scan")); From 429983161ec4011dbdec16d7cb6662c15339784e Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 19 Sep 2024 10:25:08 +0200 Subject: [PATCH 46/63] Nit --- app/src/org/commcare/models/database/InterruptedFormState.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/org/commcare/models/database/InterruptedFormState.java b/app/src/org/commcare/models/database/InterruptedFormState.java index a4dd6fbab6..c90066dc21 100644 --- a/app/src/org/commcare/models/database/InterruptedFormState.java +++ b/app/src/org/commcare/models/database/InterruptedFormState.java @@ -36,7 +36,7 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException { sessionStateDescriptorId = ExtUtil.readInt(in); formIndex = (FormIndex)ExtUtil.read(in, FormIndex.class, pf); - try{ + try { formRecordId = ExtUtil.readInt(in); } catch(EOFException e){ // this is to catch errors caused by EOF when updating from the previous model which didn't have the From 7b57114e32f2e5a878b73eebb47546179abbea3c Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 19 Sep 2024 11:14:23 +0200 Subject: [PATCH 47/63] Expand SAVE_UNRECOVERABLE_ERROR to other applicable error types --- app/src/org/commcare/tasks/SaveToDiskTask.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/org/commcare/tasks/SaveToDiskTask.java b/app/src/org/commcare/tasks/SaveToDiskTask.java index 490f30fd18..5a6d7a3c30 100644 --- a/app/src/org/commcare/tasks/SaveToDiskTask.java +++ b/app/src/org/commcare/tasks/SaveToDiskTask.java @@ -101,7 +101,7 @@ protected ResultAndError doTaskBackground(Void... nothing) { } catch (XPathException xpe) { String cleanedMessage = "An error in your form prevented it from saving: \n" + xpe.getMessage(); - return new ResultAndError<>(SaveStatus.SAVE_ERROR, cleanedMessage); + return new ResultAndError<>(SaveStatus.SAVE_UNRECOVERABLE_ERROR, cleanedMessage); } FormEntryActivity.mFormController.postProcessInstance(); @@ -110,15 +110,15 @@ protected ResultAndError doTaskBackground(Void... nothing) { exportData(mMarkCompleted); } catch (FileNotFoundException e) { e.printStackTrace(); - return new ResultAndError<>(SaveStatus.SAVE_ERROR, + return new ResultAndError<>(SaveStatus.SAVE_UNRECOVERABLE_ERROR, "Something is blocking acesss to the submission file in " + mFormRecordPath); } catch (XFormSerializer.UnsupportedUnicodeSurrogatesException e) { Logger.log(LogTypes.TYPE_ERROR_CONFIG_STRUCTURE, "Form contains invalid data encoding\n\n" + ForceCloseLogger.getStackTrace(e)); - return new ResultAndError<>(SaveStatus.SAVE_ERROR, + return new ResultAndError<>(SaveStatus.SAVE_UNRECOVERABLE_ERROR, Localization.get("form.entry.save.invalid.unicode", e.getMessage())); } catch (IOException e) { Logger.log(LogTypes.TYPE_ERROR_STORAGE, "I/O Error when serializing form\n\n" + ForceCloseLogger.getStackTrace(e)); - return new ResultAndError<>(SaveStatus.SAVE_ERROR, + return new ResultAndError<>(SaveStatus.SAVE_UNRECOVERABLE_ERROR, "Unable to write xml to " + mFormRecordPath); } catch (FormInstanceTransactionException e) { e.printStackTrace(); From e3acf14c10e740b8bded7e11c88c53f787428e1b Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 19 Sep 2024 14:33:38 +0200 Subject: [PATCH 48/63] Add interruption reason related flag to InterruptedFormState --- .../models/database/InterruptedFormState.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/org/commcare/models/database/InterruptedFormState.java b/app/src/org/commcare/models/database/InterruptedFormState.java index ce72df9729..da14a12a5f 100644 --- a/app/src/org/commcare/models/database/InterruptedFormState.java +++ b/app/src/org/commcare/models/database/InterruptedFormState.java @@ -8,6 +8,7 @@ import java.io.DataInputStream; import java.io.DataOutputStream; +import java.io.EOFException; import java.io.IOException; /** @@ -17,10 +18,12 @@ public class InterruptedFormState implements Externalizable { int sessionStateDescriptorId; FormIndex formIndex; + private boolean interruptedDueToSessionExpiration = false; - public InterruptedFormState(int sessionStateDescriptorId, FormIndex formIndex) { + public InterruptedFormState(int sessionStateDescriptorId, FormIndex formIndex, boolean sessionExpired) { this.sessionStateDescriptorId = sessionStateDescriptorId; this.formIndex = formIndex; + this.interruptedDueToSessionExpiration = sessionExpired; } public InterruptedFormState() { @@ -33,12 +36,18 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException { sessionStateDescriptorId = ExtUtil.readInt(in); formIndex = (FormIndex)ExtUtil.read(in, FormIndex.class, pf); + try { + interruptedDueToSessionExpiration = ExtUtil.readBool(in); + } catch(EOFException e){ + // interruptedDueToSessionExpiration field + } } @Override public void writeExternal(DataOutputStream out) throws IOException { ExtUtil.writeNumeric(out, sessionStateDescriptorId); ExtUtil.write(out, formIndex); + ExtUtil.writeBool(out, interruptedDueToSessionExpiration); } public int getSessionStateDescriptorId() { @@ -48,4 +57,8 @@ public int getSessionStateDescriptorId() { public FormIndex getFormIndex() { return formIndex; } + + public boolean getInterruptedDueToSessionExpiration(){ + return interruptedDueToSessionExpiration; + } } From 2c3b7bb97b874be3c036c1864433a46c6c3702df Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Mon, 16 Sep 2024 23:40:00 +0200 Subject: [PATCH 49/63] Remove exit flag from formSaveCallback --- app/src/org/commcare/activities/FormEntryActivity.java | 2 +- app/src/org/commcare/interfaces/FormSaveCallback.java | 2 +- app/src/org/commcare/services/CommCareSessionService.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 7fa37ddea2..237839eee7 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -249,7 +249,7 @@ public void onCreateSessionSafe(Bundle savedInstanceState) { } @Override - public void formSaveCallback(boolean exit, Runnable listener) { + public void formSaveCallback(Runnable listener) { // note that we have started saving the form customFormSaveCallback = listener; interruptAndSaveForm(exit); diff --git a/app/src/org/commcare/interfaces/FormSaveCallback.java b/app/src/org/commcare/interfaces/FormSaveCallback.java index 91c9047a18..f404af1692 100644 --- a/app/src/org/commcare/interfaces/FormSaveCallback.java +++ b/app/src/org/commcare/interfaces/FormSaveCallback.java @@ -8,5 +8,5 @@ public interface FormSaveCallback { * Starts a task to save the current form being edited. Will be expected to call the provided * listener when saving is complete and the current session state is no longer volatile */ - void formSaveCallback(boolean exit, Runnable callback); + void formSaveCallback(Runnable callback); } diff --git a/app/src/org/commcare/services/CommCareSessionService.java b/app/src/org/commcare/services/CommCareSessionService.java index 205a81342a..53b5750bc4 100644 --- a/app/src/org/commcare/services/CommCareSessionService.java +++ b/app/src/org/commcare/services/CommCareSessionService.java @@ -411,7 +411,7 @@ private void saveFormAndCloseSession() { // save form progress, if any synchronized (lock) { if (formSaver != null) { - formSaver.formSaveCallback(true, () -> { + formSaver.formSaveCallback(() -> { CommCareApplication.instance().expireUserSession(); }); } else { @@ -430,7 +430,7 @@ public void proceedWithSavedSessionIfNeeded(Runnable callback) { if (formSaver != null) { Toast.makeText(CommCareApplication.instance(), "Suspending existing form entry session...", Toast.LENGTH_LONG).show(); - formSaver.formSaveCallback(true, callback); + formSaver.formSaveCallback(callback); formSaver = null; return; } From 30234248cc47e11985f484f4e4d296f2211abc6b Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 19 Sep 2024 15:00:37 +0200 Subject: [PATCH 50/63] Replace exit flag with sessionExpiredOrSuspended flag --- .../org/commcare/activities/FormEntryActivity.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 237839eee7..02eedde70c 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -252,17 +252,17 @@ public void onCreateSessionSafe(Bundle savedInstanceState) { public void formSaveCallback(Runnable listener) { // note that we have started saving the form customFormSaveCallback = listener; - interruptAndSaveForm(exit); + interruptAndSaveForm(true); } - private void interruptAndSaveForm(boolean exit) { + private void interruptAndSaveForm(boolean sessionExpiredOrSustended) { if (mFormController != null) { // Set flag that will allow us to restore this form when we log back in CommCareApplication.instance().getCurrentSessionWrapper().setCurrentStateAsInterrupted( mFormController.getFormIndex()); // Start saving form; will trigger expireUserSession() on completion - saveIncompleteFormToDisk(exit); + saveIncompleteFormToDisk(sessionExpiredOrSustended); } } @@ -776,8 +776,9 @@ private void saveCompletedFormToDisk(String updatedSaveName) { saveDataToDisk(FormEntryConstants.EXIT, true, updatedSaveName, false); } - private void saveIncompleteFormToDisk(boolean exit) { - saveDataToDisk(exit, false, null, true); + private void saveIncompleteFormToDisk(boolean sessionExpiredOrSuspended) { + boolean shouldExit = sessionExpiredOrSuspended ? FormEntryConstants.EXIT : FormEntryConstants.DO_NOT_EXIT; + saveDataToDisk(shouldExit, false, null, true); } /** From 37e044ec26d4f8ad6d8dab821bc45281447c0fd7 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 19 Sep 2024 15:20:49 +0200 Subject: [PATCH 51/63] Save interruption reason flag --- app/src/org/commcare/activities/FormEntryActivity.java | 2 +- app/src/org/commcare/models/AndroidSessionWrapper.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 02eedde70c..390d3eff3e 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -259,7 +259,7 @@ private void interruptAndSaveForm(boolean sessionExpiredOrSustended) { if (mFormController != null) { // Set flag that will allow us to restore this form when we log back in CommCareApplication.instance().getCurrentSessionWrapper().setCurrentStateAsInterrupted( - mFormController.getFormIndex()); + mFormController.getFormIndex(), sessionExpiredOrSustended); // Start saving form; will trigger expireUserSession() on completion saveIncompleteFormToDisk(sessionExpiredOrSustended); diff --git a/app/src/org/commcare/models/AndroidSessionWrapper.java b/app/src/org/commcare/models/AndroidSessionWrapper.java index b2cd353db8..d24e4636d8 100755 --- a/app/src/org/commcare/models/AndroidSessionWrapper.java +++ b/app/src/org/commcare/models/AndroidSessionWrapper.java @@ -185,12 +185,13 @@ private static boolean ssdHasValidFormRecordId(int ssdId, formRecordStorage.getMetaDataFieldForRecord(correspondingFormRecordId, FormRecord.META_STATUS)); } - public void setCurrentStateAsInterrupted(FormIndex formIndex) { + public void setCurrentStateAsInterrupted(FormIndex formIndex, boolean sessionExpiredOrSuspended) { if (sessionStateRecordId != -1) { SqlStorage sessionStorage = CommCareApplication.instance().getUserStorage(SessionStateDescriptor.class); SessionStateDescriptor current = sessionStorage.read(sessionStateRecordId); - InterruptedFormState interruptedFormState = new InterruptedFormState(current.getID(), formIndex); + InterruptedFormState interruptedFormState = + new InterruptedFormState(current.getID(), formIndex, sessionExpiredOrSuspended); HiddenPreferences.setInterruptedSSD(current.getID()); HiddenPreferences.setInterruptedFormState(interruptedFormState); } From 1836d2fb100af6f0cf327b9499a3a72acedef8a2 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 19 Sep 2024 22:48:13 +0200 Subject: [PATCH 52/63] Pass InterruptionFormState to form controller --- .../org/commcare/activities/FormEntryActivity.java | 14 +++++++------- .../org/commcare/logic/AndroidFormController.java | 13 +++++++++++-- app/src/org/commcare/tasks/FormLoaderTask.java | 9 +++++---- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 390d3eff3e..0def96a6a8 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -1022,7 +1022,7 @@ private void restorePriorStates() { private void loadForm() { mFormController = null; instanceState.setFormRecordPath(null); - FormIndex lastFormIndex = null; + InterruptedFormState savedFormSession = null; Intent intent = getIntent(); if (intent != null) { @@ -1040,10 +1040,10 @@ private void loadForm() { formId = instanceAndStatus.first; instanceIsReadOnly = instanceAndStatus.second; - // only retrieve a potentially stored form index when loading an existing form record + // only retrieve a potentially stored form session when loading an existing form record AndroidSessionWrapper asw = CommCareApplication.instance().getCurrentSessionWrapper(); - lastFormIndex = retrieveAndValidateFormIndex(asw.getSessionDescriptorId()); - if (lastFormIndex != null) { + savedFormSession = retrieveAndValidateSavedFormSession(asw.getSessionDescriptorId()); + if (savedFormSession != null) { Logger.log(LogTypes.TYPE_FORM_ENTRY, "Recovering form entry session"); } } else if (intent.hasExtra(KEY_FORM_DEF_ID)) { @@ -1062,7 +1062,7 @@ private void loadForm() { } mFormLoaderTask = new FormLoaderTask(symetricKey, instanceIsReadOnly, - formEntryRestoreSession.isRecording(), FormEntryInstanceState.mFormRecordPath, this, lastFormIndex) { + formEntryRestoreSession.isRecording(), FormEntryInstanceState.mFormRecordPath, this, savedFormSession) { @Override protected void deliverResult(FormEntryActivity receiver, FECWrapper wrapperResult) { receiver.handleFormLoadCompletion(wrapperResult.getController()); @@ -1105,11 +1105,11 @@ protected void deliverError(FormEntryActivity receiver, Exception e) { } } - private FormIndex retrieveAndValidateFormIndex(int sessionDescriptorId) { + private InterruptedFormState retrieveAndValidateSavedFormSession(int sessionDescriptorId) { InterruptedFormState interruptedFormState = HiddenPreferences.getInterruptedFormState(); if (interruptedFormState!= null && interruptedFormState.getSessionStateDescriptorId() == sessionDescriptorId) { - return interruptedFormState.getFormIndex(); + return interruptedFormState; } // data format is invalid, so better to clear the data HiddenPreferences.clearInterruptedFormState(); diff --git a/app/src/org/commcare/logic/AndroidFormController.java b/app/src/org/commcare/logic/AndroidFormController.java index 57591535a2..f96ecd2aee 100644 --- a/app/src/org/commcare/logic/AndroidFormController.java +++ b/app/src/org/commcare/logic/AndroidFormController.java @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; import org.commcare.google.services.analytics.FormAnalyticsHelper; +import org.commcare.models.database.InterruptedFormState; import org.commcare.views.widgets.WidgetFactory; import org.javarosa.core.model.FormDef; import org.javarosa.core.model.FormIndex; @@ -20,13 +21,17 @@ public class AndroidFormController extends FormController implements PendingCall private boolean wasPendingCalloutCancelled; private FormIndex formIndexToReturnTo = null; private boolean formCompleteAndSaved = false; + private InterruptedFormState restoredFormSession; private FormAnalyticsHelper formAnalyticsHelper; - public AndroidFormController(FormEntryController fec, boolean readOnly, FormIndex formIndex) { + public AndroidFormController(FormEntryController fec, boolean readOnly, InterruptedFormState savedFormSession) { super(fec, readOnly); formAnalyticsHelper = new FormAnalyticsHelper(); - formIndexToReturnTo = formIndex; + this.restoredFormSession = savedFormSession; + if (savedFormSession !=null ){ + formIndexToReturnTo = savedFormSession.getFormIndex(); + } } @Override @@ -86,4 +91,8 @@ public FormAnalyticsHelper getFormAnalyticsHelper() { public FormDef getFormDef() { return mFormEntryController.getModel().getForm(); } + + public InterruptedFormState getRestoredFormSession(){ + return restoredFormSession; + } } diff --git a/app/src/org/commcare/tasks/FormLoaderTask.java b/app/src/org/commcare/tasks/FormLoaderTask.java index 6869ce3eb0..be04c28072 100644 --- a/app/src/org/commcare/tasks/FormLoaderTask.java +++ b/app/src/org/commcare/tasks/FormLoaderTask.java @@ -13,6 +13,7 @@ import org.commcare.logging.UserCausedRuntimeException; import org.commcare.logging.XPathErrorLogger; import org.commcare.logic.AndroidFormController; +import org.commcare.models.database.InterruptedFormState; import org.commcare.models.encryption.EncryptionIO; import org.commcare.preferences.DeveloperPreferences; import org.commcare.tasks.templates.CommCareTask; @@ -63,7 +64,7 @@ public abstract class FormLoaderTask extends CommCareTask extends CommCareTask Date: Thu, 19 Sep 2024 22:50:44 +0200 Subject: [PATCH 53/63] Show message when restoring session after session pause --- app/assets/locales/android_translatable_strings.txt | 1 + app/src/org/commcare/activities/FormEntryActivity.java | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/assets/locales/android_translatable_strings.txt b/app/assets/locales/android_translatable_strings.txt index 932d7b29b8..f8a7db0323 100644 --- a/app/assets/locales/android_translatable_strings.txt +++ b/app/assets/locales/android_translatable_strings.txt @@ -169,6 +169,7 @@ form.entry.save.invalid.unicode=Could not save '${0}' text in form. form.entry.finish.button=FINISH form.entry.exit.button=EXIT form.entry.restart.after.expiration=You were logged out due to session expiration. The form you were in the middle of has been saved and resumed. +form.entry.restart.after.session.pause=CommCare was closed and the form you were in the middle of has been saved and resumed. login.attempt.badcred=Username or password are incorrect. Please try again. diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 0def96a6a8..fda9e21aa5 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -1153,8 +1153,11 @@ private void handleFormLoadCompletion(AndroidFormController fc) { uiController.refreshView(); FormNavigationUI.updateNavigationCues(this, mFormController, uiController.questionsView); if (isRestartAfterSessionExpiration) { - Toast.makeText(this, - Localization.get("form.entry.restart.after.expiration"), Toast.LENGTH_LONG).show(); + String localeKey = + (fc.getRestoredFormSession() == null + || fc.getRestoredFormSession().getInterruptedDueToSessionExpiration()) + ? "form.entry.restart.after.expiration" : "form.entry.restart.after.session.pause"; + Toast.makeText(this, Localization.get(localeKey), Toast.LENGTH_LONG).show(); } } From 312ade00bdcc22e140d5a8087a15ac7a435958c0 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 19 Sep 2024 22:55:43 +0200 Subject: [PATCH 54/63] Add userTriggered flag when saving forms --- .../activities/FormEntryActivity.java | 26 +++++++++---------- .../interfaces/FormSavedListener.java | 2 +- .../org/commcare/tasks/SaveToDiskTask.java | 9 ++++--- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index fda9e21aa5..465ed30ab7 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -252,17 +252,17 @@ public void onCreateSessionSafe(Bundle savedInstanceState) { public void formSaveCallback(Runnable listener) { // note that we have started saving the form customFormSaveCallback = listener; - interruptAndSaveForm(true); + interruptAndSaveForm(true, false); } - private void interruptAndSaveForm(boolean sessionExpiredOrSustended) { + private void interruptAndSaveForm(boolean sessionExpiredOrSustended, boolean userTriggered) { if (mFormController != null) { // Set flag that will allow us to restore this form when we log back in CommCareApplication.instance().getCurrentSessionWrapper().setCurrentStateAsInterrupted( mFormController.getFormIndex(), sessionExpiredOrSustended); // Start saving form; will trigger expireUserSession() on completion - saveIncompleteFormToDisk(sessionExpiredOrSustended); + saveIncompleteFormToDisk(sessionExpiredOrSustended, userTriggered); } } @@ -766,19 +766,19 @@ protected void fireCompoundIntentDispatch() { public void saveFormToDisk(boolean exit) { if (formHasLoaded()) { boolean isFormComplete = instanceState.isFormRecordComplete(); - saveDataToDisk(exit, isFormComplete, null, false); + saveDataToDisk(exit, isFormComplete, null, false, true); } else if (exit) { showSaveErrorAndExit(); } } private void saveCompletedFormToDisk(String updatedSaveName) { - saveDataToDisk(FormEntryConstants.EXIT, true, updatedSaveName, false); + saveDataToDisk(FormEntryConstants.EXIT, true, updatedSaveName, false, true); } - private void saveIncompleteFormToDisk(boolean sessionExpiredOrSuspended) { + private void saveIncompleteFormToDisk(boolean sessionExpiredOrSuspended, boolean userTriggered) { boolean shouldExit = sessionExpiredOrSuspended ? FormEntryConstants.EXIT : FormEntryConstants.DO_NOT_EXIT; - saveDataToDisk(shouldExit, false, null, true); + saveDataToDisk(shouldExit, false, null, true, userTriggered); } /** @@ -792,7 +792,7 @@ protected void onExternalAttachmentUpdated() { } String formStatus = formRecord.getStatus(); if (FormRecord.STATUS_INCOMPLETE.equals(formStatus)) { - saveDataToDisk(false, false, null, true); + saveDataToDisk(false, false, null, true, true); } } @@ -812,8 +812,8 @@ private void showSaveErrorAndExit() { * @param headless Disables GUI warnings and lets answers that * violate constraints be saved. */ - private void saveDataToDisk(boolean exit, boolean complete, String updatedSaveName, - boolean headless) { + private void saveDataToDisk(boolean exit, boolean complete, String updatedSaveName, boolean headless, + boolean userTriggered) { if (!formHasLoaded()) { if (exit) { showSaveErrorAndExit(); @@ -852,7 +852,7 @@ private void saveDataToDisk(boolean exit, boolean complete, String updatedSaveNa mSaveToDiskTask = new SaveToDiskTask(getIntent().getIntExtra(KEY_FORM_RECORD_ID, -1), getIntent().getIntExtra(KEY_FORM_DEF_ID, -1), FormEntryInstanceState.mFormRecordPath, - exit, complete, updatedSaveName, symetricKey, headless); + exit, complete, updatedSaveName, symetricKey, headless, userTriggered); if (!headless) { mSaveToDiskTask.connect(this); } @@ -936,7 +936,7 @@ protected void onPause() { protected void onStop() { super.onStop(); if (shouldSaveFormOnStop()) { - interruptAndSaveForm(false); + interruptAndSaveForm(false, false); } } @@ -1269,7 +1269,7 @@ private void registerSessionFormSaveCallback() { * continue closing the session/logging out. */ @Override - public void savingComplete(SaveToDiskTask.SaveStatus saveStatus, String errorMessage, boolean exit) { + public void savingComplete(SaveToDiskTask.SaveStatus saveStatus, String errorMessage, boolean exit, boolean userTriggered) { // Did we just save a form because the key session // (CommCareSessionService) is ending? if (customFormSaveCallback != null) { diff --git a/app/src/org/commcare/interfaces/FormSavedListener.java b/app/src/org/commcare/interfaces/FormSavedListener.java index 095f365c7c..6179e100ee 100644 --- a/app/src/org/commcare/interfaces/FormSavedListener.java +++ b/app/src/org/commcare/interfaces/FormSavedListener.java @@ -10,5 +10,5 @@ public interface FormSavedListener { /** * Callback to be run after a form has been saved. */ - void savingComplete(SaveToDiskTask.SaveStatus formSaveStatus, String errorMessage, boolean exit); + void savingComplete(SaveToDiskTask.SaveStatus formSaveStatus, String errorMessage, boolean exit, boolean userTriggered); } diff --git a/app/src/org/commcare/tasks/SaveToDiskTask.java b/app/src/org/commcare/tasks/SaveToDiskTask.java index 3f99d0f0e0..93a8b8cf2a 100644 --- a/app/src/org/commcare/tasks/SaveToDiskTask.java +++ b/app/src/org/commcare/tasks/SaveToDiskTask.java @@ -53,6 +53,7 @@ public class SaveToDiskTask extends private final String mFormRecordPath; private final SecretKeySpec symetricKey; + private final boolean userTriggered; public enum SaveStatus { SAVED_COMPLETE, @@ -66,7 +67,7 @@ public enum SaveStatus { public SaveToDiskTask(int formRecordId, int formDefId, String formRecordPath, Boolean saveAndExit, Boolean markCompleted, String updatedName, - SecretKeySpec symetricKey, boolean headless) { + SecretKeySpec symetricKey, boolean headless, boolean userTriggered) { TAG = SaveToDiskTask.class.getSimpleName(); mFormRecordId = formRecordId; @@ -76,7 +77,7 @@ public SaveToDiskTask(int formRecordId, int formDefId, String formRecordPath, Bo mRecordName = updatedName; this.symetricKey = symetricKey; mFormRecordPath = formRecordPath; - + this.userTriggered = userTriggered; if (headless) { this.taskId = -1; @@ -270,9 +271,9 @@ protected void onPostExecute(ResultAndError result) { synchronized (this) { if (mSavedListener != null) { if (result == null) { - mSavedListener.savingComplete(SaveStatus.SAVE_ERROR, "Unknown Error", exitAfterSave); + mSavedListener.savingComplete(SaveStatus.SAVE_ERROR, "Unknown Error", exitAfterSave, userTriggered); } else { - mSavedListener.savingComplete(result.data, result.errorMessage, exitAfterSave); + mSavedListener.savingComplete(result.data, result.errorMessage, exitAfterSave, userTriggered); } } } From 2b994dbd9b7a91ca473199b04397e6f27cc0fbcf Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Thu, 19 Sep 2024 22:56:10 +0200 Subject: [PATCH 55/63] Don't show message when auto saving --- app/src/org/commcare/activities/FormEntryActivity.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 465ed30ab7..99ca3b3384 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -1288,7 +1288,9 @@ public void savingComplete(SaveToDiskTask.SaveStatus saveStatus, String errorMes hasSaved = true; break; case SAVED_INCOMPLETE: - toastMessage = Localization.get("form.entry.incomplete.save.success"); + if (userTriggered) { + toastMessage = Localization.get("form.entry.incomplete.save.success"); + } hasSaved = true; break; case SAVED_AND_EXIT: From eaba92bf8e6fd9ed1389de616a90c89dfefc6c6b Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Fri, 20 Sep 2024 15:37:24 +0200 Subject: [PATCH 56/63] Nit --- app/src/org/commcare/activities/FormEntryActivity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index b87959b42a..1a2a5cf59d 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -1309,8 +1309,8 @@ public void savingComplete(SaveToDiskTask.SaveStatus saveStatus, String errorMes Localization.get("notification.formentry.save_error.title"), FormEntryConstants.EXIT); } - String reasonType = (saveStatus == SAVE_UNRECOVERABLE_ERROR) ? QuarantineReason_LOCAL_PROCESSING_ERROR - : QuarantineReason_RECORD_ERROR ; + String reasonType = (saveStatus == SAVE_UNRECOVERABLE_ERROR) ? + QuarantineReason_LOCAL_PROCESSING_ERROR : QuarantineReason_RECORD_ERROR; quarantineRecordOnError(errorMessage, reasonType); return; } From 296dc38ac5c57414c3adde2a834ac3bb0cea5ff6 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Fri, 27 Sep 2024 16:03:25 +0200 Subject: [PATCH 57/63] Refactor --- .../org/commcare/activities/FormEntryActivity.java | 10 +++++----- .../org/commcare/logic/AndroidFormController.java | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 99ca3b3384..a3fb02c736 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -255,14 +255,14 @@ public void formSaveCallback(Runnable listener) { interruptAndSaveForm(true, false); } - private void interruptAndSaveForm(boolean sessionExpiredOrSustended, boolean userTriggered) { + private void interruptAndSaveForm(boolean sessionExpired, boolean userTriggered) { if (mFormController != null) { // Set flag that will allow us to restore this form when we log back in CommCareApplication.instance().getCurrentSessionWrapper().setCurrentStateAsInterrupted( - mFormController.getFormIndex(), sessionExpiredOrSustended); + mFormController.getFormIndex(), sessionExpired); // Start saving form; will trigger expireUserSession() on completion - saveIncompleteFormToDisk(sessionExpiredOrSustended, userTriggered); + saveIncompleteFormToDisk(sessionExpired, userTriggered); } } @@ -1154,8 +1154,8 @@ private void handleFormLoadCompletion(AndroidFormController fc) { FormNavigationUI.updateNavigationCues(this, mFormController, uiController.questionsView); if (isRestartAfterSessionExpiration) { String localeKey = - (fc.getRestoredFormSession() == null - || fc.getRestoredFormSession().getInterruptedDueToSessionExpiration()) + (fc.getInterruptedFormState() == null + || fc.getInterruptedFormState().getInterruptedDueToSessionExpiration()) ? "form.entry.restart.after.expiration" : "form.entry.restart.after.session.pause"; Toast.makeText(this, Localization.get(localeKey), Toast.LENGTH_LONG).show(); } diff --git a/app/src/org/commcare/logic/AndroidFormController.java b/app/src/org/commcare/logic/AndroidFormController.java index f96ecd2aee..9d14f93cd7 100644 --- a/app/src/org/commcare/logic/AndroidFormController.java +++ b/app/src/org/commcare/logic/AndroidFormController.java @@ -21,16 +21,16 @@ public class AndroidFormController extends FormController implements PendingCall private boolean wasPendingCalloutCancelled; private FormIndex formIndexToReturnTo = null; private boolean formCompleteAndSaved = false; - private InterruptedFormState restoredFormSession; + private InterruptedFormState interruptedFormState; private FormAnalyticsHelper formAnalyticsHelper; - public AndroidFormController(FormEntryController fec, boolean readOnly, InterruptedFormState savedFormSession) { + public AndroidFormController(FormEntryController fec, boolean readOnly, InterruptedFormState interruptedFormState) { super(fec, readOnly); formAnalyticsHelper = new FormAnalyticsHelper(); - this.restoredFormSession = savedFormSession; - if (savedFormSession !=null ){ - formIndexToReturnTo = savedFormSession.getFormIndex(); + this.interruptedFormState = interruptedFormState; + if (interruptedFormState !=null ){ + formIndexToReturnTo = interruptedFormState.getFormIndex(); } } @@ -92,7 +92,7 @@ public FormDef getFormDef() { return mFormEntryController.getModel().getForm(); } - public InterruptedFormState getRestoredFormSession(){ - return restoredFormSession; + public InterruptedFormState getInterruptedFormState(){ + return interruptedFormState; } } From 6aaf0ff9fcc97a178f1b624021686907ad7556ff Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 22 Oct 2024 18:10:46 +0200 Subject: [PATCH 58/63] Revert "Remove exit flag from formSaveCallback" This reverts commit 2c3b7bb97b874be3c036c1864433a46c6c3702df. --- app/src/org/commcare/activities/FormEntryActivity.java | 2 +- app/src/org/commcare/interfaces/FormSaveCallback.java | 2 +- app/src/org/commcare/services/CommCareSessionService.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index a3fb02c736..f0876e01e6 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -249,7 +249,7 @@ public void onCreateSessionSafe(Bundle savedInstanceState) { } @Override - public void formSaveCallback(Runnable listener) { + public void formSaveCallback(boolean exit, Runnable listener) { // note that we have started saving the form customFormSaveCallback = listener; interruptAndSaveForm(true, false); diff --git a/app/src/org/commcare/interfaces/FormSaveCallback.java b/app/src/org/commcare/interfaces/FormSaveCallback.java index f404af1692..91c9047a18 100644 --- a/app/src/org/commcare/interfaces/FormSaveCallback.java +++ b/app/src/org/commcare/interfaces/FormSaveCallback.java @@ -8,5 +8,5 @@ public interface FormSaveCallback { * Starts a task to save the current form being edited. Will be expected to call the provided * listener when saving is complete and the current session state is no longer volatile */ - void formSaveCallback(Runnable callback); + void formSaveCallback(boolean exit, Runnable callback); } diff --git a/app/src/org/commcare/services/CommCareSessionService.java b/app/src/org/commcare/services/CommCareSessionService.java index 53b5750bc4..205a81342a 100644 --- a/app/src/org/commcare/services/CommCareSessionService.java +++ b/app/src/org/commcare/services/CommCareSessionService.java @@ -411,7 +411,7 @@ private void saveFormAndCloseSession() { // save form progress, if any synchronized (lock) { if (formSaver != null) { - formSaver.formSaveCallback(() -> { + formSaver.formSaveCallback(true, () -> { CommCareApplication.instance().expireUserSession(); }); } else { @@ -430,7 +430,7 @@ public void proceedWithSavedSessionIfNeeded(Runnable callback) { if (formSaver != null) { Toast.makeText(CommCareApplication.instance(), "Suspending existing form entry session...", Toast.LENGTH_LONG).show(); - formSaver.formSaveCallback(callback); + formSaver.formSaveCallback(true, callback); formSaver = null; return; } From 3ade9b262f34d6db51cfa01eb3f6a5102e6d5e25 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 22 Oct 2024 18:45:18 +0200 Subject: [PATCH 59/63] Refactor --- .../org/commcare/activities/FormEntryActivity.java | 14 ++++++++------ .../org/commcare/interfaces/FormSaveCallback.java | 2 +- .../org/commcare/logic/AndroidFormController.java | 2 +- .../org/commcare/models/AndroidSessionWrapper.java | 4 ++-- .../commcare/services/CommCareSessionService.java | 4 ++-- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index f0876e01e6..0cbd24057e 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -107,6 +107,8 @@ import androidx.appcompat.app.ActionBar; import androidx.core.app.ActivityCompat; +import static org.commcare.activities.components.FormEntryConstants.DO_NOT_EXIT; +import static org.commcare.activities.components.FormEntryConstants.EXIT; import static org.commcare.android.database.user.models.FormRecord.QuarantineReason_LOCAL_PROCESSING_ERROR; import static org.commcare.sync.FirebaseMessagingDataSyncer.PENGING_SYNC_ALERT_ACTION; @@ -249,10 +251,10 @@ public void onCreateSessionSafe(Bundle savedInstanceState) { } @Override - public void formSaveCallback(boolean exit, Runnable listener) { + public void formSaveCallback(boolean sessionExpired, boolean userTriggered, Runnable listener) { // note that we have started saving the form customFormSaveCallback = listener; - interruptAndSaveForm(true, false); + interruptAndSaveForm(sessionExpired, userTriggered); } private void interruptAndSaveForm(boolean sessionExpired, boolean userTriggered) { @@ -262,7 +264,8 @@ private void interruptAndSaveForm(boolean sessionExpired, boolean userTriggered) mFormController.getFormIndex(), sessionExpired); // Start saving form; will trigger expireUserSession() on completion - saveIncompleteFormToDisk(sessionExpired, userTriggered); + boolean exit = sessionExpired ? FormEntryConstants.EXIT : FormEntryConstants.DO_NOT_EXIT; + saveIncompleteFormToDisk(exit, userTriggered); } } @@ -776,9 +779,8 @@ private void saveCompletedFormToDisk(String updatedSaveName) { saveDataToDisk(FormEntryConstants.EXIT, true, updatedSaveName, false, true); } - private void saveIncompleteFormToDisk(boolean sessionExpiredOrSuspended, boolean userTriggered) { - boolean shouldExit = sessionExpiredOrSuspended ? FormEntryConstants.EXIT : FormEntryConstants.DO_NOT_EXIT; - saveDataToDisk(shouldExit, false, null, true, userTriggered); + private void saveIncompleteFormToDisk(boolean exit, boolean userTriggered) { + saveDataToDisk(exit, false, null, true, userTriggered); } /** diff --git a/app/src/org/commcare/interfaces/FormSaveCallback.java b/app/src/org/commcare/interfaces/FormSaveCallback.java index 91c9047a18..9029fb7d23 100644 --- a/app/src/org/commcare/interfaces/FormSaveCallback.java +++ b/app/src/org/commcare/interfaces/FormSaveCallback.java @@ -8,5 +8,5 @@ public interface FormSaveCallback { * Starts a task to save the current form being edited. Will be expected to call the provided * listener when saving is complete and the current session state is no longer volatile */ - void formSaveCallback(boolean exit, Runnable callback); + void formSaveCallback(boolean sessionExpired, boolean userTriggered, Runnable callback); } diff --git a/app/src/org/commcare/logic/AndroidFormController.java b/app/src/org/commcare/logic/AndroidFormController.java index 9d14f93cd7..8845eb7750 100644 --- a/app/src/org/commcare/logic/AndroidFormController.java +++ b/app/src/org/commcare/logic/AndroidFormController.java @@ -29,7 +29,7 @@ public AndroidFormController(FormEntryController fec, boolean readOnly, Interrup super(fec, readOnly); formAnalyticsHelper = new FormAnalyticsHelper(); this.interruptedFormState = interruptedFormState; - if (interruptedFormState !=null ){ + if (interruptedFormState !=null) { formIndexToReturnTo = interruptedFormState.getFormIndex(); } } diff --git a/app/src/org/commcare/models/AndroidSessionWrapper.java b/app/src/org/commcare/models/AndroidSessionWrapper.java index d24e4636d8..5a05e5343c 100755 --- a/app/src/org/commcare/models/AndroidSessionWrapper.java +++ b/app/src/org/commcare/models/AndroidSessionWrapper.java @@ -185,13 +185,13 @@ private static boolean ssdHasValidFormRecordId(int ssdId, formRecordStorage.getMetaDataFieldForRecord(correspondingFormRecordId, FormRecord.META_STATUS)); } - public void setCurrentStateAsInterrupted(FormIndex formIndex, boolean sessionExpiredOrSuspended) { + public void setCurrentStateAsInterrupted(FormIndex formIndex, boolean sessionExpired) { if (sessionStateRecordId != -1) { SqlStorage sessionStorage = CommCareApplication.instance().getUserStorage(SessionStateDescriptor.class); SessionStateDescriptor current = sessionStorage.read(sessionStateRecordId); InterruptedFormState interruptedFormState = - new InterruptedFormState(current.getID(), formIndex, sessionExpiredOrSuspended); + new InterruptedFormState(current.getID(), formIndex, sessionExpired); HiddenPreferences.setInterruptedSSD(current.getID()); HiddenPreferences.setInterruptedFormState(interruptedFormState); } diff --git a/app/src/org/commcare/services/CommCareSessionService.java b/app/src/org/commcare/services/CommCareSessionService.java index 205a81342a..2446ba6e9c 100644 --- a/app/src/org/commcare/services/CommCareSessionService.java +++ b/app/src/org/commcare/services/CommCareSessionService.java @@ -411,7 +411,7 @@ private void saveFormAndCloseSession() { // save form progress, if any synchronized (lock) { if (formSaver != null) { - formSaver.formSaveCallback(true, () -> { + formSaver.formSaveCallback(true, false, () -> { CommCareApplication.instance().expireUserSession(); }); } else { @@ -430,7 +430,7 @@ public void proceedWithSavedSessionIfNeeded(Runnable callback) { if (formSaver != null) { Toast.makeText(CommCareApplication.instance(), "Suspending existing form entry session...", Toast.LENGTH_LONG).show(); - formSaver.formSaveCallback(true, callback); + formSaver.formSaveCallback(true, false, callback); formSaver = null; return; } From 7218aba874042f32b616a363889118bfc7bd2516 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Tue, 22 Oct 2024 18:57:24 +0200 Subject: [PATCH 60/63] Make class field nullable --- app/src/org/commcare/logic/AndroidFormController.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/org/commcare/logic/AndroidFormController.java b/app/src/org/commcare/logic/AndroidFormController.java index 8845eb7750..65e6741a09 100644 --- a/app/src/org/commcare/logic/AndroidFormController.java +++ b/app/src/org/commcare/logic/AndroidFormController.java @@ -2,6 +2,7 @@ import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.commcare.google.services.analytics.FormAnalyticsHelper; import org.commcare.models.database.InterruptedFormState; @@ -21,11 +22,12 @@ public class AndroidFormController extends FormController implements PendingCall private boolean wasPendingCalloutCancelled; private FormIndex formIndexToReturnTo = null; private boolean formCompleteAndSaved = false; + @Nullable private InterruptedFormState interruptedFormState; private FormAnalyticsHelper formAnalyticsHelper; - public AndroidFormController(FormEntryController fec, boolean readOnly, InterruptedFormState interruptedFormState) { + public AndroidFormController(FormEntryController fec, boolean readOnly, @Nullable InterruptedFormState interruptedFormState) { super(fec, readOnly); formAnalyticsHelper = new FormAnalyticsHelper(); this.interruptedFormState = interruptedFormState; From a27ab98c1f70f7afd060af5cf31ddd25f8e3def4 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 23 Oct 2024 13:33:44 +0200 Subject: [PATCH 61/63] Nit --- app/src/org/commcare/activities/FormEntryActivity.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 95a90488b8..de074f311c 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -1159,6 +1159,7 @@ private void handleFormLoadCompletion(AndroidFormController fc) { uiController.refreshView(); FormNavigationUI.updateNavigationCues(this, mFormController, uiController.questionsView); if (isRestartAfterSessionExpiration) { + // InterruptedFormState null check is important to ensure backward compatibility String localeKey = (fc.getInterruptedFormState() == null || fc.getInterruptedFormState().getInterruptedDueToSessionExpiration()) From 019cd2d0521de8f6a49e3c3c1092fe59b0a49e38 Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 23 Oct 2024 13:39:02 +0200 Subject: [PATCH 62/63] Correct userTriggered flag when updating attachments --- app/src/org/commcare/activities/FormEntryActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index de074f311c..214ea5f120 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -796,7 +796,7 @@ protected void onExternalAttachmentUpdated() { } String formStatus = formRecord.getStatus(); if (FormRecord.STATUS_INCOMPLETE.equals(formStatus)) { - saveDataToDisk(false, false, null, true, true); + saveDataToDisk(false, false, null, true, false); } } From dec75686a49af9085f4b2889769c0a1c812ae85a Mon Sep 17 00:00:00 2001 From: Ahmad Vazirna Date: Wed, 23 Oct 2024 13:40:31 +0200 Subject: [PATCH 63/63] Refactor --- app/src/org/commcare/activities/FormEntryActivity.java | 2 +- app/src/org/commcare/models/database/InterruptedFormState.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/org/commcare/activities/FormEntryActivity.java b/app/src/org/commcare/activities/FormEntryActivity.java index 214ea5f120..0f80dfed31 100644 --- a/app/src/org/commcare/activities/FormEntryActivity.java +++ b/app/src/org/commcare/activities/FormEntryActivity.java @@ -1162,7 +1162,7 @@ private void handleFormLoadCompletion(AndroidFormController fc) { // InterruptedFormState null check is important to ensure backward compatibility String localeKey = (fc.getInterruptedFormState() == null - || fc.getInterruptedFormState().getInterruptedDueToSessionExpiration()) + || fc.getInterruptedFormState().isInterruptedDueToSessionExpiration()) ? "form.entry.restart.after.expiration" : "form.entry.restart.after.session.pause"; Toast.makeText(this, Localization.get(localeKey), Toast.LENGTH_LONG).show(); } diff --git a/app/src/org/commcare/models/database/InterruptedFormState.java b/app/src/org/commcare/models/database/InterruptedFormState.java index 5154087a62..4cbf00bd81 100644 --- a/app/src/org/commcare/models/database/InterruptedFormState.java +++ b/app/src/org/commcare/models/database/InterruptedFormState.java @@ -62,7 +62,7 @@ public FormIndex getFormIndex() { return formIndex; } - public boolean getInterruptedDueToSessionExpiration(){ + public boolean isInterruptedDueToSessionExpiration(){ return interruptedDueToSessionExpiration; }