package org.commcare.activities; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.util.Log; import android.widget.Toast; import org.commcare.AppUtils; import org.commcare.CommCareApp; import org.commcare.CommCareApplication; import org.commcare.dalvik.R; import org.commcare.android.database.global.models.ApplicationRecord; import org.commcare.android.database.user.models.SessionStateDescriptor; import org.commcare.preferences.DeveloperPreferences; import org.commcare.utils.AndroidShortcuts; import org.commcare.utils.LifecycleUtils; import org.commcare.utils.MultipleAppsUtil; import org.commcare.utils.SessionUnavailableException; import org.commcare.views.dialogs.AlertDialogFragment; import org.commcare.views.dialogs.StandardAlertDialog; import org.javarosa.core.services.locale.Localization; /** * Dispatches install, login, and home screen activities. * * @author Phillip Mates (pmates@dimagi.com). */ public class DispatchActivity extends FragmentActivity { private static final String TAG = DispatchActivity.class.getSimpleName(); private static final String SESSION_REQUEST = "ccodk_session_request"; public static final String WAS_EXTERNAL = "launch_from_external"; public static final String WAS_SHORTCUT_LAUNCH = "launch_from_shortcut"; public static final String START_FROM_LOGIN = "process_successful_login"; private static final int LOGIN_USER = 0; private static final int HOME_SCREEN = 1; public static final int INIT_APP = 2; /** * Request code for automatically validating media. * Should signal a return from CommCareVerificationActivity. */ public static final int MISSING_MEDIA_ACTIVITY = 4; private boolean startFromLogin; private LoginMode lastLoginMode; private boolean userManuallyEnteredPasswordMode; private boolean shouldFinish; private boolean userTriggeredLogout; private boolean shortcutExtraWasConsumed; private static final String EXTRA_CONSUMED_KEY = "shortcut_extra_was_consumed"; private static final String KEY_APP_FILES_CHECK_OCCURRED = "check-for-changed-app-files-occurred"; private static final String KEY_WAITING_FOR_ACTIVITY_RESULT = "waiting-for-login-activity-result"; private boolean waitingForActivityResultFromLogin; boolean alreadyCheckedForAppFilesChange; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (finishIfNotRoot()) { return; } if (savedInstanceState != null) { shortcutExtraWasConsumed = savedInstanceState.getBoolean(EXTRA_CONSUMED_KEY); alreadyCheckedForAppFilesChange = savedInstanceState.getBoolean(KEY_APP_FILES_CHECK_OCCURRED); waitingForActivityResultFromLogin = savedInstanceState.getBoolean(KEY_WAITING_FOR_ACTIVITY_RESULT); } } /** * A workaround required by Android Bug #2373 -- An app launched from the Google Play store * has different intent flags than one launched from the App launcher, which ruins the back * stack and prevents the app from launching a high affinity task. * * @return if finish() was called */ private boolean finishIfNotRoot() { if (!isTaskRoot()) { Intent intent = getIntent(); String action = intent.getAction(); if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && action != null && action.equals(Intent.ACTION_MAIN)) { finish(); return true; } } return false; } @Override protected void onResume() { super.onResume(); if (shouldFinish) { finish(); } else { dispatch(); } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(EXTRA_CONSUMED_KEY, shortcutExtraWasConsumed); outState.putBoolean(KEY_APP_FILES_CHECK_OCCURRED, alreadyCheckedForAppFilesChange); outState.putBoolean(KEY_WAITING_FOR_ACTIVITY_RESULT, waitingForActivityResultFromLogin); } private void checkForChangedCCZ() { alreadyCheckedForAppFilesChange = true; Intent i = new Intent(this, UpdateActivity.class); startActivity(i); } private void dispatch() { if (isDbInBadState()) { // appropriate error dialog has been triggered, don't continue w/ dispatch return; } CommCareApp currentApp = CommCareApplication.instance().getCurrentApp(); if (currentApp == null) { if (MultipleAppsUtil.usableAppsPresent()) { AppUtils.initFirstUsableAppRecord(); // Recurse in order to make the correct decision based on the new state dispatch(); } else { Intent i = new Intent(getApplicationContext(), CommCareSetupActivity.class); this.startActivityForResult(i, INIT_APP); } } else { // Note that the order in which these conditions are checked matters!! if (CommCareApplication.instance().isConsumerApp() && !alreadyCheckedForAppFilesChange) { checkForChangedCCZ(); return; } ApplicationRecord currentRecord = currentApp.getAppRecord(); try { if (currentApp.getAppResourceState() == CommCareApplication.STATE_CORRUPTED) { // The seated app is damaged or corrupted handleDamagedApp(); } else if (!currentRecord.isUsable()) { // The seated app is unusable (means either it is archived or is // missing its MM or both) boolean unseated = handleUnusableApp(currentRecord); if (unseated) { // Recurse in order to make the correct decision based on the new state dispatch(); } } else if (!CommCareApplication.instance().getSession().isActive()) { launchLoginScreen(); } else if (this.getIntent().hasExtra(SESSION_REQUEST)) { // CommCare was launched from an external app, with a session descriptor handleExternalLaunch(); } else if (this.getIntent().hasExtra(AndroidShortcuts.EXTRA_KEY_SHORTCUT) && !shortcutExtraWasConsumed) { // CommCare was launched from a shortcut handleShortcutLaunch(); } else { launchHomeScreen(); } } catch (SessionUnavailableException sue) { launchLoginScreen(); } } } private boolean isDbInBadState() { int dbState = CommCareApplication.instance().getDatabaseState(); if (dbState == CommCareApplication.STATE_MIGRATION_FAILED) { LifecycleUtils.triggerHandledAppExit(this, getString(R.string.migration_definite_failure), getString(R.string.migration_failure_title), false); return true; } else if (dbState == CommCareApplication.STATE_MIGRATION_QUESTIONABLE) { LifecycleUtils.triggerHandledAppExit(this, getString(R.string.migration_possible_failure), getString(R.string.migration_failure_title), false); return true; } else if (dbState == CommCareApplication.STATE_CORRUPTED) { handleDamagedApp(); return true; } return false; } private void handleDamagedApp() { if (!CommCareApplication.instance().isStorageAvailable()) { createNoStorageDialog(); } else { // See if we're logged in. If so, prompt for recovery. try { CommCareApplication.instance().getSession(); createAskFixDialog().show(getSupportFragmentManager(), "damage-dialog"); } catch (SessionUnavailableException e) { // Otherwise, log in first launchLoginScreen(); } } } private void createNoStorageDialog() { LifecycleUtils.triggerHandledAppExit(this, Localization.get("app.storage.missing.message"), Localization.get("app.storage.missing.title")); } private void launchLoginScreen() { if (!waitingForActivityResultFromLogin) { // AMS 06/09/16: This check is needed due to what we believe is a bug in the Android platform Intent i = new Intent(this, LoginActivity.class); i.putExtra(LoginActivity.USER_TRIGGERED_LOGOUT, userTriggeredLogout); startActivityForResult(i, LOGIN_USER); waitingForActivityResultFromLogin = true; } else { Log.w(TAG, "Login redirection bug occurred; DispatchActivity is attempting to launch " + "a new LoginActivity while it is still waiting for a result from " + "another one."); } } private void launchHomeScreen() { Intent i; if (useRootMenuHomeActivity()) { i = new Intent(this, RootMenuHomeActivity.class); // Since we are entering a menu list, the session state will expect this later HomeScreenBaseActivity.addPendingDataExtra(i, CommCareApplication.instance().getCurrentSessionWrapper().getSession()); } else { i = new Intent(this, StandardHomeActivity.class); } i.putExtra(START_FROM_LOGIN, startFromLogin); i.putExtra(LoginActivity.LOGIN_MODE, lastLoginMode); i.putExtra(LoginActivity.MANUAL_SWITCH_TO_PW_MODE, userManuallyEnteredPasswordMode); startFromLogin = false; startActivityForResult(i, HOME_SCREEN); } public static boolean useRootMenuHomeActivity() { return DeveloperPreferences.useRootModuleMenuAsHomeScreen() || CommCareApplication.instance().isConsumerApp(); } /** * @param record the ApplicationRecord corresponding to the seated, unusable app * @return if the unusable app was unseated by this method */ private boolean handleUnusableApp(ApplicationRecord record) { if (record.isArchived()) { // If the app is archived, unseat it and try to seat another one CommCareApplication.instance().unseat(record); AppUtils.initFirstUsableAppRecord(); return true; } else { // This app has unvalidated MM if (MultipleAppsUtil.usableAppsPresent()) { // If there are other usable apps, unseat it and seat another one CommCareApplication.instance().unseat(record); AppUtils.initFirstUsableAppRecord(); return true; } else { handleUnvalidatedApp(); return false; } } } /** * Handles the case where the seated app is unvalidated and there are no other usable apps * to seat instead -- Either calls out to verification activity or quits out of the app */ private void handleUnvalidatedApp() { if (MultipleAppsUtil.shouldSeeMMVerification()) { Intent i = new Intent(this, CommCareVerificationActivity.class); this.startActivityForResult(i, MISSING_MEDIA_ACTIVITY); } else { // Means that there are no usable apps, but there are multiple apps who all don't have // MM verified -- show an error message and shut down LifecycleUtils.triggerHandledAppExit(this, Localization.get("multiple.apps.unverified.message"), Localization.get("multiple.apps.unverified.title")); } } private void handleExternalLaunch() { String sessionRequest = this.getIntent().getStringExtra(SESSION_REQUEST); SessionStateDescriptor ssd = new SessionStateDescriptor(); ssd.fromBundle(sessionRequest); CommCareApplication.instance().getCurrentSessionWrapper().loadFromStateDescription(ssd); Intent i = new Intent(this, StandardHomeActivity.class); i.putExtra(WAS_EXTERNAL, true); startActivityForResult(i, HOME_SCREEN); } private void handleShortcutLaunch() { if (!triggerLoginIfNeeded()) { //We were launched in shortcut mode. Get the command and load us up. CommCareApplication.instance().getCurrentSession().setCommand( this.getIntent().getStringExtra(AndroidShortcuts.EXTRA_KEY_SHORTCUT)); getIntent().removeExtra(AndroidShortcuts.EXTRA_KEY_SHORTCUT); shortcutExtraWasConsumed = true; Intent i = new Intent(this, StandardHomeActivity.class); i.putExtra(WAS_SHORTCUT_LAUNCH, true); startActivityForResult(i, HOME_SCREEN); } } private boolean triggerLoginIfNeeded() { try { if (!CommCareApplication.instance().getSession().isActive()) { launchLoginScreen(); return true; } } catch (SessionUnavailableException e) { launchLoginScreen(); return true; } return false; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { // if handling new return code (want to return to home screen) but a return at the end of your statement switch (requestCode) { case INIT_APP: if (resultCode == RESULT_CANCELED) { // User pressed back button from install screen, so take them out of CommCare shouldFinish = true; } return; case MISSING_MEDIA_ACTIVITY: if (resultCode == RESULT_CANCELED) { // exit the app if media wasn't validated on automatic // validation check. shouldFinish = true; } else if (resultCode == RESULT_OK && !CommCareApplication.instance().isConsumerApp()) { Toast.makeText(this, "Media Validated!", Toast.LENGTH_LONG).show(); } return; case LOGIN_USER: waitingForActivityResultFromLogin = false; if (resultCode == RESULT_CANCELED) { shouldFinish = true; } else if (intent != null) { lastLoginMode = (LoginMode)intent.getSerializableExtra(LoginActivity.LOGIN_MODE); userManuallyEnteredPasswordMode = intent.getBooleanExtra(LoginActivity.MANUAL_SWITCH_TO_PW_MODE, false); startFromLogin = true; } return; case HOME_SCREEN: if (resultCode == RESULT_CANCELED) { shouldFinish = true; return; } else { userTriggeredLogout = true; } return; } super.onActivityResult(requestCode, resultCode, intent); } private AlertDialogFragment createAskFixDialog() { //TODO: Localize this in theory, but really shift it to the upgrade/management state String title = "Storage is Corrupt :/"; String message = "Sorry, something really bad has happened, and the app can't start up. " + "With your permission CommCare can try to repair itself if you have network access."; StandardAlertDialog d = new StandardAlertDialog(this, title, message); DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int i) { switch (i) { case DialogInterface.BUTTON_POSITIVE: // attempt repair Intent intent = new Intent(DispatchActivity.this, RecoveryActivity.class); startActivity(intent); break; case DialogInterface.BUTTON_NEGATIVE: // Shut down DispatchActivity.this.finish(); break; } } }; d.setPositiveButton("Enter Recovery Mode", listener); d.setNegativeButton("Shut Down", listener); return AlertDialogFragment.fromCommCareAlertDialog(d); } }