org.opendatakit.survey.activities.MainMenuActivity.java Source code

Java tutorial

Introduction

Here is the source code for org.opendatakit.survey.activities.MainMenuActivity.java

Source

/*
 * Copyright (C) 2009-2013 University of Washington
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package org.opendatakit.survey.activities;

import android.annotation.SuppressLint;
import android.app.ActionBar;
import android.app.AlertDialog;
import android.app.DialogFragment;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentManager.BackStackEntry;
import android.app.FragmentTransaction;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

import org.apache.commons.lang3.StringEscapeUtils;
import org.json.JSONObject;
import org.opendatakit.demoAndroidlibraryClasses.consts.IntentConsts;
import org.opendatakit.demoAndroidCommonClasses.activities.BaseActivity;
import org.opendatakit.demoAndroidCommonClasses.application.CommonApplication;
import org.opendatakit.demoAndroidlibraryClasses.database.data.OrderedColumns;
import org.opendatakit.demoAndroidlibraryClasses.database.data.UserTable;
import org.opendatakit.demoAndroidlibraryClasses.exception.ActionNotAuthorizedException;
import org.opendatakit.demoAndroidlibraryClasses.exception.ServicesAvailabilityException;
import org.opendatakit.demoAndroidlibraryClasses.fragment.AboutMenuFragment;
import org.opendatakit.demoAndroidCommonClasses.listener.DatabaseConnectionListener;
import org.opendatakit.demoAndroidlibraryClasses.properties.CommonToolProperties;
import org.opendatakit.demoAndroidlibraryClasses.properties.DynamicPropertiesCallback;
import org.opendatakit.demoAndroidlibraryClasses.properties.PropertiesSingleton;
import org.opendatakit.demoAndroidlibraryClasses.properties.PropertyManager;
import org.opendatakit.demoAndroidlibraryClasses.provider.FormsColumns;
import org.opendatakit.demoAndroidlibraryClasses.provider.FormsProviderAPI;
import org.opendatakit.survey.fragments.FrontPageFragment;
import org.opendatakit.demoAndroidCommonClasses.webkitserver.utilities.SerializationUtils;
import org.opendatakit.demoAndroidCommonClasses.webkitserver.utilities.SerializationUtils.MacroStringExpander;
import org.opendatakit.demoAndroidlibraryClasses.utilities.ODKFileUtils;
import org.opendatakit.demoAndroidCommonClasses.webkitserver.utilities.UrlUtils;
import org.opendatakit.demoAndroidlibraryClasses.logging.WebLogger;
import org.opendatakit.demoAndroidlibraryClasses.logging.WebLoggerIf;
import org.opendatakit.demoAndroidCommonClasses.views.ExecutorContext;
import org.opendatakit.demoAndroidCommonClasses.views.ExecutorProcessor;
import org.opendatakit.demoAndroidCommonClasses.views.ODKWebView;
import org.opendatakit.demoAndroidlibraryClasses.database.service.UserDbInterface;
import org.opendatakit.demoAndroidlibraryClasses.database.service.DbHandle;
import org.opendatakit.demoAndroidlibraryClasses.database.service.TableHealthInfo;
import org.opendatakit.demoAndroidlibraryClasses.database.service.TableHealthStatus;
import org.opendatakit.survey.R;
import org.opendatakit.survey.application.Survey;
import org.opendatakit.survey.fragments.BackPressWebkitConfirmationDialogFragment;
import org.opendatakit.survey.fragments.FormChooserListFragment;
import org.opendatakit.survey.fragments.InitializationFragment;
import org.opendatakit.survey.fragments.WebViewFragment;
import org.opendatakit.survey.logic.FormIdStruct;
import org.opendatakit.survey.logic.SurveyDataExecutorProcessor;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;

/**
 * Responsible for displaying buttons to launch the major activities. Launches
 * some activities based on returns of others.
 *
 * @author mitchellsundt@gmail.com
 */
