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 d76c7b4125..0f80dfed31 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.android.database.user.models.FormRecord.QuarantineReason_RECORD_ERROR; import static org.commcare.sync.FirebaseMessagingDataSyncer.PENGING_SYNC_ALERT_ACTION; @@ -251,20 +253,21 @@ 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(exit); + interruptAndSaveForm(sessionExpired, userTriggered); } - private void interruptAndSaveForm(boolean exit) { + 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()); + mFormController.getFormIndex(), sessionExpired); // Start saving form; will trigger expireUserSession() on completion - saveIncompleteFormToDisk(exit); + boolean exit = sessionExpired ? FormEntryConstants.EXIT : FormEntryConstants.DO_NOT_EXIT; + saveIncompleteFormToDisk(exit, userTriggered); } } @@ -768,18 +771,18 @@ 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 exit) { - saveDataToDisk(exit, false, null, true); + private void saveIncompleteFormToDisk(boolean exit, boolean userTriggered) { + saveDataToDisk(exit, false, null, true, userTriggered); } /** @@ -793,7 +796,7 @@ protected void onExternalAttachmentUpdated() { } String formStatus = formRecord.getStatus(); if (FormRecord.STATUS_INCOMPLETE.equals(formStatus)) { - saveDataToDisk(false, false, null, true); + saveDataToDisk(false, false, null, true, false); } } @@ -813,8 +816,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(); @@ -853,7 +856,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); } @@ -937,7 +940,7 @@ protected void onPause() { protected void onStop() { super.onStop(); if (shouldSaveFormOnStop()) { - interruptAndSaveForm(false); + interruptAndSaveForm(false, false); } } @@ -1023,7 +1026,7 @@ private void restorePriorStates() { private void loadForm() { mFormController = null; instanceState.setFormRecordPath(null); - FormIndex lastFormIndex = null; + InterruptedFormState savedFormSession = null; Intent intent = getIntent(); if (intent != null) { @@ -1042,9 +1045,9 @@ private void loadForm() { instanceIsReadOnly = instanceAndStatus.second; // only retrieve a potentially stored form index when loading an existing form record - lastFormIndex = retrieveAndValidateFormIndex( + savedFormSession = retrieveAndValidateFormIndex( CommCareApplication.instance().getCurrentSessionWrapper()); - if (lastFormIndex != null) { + if (savedFormSession != null) { Logger.log(LogTypes.TYPE_FORM_ENTRY, "Recovering form entry session"); } } else if (intent.hasExtra(KEY_FORM_DEF_ID)) { @@ -1063,7 +1066,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()); @@ -1106,14 +1109,13 @@ protected void deliverError(FormEntryActivity receiver, Exception e) { } } - private FormIndex retrieveAndValidateFormIndex(AndroidSessionWrapper androidSessionWrapper) { - InterruptedFormState interruptedFormState = - HiddenPreferences.getInterruptedFormState(); + private InterruptedFormState retrieveAndValidateFormIndex(AndroidSessionWrapper androidSessionWrapper) { + InterruptedFormState interruptedFormState = HiddenPreferences.getInterruptedFormState(); if (interruptedFormState!= null && interruptedFormState.getSessionStateDescriptorId() == androidSessionWrapper.getSessionDescriptorId() && (interruptedFormState.getFormRecordId() == -1 || interruptedFormState.getFormRecordId() == androidSessionWrapper.getFormRecordId())) { - return interruptedFormState.getFormIndex(); + return interruptedFormState; } // data format is invalid, so better to clear the data HiddenPreferences.clearInterruptedFormState(); @@ -1157,8 +1159,12 @@ 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(); + // InterruptedFormState null check is important to ensure backward compatibility + String localeKey = + (fc.getInterruptedFormState() == null + || fc.getInterruptedFormState().isInterruptedDueToSessionExpiration()) + ? "form.entry.restart.after.expiration" : "form.entry.restart.after.session.pause"; + Toast.makeText(this, Localization.get(localeKey), Toast.LENGTH_LONG).show(); } } @@ -1270,7 +1276,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) { @@ -1289,7 +1295,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: 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/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/logic/AndroidFormController.java b/app/src/org/commcare/logic/AndroidFormController.java index 57591535a2..65e6741a09 100644 --- a/app/src/org/commcare/logic/AndroidFormController.java +++ b/app/src/org/commcare/logic/AndroidFormController.java @@ -2,8 +2,10 @@ import androidx.annotation.NonNull; +import androidx.annotation.Nullable; 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 +22,18 @@ 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, FormIndex formIndex) { + public AndroidFormController(FormEntryController fec, boolean readOnly, @Nullable InterruptedFormState interruptedFormState) { super(fec, readOnly); formAnalyticsHelper = new FormAnalyticsHelper(); - formIndexToReturnTo = formIndex; + this.interruptedFormState = interruptedFormState; + if (interruptedFormState !=null) { + formIndexToReturnTo = interruptedFormState.getFormIndex(); + } } @Override @@ -86,4 +93,8 @@ public FormAnalyticsHelper getFormAnalyticsHelper() { public FormDef getFormDef() { return mFormEntryController.getModel().getForm(); } + + public InterruptedFormState getInterruptedFormState(){ + return interruptedFormState; + } } diff --git a/app/src/org/commcare/models/AndroidSessionWrapper.java b/app/src/org/commcare/models/AndroidSessionWrapper.java index 8b045acb66..9419371875 100755 --- a/app/src/org/commcare/models/AndroidSessionWrapper.java +++ b/app/src/org/commcare/models/AndroidSessionWrapper.java @@ -185,12 +185,14 @@ private static boolean ssdHasValidFormRecordId(int ssdId, formRecordStorage.getMetaDataFieldForRecord(correspondingFormRecordId, FormRecord.META_STATUS)); } - public void setCurrentStateAsInterrupted(FormIndex formIndex) { + 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, current.getFormRecordId()); + + InterruptedFormState interruptedFormState = + new InterruptedFormState(current.getID(), formIndex, current.getFormRecordId(), sessionExpired); 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 c90066dc21..4cbf00bd81 100644 --- a/app/src/org/commcare/models/database/InterruptedFormState.java +++ b/app/src/org/commcare/models/database/InterruptedFormState.java @@ -19,18 +19,19 @@ public class InterruptedFormState implements Externalizable { private int sessionStateDescriptorId; private FormIndex formIndex; private int formRecordId = -1; + private boolean interruptedDueToSessionExpiration = false; - public InterruptedFormState(int sessionStateDescriptorId, FormIndex formIndex, int formRecordId) { + public InterruptedFormState(int sessionStateDescriptorId, FormIndex formIndex, int formRecordId, boolean sessionExpired) { this.sessionStateDescriptorId = sessionStateDescriptorId; this.formIndex = formIndex; this.formRecordId = formRecordId; + this.interruptedDueToSessionExpiration = sessionExpired; } public InterruptedFormState() { // serialization only } - @Override public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException { @@ -38,9 +39,10 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) formIndex = (FormIndex)ExtUtil.read(in, FormIndex.class, pf); try { formRecordId = ExtUtil.readInt(in); + interruptedDueToSessionExpiration = ExtUtil.readBool(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 + // formRecordId and interruptedDueToSessionExpiration fields } } @@ -49,6 +51,7 @@ public void writeExternal(DataOutputStream out) throws IOException { ExtUtil.writeNumeric(out, sessionStateDescriptorId); ExtUtil.write(out, formIndex); ExtUtil.writeNumeric(out, formRecordId); + ExtUtil.writeBool(out, interruptedDueToSessionExpiration); } public int getSessionStateDescriptorId() { @@ -59,6 +62,10 @@ public FormIndex getFormIndex() { return formIndex; } + public boolean isInterruptedDueToSessionExpiration(){ + return interruptedDueToSessionExpiration; + } + public int getFormRecordId() { return formRecordId; } 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; } 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 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); } } }