Skip to content

Commit

Permalink
Merge pull request #2858 from dimagi/adjust-user-messages-when-autosa…
Browse files Browse the repository at this point in the history
…ving-session-form

Adjust user messages around the Session Pause feature
  • Loading branch information
avazirna authored Oct 23, 2024
2 parents 503c57c + dec7568 commit aebd94e
Show file tree
Hide file tree
Showing 10 changed files with 76 additions and 45 deletions.
1 change: 1 addition & 0 deletions app/assets/locales/android_translatable_strings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
60 changes: 34 additions & 26 deletions app/src/org/commcare/activities/FormEntryActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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);
}
}

Expand All @@ -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();
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -937,7 +940,7 @@ protected void onPause() {
protected void onStop() {
super.onStop();
if (shouldSaveFormOnStop()) {
interruptAndSaveForm(false);
interruptAndSaveForm(false, false);
}
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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)) {
Expand All @@ -1063,7 +1066,7 @@ private void loadForm() {
}

mFormLoaderTask = new FormLoaderTask<FormEntryActivity>(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());
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion app/src/org/commcare/interfaces/FormSaveCallback.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
2 changes: 1 addition & 1 deletion app/src/org/commcare/interfaces/FormSavedListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
15 changes: 13 additions & 2 deletions app/src/org/commcare/logic/AndroidFormController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -86,4 +93,8 @@ public FormAnalyticsHelper getFormAnalyticsHelper() {
public FormDef getFormDef() {
return mFormEntryController.getModel().getForm();
}

public InterruptedFormState getInterruptedFormState(){
return interruptedFormState;
}
}
6 changes: 4 additions & 2 deletions app/src/org/commcare/models/AndroidSessionWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<SessionStateDescriptor> 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);
}
Expand Down
13 changes: 10 additions & 3 deletions app/src/org/commcare/models/database/InterruptedFormState.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,30 @@ 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 {
sessionStateDescriptorId = ExtUtil.readInt(in);
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
}
}

Expand All @@ -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() {
Expand All @@ -59,6 +62,10 @@ public FormIndex getFormIndex() {
return formIndex;
}

public boolean isInterruptedDueToSessionExpiration(){
return interruptedDueToSessionExpiration;
}

public int getFormRecordId() {
return formRecordId;
}
Expand Down
4 changes: 2 additions & 2 deletions app/src/org/commcare/services/CommCareSessionService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
Expand Down
9 changes: 5 additions & 4 deletions app/src/org/commcare/tasks/FormLoaderTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,7 +64,7 @@ public abstract class FormLoaderTask<R> extends CommCareTask<Integer, String, Fo
private final SecretKeySpec mSymetricKey;
private final boolean mReadOnly;
private final boolean recordEntrySession;
private final FormIndex lastFormIndex;
private final InterruptedFormState savedFormSession;

private EvaluationTraceReporter traceReporterForFullForm;
private final boolean profilingEnabledForFormLoad = false;
Expand All @@ -76,15 +77,15 @@ public abstract class FormLoaderTask<R> extends CommCareTask<Integer, String, Fo
public static final int FORM_LOADER_TASK_ID = 16;

public FormLoaderTask(SecretKeySpec symetricKey, boolean readOnly,
boolean recordEntrySession, String formRecordPath, R activity, FormIndex lastFormIndex) {
boolean recordEntrySession, String formRecordPath, R activity, InterruptedFormState savedFormSession) {
this.mSymetricKey = symetricKey;
this.mReadOnly = readOnly;
this.activity = activity;
this.taskId = FORM_LOADER_TASK_ID;
this.recordEntrySession = recordEntrySession;
this.formRecordPath = formRecordPath;
TAG = FormLoaderTask.class.getSimpleName();
this.lastFormIndex = lastFormIndex;
this.savedFormSession = savedFormSession;
}

/**
Expand Down Expand Up @@ -138,7 +139,7 @@ protected FECWrapper doTaskBackground(Integer... formDefId) {

setupFormMedia(formDefRecord.getMediaPath());

AndroidFormController formController = new AndroidFormController(fec, mReadOnly, lastFormIndex);
AndroidFormController formController = new AndroidFormController(fec, mReadOnly, savedFormSession);

data = new FECWrapper(formController);
return data;
Expand Down
Loading

0 comments on commit aebd94e

Please sign in to comment.