Skip to content

Commit

Permalink
Merge pull request #2802 from dimagi/saveFormOnPause
Browse files Browse the repository at this point in the history
Auto-Save Form on Pause
  • Loading branch information
shubham1g5 authored Aug 8, 2024
2 parents 7ff6d6f + 4a9e2ef commit c36a831
Show file tree
Hide file tree
Showing 12 changed files with 99 additions and 22 deletions.
7 changes: 7 additions & 0 deletions app/res/xml/preferences_developer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,11 @@
android:entryValues="@array/pref_enabled_vals"
android:key="cc-enable-certificate-transparency"
android:title="Certificate Transparency"/>
<androidx.preference.ListPreference
android:defaultValue="no"
android:enabled="true"
android:entries="@array/pref_enabled_labels"
android:entryValues="@array/pref_enabled_vals"
android:key="cc-auto-form-save-on-pause"
android:title="Auto Save Form on Pause"/>
</PreferenceScreen>
38 changes: 31 additions & 7 deletions app/src/org/commcare/activities/FormEntryActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -148,6 +149,7 @@ public class FormEntryActivity extends SaveSessionCommCareActivity<FormEntryActi
private boolean instanceIsReadOnly = false;
private boolean hasFormLoadBeenTriggered = false;
private boolean hasFormLoadFailed = false;
private boolean triggeredExit = false;
private String locationRecieverErrorAction = null;
private String badLocationXpath = null;

Expand Down Expand Up @@ -242,18 +244,23 @@ public void onCreateSessionSafe(Bundle savedInstanceState) {
uiController.refreshView();
}
TextToSpeechConverter.INSTANCE.setListener(mTTSCallback);
HiddenPreferences.clearInterruptedSSD();
}

@Override
public void formSaveCallback(Runnable listener) {
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();
CommCareApplication.instance().getCurrentSessionWrapper()
.setCurrentStateAsInterrupted(mFormController.getSerializedFormIndex());

// Start saving form; will trigger expireUserSession() on completion
saveIncompleteFormToDisk();
saveIncompleteFormToDisk(exit);
}

private void handleLocationErrorAction() {
Expand Down Expand Up @@ -766,8 +773,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);
}

/**
Expand Down Expand Up @@ -912,6 +919,20 @@ protected void onPause() {
unregisterReceiver(pendingSyncAlertBroadcastReceiver);
}

@Override
protected void onStop() {
super.onStop();
if (shouldSaveFormOnStop()) {
interruptAndSaveForm(false);
}
}

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;
}

private void saveInlineVideoState() {
if (uiController.questionsView != null) {
for (int i = 0; i < uiController.questionsView.getWidgets().size(); i++) {
Expand Down Expand Up @@ -1208,15 +1229,17 @@ 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) {
Runnable toCall = customFormSaveCallback;
customFormSaveCallback = null;

toCall.run();
returnAsInterrupted();
if (exit) {
returnAsInterrupted();
}
} else if (saveStatus != null) {
String toastMessage = "";
switch (saveStatus) {
Expand Down Expand Up @@ -1344,6 +1367,7 @@ private void finishReturnInstance(boolean reportSaved) {

dismissCurrentProgressDialog();
reportFormExitTime();
triggeredExit = true;
finish();
}

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(Runnable callback);
void formSaveCallback(boolean exit, 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);
void savingComplete(SaveToDiskTask.SaveStatus formSaveStatus, String errorMessage, boolean exit);
}
16 changes: 12 additions & 4 deletions app/src/org/commcare/logic/AndroidFormController.java
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down Expand Up @@ -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;
}
}
}
9 changes: 6 additions & 3 deletions app/src/org/commcare/models/AndroidSessionWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
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;
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;
Expand All @@ -20,7 +20,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;
Expand Down Expand Up @@ -184,12 +183,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<SessionStateDescriptor> sessionStorage =
CommCareApplication.instance().getUserStorage(SessionStateDescriptor.class);
SessionStateDescriptor current = sessionStorage.read(sessionStateRecordId);
HiddenPreferences.setInterruptedSSD(current.getID());

if (serializedFormIndex != null) {
HiddenPreferences.setInterruptedFormIndex(new Pair<>(current.getID(), serializedFormIndex));
}
}
}

Expand Down
7 changes: 7 additions & 0 deletions app/src/org/commcare/preferences/DeveloperPreferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> WHITELISTED_DEVELOPER_PREF_KEYS = new HashSet<>();

static {
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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) {
Expand Down
11 changes: 11 additions & 0 deletions app/src/org/commcare/preferences/HiddenPreferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -112,6 +114,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?
Expand Down Expand Up @@ -629,4 +632,12 @@ public static void setPendingSyncDialogDisabled(boolean dialogDisabled) {
public static boolean isBackgroundSyncEnabled() {
return DeveloperPreferences.doesPropertyMatch(ENABLE_BACKGROUND_SYNC, PrefValues.NO, PrefValues.YES);
}

public static void setInterruptedFormIndex(Pair<Integer, String> ssIdAndSerializedFormIndexPair) {
String currentUserId = CommCareApplication.instance().getCurrentUserId();
CommCareApplication.instance().getCurrentApp().getAppPreferences().edit()
.putString(INTERRUPTED_FORM_INDEX + currentUserId,
StringUtils.convertPairToJsonString(ssIdAndSerializedFormIndexPair))
.apply();
}
}
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 @@ -402,7 +402,7 @@ private void saveFormAndCloseSession() {
// save form progress, if any
synchronized (lock) {
if (formSaver != null) {
formSaver.formSaveCallback(() -> {
formSaver.formSaveCallback(true, () -> {
CommCareApplication.instance().expireUserSession();
});
} else {
Expand All @@ -421,7 +421,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;
}
Expand Down
6 changes: 2 additions & 4 deletions app/src/org/commcare/tasks/SaveToDiskTask.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -272,9 +270,9 @@ protected void onPostExecute(ResultAndError<SaveStatus> 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);
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions app/src/org/commcare/utils/StringUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -55,4 +60,15 @@ public static Spannable getStringSpannableRobust(Context c, int resId, String ar
}
return MarkupUtil.styleSpannable(c, ret);
}

public static String convertPairToJsonString(Pair<? extends Serializable, ? extends Serializable> pair){
Gson gson = new Gson();
try{
String jsonString = gson.toJson(pair);
return jsonString;
} catch(JsonIOException e){
// default to null
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,9 @@ public class FormStorageTest {
, "org.commcare.suite.model.EndpointArgument"
, "org.commcare.suite.model.EndpointAction"
, "org.commcare.suite.model.QueryGroup"

// Added in 2.55
, "org.javarosa.core.model.FormIndex"
);


Expand Down

0 comments on commit c36a831

Please sign in to comment.