package org.commcare.activities; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.util.Base64; import android.view.View; import android.widget.AdapterView; import android.widget.Toast; import org.commcare.CommCareApplication; import org.commcare.heartbeat.UpdatePromptHelper; import org.commcare.heartbeat.UpdateToPrompt; import org.commcare.activities.components.FormEntryConstants; import org.commcare.activities.components.FormEntryInstanceState; import org.commcare.activities.components.FormEntrySessionWrapper; import org.commcare.android.database.app.models.UserKeyRecord; import org.commcare.android.database.user.models.FormRecord; import org.commcare.android.database.user.models.SessionStateDescriptor; import org.commcare.core.process.CommCareInstanceInitializer; import org.commcare.dalvik.BuildConfig; import org.commcare.dalvik.R; import org.commcare.google.services.ads.AdMobManager; import org.commcare.interfaces.CommCareActivityUIController; import org.commcare.logging.AndroidLogger; import org.commcare.google.services.analytics.GoogleAnalyticsFields; import org.commcare.google.services.analytics.GoogleAnalyticsUtils; import org.commcare.models.AndroidSessionWrapper; import org.commcare.models.database.SqlStorage; import org.commcare.preferences.CommCarePreferences; import org.commcare.preferences.DeveloperPreferences; import org.commcare.provider.FormsProviderAPI; import org.commcare.provider.InstanceProviderAPI; import org.commcare.session.CommCareSession; import org.commcare.session.SessionFrame; import org.commcare.session.SessionNavigationResponder; import org.commcare.session.SessionNavigator; import org.commcare.suite.model.EntityDatum; import org.commcare.suite.model.Entry; import org.commcare.suite.model.PostRequest; import org.commcare.suite.model.RemoteRequestEntry; import org.commcare.suite.model.SessionDatum; import org.commcare.suite.model.StackFrameStep; import org.commcare.suite.model.Text; import org.commcare.tasks.FormLoaderTask; import org.commcare.tasks.FormRecordCleanupTask; import org.commcare.utils.ACRAUtil; import org.commcare.utils.AndroidCommCarePlatform; import org.commcare.utils.AndroidInstanceInitializer; import org.commcare.utils.ChangeLocaleUtil; import org.commcare.utils.EntityDetailUtils; import org.commcare.utils.GlobalConstants; import org.commcare.utils.SessionUnavailableException; import org.commcare.utils.StorageUtils; import org.commcare.utils.UriToFilePath; import org.commcare.views.UserfacingErrorHandling; import org.commcare.views.dialogs.CommCareAlertDialog; import org.commcare.views.dialogs.DialogChoiceItem; import org.commcare.views.dialogs.DialogCreationHelpers; import org.commcare.views.dialogs.PaneledChoiceDialog; import org.commcare.views.dialogs.StandardAlertDialog; import org.commcare.views.notifications.NotificationMessageFactory; import org.javarosa.core.model.User; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.services.Logger; import org.javarosa.core.services.locale.Localization; import org.javarosa.xpath.XPathTypeMismatchException; import java.io.File; import java.text.SimpleDateFormat; import java.util.HashMap; import java.util.Vector; /** * Manages all of the shared (mostly non-UI) components of a CommCare home screen: * activity lifecycle, implementation of available actions, session navigation, etc. */ public abstract class HomeScreenBaseActivity<T> extends SyncCapableCommCareActivity<T> implements SessionNavigationResponder { /** * Request code for launching a menu list or menu grid */ public static final int GET_COMMAND = 1; /** * Request code for launching EntitySelectActivity (to allow user to select a case), * or EntityDetailActivity (to allow user to confirm an auto-selected case) */ protected static final int GET_CASE = 2; protected static final int GET_REMOTE_DATA = 3; /** * Request code for launching FormEntryActivity */ protected static final int MODEL_RESULT = 4; protected static final int MAKE_REMOTE_POST = 5; public static final int GET_INCOMPLETE_FORM = 6; protected static final int PREFERENCES_ACTIVITY = 7; protected static final int ADVANCED_ACTIONS_ACTIVITY = 8; protected static final int CREATE_PIN = 9; protected static final int AUTHENTICATION_FOR_PIN = 10; private static final String KEY_PENDING_SESSION_DATA = "pending-session-data-id"; private static final String KEY_PENDING_SESSION_DATUM_ID = "pending-session-datum-id"; /** * Restart is a special CommCare activity result code which means that the session was * invalidated in the calling activity and that the current session should be resynced */ public static final int RESULT_RESTART = 3; private int mDeveloperModeClicks = 0; private SessionNavigator sessionNavigator; private boolean sessionNavigationProceedingAfterOnResume; private boolean loginExtraWasConsumed; private static final String EXTRA_CONSUMED_KEY = "login_extra_was_consumed"; private boolean isRestoringSession = false; // The API allows for external calls. When this occurs, redispatch to their // activity instead of commcare. private boolean wasExternal = false; private static final String WAS_EXTERNAL_KEY = "was_external"; @Override protected void onCreateSessionSafe(Bundle savedInstanceState) { super.onCreateSessionSafe(savedInstanceState); loadInstanceState(savedInstanceState); ACRAUtil.registerAppData(); AdMobManager.initAdsForCurrentConsumerApp(getApplicationContext()); sessionNavigator = new SessionNavigator(this); processFromExternalLaunch(savedInstanceState); processFromShortcutLaunch(); processFromLoginLaunch(); } private void loadInstanceState(Bundle savedInstanceState) { if (savedInstanceState != null) { loginExtraWasConsumed = savedInstanceState.getBoolean(EXTRA_CONSUMED_KEY); wasExternal = savedInstanceState.getBoolean(WAS_EXTERNAL_KEY); } } /** * Set state that signifies activity was launch from external app. */ private void processFromExternalLaunch(Bundle savedInstanceState) { if (savedInstanceState == null && getIntent().hasExtra(DispatchActivity.WAS_EXTERNAL)) { wasExternal = true; sessionNavigator.startNextSessionStep(); } } private void processFromShortcutLaunch() { if (getIntent().getBooleanExtra(DispatchActivity.WAS_SHORTCUT_LAUNCH, false)) { sessionNavigator.startNextSessionStep(); } } private void processFromLoginLaunch() { if (getIntent().getBooleanExtra(DispatchActivity.START_FROM_LOGIN, false) && !loginExtraWasConsumed) { getIntent().removeExtra(DispatchActivity.START_FROM_LOGIN); loginExtraWasConsumed = true; CommCareSession session = CommCareApplication.instance().getCurrentSession(); if (session.getCommand() != null) { // restore the session state if there is a command. // For debugging and occurs when a serialized // session is stored upon login isRestoringSession = true; sessionNavigator.startNextSessionStep(); return; } // Trigger off a regular unsent task processor, unless we're about to sync (which will // then handle this in a blocking fashion) if (!CommCareApplication.instance().isSyncPending(false)) { checkAndStartUnsentFormsTask(false, false); } if (isDemoUser()) { showDemoModeWarning(); return; } if (UpdatePromptHelper.promptForUpdateIfNeeded(this)) { return; } if (checkForPinLaunchConditions()) { return; } } } /** * See if we should launch either the pin choice dialog, or the create pin activity directly * * @return true if we launched a dialog */ private boolean checkForPinLaunchConditions() { LoginMode loginMode = (LoginMode)getIntent().getSerializableExtra(LoginActivity.LOGIN_MODE); if (loginMode == LoginMode.PRIMED) { launchPinCreateScreen(loginMode); return true; } if (loginMode == LoginMode.PASSWORD && DeveloperPreferences.shouldOfferPinForLogin()) { boolean userManuallyEnteredPasswordMode = getIntent() .getBooleanExtra(LoginActivity.MANUAL_SWITCH_TO_PW_MODE, false); boolean alreadyDismissedPinCreation = CommCareApplication.instance().getCurrentApp().getAppPreferences() .getBoolean(CommCarePreferences.HAS_DISMISSED_PIN_CREATION, false); if (!alreadyDismissedPinCreation || userManuallyEnteredPasswordMode) { showPinChoiceDialog(loginMode); return true; } } return false; } private void showPinChoiceDialog(final LoginMode loginMode) { String promptMessage; UserKeyRecord currentUserRecord = CommCareApplication.instance().getRecordForCurrentUser(); if (currentUserRecord.hasPinSet()) { promptMessage = Localization.get("pin.dialog.prompt.reset"); } else { promptMessage = Localization.get("pin.dialog.prompt.set"); } final PaneledChoiceDialog dialog = new PaneledChoiceDialog(this, promptMessage); DialogChoiceItem createPinChoice = new DialogChoiceItem( Localization.get("pin.dialog.yes"), -1, new View.OnClickListener() { @Override public void onClick(View v) { dismissAlertDialog(); launchPinCreateScreen(loginMode); } }); DialogChoiceItem nextTimeChoice = new DialogChoiceItem( Localization.get("pin.dialog.not.now"), -1, new View.OnClickListener() { @Override public void onClick(View v) { dismissAlertDialog(); } }); DialogChoiceItem notAgainChoice = new DialogChoiceItem( Localization.get("pin.dialog.never"), -1, new View.OnClickListener() { @Override public void onClick(View v) { dismissAlertDialog(); CommCareApplication.instance().getCurrentApp().getAppPreferences() .edit() .putBoolean(CommCarePreferences.HAS_DISMISSED_PIN_CREATION, true) .commit(); showPinFutureAccessDialog(); } }); dialog.setChoiceItems(new DialogChoiceItem[]{createPinChoice, nextTimeChoice, notAgainChoice}); dialog.addCollapsibleInfoPane(Localization.get("pin.dialog.extra.info")); showAlertDialog(dialog); } private void showPinFutureAccessDialog() { StandardAlertDialog.getBasicAlertDialog(this, Localization.get("pin.dialog.set.later.title"), Localization.get("pin.dialog.set.later.message"), null).showNonPersistentDialog(); } protected void launchPinAuthentication() { Intent i = new Intent(this, PinAuthenticationActivity.class); startActivityForResult(i, AUTHENTICATION_FOR_PIN); } private void launchPinCreateScreen(LoginMode loginMode) { Intent i = new Intent(this, CreatePinActivity.class); i.putExtra(LoginActivity.LOGIN_MODE, loginMode); startActivityForResult(i, CREATE_PIN); } protected void showLocaleChangeMenu(final CommCareActivityUIController uiController) { final PaneledChoiceDialog dialog = new PaneledChoiceDialog(this, Localization.get("home.menu.locale.select")); AdapterView.OnItemClickListener listClickListener = new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { String[] localeCodes = ChangeLocaleUtil.getLocaleCodes(); if (position >= localeCodes.length) { Localization.setLocale("default"); } else { Localization.setLocale(localeCodes[position]); } // rebuild home buttons in case language changed; if (uiController != null) { uiController.setupUI(); } rebuildOptionsMenu(); dismissAlertDialog(); } }; dialog.setChoiceItems(buildLocaleChoices(), listClickListener); showAlertDialog(dialog); } private static DialogChoiceItem[] buildLocaleChoices() { String[] locales = ChangeLocaleUtil.getLocaleNames(); DialogChoiceItem[] choices = new DialogChoiceItem[locales.length]; for (int i = 0; i < choices.length; i++) { choices[i] = DialogChoiceItem.nonListenerItem(locales[i]); } return choices; } protected void goToFormArchive(boolean incomplete) { goToFormArchive(incomplete, null); } protected void goToFormArchive(boolean incomplete, FormRecord record) { if (incomplete) { GoogleAnalyticsUtils.reportViewArchivedFormsList(GoogleAnalyticsFields.LABEL_INCOMPLETE); } else { GoogleAnalyticsUtils.reportViewArchivedFormsList(GoogleAnalyticsFields.LABEL_COMPLETE); } Intent i = new Intent(getApplicationContext(), FormRecordListActivity.class); if (incomplete) { i.putExtra(FormRecord.META_STATUS, FormRecord.STATUS_INCOMPLETE); } if (record != null) { i.putExtra(FormRecordListActivity.KEY_INITIAL_RECORD_ID, record.getID()); } startActivityForResult(i, GET_INCOMPLETE_FORM); } protected void userTriggeredLogout() { CommCareApplication.instance().closeUserSession(); setResult(RESULT_OK); finish(); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(WAS_EXTERNAL_KEY, wasExternal); outState.putBoolean(EXTRA_CONSUMED_KEY, loginExtraWasConsumed); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { if(resultCode == RESULT_RESTART) { sessionNavigator.startNextSessionStep(); } else { // if handling new return code (want to return to home screen) but a return at the end of your statement switch(requestCode) { case PREFERENCES_ACTIVITY: if (resultCode == AdvancedActionsActivity.RESULT_DATA_RESET) { finish(); } else if (resultCode == DeveloperPreferences.RESULT_SYNC_CUSTOM) { try { Uri uri = intent.getData(); String filePath = UriToFilePath.getPathFromUri(CommCareApplication.instance(), uri); if(filePath != null) { File f = new File(filePath); if (f != null && f.exists()) { formAndDataSyncer.performCustomRestoreFromFile(this, f); } } } catch(Exception e) { Toast.makeText(this, "Error loading custom sync...", Toast.LENGTH_LONG).show(); } } return; case ADVANCED_ACTIONS_ACTIVITY: handleAdvancedActionResult(resultCode, intent); return; case GET_INCOMPLETE_FORM: //TODO: We might need to load this from serialized state? if (resultCode == RESULT_CANCELED) { refreshUI(); return; } else if(resultCode == RESULT_OK) { int record = intent.getIntExtra("FORMRECORDS", -1); if (record == -1) { //Hm, what to do here? break; } FormRecord r = CommCareApplication.instance().getUserStorage(FormRecord.class).read(record); //Retrieve and load the appropriate ssd SqlStorage<SessionStateDescriptor> ssdStorage = CommCareApplication.instance().getUserStorage(SessionStateDescriptor.class); Vector<Integer> ssds = ssdStorage.getIDsForValue(SessionStateDescriptor.META_FORM_RECORD_ID, r.getID()); AndroidSessionWrapper currentState = CommCareApplication.instance().getCurrentSessionWrapper(); if (ssds.size() == 1) { currentState.loadFromStateDescription(ssdStorage.read(ssds.firstElement())); } else { currentState.setFormRecordId(r.getID()); } AndroidCommCarePlatform platform = CommCareApplication.instance().getCommCarePlatform(); formEntry(platform.getFormContentUri(r.getFormNamespace()), r); return; } break; case GET_COMMAND: boolean fetchNext = processReturnFromGetCommand(resultCode, intent); if (!fetchNext) { return; } break; case GET_CASE: fetchNext = processReturnFromGetCase(resultCode, intent); if (!fetchNext) { return; } break; case MODEL_RESULT: fetchNext = processReturnFromFormEntry(resultCode, intent); if (!fetchNext) { return; } break; case AUTHENTICATION_FOR_PIN: if (resultCode == RESULT_OK) { launchPinCreateScreen(LoginMode.PASSWORD); } return; case CREATE_PIN: boolean choseRememberPassword = intent != null && intent.getBooleanExtra(CreatePinActivity.CHOSE_REMEMBER_PASSWORD, false); if (choseRememberPassword) { CommCareApplication.instance().closeUserSession(); } else if (resultCode == RESULT_OK) { Toast.makeText(this, Localization.get("pin.set.success"), Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, Localization.get("pin.not.set"), Toast.LENGTH_SHORT).show(); } return; case MAKE_REMOTE_POST: stepBackIfCancelled(resultCode); if (resultCode == RESULT_OK) { CommCareApplication.instance().getCurrentSessionWrapper().terminateSession(); } break; case GET_REMOTE_DATA: stepBackIfCancelled(resultCode); break; } sessionNavigationProceedingAfterOnResume = true; startNextSessionStepSafe(); } super.onActivityResult(requestCode, resultCode, intent); } private boolean processReturnFromGetCase(int resultCode, Intent intent) { if (resultCode == RESULT_CANCELED) { return processCanceledGetCommandOrCase(); } else if (resultCode == RESULT_OK) { return processSuccessfulGetCase(intent); } return false; } public boolean processReturnFromGetCommand(int resultCode, Intent intent) { if (resultCode == RESULT_CANCELED) { return processCanceledGetCommandOrCase(); } else if (resultCode == RESULT_OK) { return processSuccessfulGetCommand(intent); } return true; } private boolean processSuccessfulGetCommand(Intent intent) { AndroidSessionWrapper currentState = CommCareApplication.instance().getCurrentSessionWrapper(); CommCareSession session = currentState.getSession(); if (sessionStateUnchangedSinceCallout(session, intent)) { // Get our command, set it, and continue forward String command = intent.getStringExtra(SessionFrame.STATE_COMMAND_ID); session.setCommand(command); return true; } else { clearSessionAndExit(currentState, true); return false; } } private boolean processSuccessfulGetCase(Intent intent) { AndroidSessionWrapper asw = CommCareApplication.instance().getCurrentSessionWrapper(); CommCareSession currentSession = asw.getSession(); if (sessionStateUnchangedSinceCallout(currentSession, intent)) { String sessionDatumId = currentSession.getNeededDatum().getDataId(); String chosenCaseId = intent.getStringExtra(SessionFrame.STATE_DATUM_VAL); currentSession.setDatum(sessionDatumId, chosenCaseId); return true; } else { clearSessionAndExit(asw, true); return false; } } private boolean processCanceledGetCommandOrCase() { AndroidSessionWrapper currentState = CommCareApplication.instance().getCurrentSessionWrapper(); if (currentState.getSession().getCommand() == null) { // Needed a command, and didn't already have one. Stepping back from // an empty state, Go home! currentState.reset(); refreshUI(); return false; } else { currentState.getSession().stepBack(currentState.getEvaluationContext()); return true; } } private void handleAdvancedActionResult(int resultCode, Intent intent) { if (resultCode == AdvancedActionsActivity.RESULT_FORMS_PROCESSED) { int formProcessCount = intent.getIntExtra(AdvancedActionsActivity.FORM_PROCESS_COUNT_KEY, 0); String localizationKey = intent.getStringExtra(AdvancedActionsActivity.FORM_PROCESS_MESSAGE_KEY); displayToast(Localization.get(localizationKey, new String[]{"" + formProcessCount})); refreshUI(); } } private static void stepBackIfCancelled(int resultCode) { if (resultCode == RESULT_CANCELED) { AndroidSessionWrapper asw = CommCareApplication.instance().getCurrentSessionWrapper(); CommCareSession currentSession = asw.getSession(); currentSession.stepBack(asw.getEvaluationContext()); } } public void startNextSessionStepSafe() { try { sessionNavigator.startNextSessionStep(); } catch (CommCareInstanceInitializer.FixtureInitializationException e) { sessionNavigator.stepBack(); if (isDemoUser()) { // most likely crashing due to data not being available in demo mode UserfacingErrorHandling.createErrorDialog(this, Localization.get("demo.mode.feature.unavailable"), false); } else { UserfacingErrorHandling.createErrorDialog(this, e.getMessage(), false); } } } /** * @return If the nature of the data that the session is waiting for has not changed since the * callout that we are returning from was made */ private boolean sessionStateUnchangedSinceCallout(CommCareSession session, Intent intent) { EvaluationContext evalContext = CommCareApplication.instance().getCurrentSessionWrapper().getEvaluationContext(); String pendingSessionData = intent.getStringExtra(KEY_PENDING_SESSION_DATA); String sessionNeededData = session.getNeededData(evalContext); boolean neededDataUnchanged = (pendingSessionData == null && sessionNeededData == null) || (pendingSessionData != null && pendingSessionData.equals(sessionNeededData)); String intentDatum = intent.getStringExtra(KEY_PENDING_SESSION_DATUM_ID); boolean datumIdsUnchanged = intentDatum == null || intentDatum.equals(session.getNeededDatum().getDataId()); return neededDataUnchanged && datumIdsUnchanged; } /** * Process user returning home from the form entry activity. * Triggers form submission cycle, cleans up some session state. * * @param resultCode exit code of form entry activity * @param intent The intent of the returning activity, with the * saved form provided as the intent URI data. Null if * the form didn't exit cleanly * @return Flag signifying that caller should fetch the next activity in * the session to launch. If false then caller should exit or spawn home * activity. */ private boolean processReturnFromFormEntry(int resultCode, Intent intent) { // TODO: We might need to load this from serialized state? AndroidSessionWrapper currentState = CommCareApplication.instance().getCurrentSessionWrapper(); // This is the state we were in when we _Started_ form entry FormRecord current = currentState.getFormRecord(); if (current == null) { // somehow we lost the form record for the current session Toast.makeText(this, "Error while trying to save the form!", Toast.LENGTH_LONG).show(); Logger.log(AndroidLogger.TYPE_ERROR_WORKFLOW, "Form Entry couldn't save because of corrupt state."); clearSessionAndExit(currentState, true); return false; } // TODO: This should be the default unless we're in some "Uninit" or "incomplete" state if ((intent != null && intent.getBooleanExtra(FormEntryConstants.IS_ARCHIVED_FORM, false)) || FormRecord.STATUS_COMPLETE.equals(current.getStatus()) || FormRecord.STATUS_SAVED.equals(current.getStatus())) { // Viewing an old form, so don't change the historical record // regardless of the exit code currentState.reset(); if (wasExternal) { setResult(RESULT_CANCELED); this.finish(); } else { // Return to where we started goToFormArchive(false, current); } return false; } if (resultCode == RESULT_OK) { // Determine if the form instance is complete Uri resultInstanceURI = null; if (intent != null) { resultInstanceURI = intent.getData(); } if (resultInstanceURI == null) { CommCareApplication.notificationManager().reportNotificationMessage( NotificationMessageFactory.message( NotificationMessageFactory.StockMessages.FormEntry_Unretrievable)); Toast.makeText(this, "Error while trying to read the form! See the notification", Toast.LENGTH_LONG).show(); Logger.log(AndroidLogger.TYPE_ERROR_WORKFLOW, "Form Entry did not return a form"); clearSessionAndExit(currentState, true); return false; } Cursor c = null; String instanceStatus; try { c = getContentResolver().query(resultInstanceURI, null, null, null, null); if (!c.moveToFirst()) { throw new IllegalArgumentException("Empty query for instance record!"); } instanceStatus = c.getString(c.getColumnIndexOrThrow(InstanceProviderAPI.InstanceColumns.STATUS)); } finally { if (c != null) { c.close(); } } // was the record marked complete? boolean complete = InstanceProviderAPI.STATUS_COMPLETE.equals(instanceStatus); // The form is either ready for processing, or not, depending on how it was saved if (complete) { // Now that we know this form is completed, we can give it the next available // submission ordering number current.setFormNumberForSubmissionOrdering(StorageUtils.getNextFormSubmissionNumber()); CommCareApplication.instance().getUserStorage(FormRecord.class).write(current); checkAndStartUnsentFormsTask(false, false); refreshUI(); if (wasExternal) { setResult(RESULT_CANCELED); this.finish(); return false; } // Before we can terminate the session, we need to know that the form has been // processed in case there is state that depends on it. boolean terminateSuccessful; try { terminateSuccessful = currentState.terminateSession(); } catch (XPathTypeMismatchException e) { UserfacingErrorHandling.logErrorAndShowDialog(this, e, true); return false; } if (!terminateSuccessful) { // If we didn't find somewhere to go, we're gonna stay here return false; } // Otherwise, we want to keep proceeding in order // to keep running the workflow } else { // Form record is now stored. // TODO: session state clearing might be something we want to do in InstanceProvider.bindToFormRecord. clearSessionAndExit(currentState, false); return false; } } else if (resultCode == RESULT_CANCELED) { // Nothing was saved during the form entry activity Logger.log(AndroidLogger.TYPE_FORM_ENTRY, "Form Entry Cancelled"); // If the form was unstarted, we want to wipe the record. if (current.getStatus().equals(FormRecord.STATUS_UNSTARTED)) { // Entry was cancelled. FormRecordCleanupTask.wipeRecord(this, currentState); } if (wasExternal) { currentState.reset(); setResult(RESULT_CANCELED); this.finish(); return false; } else if (current.getStatus().equals(FormRecord.STATUS_INCOMPLETE)) { currentState.reset(); // We should head back to the incomplete forms screen goToFormArchive(true, current); return false; } else { // If we cancelled form entry from a normal menu entry // we want to go back to where were were right before we started // entering the form. currentState.getSession().stepBack(currentState.getEvaluationContext()); currentState.setFormRecordId(-1); } } return true; } private void clearSessionAndExit(AndroidSessionWrapper currentState, boolean shouldWarnUser) { currentState.reset(); if (wasExternal) { setResult(RESULT_CANCELED); this.finish(); } refreshUI(); if (shouldWarnUser) { showSessionRefreshWarning(); } } private void showSessionRefreshWarning() { showAlertDialog(StandardAlertDialog.getBasicAlertDialog(this, Localization.get("session.refresh.error.title"), Localization.get("session.refresh.error.message"), null)); } private void showDemoModeWarning() { showAlertDialog(StandardAlertDialog.getBasicAlertDialogWithIcon(this, Localization.get("demo.mode.warning.title"), Localization.get("demo.mode.warning"), android.R.drawable.ic_dialog_info, null)); } private void createErrorDialog(String errorMsg, AlertDialog.OnClickListener errorListener) { showAlertDialog(StandardAlertDialog.getBasicAlertDialogWithIcon(this, Localization.get("app.handled.error.title"), errorMsg, android.R.drawable.ic_dialog_info, errorListener)); } @Override public void processSessionResponse(int statusCode) { AndroidSessionWrapper asw = CommCareApplication.instance().getCurrentSessionWrapper(); switch(statusCode) { case SessionNavigator.ASSERTION_FAILURE: handleAssertionFailureFromSessionNav(asw); break; case SessionNavigator.NO_CURRENT_FORM: handleNoFormFromSessionNav(asw); break; case SessionNavigator.START_FORM_ENTRY: startFormEntry(asw); break; case SessionNavigator.GET_COMMAND: handleGetCommand(asw); break; case SessionNavigator.START_ENTITY_SELECTION: launchEntitySelect(asw.getSession()); break; case SessionNavigator.LAUNCH_CONFIRM_DETAIL: launchConfirmDetail(asw); break; case SessionNavigator.PROCESS_QUERY_REQUEST: launchQueryMaker(); break; case SessionNavigator.START_SYNC_REQUEST: launchRemoteSync(asw); break; case SessionNavigator.XPATH_EXCEPTION_THROWN: UserfacingErrorHandling .logErrorAndShowDialog(this, sessionNavigator.getCurrentException(), false); asw.reset(); break; case SessionNavigator.REPORT_CASE_AUTOSELECT: GoogleAnalyticsUtils.reportFeatureUsage(GoogleAnalyticsFields.ACTION_CASE_AUTOSELECT_USED); break; } } @Override public CommCareSession getSessionForNavigator() { return CommCareApplication.instance().getCurrentSession(); } @Override public EvaluationContext getEvalContextForNavigator() { return CommCareApplication.instance().getCurrentSessionWrapper().getEvaluationContext(); } private void handleAssertionFailureFromSessionNav(final AndroidSessionWrapper asw) { EvaluationContext ec = asw.getEvaluationContext(); Text text = asw.getSession().getCurrentEntry().getAssertions().getAssertionFailure(ec); createErrorDialog(text.evaluate(ec), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int i) { dismissAlertDialog(); asw.getSession().stepBack(asw.getEvaluationContext()); HomeScreenBaseActivity.this.sessionNavigator.startNextSessionStep(); } }); } private void handleNoFormFromSessionNav(AndroidSessionWrapper asw) { boolean terminateSuccesful; try { terminateSuccesful = asw.terminateSession(); } catch (XPathTypeMismatchException e) { UserfacingErrorHandling.logErrorAndShowDialog(this, e, true); return; } if (terminateSuccesful) { sessionNavigator.startNextSessionStep(); } else { refreshUI(); } } private void handleGetCommand(AndroidSessionWrapper asw) { Intent i = new Intent(this, MenuActivity.class); String command = asw.getSession().getCommand(); i.putExtra(SessionFrame.STATE_COMMAND_ID, command); addPendingDataExtra(i, asw.getSession()); startActivityForResult(i, GET_COMMAND); } private void launchRemoteSync(AndroidSessionWrapper asw) { String command = asw.getSession().getCommand(); Entry commandEntry = CommCareApplication.instance().getCommCarePlatform().getEntry(command); if (commandEntry instanceof RemoteRequestEntry) { PostRequest postRequest = ((RemoteRequestEntry)commandEntry).getPostRequest(); Intent i = new Intent(getApplicationContext(), PostRequestActivity.class); i.putExtra(PostRequestActivity.URL_KEY, postRequest.getUrl()); i.putExtra(PostRequestActivity.PARAMS_KEY, new HashMap<>(postRequest.getEvaluatedParams(asw.getEvaluationContext()))); startActivityForResult(i, MAKE_REMOTE_POST); } else { // expected a sync entry; clear session and show vague 'session error' message to user clearSessionAndExit(asw, true); } } private void launchQueryMaker() { Intent i = new Intent(getApplicationContext(), QueryRequestActivity.class); startActivityForResult(i, GET_REMOTE_DATA); } private void launchEntitySelect(CommCareSession session) { startActivityForResult(getSelectIntent(session), GET_CASE); } private Intent getSelectIntent(CommCareSession session) { Intent i = new Intent(getApplicationContext(), EntitySelectActivity.class); i.putExtra(SessionFrame.STATE_COMMAND_ID, session.getCommand()); StackFrameStep lastPopped = session.getPoppedStep(); if (lastPopped != null && SessionFrame.STATE_DATUM_VAL.equals(lastPopped.getType())) { i.putExtra(EntitySelectActivity.EXTRA_ENTITY_KEY, lastPopped.getValue()); } addPendingDataExtra(i, session); addPendingDatumIdExtra(i, session); return i; } public void launchUpdateActivity() { Intent i = new Intent(getApplicationContext(), UpdateActivity.class); startActivity(i); } // Launch an intent to load the confirmation screen for the current selection private void launchConfirmDetail(AndroidSessionWrapper asw) { CommCareSession session = asw.getSession(); SessionDatum selectDatum = session.getNeededDatum(); if (selectDatum instanceof EntityDatum) { EntityDatum entityDatum = (EntityDatum) selectDatum; TreeReference contextRef = sessionNavigator.getCurrentAutoSelection(); if (this.getString(R.string.panes).equals("two") && getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { // Large tablet in landscape: send to entity select activity // (awesome mode, with case pre-selected) instead of entity detail Intent i = getSelectIntent(session); String caseId = EntityDatum.getCaseIdFromReference( contextRef, entityDatum, asw.getEvaluationContext()); i.putExtra(EntitySelectActivity.EXTRA_ENTITY_KEY, caseId); startActivityForResult(i, GET_CASE); } else { // Launch entity detail activity Intent detailIntent = new Intent(getApplicationContext(), EntityDetailActivity.class); EntityDetailUtils.populateDetailIntent( detailIntent, contextRef, entityDatum, asw); addPendingDataExtra(detailIntent, session); addPendingDatumIdExtra(detailIntent, session); startActivityForResult(detailIntent, GET_CASE); } } } protected static void addPendingDataExtra(Intent i, CommCareSession session) { EvaluationContext evalContext = CommCareApplication.instance().getCurrentSessionWrapper().getEvaluationContext(); i.putExtra(KEY_PENDING_SESSION_DATA, session.getNeededData(evalContext)); } private static void addPendingDatumIdExtra(Intent i, CommCareSession session) { i.putExtra(KEY_PENDING_SESSION_DATUM_ID, session.getNeededDatum().getDataId()); } /** * Create (or re-use) a form record and pass it to the form entry activity * launcher. If there is an existing incomplete form that uses the same * case, ask the user if they want to edit or delete that one. * * @param state Needed for FormRecord manipulations */ private void startFormEntry(AndroidSessionWrapper state) { if (state.getFormRecordId() == -1) { if (CommCarePreferences.isIncompleteFormsEnabled()) { // Are existing (incomplete) forms using the same case? SessionStateDescriptor existing = state.getExistingIncompleteCaseDescriptor(); if (existing != null) { // Ask user if they want to just edit existing form that // uses the same case. createAskUseOldDialog(state, existing); return; } } // Generate a stub form record and commit it state.commitStub(); } else { Logger.log("form-entry", "Somehow ended up starting form entry with old state?"); } FormRecord record = state.getFormRecord(); AndroidCommCarePlatform platform = CommCareApplication.instance().getCommCarePlatform(); formEntry(platform.getFormContentUri(record.getFormNamespace()), record, CommCareActivity.getTitle(this, null)); } private void formEntry(Uri formUri, FormRecord r) { formEntry(formUri, r, null); } private void formEntry(Uri formUri, FormRecord r, String headerTitle) { Logger.log(AndroidLogger.TYPE_FORM_ENTRY, "Form Entry Starting|" + r.getFormNamespace()); //TODO: This is... just terrible. Specify where external instance data should come from FormLoaderTask.iif = new AndroidInstanceInitializer(CommCareApplication.instance().getCurrentSession()); // Create our form entry activity callout Intent i = new Intent(getApplicationContext(), FormEntryActivity.class); i.setAction(Intent.ACTION_EDIT); i.putExtra(FormEntryInstanceState.KEY_INSTANCEDESTINATION, CommCareApplication.instance().getCurrentApp().fsPath((GlobalConstants.FILE_CC_FORMS))); // See if there's existing form data that we want to continue entering // (note, this should be stored in the form record as a URI link to // the instance provider in the future) if(r.getInstanceURI() != null) { i.setData(r.getInstanceURI()); } else { i.setData(formUri); } i.putExtra(FormEntryActivity.KEY_RESIZING_ENABLED, CommCarePreferences.getResizeMethod()); i.putExtra(FormEntryActivity.KEY_INCOMPLETE_ENABLED, CommCarePreferences.isIncompleteFormsEnabled()); i.putExtra(FormEntryActivity.KEY_AES_STORAGE_KEY, Base64.encodeToString(r.getAesKey(), Base64.DEFAULT)); i.putExtra(FormEntryActivity.KEY_FORM_CONTENT_URI, FormsProviderAPI.FormsColumns.CONTENT_URI.toString()); i.putExtra(FormEntryActivity.KEY_INSTANCE_CONTENT_URI, InstanceProviderAPI.InstanceColumns.CONTENT_URI.toString()); i.putExtra(FormEntrySessionWrapper.KEY_RECORD_FORM_ENTRY_SESSION, DeveloperPreferences.isSessionSavingEnabled()); if (headerTitle != null) { i.putExtra(FormEntryActivity.KEY_HEADER_STRING, headerTitle); } if (isRestoringSession) { isRestoringSession = false; SharedPreferences prefs = CommCareApplication.instance().getCurrentApp().getAppPreferences(); String formEntrySession = prefs.getString(CommCarePreferences.CURRENT_FORM_ENTRY_SESSION, ""); if (!"".equals(formEntrySession)) { i.putExtra(FormEntrySessionWrapper.KEY_FORM_ENTRY_SESSION, formEntrySession); } } startActivityForResult(i, MODEL_RESULT); } /** * Triggered when an automatic sync is pending */ private void handlePendingSync() { long lastSync = CommCareApplication.instance().getCurrentApp().getAppPreferences().getLong("last-ota-restore", 0); String footer = lastSync == 0 ? "never" : SimpleDateFormat.getDateTimeInstance().format(lastSync); Logger.log(AndroidLogger.TYPE_USER, "autosync triggered. Last Sync|" + footer); refreshUI(); sendFormsOrSync(false); } @Override protected void onResumeSessionSafe() { if (!sessionNavigationProceedingAfterOnResume) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { refreshActionBar(); } attemptDispatchHomeScreen(); } sessionNavigationProceedingAfterOnResume = false; } /** * Decides if we should actually be on the home screen, or else should redirect elsewhere */ private void attemptDispatchHomeScreen() { try { CommCareApplication.instance().getSession(); } catch (SessionUnavailableException e) { // User was logged out somehow, so we want to return to dispatch activity setResult(RESULT_OK); this.finish(); return; } if (CommCareApplication.instance().isSyncPending(false)) { // There is a sync pending handlePendingSync(); } else { // Display the home screen! refreshUI(); } } private void createAskUseOldDialog(final AndroidSessionWrapper state, final SessionStateDescriptor existing) { final AndroidCommCarePlatform platform = CommCareApplication.instance().getCommCarePlatform(); String title = Localization.get("app.workflow.incomplete.continue.title"); String msg = Localization.get("app.workflow.incomplete.continue"); StandardAlertDialog d = new StandardAlertDialog(this, title, msg); DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int i) { switch (i) { case DialogInterface.BUTTON_POSITIVE: // use the old form instance and load the it's state from the descriptor state.loadFromStateDescription(existing); formEntry(platform.getFormContentUri(state.getSession().getForm()), state.getFormRecord()); break; case DialogInterface.BUTTON_NEGATIVE: // delete the old incomplete form FormRecordCleanupTask.wipeRecord(HomeScreenBaseActivity.this, existing); // fallthrough to new now that old record is gone case DialogInterface.BUTTON_NEUTRAL: // create a new form record and begin form entry state.commitStub(); formEntry(platform.getFormContentUri(state.getSession().getForm()), state.getFormRecord()); } dismissAlertDialog(); } }; d.setPositiveButton(Localization.get("option.yes"), listener); d.setNegativeButton(Localization.get("app.workflow.incomplete.continue.option.delete"), listener); d.setNeutralButton(Localization.get("option.no"), listener); showAlertDialog(d); } protected static boolean isDemoUser() { try { User u = CommCareApplication.instance().getSession().getLoggedInUser(); return (User.TYPE_DEMO.equals(u.getUserType())); } catch (SessionUnavailableException e) { // Default to a normal user: this should only happen if session // expires and hasn't redirected to login. return false; } } public static void createPreferencesMenu(Activity activity) { Intent i = new Intent(activity, CommCarePreferences.class); activity.startActivityForResult(i, PREFERENCES_ACTIVITY); } protected void startAdvancedActionsActivity() { startActivityForResult(new Intent(this, AdvancedActionsActivity.class), ADVANCED_ACTIONS_ACTIVITY); } protected void showAboutCommCareDialog() { CommCareAlertDialog dialog = DialogCreationHelpers.buildAboutCommCareDialog(this); dialog.makeCancelable(); dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { handleDeveloperModeClicks(); } }); showAlertDialog(dialog); } private void handleDeveloperModeClicks() { mDeveloperModeClicks++; if (mDeveloperModeClicks == 4) { CommCareApplication.instance().getCurrentApp().getAppPreferences() .edit() .putString(DeveloperPreferences.SUPERUSER_ENABLED, CommCarePreferences.YES) .commit(); Toast.makeText(this, Localization.get("home.developer.options.enabled"), Toast.LENGTH_SHORT).show(); } } @Override public boolean isBackEnabled() { return false; } /** * For Testing purposes only */ public SessionNavigator getSessionNavigator() { if (BuildConfig.DEBUG) { return sessionNavigator; } else { throw new RuntimeException("On principal of design, only meant for testing purposes"); } } /** * For Testing purposes only */ public void setFormAndDataSyncer(FormAndDataSyncer formAndDataSyncer) { if (BuildConfig.DEBUG) { this.formAndDataSyncer = formAndDataSyncer; } else { throw new RuntimeException("On principal of design, only meant for testing purposes"); } } abstract void refreshUI(); }