/** Copyright 2015 Tim Engler, Rareventure LLC This file is part of Tiny Travel Tracker. Tiny Travel Tracker is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Tiny Travel Tracker is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Tiny Travel Tracker. If not, see <http://www.gnu.org/licenses/>. */ package com.rareventure.gps2; import com.rareventure.android.FatalErrorActivity; import com.rareventure.gps2.GTG.Requirement; import com.rareventure.gps2.gpx.RestoreGpxBackup; import com.rareventure.gps2.reviewer.DbDoesntExistActivity; import com.rareventure.gps2.reviewer.TimmyCorruptActivity; import com.rareventure.gps2.reviewer.TimmyNeedsProcessingActivity; import com.rareventure.gps2.reviewer.TimmyNeedsUpgradeActivity; import com.rareventure.gps2.reviewer.TrialExpiredActivity; import com.rareventure.gps2.reviewer.password.EnterPasswordActivity; import com.rareventure.gps2.reviewer.wizard.EnterNewPasswordPage; import com.rareventure.gps2.reviewer.wizard.WelcomePage; import android.app.Activity; import android.content.Intent; import android.os.Bundle; public class GTGActivityHelper { private Activity activity; private IGTGActivity iGtgActivity; private Bundle doOnCreateBundle; /** * All the requirements needed to run the activity in the current state. This will also * include requirements inherited by the intent. (This is so if the app is killed and * restarted and the page doesn't require, for example, a password, but the previous * page does, it will still be asked for, in order that if the user hits the back button, * it won't then ask for a password) */ private int neededRequirements; private static enum State { START, IN_DO_ON_CREATE, DO_CREATE_CALLED, IN_DO_ON_RESUME, DO_RESUME_CALLED, DO_PAUSE_CALLED } private boolean isNextActionInternal = false; private State state = State.START; /** * Note this is static so that an activity in the stack can set the intent and * then back out to the root which will then execute it. */ private static Intent exitingFromAppIntent; private boolean forwarded; /** * true if we want to exit from the app completely */ private static boolean exitingFromApp; public static final String INTENT_REQUIREMENTS_BITMAP = GTGActivityHelper.class+".INTENT_REQUIREMENTS_BITMAP"; /** * Did the user hit back? If so we don't ever prompt for anything * and simply assume the user doesn't want to enter */ private static boolean isBackAction; public GTGActivityHelper(Activity activity, int activityRequirements) { this.activity = activity; this.iGtgActivity = (IGTGActivity) activity; this.neededRequirements = activityRequirements; } /** * We store requirements from the previous pages in the call stack * in the intent itself. We make sure all these requirements are * satisfied for the current page regardless if it requires them * or not, so if the user hits back, they won't be prompted for * some weird requirements. */ private void loadRequirementsFromIntent() { Intent i = activity.getIntent(); neededRequirements = (neededRequirements | i.getIntExtra(INTENT_REQUIREMENTS_BITMAP, 0)); } public void onCreate(Bundle bundle) { //we always start the service incase the process was killed. If the service shouldn't //be running, it will stop itself //PERF maybe a little wasteful to keep restarting the service if it is not supposed to //be running activity.startService(new Intent(activity, GpsTrailerService.class)); if(state != State.START) throw new IllegalStateException("Don't call me from doOnCreate " + "(or some other problem, why is state "+state+"?)"); //load previous requirements from back stack loadRequirementsFromIntent(); //if something is required that is not present but can be solved by user interaction // we do so here //note, it's really important that we don't call initAndForwardToHandlingScrenIfNecessary twice. //This is because when exitingFromApp is true, this value gets immediately shut off when //we reach the top of the task. So if we let initAndForward be called again, it would try //to forward to fix the requirements that are unfulfilled if(initAndForwardToHandlingScreenIfNecessary()) { this.doOnCreateBundle = bundle; this.forwarded = true; return; } state = State.IN_DO_ON_CREATE; iGtgActivity.doOnCreate(bundle); state = State.DO_CREATE_CALLED; } public void onResume() { if(forwarded) return; loadRequirementsFromIntent(); //if something is required that is not present but can be solved by user interaction // we do so here if(initAndForwardToHandlingScreenIfNecessary()) { return; } //if we had to forwardToHandlingScreenIfNecessary() from onCreate(), we haven't yet called DO_ON_CREATE, //so we do so now if(state == State.START) { state = State.IN_DO_ON_CREATE; iGtgActivity.doOnCreate(doOnCreateBundle); doOnCreateBundle = null; state = State.DO_CREATE_CALLED; } //I don't quite trust android to always get the states right (I have noticed that onDestroy() // sometimes is not called when screens are flipping in fast succession) so I don't worry // as long as it's not being called from doOnResume if(state == State.IN_DO_ON_RESUME) throw new IllegalStateException("Don't call me from doOnResume" + "(or some other problem, why is state "+state+"?)"); state = State.IN_DO_ON_RESUME; //if we are going to actually display something, we no longer are backing out isBackAction = false; //we don't know what the next action will be.. if the user hits home or did some other action //that leaves the app, then when we get to onPause this value won't change and we know this isNextActionInternal = false; iGtgActivity.doOnResume(); state = State.DO_RESUME_CALLED; } /** * Inits the system according to the requirements. If necessary, goes to a support * activity for user interactive intialization (such as entering password, or reporting * the db needs upgrading, etc.) */ private boolean initAndForwardToHandlingScreenIfNecessary() { if (exitingFromApp) { activity.finish(); if(activity.isTaskRoot()) { if (exitingFromAppIntent != null) activity.startActivity(exitingFromAppIntent); exitingFromApp = false; isBackAction = false; } return true; } GTG.initRwtm.registerWritingThread(); try { //these are the requirements that haven't already been fulfilled, but need to be int requirementsDiff = neededRequirements & (~GTG.fulfilledRequirements); GTG.requireInitialSetup(activity, true); if (Requirement.PREFS_LOADED.isOn(requirementsDiff)) GTG.requirePrefsLoaded(activity); if (Requirement.NOT_IN_RESTORE.isOn(requirementsDiff)) { if (!GTG.requireNotInRestore()) { if(!isBackAction) //start an internal activity without requiring the back stack requirements startInternalActivity(new Intent(activity, RestoreGpxBackup.class), false); else activity.finish(); return true; } } if (Requirement.NOT_TRIAL_WHEN_PREMIUM_IS_AVAILABLE .isOn(requirementsDiff)) { Intent i = GTG.requireNotTrialWhenPremiumIsAvailable(activity); if (i != null) { exitFromApp(i); return true; } } if (Requirement.NOT_TRIAL_EXPIRED.isOn(requirementsDiff)) { if (!GTG.requireNotTrialExpired()) { if(!isBackAction) //start an internal activity without requiring the back stack requirements startInternalActivity(new Intent(activity, TrialExpiredActivity.class), false); else activity.finish(); return true; } } if (Requirement.SDCARD_PRESENT.isOn(requirementsDiff)) { if (!GTG.requireSdcardPresent(activity)) { if(!isBackAction) startInternalActivity(new Intent(activity, FatalErrorActivity.class).putExtra( FatalErrorActivity.MESSAGE_RESOURCE_ID, R.string.error_reviewer_sdcard_not_mounted), false); else activity.finish(); return true; } } if (Requirement.SYSTEM_INSTALLED.isOn(requirementsDiff)) { if (!GTG.requireSystemInstalled(activity)) { if(!isBackAction) startInternalActivity(new Intent(activity, WelcomePage.class), false); else activity.finish(); return true; } } if (Requirement.DB_READY.isOn(requirementsDiff)) { int status = GTG.requireDbReady(); if (status == GTG.REQUIRE_DB_READY_DB_DOESNT_EXIST) { if(!isBackAction) startInternalActivity(new Intent(activity, DbDoesntExistActivity.class), false); else activity.finish(); return true; } } if (Requirement.DECRYPT.isOn(requirementsDiff)) { //we don't want the user to have to retype their password right after creating it if (GTG.requireEncryptAndDecrypt(EnterNewPasswordPage.passwordInitializedWith) != GTG.REQUIRE_DECRYPT_OK) { if(!isBackAction) startInternalActivity( new Intent(activity, EnterPasswordActivity.class) .putExtra( EnterPasswordActivity.EXTRA_DECRYPT_OR_VERIFY_PASSWORD_BOOL, true), false); else activity.finish(); return true; } } if (Requirement.ENCRYPT.isOn(requirementsDiff)) { GTG.requireEncrypt(); } if (Requirement.PASSWORD_ENTERED.isOn(requirementsDiff)) { //we don't want the user to have to retype their password right after creating it if (!GTG.requirePasswordEntered(EnterNewPasswordPage.passwordInitializedWith, GTG.lastGtgClosedMS)) { if(!isBackAction) startInternalActivity( new Intent(activity, EnterPasswordActivity.class) .putExtra( EnterPasswordActivity.EXTRA_DECRYPT_OR_VERIFY_PASSWORD_BOOL, false), false); else activity.finish(); return true; } } if (Requirement.TIMMY_DB_READY.isOn(requirementsDiff)) { int status = GTG.requireTimmyDbReady(false); if (status == GTG.REQUIRE_TIMMY_DB_IS_CORRUPT) { if(!isBackAction) startInternalActivity(new Intent(activity, TimmyCorruptActivity.class), false); else activity.finish(); return true; } if (status == GTG.REQUIRE_TIMMY_DB_NEEDS_UPGRADING) { if(!isBackAction) startInternalActivity(new Intent(activity, TimmyNeedsUpgradeActivity.class), false); else activity.finish(); return true; } if (status == GTG.REQUIRE_TIMMY_DB_NEEDS_PROCESSING_TIME) { if(!isBackAction) startInternalActivity(new Intent(activity, TimmyNeedsProcessingActivity.class), false); else activity.finish(); return true; } } return false; } finally { GTG.initRwtm.unregisterWritingThread(); } } /** * Finishes the current activity and goes to the previous activity in the task. */ public void finish() { if(!activity.isTaskRoot()) isNextActionInternal = true; iGtgActivity.superFinish(); } /** * Starts a new activity within the application * @param requireBackStack true if the requirements from the activities in the * back stack should be merged with the requirements for the current activity */ public void startInternalActivity(Intent intent, boolean requireBackStack) { isNextActionInternal = true; if(requireBackStack) addRequirementsToIntent(intent); activity.startActivity(intent); } public void startInternalActivity(Intent intent) { startInternalActivity(intent, true); } /** * Adds requirements needed for this page and pages in the back stack * to the intent */ private void addRequirementsToIntent(Intent intent) { intent.putExtra(INTENT_REQUIREMENTS_BITMAP, neededRequirements); } public void onPause() { forwarded = false; if(activity.isTaskRoot()) { isBackAction = false; exitingFromApp = false; } if(!isNextActionInternal) { //we want to clear the initial password if the user leaves the app EnterNewPasswordPage.passwordInitializedWith = null; GTG.setAppPasswordNotEntered(); //they might have went to buy the premium package Requirement.NOT_TRIAL_WHEN_PREMIUM_IS_AVAILABLE.reset(); } iGtgActivity.doOnPause(state == State.DO_RESUME_CALLED); } public void onBackPressed() { isBackAction = true; finish(); } public void performCancel() { isBackAction = true; finish(); } public void exitFromApp() { exitFromApp(null); } public void exitFromApp(Intent i) { if(activity.isTaskRoot()) { activity.startActivity(i); isBackAction = false; } else { exitingFromApp = true; exitingFromAppIntent = i; } activity.finish(); return; } public void startInternalActivityForResult(Intent i, int s) { isNextActionInternal = true; activity.startActivityForResult(i, s); } }