Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adjust user messages around the Session Pause feature #2858

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
shubham1g5 marked this conversation as resolved.
Show resolved Hide resolved
|| 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;
shubham1g5 marked this conversation as resolved.
Show resolved Hide resolved

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