public class MainMenuActivity extends BaseActivity
        implements IOdkSurveyActivity, FrontPageFragment.OnFragmentInteractionListener {

    private static final String t = "MainMenuActivity";

    public static enum ScreenList {
        MAIN_SCREEN, FORM_CHOOSER, WEBKIT, INITIALIZATION_DIALOG, ABOUT_MENU, FRONT_PAGE
    };

    // Extra returned from gp activity
    // TODO: move to Survey???
    public static final String LOCATION_LATITUDE_RESULT = "latitude";
    public static final String LOCATION_LONGITUDE_RESULT = "longitude";
    public static final String LOCATION_ALTITUDE_RESULT = "altitude";
    public static final String LOCATION_ACCURACY_RESULT = "accuracy";

    // tags for retained context
    private static final String DISPATCH_STRING_WAITING_FOR_DATA = "dispatchStringWaitingForData";
    private static final String ACTION_WAITING_FOR_DATA = "actionWaitingForData";

    private static final String FORM_URI = "formUri";
    private static final String UPLOAD_TABLE_ID = "uploadTableId";
    private static final String INSTANCE_ID = "instanceId";
    private static final String SCREEN_PATH = "screenPath";
    private static final String CONTROLLER_STATE = "controllerState";
    private static final String AUXILLARY_HASH = "auxillaryHash";
    private static final String SESSION_VARIABLES = "sessionVariables";
    private static final String SECTION_STATE_SCREEN_HISTORY = "sectionStateScreenHistory";

    private static final String CURRENT_FRAGMENT = "currentFragment";

    private static final String QUEUED_ACTIONS = "queuedActions";
    private static final String RESPONSE_JSON = "responseJSON";

    /** tables that have conflict rows */
    public static final String CONFLICT_TABLES = "conflictTables";

    // menu options

    private static final int MENU_CLOUD_FORMS = Menu.FIRST;
    private static final int MENU_ABOUT = Menu.FIRST + 1;

    // activity callback codes
    private static final int HANDLER_ACTIVITY_CODE = 20;
    private static final int INTERNAL_ACTIVITY_CODE = 21;
    private static final int SYNC_ACTIVITY_CODE = 22;
    private static final int CONFLICT_ACTIVITY_CODE = 23;

    private static final String BACKPRESS_DIALOG_TAG = "backPressDialog";

    private static final boolean EXIT = true;

    private static class ScreenState {
        String screenPath;
        String state;

        ScreenState(String screenPath, String state) {
            this.screenPath = screenPath;
            this.state = state;
        }
    }

    private static class SectionScreenStateHistory implements Parcelable {
        ScreenState currentScreen = new ScreenState(null, null);
        ArrayList<ScreenState> history = new ArrayList<ScreenState>();

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeString(currentScreen.screenPath);
            dest.writeString(currentScreen.state);

            dest.writeInt(history.size());
            for (int i = 0; i < history.size(); ++i) {
                ScreenState screen = history.get(i);
                dest.writeString(screen.screenPath);
                dest.writeString(screen.state);
            }
        }

        public static final Parcelable.Creator<SectionScreenStateHistory> CREATOR = new Parcelable.Creator<SectionScreenStateHistory>() {
            public SectionScreenStateHistory createFromParcel(Parcel in) {
                SectionScreenStateHistory cur = new SectionScreenStateHistory();
                String screenPath = in.readString();
                String state = in.readString();
                cur.currentScreen = new ScreenState(screenPath, state);
                int count = in.readInt();
                for (int i = 0; i < count; ++i) {
                    screenPath = in.readString();
                    state = in.readString();
                    cur.history.add(new ScreenState(screenPath, state));
                }
                return cur;
            }

            @Override
            public SectionScreenStateHistory[] newArray(int size) {
                SectionScreenStateHistory[] array = new SectionScreenStateHistory[size];
                for (int i = 0; i < size; ++i) {
                    array[i] = null;
                }
                return array;
            }
        };
    }

    /**
     * Member variables that are saved and restored across orientation changes.
     */

    private ScreenList currentFragment = ScreenList.FRONT_PAGE;

    private String dispatchStringWaitingForData = null;
    private String actionWaitingForData = null;

    private String appName = null;
    private String uploadTableId = null;
    private FormIdStruct currentForm = null; // via FORM_URI (formUri)
    private String instanceId = null;

    private Bundle sessionVariables = new Bundle();
    private ArrayList<SectionScreenStateHistory> sectionStateScreenHistory = new ArrayList<SectionScreenStateHistory>();

    private final String refId = UUID.randomUUID().toString();
    private String auxillaryHash = null;

    private String frameworkBaseUrl = null;
    private Long frameworkLastModifiedDate = 0L;

    private LinkedList<String> queuedActions = new LinkedList<String>();

    LinkedList<String> queueResponseJSON = new LinkedList<String>();

    // DO NOT USE THESE -- only used to determine if the current form has changed.
    private String trackingFormPath = null;
    private Long trackingFormLastModifiedDate = 0L;

    private String submenuPage = null;
    /**
     * track which tables have conflicts (these need to be resolved before Survey
     * can operate)
     */
    Bundle mConflictTables = new Bundle();

    /**
     * Member variables that do not need to be preserved across orientation
     * changes, etc.
     */

    private DatabaseConnectionListener mIOdkDataDatabaseListener;
    // no need to preserve
    private PropertyManager mPropertyManager;

    // no need to preserve
    private AlertDialog mAlertDialog;

    @Override
    protected void onPause() {
        // if (mAlertDialog != null && mAlertDialog.isShowing()) {
        // mAlertDialog.dismiss();
        // }
        super.onPause();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        // TODO Auto-generated method stub
        super.onSaveInstanceState(outState);

        if (dispatchStringWaitingForData != null) {
            outState.putString(DISPATCH_STRING_WAITING_FOR_DATA, dispatchStringWaitingForData);
        }
        if (actionWaitingForData != null) {
            outState.putString(ACTION_WAITING_FOR_DATA, actionWaitingForData);
        }
        outState.putString(CURRENT_FRAGMENT, currentFragment.name());

        if (getCurrentForm() != null) {
            outState.putString(FORM_URI, getCurrentForm().formUri.toString());
        }
        if (getInstanceId() != null) {
            outState.putString(INSTANCE_ID, getInstanceId());
        }
        if (getUploadTableId() != null) {
            outState.putString(UPLOAD_TABLE_ID, getUploadTableId());
        }
        if (getScreenPath() != null) {
            outState.putString(SCREEN_PATH, getScreenPath());
        }
        if (getControllerState() != null) {
            outState.putString(CONTROLLER_STATE, getControllerState());
        }
        if (getAuxillaryHash() != null) {
            outState.putString(AUXILLARY_HASH, getAuxillaryHash());
        }
        if (getAppName() != null) {
            outState.putString(IntentConsts.INTENT_KEY_APP_NAME, getAppName());
        }
        outState.putBundle(SESSION_VARIABLES, sessionVariables);

        outState.putParcelableArrayList(SECTION_STATE_SCREEN_HISTORY, sectionStateScreenHistory);

        if (!queuedActions.isEmpty()) {
            String[] actionOutcomesArray = new String[queuedActions.size()];
            queuedActions.toArray(actionOutcomesArray);
            outState.putStringArray(QUEUED_ACTIONS, actionOutcomesArray);
        }

        if (!queueResponseJSON.isEmpty()) {
            String[] qra = queueResponseJSON.toArray(new String[queueResponseJSON.size()]);
            outState.putStringArray(RESPONSE_JSON, qra);
        }

        if (mConflictTables != null && !mConflictTables.isEmpty()) {
            outState.putBundle(CONFLICT_TABLES, mConflictTables);
        }

    }

    private void transitionToFormHelper(Uri uri, FormIdStruct newForm) {
        // work through switching to that form
        setAppName(newForm.appName);
        setCurrentForm(newForm);
        clearSectionScreenState();
        String fragment = uri.getFragment();
        if (fragment != null && fragment.length() != 0) {
            // and process the fragment to find the instanceId, screenPath and other
            // kv pairs
            String[] pargs = fragment.split("&");
            boolean first = true;
            StringBuilder b = new StringBuilder();
            int i;
            for (i = 0; i < pargs.length; ++i) {
                String[] keyValue = pargs[i].split("=");
                if ("instanceId".equals(keyValue[0])) {
                    if (keyValue.length == 2) {
                        setInstanceId(StringEscapeUtils.unescapeHtml4(keyValue[1]));
                    }
                } else if ("screenPath".equals(keyValue[0])) {
                    if (keyValue.length == 2) {
                        setSectionScreenState(StringEscapeUtils.unescapeHtml4(keyValue[1]), null);
                    }
                } else if ("refId".equals(keyValue[0]) || "formPath".equals(keyValue[0])) {
                    // ignore
                } else {
                    if (!first) {
                        b.append("&");
                    }
                    first = false;
                    b.append(pargs[i]);
                }
            }
            String aux = b.toString();
            if (aux.length() != 0) {
                setAuxillaryHash(aux);
            }
        } else {
            setInstanceId(null);
            setAuxillaryHash(null);
        }
        currentFragment = ScreenList.WEBKIT;
    }

    @Override
    protected void onStop() {
        super.onStop();
    }

    @SuppressLint("InlinedApi")
    @Override
    protected void onStart() {
        super.onStart();
    }

    public void scanForConflictAllTables() {

        UserDbInterface db = ((Survey) getApplication()).getDatabase();
        if (db != null) {
            List<TableHealthInfo> info;
            DbHandle dbHandle = null;
            try {
                dbHandle = db.openDatabase(appName);
                info = db.getTableHealthStatuses(getAppName(), dbHandle);
            } catch (ServicesAvailabilityException e) {
                WebLogger.getLogger(appName).printStackTrace(e);
                return;
            } finally {
                try {
                    if (dbHandle != null) {
                        db.closeDatabase(appName, dbHandle);
                    }
                } catch (ServicesAvailabilityException e) {
                    WebLogger.getLogger(appName).printStackTrace(e);
                }
            }

            if (info != null) {

                Bundle conflictTables = new Bundle();

                for (TableHealthInfo tableInfo : info) {
                    TableHealthStatus status = tableInfo.getHealthStatus();
                    if (status == TableHealthStatus.TABLE_HEALTH_HAS_CONFLICTS
                            || status == TableHealthStatus.TABLE_HEALTH_HAS_CHECKPOINTS_AND_CONFLICTS) {
                        conflictTables.putString(tableInfo.getTableId(), tableInfo.getTableId());
                    }
                }

                mConflictTables = conflictTables;
            }
        }
    }

    private void resolveAnyConflicts() {
        if (mConflictTables == null || mConflictTables.isEmpty()) {
            scanForConflictAllTables();
        }

        if ((mConflictTables != null) && !mConflictTables.isEmpty()) {
            Iterator<String> iterator = mConflictTables.keySet().iterator();
            String tableId = iterator.next();
            mConflictTables.remove(tableId);

            Intent i;
            i = new Intent();
            i.setComponent(new ComponentName(IntentConsts.ResolveConflict.APPLICATION_NAME,
                    IntentConsts.ResolveConflict.ACTIVITY_NAME));
            i.setAction(Intent.ACTION_EDIT);
            i.putExtra(IntentConsts.INTENT_KEY_APP_NAME, getAppName());
            i.putExtra(IntentConsts.INTENT_KEY_TABLE_ID, tableId);
            try {
                this.startActivityForResult(i, CONFLICT_ACTIVITY_CODE);
            } catch (ActivityNotFoundException e) {
                Toast.makeText(this,
                        getString(R.string.activity_not_found, IntentConsts.ResolveConflict.ACTIVITY_NAME),
                        Toast.LENGTH_LONG).show();
            }
        }
    }

    @Override
    public void databaseAvailable() {
        if (getAppName() != null) {
            resolveAnyConflicts();
        }
        FragmentManager mgr = this.getFragmentManager();
        if (currentFragment != null) {
            Fragment fragment = mgr.findFragmentByTag(currentFragment.name());
            if (fragment instanceof DatabaseConnectionListener) {
                ((DatabaseConnectionListener) fragment).databaseAvailable();
            }
        }
        if (mIOdkDataDatabaseListener != null) {
            mIOdkDataDatabaseListener.databaseAvailable();
        }
    }

    @Override
    public void databaseUnavailable() {
        FragmentManager mgr = this.getFragmentManager();
        if (currentFragment != null) {
            Fragment fragment = mgr.findFragmentByTag(currentFragment.name());
            if (fragment instanceof DatabaseConnectionListener) {
                ((DatabaseConnectionListener) fragment).databaseUnavailable();
            }
        }
        if (mIOdkDataDatabaseListener != null) {
            mIOdkDataDatabaseListener.databaseUnavailable();
        }
    }

    public void setCurrentForm(FormIdStruct currentForm) {
        WebLogger.getLogger(getAppName()).i(t,
                "setCurrentForm: " + ((currentForm == null) ? "null" : currentForm.formPath));
        this.currentForm = currentForm;
    }

    public FormIdStruct getCurrentForm() {
        return this.currentForm;
    }

    private void setUploadTableId(String uploadTableId) {
        WebLogger.getLogger(getAppName()).i(t, "setUploadTableId: " + uploadTableId);
        this.uploadTableId = uploadTableId;
    }

    @Override
    public String getUploadTableId() {
        return this.uploadTableId;
    }

    @Override
    public void setInstanceId(String instanceId) {
        WebLogger.getLogger(getAppName()).i(t, "setInstanceId: " + instanceId);
        this.instanceId = instanceId;
    }

    @Override
    public String getInstanceId() {
        return this.instanceId;
    }

    public void setAuxillaryHash(String auxillaryHash) {
        WebLogger.getLogger(getAppName()).i(t, "setAuxillaryHash: " + auxillaryHash);
        this.auxillaryHash = auxillaryHash;
    }

    @Override
    public String getAppName() {
        return this.appName;
    }

    @Override
    public String getActiveUser() {
        PropertiesSingleton props = CommonToolProperties.get(this, getAppName());

        return props.getActiveUser();
    }

    @Override
    public String getProperty(String propertyId) {
        FormIdStruct form = getCurrentForm();
        PropertiesSingleton props = CommonToolProperties.get(this, getAppName());

        final DynamicPropertiesCallback cb = new DynamicPropertiesCallback(getAppName(),
                form == null ? null : getCurrentForm().tableId, getInstanceId(), props.getActiveUser(),
                props.getLocale(), props.getProperty(CommonToolProperties.KEY_USERNAME),
                props.getProperty(CommonToolProperties.KEY_ACCOUNT));

        String value = mPropertyManager.getSingularProperty(propertyId, cb);
        return value;
    }

    @Override
    public String getWebViewContentUri() {
        Uri u = UrlUtils.getWebViewContentUri(this);

        String uriString = u.toString();

        // Ensures that the string always ends with '/'
        if (uriString.charAt(uriString.length() - 1) != '/') {
            return uriString + "/";
        } else {
            return uriString;
        }
    }

    @Override
    public String getUrlBaseLocation(boolean ifChanged) {
        // Find the formPath for the framework formDef.json
        File frameworkFormDef = new File(ODKFileUtils.getFormFolder(appName, FormsColumns.COMMON_BASE_FORM_ID,
                FormsColumns.COMMON_BASE_FORM_ID), "formDef.json");

        // formPath always begins ../ -- strip that off to get explicit path
        // suffix...
        File systemFolder = new File(ODKFileUtils.getSystemFolder(appName));

        // File htmlFile = new File(mediaFolder, mPrompt.getAppearanceHint());
        File htmlFile = new File(systemFolder, "index.html");

        if (!htmlFile.exists()) {
            return null;
        }

        String fullPath = UrlUtils.getAsWebViewUri(this, appName, ODKFileUtils.asUriFragment(appName, htmlFile));

        if (fullPath == null) {
            return null;
        }

        // for some reason, the jqMobile framework wants an empty search string...
        // add this here now...
        fullPath += "?";
        Long frameworkLastModified = frameworkFormDef.lastModified();

        boolean changed = false;

        if (ifChanged && frameworkBaseUrl != null && frameworkBaseUrl.equals(fullPath)) {
            // determine if there are any changes in the framework
            // or in the form. If there are, reload. Otherwise,
            // return null.

            changed = (!frameworkLastModified.equals(frameworkLastModifiedDate));
        }

        if (currentForm == null) {
            trackingFormPath = null;
            trackingFormLastModifiedDate = 0L;
            changed = true;
        } else if (trackingFormPath == null || !trackingFormPath.equals(currentForm.formPath)) {
            trackingFormPath = currentForm.formPath;
            trackingFormLastModifiedDate = currentForm.lastDownloadDate.getTime();
        } else {
            changed = changed || (Long.valueOf(trackingFormLastModifiedDate)
                    .compareTo(currentForm.lastDownloadDate.getTime()) < 0);
            trackingFormLastModifiedDate = currentForm.lastDownloadDate.getTime();
        }

        frameworkBaseUrl = fullPath;
        frameworkLastModifiedDate = frameworkLastModified;
        return (ifChanged && !changed) ? null : frameworkBaseUrl;
    }

    @Override
    public String getUrlLocationHash() {
        if (currentForm == null) {
            // we want framework...
            File frameworkFormDef = new File(ODKFileUtils.getFormFolder(appName, FormsColumns.COMMON_BASE_FORM_ID,
                    FormsColumns.COMMON_BASE_FORM_ID), "formDef.json");

            String hashUrl = "#formPath="
                    + StringEscapeUtils.escapeHtml4(ODKFileUtils.getRelativeFormPath(appName, frameworkFormDef))
                    + ((instanceId == null) ? "" : "&instanceId=" + StringEscapeUtils.escapeHtml4(instanceId))
                    + ((getScreenPath() == null) ? ""
                            : "&screenPath=" + StringEscapeUtils.escapeHtml4(getScreenPath()))
                    + ("&refId=" + StringEscapeUtils.escapeHtml4(refId))
                    + ((auxillaryHash == null) ? "" : "&" + auxillaryHash);
            return hashUrl;
        } else {
            String hashUrl = "#formPath="
                    + StringEscapeUtils.escapeHtml4((currentForm == null) ? "" : currentForm.formPath)
                    + ((instanceId == null) ? "" : "&instanceId=" + StringEscapeUtils.escapeHtml4(instanceId))
                    + ((getScreenPath() == null) ? ""
                            : "&screenPath=" + StringEscapeUtils.escapeHtml4(getScreenPath()))
                    + ("&refId=" + StringEscapeUtils.escapeHtml4(refId))
                    + ((auxillaryHash == null) ? "" : "&" + auxillaryHash);

            return hashUrl;
        }
    }

    @Override
    public void clearAuxillaryHash() {
        this.auxillaryHash = null;
    }

    public String getAuxillaryHash() {
        return this.auxillaryHash;
    }

    public void setAppName(String appName) {
        this.appName = appName;
    }

    public String getRefId() {
        return this.refId;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // android.os.Debug.waitForDebugger();
        submenuPage = getIntentExtras().getString("_sync_state");
        try {
            // ensure that we have a BackgroundTaskFragment...
            // create it programmatically because if we place it in the
            // layout XML, it will be recreated with each screen rotation
            // and we don't want that!!!
            mPropertyManager = new PropertyManager(this);

            // must be at the beginning of any activity that can be called from an
            // external intent
            setAppName(ODKFileUtils.getOdkDefaultAppName());
            Uri uri = getIntent().getData();
            Uri formUri = null;
            if (uri != null) {
                // initialize to the URI, then we will customize further based upon the
                // savedInstanceState...
                final Uri uriFormsProvider = FormsProviderAPI.CONTENT_URI;
                final Uri uriWebView = UrlUtils.getWebViewContentUri(this);
                if (uri.getScheme().equalsIgnoreCase(uriFormsProvider.getScheme())
                        && uri.getAuthority().equalsIgnoreCase(uriFormsProvider.getAuthority())) {
                    List<String> segments = uri.getPathSegments();
                    if (segments != null && segments.size() == 1) {
                        String appName = segments.get(0);
                        setAppName(appName);
                    } else if (segments != null && segments.size() >= 2) {
                        String appName = segments.get(0);
                        setAppName(appName);
                        String tableId = segments.get(1);
                        String formId = (segments.size() > 2) ? segments.get(2) : null;
                        formUri = Uri.withAppendedPath(Uri.withAppendedPath(
                                Uri.withAppendedPath(FormsProviderAPI.CONTENT_URI, appName), tableId), formId);
                    } else {
                        createErrorDialog(getString(R.string.invalid_uri_expecting_n_segments, uri.toString(), 2),
                                EXIT);
                        return;
                    }
                } else if (uri.getScheme().equals(uriWebView.getScheme())
                        && uri.getAuthority().equals(uriWebView.getAuthority())
                        && uri.getPort() == uriWebView.getPort()) {
                    List<String> segments = uri.getPathSegments();
                    if (segments != null && segments.size() == 1) {
                        String appName = segments.get(0);
                        setAppName(appName);
                    } else {
                        createErrorDialog(getString(R.string.invalid_uri_expecting_one_segment, uri.toString()),
                                EXIT);
                        return;
                    }

                } else {
                    createErrorDialog(getString(R.string.unrecognized_uri, uri.toString(), uriWebView.toString(),
                            uriFormsProvider.toString()), EXIT);
                    return;
                }
            }

            if (savedInstanceState != null) {
                // if appName is explicitly set, use it...
                setAppName(savedInstanceState.containsKey(IntentConsts.INTENT_KEY_APP_NAME)
                        ? savedInstanceState.getString(IntentConsts.INTENT_KEY_APP_NAME)
                        : getAppName());

                if (savedInstanceState.containsKey(CONFLICT_TABLES)) {
                    mConflictTables = savedInstanceState.getBundle(CONFLICT_TABLES);
                }
            }

            try {
                String appName = getAppName();
                if (appName != null && appName.length() != 0) {
                    ODKFileUtils.verifyExternalStorageAvailability();
                    ODKFileUtils.assertDirectoryStructure(appName);
                }
            } catch (RuntimeException e) {
                createErrorDialog(e.getMessage(), EXIT);
                return;
            }

            WebLogger.getLogger(getAppName()).i(t, "Starting up, creating directories");

            if (savedInstanceState != null) {
                // if we are restoring, assume that initialization has already occurred.

                dispatchStringWaitingForData = savedInstanceState.containsKey(DISPATCH_STRING_WAITING_FOR_DATA)
                        ? savedInstanceState.getString(DISPATCH_STRING_WAITING_FOR_DATA)
                        : null;
                actionWaitingForData = savedInstanceState.containsKey(ACTION_WAITING_FOR_DATA)
                        ? savedInstanceState.getString(ACTION_WAITING_FOR_DATA)
                        : null;

                currentFragment = ScreenList.valueOf(savedInstanceState.containsKey(CURRENT_FRAGMENT)
                        ? savedInstanceState.getString(CURRENT_FRAGMENT)
                        : currentFragment.name());

                if (savedInstanceState.containsKey(FORM_URI)) {
                    FormIdStruct newForm = FormIdStruct.retrieveFormIdStruct(getContentResolver(),
                            Uri.parse(savedInstanceState.getString(FORM_URI)));
                    if (newForm != null) {
                        setAppName(newForm.appName);
                        setCurrentForm(newForm);
                    }
                }
                setInstanceId(
                        savedInstanceState.containsKey(INSTANCE_ID) ? savedInstanceState.getString(INSTANCE_ID)
                                : getInstanceId());
                setUploadTableId(savedInstanceState.containsKey(UPLOAD_TABLE_ID)
                        ? savedInstanceState.getString(UPLOAD_TABLE_ID)
                        : getUploadTableId());

                String tmpScreenPath = savedInstanceState.containsKey(SCREEN_PATH)
                        ? savedInstanceState.getString(SCREEN_PATH)
                        : getScreenPath();
                String tmpControllerState = savedInstanceState.containsKey(CONTROLLER_STATE)
                        ? savedInstanceState.getString(CONTROLLER_STATE)
                        : getControllerState();
                setSectionScreenState(tmpScreenPath, tmpControllerState);

                setAuxillaryHash(savedInstanceState.containsKey(AUXILLARY_HASH)
                        ? savedInstanceState.getString(AUXILLARY_HASH)
                        : getAuxillaryHash());

                if (savedInstanceState.containsKey(SESSION_VARIABLES)) {
                    sessionVariables = savedInstanceState.getBundle(SESSION_VARIABLES);
                }

                if (savedInstanceState.containsKey(SECTION_STATE_SCREEN_HISTORY)) {
                    sectionStateScreenHistory = savedInstanceState
                            .getParcelableArrayList(SECTION_STATE_SCREEN_HISTORY);
                }

                if (savedInstanceState.containsKey(QUEUED_ACTIONS)) {
                    String[] actionOutcomesArray = savedInstanceState.getStringArray(QUEUED_ACTIONS);
                    queuedActions.clear();
                    queuedActions.addAll(Arrays.asList(actionOutcomesArray));
                }

                if (savedInstanceState != null && savedInstanceState.containsKey(RESPONSE_JSON)) {
                    String[] pendingResponseJSON = savedInstanceState.getStringArray(RESPONSE_JSON);
                    queueResponseJSON.addAll(Arrays.asList(pendingResponseJSON));
                }
            } else if (formUri != null) {
                // request specifies a specific formUri -- try to open that
                FormIdStruct newForm = FormIdStruct.retrieveFormIdStruct(getContentResolver(), formUri);
                if (newForm == null) {
                    // can't find it -- launch the initialization dialog to hopefully
                    // discover it.
                    WebLogger.getLogger(getAppName()).i(t, "onCreate -- calling setRunInitializationTask");
                    ((Survey) getApplication()).setRunInitializationTask(getAppName());
                    currentFragment = ScreenList.WEBKIT;
                } else {
                    transitionToFormHelper(uri, newForm);
                }
            }
        } catch (Exception e) {
            createErrorDialog(e.getMessage(), EXIT);
        } finally {
            setContentView(R.layout.main_screen);

            ActionBar actionBar = getActionBar();
            actionBar.show();
        }
    }

    @Override
    public void onResume() {
        super.onResume();

        ((Survey) getApplication()).establishDoNotFireDatabaseConnectionListener(this);

        swapToFragmentView(currentFragment);
    }

    @Override
    public void onPostResume() {
        super.onPostResume();
        ((Survey) getApplication()).fireDatabaseConnectionListener();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        PropertiesSingleton props = CommonToolProperties.get(this, getAppName());

        int showOption = MenuItem.SHOW_AS_ACTION_IF_ROOM;
        MenuItem item;
        if (currentFragment != ScreenList.WEBKIT) {
            ActionBar actionBar = getActionBar();
            actionBar.setCustomView(R.layout.action_bar);
            actionBar.setDisplayShowCustomEnabled(true);
            actionBar.show();

            item = menu.add(Menu.NONE, MENU_CLOUD_FORMS, Menu.NONE, R.string.sync);
            item.setIcon(R.drawable.ic_cached_black_24dp).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);

            item = menu.add(Menu.NONE, MENU_ABOUT, Menu.NONE, R.string.about);
            item.setIcon(R.drawable.ic_info_outline_black_24dp).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
        } else {
            ActionBar actionBar = getActionBar();
            actionBar.hide();
        }

        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {

        if (item.getItemId() == MENU_CLOUD_FORMS) {
            try {
                Intent syncIntent = new Intent();
                syncIntent.setComponent(
                        new ComponentName(IntentConsts.Sync.APPLICATION_NAME, IntentConsts.Sync.ACTIVITY_NAME));
                syncIntent.setAction(Intent.ACTION_DEFAULT);
                Bundle bundle = new Bundle();
                bundle.putString(IntentConsts.INTENT_KEY_APP_NAME, appName);
                syncIntent.putExtras(bundle);
                this.startActivityForResult(syncIntent, SYNC_ACTIVITY_CODE);
            } catch (ActivityNotFoundException e) {
                WebLogger.getLogger(getAppName()).printStackTrace(e);
                Toast.makeText(this, R.string.sync_not_found, Toast.LENGTH_LONG).show();
            }
            return true;
        } else if (item.getItemId() == MENU_ABOUT) {
            swapToFragmentView(ScreenList.ABOUT_MENU);
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public void chooseForm(Uri formUri) {
        Intent i = new Intent(Intent.ACTION_EDIT, formUri, this, MainMenuActivity.class);
        i.putExtra("_sync_state", submenuPage);
        startActivityForResult(i, INTERNAL_ACTIVITY_CODE);
    }

    private void createErrorDialog(String errorMsg, final boolean shouldExit) {
        WebLogger.getLogger(getAppName()).e(t, errorMsg);
        if (mAlertDialog != null) {
            mAlertDialog.dismiss();
            mAlertDialog = null;
        }
        mAlertDialog = new AlertDialog.Builder(this).create();
        mAlertDialog.setIcon(android.R.drawable.ic_dialog_info);
        mAlertDialog.setMessage(errorMsg);
        DialogInterface.OnClickListener errorListener = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int button) {
                switch (button) {
                case DialogInterface.BUTTON_POSITIVE:
                    if (shouldExit) {
                        Intent i = new Intent();
                        setResult(RESULT_CANCELED, i);
                        finish();
                    }
                    break;
                }
            }
        };
        mAlertDialog.setCancelable(false);
        mAlertDialog.setButton(DialogInterface.BUTTON_POSITIVE, getString(R.string.ok), errorListener);
        mAlertDialog.show();
    }

    private void popBackStack() {
        FragmentManager mgr = getFragmentManager();
        int idxLast = mgr.getBackStackEntryCount() - 2;
        if (idxLast < 0) {
            Intent result = new Intent();
            // If we are in a WEBKIT, return the instanceId and the savepoint_type...
            if (this.getInstanceId() != null && currentFragment == ScreenList.WEBKIT) {
                result.putExtra("instanceId", getInstanceId());
                // in this case, the savepoint_type is null (a checkpoint).
            }
            this.setResult(RESULT_OK, result);
            finish();
        } else {
            BackStackEntry entry = mgr.getBackStackEntryAt(idxLast);
            swapToFragmentView(ScreenList.valueOf(entry.getName()));
        }
    }

    @Override
    public void initializationCompleted() {
        popBackStack();
    }

    @Override
    public void onBackPressed() {
        if ((currentFragment == ScreenList.WEBKIT) && getInstanceId() != null && getCurrentForm() != null
                && getCurrentForm().tableId != null) {

            // try to retrieve the active dialog
            DialogFragment dialog = (DialogFragment) getFragmentManager().findFragmentByTag(BACKPRESS_DIALOG_TAG);

            if (dialog != null && dialog.getDialog() != null) {
                // as-is
            } else {
                dialog = new BackPressWebkitConfirmationDialogFragment();
            }
            dialog.show(getFragmentManager(), BACKPRESS_DIALOG_TAG);
        } else {
            popBackStack();
        }
    }

    // for back press suppression
    // trigger save of everything...
    @Override
    public void saveAllAsIncompleteThenPopBackStack() {
        String tableId = this.getCurrentForm().tableId;
        String rowId = this.getInstanceId();

        if (rowId != null && tableId != null) {
            DbHandle dbHandleName = null;
            try {
                dbHandleName = this.getDatabase().openDatabase(getAppName());
                OrderedColumns cols = this.getDatabase().getUserDefinedColumns(getAppName(), dbHandleName, tableId);
                UserTable table = this.getDatabase().saveAsIncompleteMostRecentCheckpointRowWithId(getAppName(),
                        dbHandleName, tableId, cols, rowId);
                // this should not be possible, but if somehow we exit before anything is written
                // clear instanceId if the row no longer exists
                if (table.getNumberOfRows() == 0) {
                    setInstanceId(null);
                }
            } catch (ActionNotAuthorizedException e) {
                WebLogger.getLogger(getAppName()).printStackTrace(e);
                Toast.makeText(this, R.string.database_authorization_error_occured, Toast.LENGTH_LONG).show();
            } catch (ServicesAvailabilityException e) {
                WebLogger.getLogger(getAppName()).printStackTrace(e);
                Toast.makeText(this, R.string.database_error_occured, Toast.LENGTH_LONG).show();
            } finally {
                if (dbHandleName != null) {
                    try {
                        this.getDatabase().closeDatabase(getAppName(), dbHandleName);
                    } catch (ServicesAvailabilityException e) {
                        // ignore
                        WebLogger.getLogger(getAppName()).printStackTrace(e);
                    }
                }
            }
        }
        popBackStack();
    }

    // trigger resolve UI...
    @Override
    public void resolveAllCheckpointsThenPopBackStack() {
        String tableId = this.getCurrentForm().tableId;
        String rowId = this.getInstanceId();

        if (rowId != null && tableId != null) {
            DbHandle dbHandleName = null;
            try {
                dbHandleName = this.getDatabase().openDatabase(getAppName());
                OrderedColumns cols = this.getDatabase().getUserDefinedColumns(getAppName(), dbHandleName, tableId);
                UserTable table = this.getDatabase().deleteAllCheckpointRowsWithId(getAppName(), dbHandleName,
                        tableId, cols, rowId);
                // clear instanceId if the row no longer exists
                if (table.getNumberOfRows() == 0) {
                    setInstanceId(null);
                }
            } catch (ActionNotAuthorizedException e) {
                WebLogger.getLogger(getAppName()).printStackTrace(e);
                Toast.makeText(this, R.string.database_authorization_error_occured, Toast.LENGTH_LONG).show();
            } catch (ServicesAvailabilityException e) {
                WebLogger.getLogger(getAppName()).printStackTrace(e);
                Toast.makeText(this, R.string.database_error_occured, Toast.LENGTH_LONG).show();
            } finally {
                if (dbHandleName != null) {
                    try {
                        this.getDatabase().closeDatabase(getAppName(), dbHandleName);
                    } catch (ServicesAvailabilityException e) {
                        // ignore
                        WebLogger.getLogger(getAppName()).printStackTrace(e);
                    }
                }
            }
        }
        popBackStack();
    }

    public void hideWebkitView() {
        // This is a callback thread.
        // We must invalidate the options menu on the UI thread
        this.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                WebLogger.getLogger(getAppName()).i(t, "hideWebkitView");
                // In the fragment UI, we want to return to not having any
                // instanceId defined.
                // JQueryODKView webkitView = (JQueryODKView)
                // findViewById(R.id.webkit);
                // setInstanceId(null);
                // setSectionScreenState(null,null);
                // setAuxillaryHash(null);
                // webkitView.loadPage();
                levelSafeInvalidateOptionsMenu();
            }
        });
    }

    public void swapToFragmentView(ScreenList newScreenType) {
        WebLogger.getLogger(getAppName()).i(t, "swapToFragmentView: " + newScreenType.name());
        FragmentManager mgr = getFragmentManager();
        FragmentTransaction trans = null;
        Fragment newFragment = null;
        if (newScreenType == ScreenList.MAIN_SCREEN) {
            throw new IllegalStateException("unexpected reference to generic main screen");
        } else if (newScreenType == ScreenList.FORM_CHOOSER) {
            newFragment = mgr.findFragmentByTag(newScreenType.name());
            if (newFragment == null) {
                newFragment = new FormChooserListFragment();
            }
        } else if (newScreenType == ScreenList.FRONT_PAGE) {
            newFragment = mgr.findFragmentByTag(newScreenType.name());
            if (newFragment == null) {
                newFragment = new FrontPageFragment();
            }
        } else if (newScreenType == ScreenList.INITIALIZATION_DIALOG) {
            newFragment = mgr.findFragmentByTag(newScreenType.name());
            if (newFragment == null) {
                newFragment = new InitializationFragment();
            }
        } else if (newScreenType == ScreenList.WEBKIT) {
            newFragment = mgr.findFragmentByTag(newScreenType.name());
            if (newFragment == null) {
                WebLogger.getLogger(getAppName()).i(t,
                        "[" + this.hashCode() + "] creating new webkit fragment " + newScreenType.name());
                newFragment = new WebViewFragment();
            }
        } else if (newScreenType == ScreenList.ABOUT_MENU) {
            newFragment = mgr.findFragmentByTag(newScreenType.name());
            if (newFragment == null) {
                newFragment = new AboutMenuFragment();
            }

        } else {
            throw new IllegalStateException("Unrecognized ScreenList type");
        }

        boolean matchingBackStackEntry = false;
        for (int i = 0; i < mgr.getBackStackEntryCount(); ++i) {
            BackStackEntry e = mgr.getBackStackEntryAt(i);
            WebLogger.getLogger(getAppName()).i(t, "BackStackEntry[" + i + "] " + e.getName());
            if (e.getName().equals(newScreenType.name())) {
                matchingBackStackEntry = true;
            }
        }

        if (matchingBackStackEntry) {
            if (trans != null) {
                WebLogger.getLogger(getAppName()).e(t, "Unexpected active transaction when popping state!");
                trans = null;
            }
            // flush backward, to the screen we want to go back to
            currentFragment = newScreenType;
            WebLogger.getLogger(getAppName()).e(t,
                    "[" + this.hashCode() + "] popping back stack " + currentFragment.name());
            mgr.popBackStackImmediate(currentFragment.name(), 0);
        } else {
            // add transaction to show the screen we want
            if (trans == null) {
                trans = mgr.beginTransaction();
            }
            currentFragment = newScreenType;
            trans.replace(R.id.main_content, newFragment, currentFragment.name());
            WebLogger.getLogger(getAppName()).i(t,
                    "[" + this.hashCode() + "] adding to back stack " + currentFragment.name());
            trans.addToBackStack(currentFragment.name());
        }

        // and see if we should re-initialize...
        if ((currentFragment != ScreenList.INITIALIZATION_DIALOG)
                && ((Survey) getApplication()).shouldRunInitializationTask(getAppName())) {
            WebLogger.getLogger(getAppName()).i(t, "swapToFragmentView -- calling clearRunInitializationTask");
            // and immediately clear the should-run flag...
            ((Survey) getApplication()).clearRunInitializationTask(getAppName());
            // OK we should swap to the InitializationFragment view
            // this will skip the transition to whatever screen we were trying to 
            // go to and will instead show the InitializationFragment view. We
            // restore to the desired screen via the setFragmentToShowNext()
            //
            // NOTE: this discards the uncommitted transaction.
            // Robolectric complains about a recursive state transition.
            if (trans != null) {
                trans.commit();
            }
            swapToFragmentView(ScreenList.INITIALIZATION_DIALOG);
        } else {
            // before we actually switch to a WebKit, be sure
            // we have the form definition for it...
            if (currentFragment == ScreenList.WEBKIT && getCurrentForm() == null) {
                // we were sent off to the initialization dialog to try to
                // discover the form. We need to inquire about the form again
                // and, if we cannot find it, report an error to the user.
                final Uri uriFormsProvider = FormsProviderAPI.CONTENT_URI;
                Uri uri = getIntent().getData();
                Uri formUri = null;

                if (uri.getScheme().equalsIgnoreCase(uriFormsProvider.getScheme())
                        && uri.getAuthority().equalsIgnoreCase(uriFormsProvider.getAuthority())) {
                    List<String> segments = uri.getPathSegments();
                    if (segments != null && segments.size() >= 2) {
                        String appName = segments.get(0);
                        setAppName(appName);
                        String tableId = segments.get(1);
                        String formId = (segments.size() > 2) ? segments.get(2) : null;
                        formUri = Uri.withAppendedPath(Uri.withAppendedPath(
                                Uri.withAppendedPath(FormsProviderAPI.CONTENT_URI, appName), tableId), formId);
                    } else {
                        swapToFragmentView(ScreenList.FRONT_PAGE);
                        createErrorDialog(getString(R.string.invalid_uri_expecting_n_segments, uri.toString(), 2),
                                EXIT);
                        return;
                    }
                    // request specifies a specific formUri -- try to open that
                    FormIdStruct newForm = FormIdStruct.retrieveFormIdStruct(getContentResolver(), formUri);
                    if (newForm == null) {
                        // error
                        swapToFragmentView(ScreenList.FRONT_PAGE);
                        createErrorDialog(getString(R.string.form_not_found, segments.get(1)), EXIT);
                        return;
                    } else {
                        transitionToFormHelper(uri, newForm);
                    }
                }
            }

            if (trans != null) {
                trans.commit();
            }
            invalidateOptionsMenu();
        }
    }

    private void levelSafeInvalidateOptionsMenu() {
        invalidateOptionsMenu();
    }

    /***********************************************************************
     * *********************************************************************
     * *********************************************************************
     * *********************************************************************
     * *********************************************************************
     * *********************************************************************
     * Interfaces to Javascript layer (also used in Java).
     */

    private void dumpScreenStateHistory() {
        WebLoggerIf l = WebLogger.getLogger(getAppName());

        l.d(t, "-------------*start* dumpScreenStateHistory--------------------");
        if (sectionStateScreenHistory.isEmpty()) {
            l.d(t, "sectionScreenStateHistory EMPTY");
        } else {
            for (int i = sectionStateScreenHistory.size() - 1; i >= 0; --i) {
                SectionScreenStateHistory thisSection = sectionStateScreenHistory.get(i);
                l.d(t, "[" + i + "] screenPath: " + thisSection.currentScreen.screenPath);
                l.d(t, "[" + i + "] state:      " + thisSection.currentScreen.state);
                if (thisSection.history.isEmpty()) {
                    l.d(t, "[" + i + "] history[] EMPTY");
                } else {
                    for (int j = thisSection.history.size() - 1; j >= 0; --j) {
                        ScreenState ss = thisSection.history.get(j);
                        l.d(t, "[" + i + "] history[" + j + "] screenPath: " + ss.screenPath);
                        l.d(t, "[" + i + "] history[" + j + "] state:      " + ss.state);
                    }
                }
            }
        }
        l.d(t, "------------- *end*  dumpScreenStateHistory--------------------");
    }

    @Override
    public void pushSectionScreenState() {
        if (sectionStateScreenHistory.size() == 0) {
            WebLogger.getLogger(getAppName()).i(t, "pushSectionScreenState: NULL!");
            return;
        }
        SectionScreenStateHistory lastSection = sectionStateScreenHistory.get(sectionStateScreenHistory.size() - 1);
        lastSection.history
                .add(new ScreenState(lastSection.currentScreen.screenPath, lastSection.currentScreen.state));
    }

    @Override
    public void setSectionScreenState(String screenPath, String state) {
        if (screenPath == null) {
            WebLogger.getLogger(getAppName()).e(t, "setSectionScreenState: NULL currentScreen.screenPath!");
            return;
        } else {
            String[] splits = screenPath.split("/");
            String sectionName = splits[0] + "/";

            WebLogger.getLogger(getAppName()).e(t, "setSectionScreenState( " + screenPath + ", " + state + ")");

            SectionScreenStateHistory lastSection;
            if (sectionStateScreenHistory.size() == 0) {
                sectionStateScreenHistory.add(new SectionScreenStateHistory());
                lastSection = sectionStateScreenHistory.get(sectionStateScreenHistory.size() - 1);
                lastSection.currentScreen.screenPath = screenPath;
                lastSection.currentScreen.state = state;
                lastSection.history.clear();
            } else {
                lastSection = sectionStateScreenHistory.get(sectionStateScreenHistory.size() - 1);
                if (lastSection.currentScreen.screenPath.startsWith(sectionName)) {
                    lastSection.currentScreen.screenPath = screenPath;
                    lastSection.currentScreen.state = state;
                } else {
                    sectionStateScreenHistory.add(new SectionScreenStateHistory());
                    lastSection = sectionStateScreenHistory.get(sectionStateScreenHistory.size() - 1);
                    lastSection.currentScreen.screenPath = screenPath;
                    lastSection.currentScreen.state = state;
                    lastSection.history.clear();
                }
            }
        }
    }

    @Override
    public void clearSectionScreenState() {
        sectionStateScreenHistory.clear();
        sectionStateScreenHistory.add(new SectionScreenStateHistory());
        SectionScreenStateHistory lastSection = sectionStateScreenHistory.get(sectionStateScreenHistory.size() - 1);
        lastSection.currentScreen.screenPath = "initial/0";
        lastSection.currentScreen.state = null;
        lastSection.history.clear();
    }

    @Override
    public String getControllerState() {
        if (sectionStateScreenHistory.size() == 0) {
            WebLogger.getLogger(getAppName()).i(t, "getControllerState: NULL!");
            return null;
        }
        SectionScreenStateHistory lastSection = sectionStateScreenHistory.get(sectionStateScreenHistory.size() - 1);
        return lastSection.currentScreen.state;
    }

    public String getScreenPath() {
        dumpScreenStateHistory();
        if (sectionStateScreenHistory.size() == 0) {
            WebLogger.getLogger(getAppName()).i(t, "getScreenPath: NULL!");
            return null;
        }
        SectionScreenStateHistory lastSection = sectionStateScreenHistory.get(sectionStateScreenHistory.size() - 1);
        return lastSection.currentScreen.screenPath;
    }

    @Override
    public boolean hasScreenHistory() {
        // two or more sections -- there must be history
        if (sectionStateScreenHistory.size() > 1) {
            return true;
        }
        // no sections -- no history
        if (sectionStateScreenHistory.size() == 0) {
            return false;
        }

        SectionScreenStateHistory thisSection = sectionStateScreenHistory.get(0);
        return thisSection.history.size() != 0;
    }

    @Override
    public String popScreenHistory() {
        if (sectionStateScreenHistory.size() == 0) {
            return null;
        }

        SectionScreenStateHistory lastSection;
        lastSection = sectionStateScreenHistory.get(sectionStateScreenHistory.size() - 1);
        if (lastSection.history.size() != 0) {
            ScreenState lastHistory = lastSection.history.remove(lastSection.history.size() - 1);
            lastSection.currentScreen.screenPath = lastHistory.screenPath;
            lastSection.currentScreen.state = lastHistory.state;
            return lastSection.currentScreen.screenPath;
        }

        // pop to an enclosing screen
        sectionStateScreenHistory.remove(sectionStateScreenHistory.size() - 1);

        if (sectionStateScreenHistory.size() == 0) {
            return null;
        }

        lastSection = sectionStateScreenHistory.get(sectionStateScreenHistory.size() - 1);
        return lastSection.currentScreen.screenPath;
    }

    @Override
    public boolean hasSectionStack() {
        return sectionStateScreenHistory.size() != 0;
    }

    @Override
    public String popSectionStack() {
        if (sectionStateScreenHistory.size() != 0) {
            sectionStateScreenHistory.remove(sectionStateScreenHistory.size() - 1);
        }

        if (sectionStateScreenHistory.size() != 0) {
            SectionScreenStateHistory lastSection = sectionStateScreenHistory
                    .get(sectionStateScreenHistory.size() - 1);
            return lastSection.currentScreen.screenPath;
        }

        return null;
    }

    @Override
    public void setSessionVariable(String elementPath, String jsonValue) {
        sessionVariables.putString(elementPath, jsonValue);
    }

    @Override
    public String getSessionVariable(String elementPath) {
        return sessionVariables.getString(elementPath);
    }

    @Override
    public void saveAllChangesCompleted(String instanceId, final boolean asComplete) {
        WebLogger.getLogger(getAppName()).i(t, "MainMenuActivity:saveAllChangesCompleted");
        Intent result = new Intent();
        result.putExtra("instanceId", instanceId);
        result.putExtra("savepoint_type", "COMPLETE");
        // TODO: unclear what to put in the result intent...
        this.setResult(RESULT_OK, result);
        finish();
    }

    @Override
    public void saveAllChangesFailed(String instanceId) {
        // should we message anything?
    }

    @Override
    public void ignoreAllChangesCompleted(String instanceId) {
        Intent result = new Intent();
        result.putExtra("instanceId", instanceId);
        result.putExtra("savepoint_type", "INCOMPLETE");
        // TODO: unclear what to put in the result intent...
        this.setResult(RESULT_OK, result);
        finish();
    }

    @Override
    public void ignoreAllChangesFailed(String instanceId) {
        // should we message anything?
    }

    /**
     * Invoked from within Javascript to launch an activity.
     *
     * @param dispatchString   Opaque string -- typically identifies prompt and user action
     *
     * @param action
     *          -- the intent to be launched
     * @param valueContentMap
     *          -- parameters to pass to the intent
     *          {
     *            uri: uriValue, // parse to a uri and set as the data of the
     *                           // intent
     *            extras: extrasMap, // added as extras to the intent
     *            package: packageStr, // the name of a package to launch
     *            type: typeStr, // will be set as the type
     *            data: dataUri // will be parsed to a uri and set as the data of
     *                          // the intent. For now this is equivalent to the
     *                          // uri field, although that name is less precise.
     *          }
     */
    @Override
    public String doAction(String dispatchString, String action, JSONObject valueContentMap) {

        // android.os.Debug.waitForDebugger();

        if (isWaitingForBinaryData()) {
            WebLogger.getLogger(getAppName()).w(t, "Already waiting for data -- ignoring");
            return "IGNORE";
        }

        Intent i;
        boolean isSurveyApp = false;
        boolean isOpendatakitApp = false;
        if (action.startsWith("org.opendatakit.survey")) {
            Class<?> clazz;
            try {
                clazz = Class.forName(action);
                i = new Intent(this, clazz);
                isSurveyApp = true;
            } catch (ClassNotFoundException e) {
                WebLogger.getLogger(getAppName()).printStackTrace(e);
                i = new Intent(action);
            }
        } else {
            i = new Intent(action);
        }

        if (action.startsWith("org.opendatakit.")) {
            isOpendatakitApp = true;
        }

        try {

            String uriKey = "uri";
            String extrasKey = "extras";
            String packageKey = "package";
            String typeKey = "type";
            String dataKey = "data";

            JSONObject valueMap = null;
            if (valueContentMap != null) {

                // do type first, as it says in the spec this call deletes any other
                // data (eg by setData()) on the intent.
                if (valueContentMap.has(typeKey)) {
                    String type = valueContentMap.getString(typeKey);
                    i.setType(type);
                }

                if (valueContentMap.has(uriKey) || valueContentMap.has(dataKey)) {
                    // as it currently stands, the data property can be in either the uri
                    // or data keys.
                    String uriValueStr = null;
                    if (valueContentMap.has(uriKey)) {
                        uriValueStr = valueContentMap.getString(uriKey);
                    }
                    // go ahead and overwrite with data if it's present.
                    if (valueContentMap.has(dataKey)) {
                        uriValueStr = valueContentMap.getString(dataKey);
                    }
                    if (uriValueStr != null) {
                        Uri uri = Uri.parse(uriValueStr);
                        i.setData(uri);
                    }
                }

                if (valueContentMap.has(extrasKey)) {
                    valueMap = valueContentMap.getJSONObject(extrasKey);
                }

                if (valueContentMap.has(packageKey)) {
                    String packageStr = valueContentMap.getString(packageKey);
                    i.setPackage(packageStr);
                }

            }

            if (valueMap != null) {
                Bundle b;
                PropertiesSingleton props = CommonToolProperties.get(MainMenuActivity.this, getAppName());

                final DynamicPropertiesCallback cb = new DynamicPropertiesCallback(getAppName(),
                        getCurrentForm().tableId, getInstanceId(), props.getActiveUser(), props.getLocale(),
                        props.getProperty(CommonToolProperties.KEY_USERNAME),
                        props.getProperty(CommonToolProperties.KEY_ACCOUNT));

                b = SerializationUtils.convertToBundle(valueMap, new MacroStringExpander() {

                    @Override
                    public String expandString(String value) {
                        if (value != null && value.startsWith("opendatakit-macro(") && value.endsWith(")")) {
                            String term = value.substring("opendatakit-macro(".length(), value.length() - 1).trim();
                            String v = mPropertyManager.getSingularProperty(term, cb);
                            if (v != null) {
                                return v;
                            } else {
                                WebLogger.getLogger(getAppName()).e(t,
                                        "Unable to process opendatakit-macro: " + value);
                                throw new IllegalArgumentException(
                                        "Unable to process opendatakit-macro expression: " + value);
                            }
                        } else {
                            return value;
                        }
                    }
                });

                i.putExtras(b);
            }

            if (isSurveyApp || isOpendatakitApp) {
                // ensure that we supply our appName...
                if (!i.hasExtra(IntentConsts.INTENT_KEY_APP_NAME)) {
                    i.putExtra(IntentConsts.INTENT_KEY_APP_NAME, getAppName());
                    WebLogger.getLogger(getAppName()).w(t,
                            "doAction into Survey or Tables does not supply an appName. Adding: " + getAppName());
                }
            }
        } catch (Exception ex) {
            WebLogger.getLogger(getAppName()).e(t, "JSONException: " + ex.toString());
            WebLogger.getLogger(getAppName()).printStackTrace(ex);
            return "JSONException: " + ex.toString();
        }

        dispatchStringWaitingForData = dispatchString;
        actionWaitingForData = action;

        try {
            startActivityForResult(i, HANDLER_ACTIVITY_CODE);
            return "OK";
        } catch (ActivityNotFoundException ex) {
            WebLogger.getLogger(getAppName()).e(t, "Unable to launch activity: " + ex.toString());
            WebLogger.getLogger(getAppName()).printStackTrace(ex);
            return "Application not found";
        }
    }

    @Override
    public void queueActionOutcome(String outcome) {
        queuedActions.addLast(outcome);
    }

    @Override
    public void queueUrlChange(String hash) {
        try {
            String jsonEncoded = ODKFileUtils.mapper.writeValueAsString(hash);
            queuedActions.addLast(jsonEncoded);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public String viewFirstQueuedAction() {
        String outcome = queuedActions.isEmpty() ? null : queuedActions.getFirst();
        return outcome;
    }

    @Override
    public void removeFirstQueuedAction() {
        if (!queuedActions.isEmpty()) {
            queuedActions.removeFirst();
        }
    }

    @Override
    public void signalResponseAvailable(String responseJSON) {
        if (responseJSON == null) {
            WebLogger.getLogger(getAppName()).e(t, "signalResponseAvailable -- got null responseJSON!");
        } else {
            WebLogger.getLogger(getAppName()).e(t,
                    "signalResponseAvailable -- got " + responseJSON.length() + " long responseJSON!");
        }
        if (responseJSON != null) {
            this.queueResponseJSON.push(responseJSON);
            final ODKWebView webView = (ODKWebView) findViewById(R.id.webkit);
            if (webView != null) {
                WebLogger.getLogger(getAppName()).i(t, "[" + this.hashCode() + "][WebView: " + webView.hashCode()
                        + "] signalResponseAvailable webView.loadUrl will be called");
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        webView.loadUrl("javascript:odkData.responseAvailable();");
                    }
                });
            }
        }
    }

    @Override
    public String getResponseJSON() {
        if (queueResponseJSON.isEmpty()) {
            return null;
        }
        String responseJSON = queueResponseJSON.removeFirst();
        return responseJSON;
    }

    @Override
    public ExecutorProcessor newExecutorProcessor(ExecutorContext context) {
        return new SurveyDataExecutorProcessor(context);
    }

    @Override
    public void registerDatabaseConnectionBackgroundListener(DatabaseConnectionListener listener) {
        mIOdkDataDatabaseListener = listener;
    }

    @Override
    public UserDbInterface getDatabase() {
        return ((CommonApplication) getApplication()).getDatabase();
    }

    @Override
    public Bundle getIntentExtras() {
        return this.getIntent().getExtras();
    }

    /*
     * END - Interfaces to Javascript layer (also used in Java).
     * *********************************************************************
     * *********************************************************************
     * *********************************************************************
     * *********************************************************************
     * *********************************************************************
     * *********************************************************************
     */

    public boolean isWaitingForBinaryData() {
        return actionWaitingForData != null;
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
        WebLogger.getLogger(getAppName()).i(t, "onActivityResult");
        ODKWebView view = (ODKWebView) findViewById(R.id.webkit);

        if (requestCode == HANDLER_ACTIVITY_CODE) {
            try {
                String jsonObject = null;
                Bundle b = (intent == null) ? null : intent.getExtras();
                JSONObject val = (b == null) ? null : SerializationUtils.convertFromBundle(getAppName(), b);
                JSONObject jsonValue = new JSONObject();
                jsonValue.put("status", resultCode);
                if (val != null) {
                    jsonValue.put("result", val);
                }
                JSONObject result = new JSONObject();
                result.put("dispatchString", dispatchStringWaitingForData);
                result.put("action", actionWaitingForData);
                result.put("jsonValue", jsonValue);

                String actionOutcome = result.toString();
                this.queueActionOutcome(actionOutcome);

                WebLogger.getLogger(getAppName()).i(t, "HANDLER_ACTIVITY_CODE: " + jsonObject);

                view.signalQueuedActionAvailable();
            } catch (Exception e) {
                try {
                    JSONObject jsonValue = new JSONObject();
                    jsonValue.put("status", 0);
                    jsonValue.put("result", e.toString());
                    JSONObject result = new JSONObject();
                    result.put("dispatchString", dispatchStringWaitingForData);
                    result.put("action", actionWaitingForData);
                    result.put("jsonValue", jsonValue);
                    this.queueActionOutcome(result.toString());

                    view.signalQueuedActionAvailable();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            } finally {
                dispatchStringWaitingForData = null;
                actionWaitingForData = null;
            }
        } else if (requestCode == SYNC_ACTIVITY_CODE) {
            ((Survey) getApplication()).setRunInitializationTask(getAppName());
            this.swapToFragmentView((currentFragment == null) ? ScreenList.FRONT_PAGE : currentFragment);
        }
    }

    public void onFragmentInteraction(ScreenList screen) {
        swapToFragmentView(screen);
    }

    @Override
    public String getSubmenuPage() {
        return this.submenuPage;
    }

    public void setSubmenuPage(String value) {
        this.submenuPage = value;
    }
}