com.pacoapp.paco.ui.ExperimentExecutorCustomRendering.java Source code

Java tutorial

Introduction

Here is the source code for com.pacoapp.paco.ui.ExperimentExecutorCustomRendering.java

Source

/*
 * Copyright 2011 Google Inc. All Rights Reserved.
 *
 * 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 com.pacoapp.paco.ui;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.NotificationManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.drawable.ColorDrawable;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.provider.Settings;
import android.speech.RecognizerIntent;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarActivity;
import android.util.Base64;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.webkit.ConsoleMessage;
import android.webkit.JavascriptInterface;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.Toast;

import com.google.android.apps.paco.questioncondparser.Binding;
import com.google.android.apps.paco.questioncondparser.ExpressionEvaluator;
import com.google.common.base.Strings;
import com.pacoapp.paco.PacoConstants;
import com.pacoapp.paco.R;
import com.pacoapp.paco.js.bridge.Environment;
import com.pacoapp.paco.js.bridge.JavascriptCalendarManager;
import com.pacoapp.paco.js.bridge.JavascriptEmail;
import com.pacoapp.paco.js.bridge.JavascriptEventLoader;
import com.pacoapp.paco.js.bridge.JavascriptExperimentLoader;
import com.pacoapp.paco.js.bridge.JavascriptNotificationService;
import com.pacoapp.paco.js.bridge.JavascriptPackageManager;
import com.pacoapp.paco.js.bridge.JavascriptPhotoService;
import com.pacoapp.paco.js.bridge.JavascriptStringResources;
import com.pacoapp.paco.model.Event;
import com.pacoapp.paco.model.EventUtil;
import com.pacoapp.paco.model.Experiment;
import com.pacoapp.paco.model.ExperimentProviderUtil;
import com.pacoapp.paco.model.NotificationHolder;
import com.pacoapp.paco.model.Output;
import com.pacoapp.paco.net.SyncService;
import com.pacoapp.paco.sensors.android.BroadcastTriggerReceiver;
import com.pacoapp.paco.shared.model.FeedbackDAO;
import com.pacoapp.paco.shared.model2.ExperimentGroup;
import com.pacoapp.paco.shared.model2.Input2;
import com.pacoapp.paco.shared.util.ExperimentHelper;
import com.pacoapp.paco.triggering.AndroidEsmSignalStore;
import com.pacoapp.paco.triggering.BeeperService;
import com.pacoapp.paco.triggering.ExperimentExpirationManagerService;
import com.pacoapp.paco.triggering.NotificationCreator;
import com.pacoapp.paco.utils.IntentExtraHelper;

import org.joda.time.DateTime;
import org.json.JSONArray;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

public class ExperimentExecutorCustomRendering extends ActionBarActivity
        implements ChangeListener, LocationListener, ExperimentLoadingActivity {

    private Experiment experiment;
    private ExperimentGroup experimentGroup;
    private Long actionTriggerId;
    private Long actionId;
    private Long actionTriggerSpecId;

    private Long notificationHolderId;
    private boolean shouldExpireNotificationHolder;
    private Long scheduledTime = 0L;

    private ExperimentProviderUtil experimentProviderUtil;
    private List<InputLayout> inputs = new ArrayList<InputLayout>();
    private LayoutInflater inflater;
    private LinearLayout mainLayout;
    private OptionsMenu optionsMenu;

    private Object updateLock = new Object();

    private ArrayList<InputLayout> locationInputs;
    private Location location;

    private View buttonView;
    private Button doOnPhoneButton;
    private Button doOnWebButton;

    private List<SpeechRecognitionListener> speechRecognitionListeners = new ArrayList<SpeechRecognitionListener>();

    public static final int RESULT_SPEECH = 3001;
    public static final int RESULT_CAMERA = 3002;
    public static final int RESULT_GALLERY = 3003;

    private WebView webView;
    private Environment env;

    private final int IMAGE_MAX_SIZE = 600;
    private File photoFile;

    boolean showDialog = true;

    private String notificationMessage;
    private String notificationSource;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActionBar actionBar = getSupportActionBar();
        actionBar.setDisplayUseLogoEnabled(true);
        actionBar.setDisplayShowHomeEnabled(true);
        actionBar.setDisplayShowTitleEnabled(false);

        experimentProviderUtil = new ExperimentProviderUtil(this);
        if (experiment == null || experimentGroup == null) {
            IntentExtraHelper.loadExperimentInfoFromIntent(this, getIntent(), experimentProviderUtil);
        }
        loadNotificationData();
        if (experiment == null || experimentGroup == null) {
            displayNoExperimentMessage();
        } else {

            actionBar.setTitle(experiment.getExperimentDAO().getTitle());

            inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            optionsMenu = new OptionsMenu(this, experiment.getExperimentDAO().getId(),
                    scheduledTime != null && scheduledTime != 0L);

            mainLayout = (LinearLayout) inflater.inflate(R.layout.experiment_executor_custom_rendering, null);
            setContentView(mainLayout);

            // webRecommended layout pieces
            ((LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE))
                    .inflate(R.layout.experiment_web_recommended_buttons, mainLayout, true);
            buttonView = findViewById(R.id.ExecutorButtonLayout);
            buttonView.setVisibility(View.GONE);

            doOnPhoneButton = (Button) findViewById(R.id.DoOnPhoneButton);
            doOnPhoneButton.setVisibility(View.GONE);
            // doOnPhoneButton.setOnClickListener(new OnClickListener() {
            // public void onClick(View v) {
            // buttonView.setVisibility(View.GONE);
            // //mainLayout.removeView(buttonView);
            // scrollView.setVisibility(View.VISIBLE);
            // showForm();
            // }
            // });

            doOnWebButton = (Button) findViewById(R.id.DoOnWebButtonButton);
            doOnWebButton.setOnClickListener(new OnClickListener() {
                public void onClick(View v) {
                    deleteNotification();
                    finish();
                }
            });

            showForm(savedInstanceState);
        }
    }

    private void renderWebRecommendedMessage() {
        final ScrollView scrollView = (ScrollView) findViewById(R.id.ScrollView01);
        scrollView.setVisibility(View.GONE);
        buttonView.setVisibility(View.VISIBLE);

    }

    private void loadNotificationData() {
        Bundle extras = getIntent().getExtras();
        NotificationHolder notificationHolder = null;
        if (extras != null) {
            notificationHolderId = extras.getLong(NotificationCreator.NOTIFICATION_ID);
            notificationHolder = experimentProviderUtil.getNotificationById(notificationHolderId);
            Long timeoutMillis = null;
            if (notificationHolder != null) {
                experiment = experimentProviderUtil.getExperimentByServerId(notificationHolder.getExperimentId());
                experimentGroup = experiment.getExperimentDAO()
                        .getGroupByName(notificationHolder.getExperimentGroupName());
                actionTriggerId = notificationHolder.getActionTriggerId();
                actionId = notificationHolder.getActionId();
                actionTriggerSpecId = notificationHolder.getActionTriggerSpecId();
                scheduledTime = notificationHolder.getAlarmTime();
                Log.i(PacoConstants.TAG,
                        "Starting experimentExecutor from signal: " + experiment.getExperimentDAO().getTitle()
                                + ". alarmTime: " + new DateTime(scheduledTime).toString());
                timeoutMillis = notificationHolder.getTimeoutMillis();
            } else {
                scheduledTime = null;
            }

            if (isExpiredEsmPing(timeoutMillis)) {
                Toast.makeText(this, R.string.survey_expired, Toast.LENGTH_LONG).show();
                finish();
            }
        }
        if (notificationHolder == null) {
            lookForActiveNotificationForExperiment();
        }
    }

    /**
     * If the user is self-reporting there might still be an active notification for this experiment. If so, we should
     * add its scheduleTime into this response. There should only ever be one.
     */
    private void lookForActiveNotificationForExperiment() {
        NotificationHolder notificationHolder = null;
        if (getExperiment() == null) {
            return;
        }
        final long experimentId = getExperiment().getExperimentDAO().getId().longValue();
        List<NotificationHolder> notificationHolders = experimentProviderUtil.getNotificationsFor(experimentId,
                experimentGroup.getName());
        if (notificationHolders != null && !notificationHolders.isEmpty()) {
            notificationHolder = notificationHolders.get(0); // TODO can we ever have more than one for a group?
        }
        if (notificationHolder != null) {
            experiment = experimentProviderUtil.getExperimentByServerId(notificationHolder.getExperimentId());
            experimentGroup = getExperiment().getExperimentDAO()
                    .getGroupByName(notificationHolder.getExperimentGroupName());

            if (notificationHolder.isActive(new DateTime())) {
                notificationHolderId = notificationHolder.getId();
                scheduledTime = notificationHolder.getAlarmTime();
                shouldExpireNotificationHolder = true;
                Log.i(PacoConstants.TAG,
                        "ExperimentExecutor: Self report, but found signal still active : "
                                + experiment.getExperimentDAO().getTitle() + ". alarmTime: "
                                + new DateTime(scheduledTime).toString());
            } else {
                NotificationCreator.create(this).timeoutNotification(notificationHolder);
            }
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        registerLocationListenerIfNecessary();
    }

    @Override
    protected void onPause() {
        super.onPause();
        for (InputLayout inputLayout : inputs) {
            inputLayout.onPause();
        }
        unregisterLocationListenerIfNecessary();
    }

    private void unregisterLocationListenerIfNecessary() {
        if (locationInputs.size() > 0) {
            locationInputs.clear();
            unregisterLocationListener();
        }
    }

    private void registerLocationListenerIfNecessary() {
        locationInputs = new ArrayList<InputLayout>();
        for (InputLayout input : inputs) {
            if (input.getInput().getResponseType().equals(Input2.LOCATION)) {
                locationInputs.add(input);
            }
        }
        if (locationInputs.size() > 0) {
            registerLocationListener();
        }
    }

    // Location

    private void registerLocationListener() {
        LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        if (!lm.isProviderEnabled("gps")) {
            new AlertDialog.Builder(this).setMessage(R.string.gps_message).setCancelable(true)
                    .setPositiveButton(R.string.enable_button, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int which) {
                            launchGpsSettings();
                            dialog.dismiss();
                        }
                    }).setNegativeButton(R.string.cancel_button, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.cancel();
                        }
                    }).create().show();
        }
        if (lm != null) {
            getBestProvider(lm);
        }
    }

    void launchGpsSettings() {
        startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
    }

    private void getBestProvider(LocationManager lm) {
        Criteria criteria = new Criteria();
        criteria.setAccuracy(Criteria.ACCURACY_COARSE);
        criteria.setAccuracy(Criteria.ACCURACY_FINE);

        String bestProvider = lm.getBestProvider(criteria, true);
        if (bestProvider != null) {
            lm.requestLocationUpdates(bestProvider, 0, 0, this);
            location = lm.getLastKnownLocation(bestProvider);
            for (InputLayout input : locationInputs) {
                input.setLocation(location);
            }

        } else {
            new AlertDialog.Builder(this).setMessage(R.string.need_location).setCancelable(true)
                    .setPositiveButton(R.string.enable_button, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int which) {
                            launchGpsSettings();
                            dialog.dismiss();
                        }
                    }).setNegativeButton(R.string.cancel_button, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.cancel();
                        }
                    }).create().show();
        }
    }

    public void onLocationChanged(Location location) {
        this.location = location;
        for (InputLayout input : locationInputs) {
            input.setLocation(location);
        }
    }

    public void onProviderDisabled(String provider) {
        unregisterLocationListener();
        LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        getBestProvider(lm);
    }

    public void onProviderEnabled(String provider) {
        unregisterLocationListener();
        LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        getBestProvider(lm);
    }

    public void onStatusChanged(String provider, int status, Bundle extras) {
        unregisterLocationListener();
        LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        getBestProvider(lm);
    }

    private void unregisterLocationListener() {
        LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        if (lm != null) {
            lm.removeUpdates(this);
        }
    }
    //End Location

    private boolean isExpiredEsmPing(Long timeoutMillis) {
        return (scheduledTime != null && scheduledTime != 0L && timeoutMillis != null)
                && (new DateTime(scheduledTime)).plus(timeoutMillis).isBefore(new DateTime());
    }

    private void showForm(Bundle savedInstanceState) {
        renderWebView(savedInstanceState);
    }

    private void renderWebView(Bundle savedInstanceState) {
        //webView = (WebView)findViewById(R.id.experimentExecutorView);
        webView = new CustomInputWebview(this);
        mainLayout.addView(webView);
        webView.getSettings().setJavaScriptEnabled(true);

        injectObjectsIntoJavascriptEnvironment();
        setWebChromeClientThatHandlesAlertsAsDialogs();
        WebViewClient webViewClient = createWebViewClientThatHandlesFileLinksForCharts();
        webView.setWebViewClient(webViewClient);
        loadCustomRendererIntoWebView();
        if (savedInstanceState != null) {
            webView.loadUrl((String) savedInstanceState.get("url"));
            String showDialogString = (String) savedInstanceState.get("showDialog");
            if (showDialogString == null || showDialogString.equals("false")) {
                showDialog = false;
            } else {
                showDialog = true;
            }
        }
    }

    private void injectObjectsIntoJavascriptEnvironment() {
        final Map<String, String> map = new HashMap<String, String>();
        map.put("test", "false");

        map.put("title", experiment.getExperimentDAO().getTitle());
        if (scheduledTime != null) {
            map.put("scheduledTime", Long.toString(scheduledTime));
        }
        map.put("notificationLabel", notificationMessage);
        map.put("notificationSource", notificationSource);
        map.put("experimentGroupName", experimentGroup.getName());
        if (actionTriggerId != null) {
            map.put("actionTriggerId", Long.toString(actionTriggerId));
        }
        if (actionId != null) {
            map.put("actionId", Long.toString(actionId));
        }
        if (actionTriggerSpecId != null) {
            map.put("actionTriggerSpecId", Long.toString(actionTriggerSpecId));
        }

        env = new Environment(map);
        webView.addJavascriptInterface(env, "env");

        String text = experimentGroup.getCustomRenderingCode();
        webView.addJavascriptInterface(text, "additions");

        webView.addJavascriptInterface(new JavascriptExperimentLoader(this, experimentProviderUtil,
                experiment.getExperimentDAO(), experiment, experimentGroup), "experimentLoader");

        JavascriptEventLoader javascriptEventLoader = new JavascriptEventLoader(experimentProviderUtil, experiment,
                experiment.getExperimentDAO(), experimentGroup);
        webView.addJavascriptInterface(javascriptEventLoader, "db");
        // deprecated name - use "db" in all new experiments
        webView.addJavascriptInterface(javascriptEventLoader, "eventLoader");

        webView.addJavascriptInterface(new JavascriptCalendarManager(this), "calendar");
        webView.addJavascriptInterface(new JavascriptEmail(this), "email");
        webView.addJavascriptInterface(new JavascriptNotificationService(this, experiment.getExperimentDAO(),
                experimentGroup, actionTriggerSpecId, actionTriggerId, actionId), "notificationService");
        webView.addJavascriptInterface(new JavascriptPhotoService(this), "photoService");
        webView.addJavascriptInterface(new JavascriptExecutorListener(experiment), "executor");
        webView.addJavascriptInterface(new JavascriptPackageManager(this), "packageManager");
        webView.addJavascriptInterface(new JavascriptStringResources(getApplicationContext()), "strings");

    }

    private void loadCustomRendererIntoWebView() {
        if (experimentGroup.getEndOfDayGroup()) {
            loadEndOfDayUrl();
        } else if (!experimentGroup.getEndOfDayGroup()) {
            loadUrl();
        } /*else {
          loadData();
          }*/
    }

    private void loadEndOfDayUrl() {
        webView.loadUrl("file:///android_asset/eod_skeleton.html");

    }

    public void loadUrl() {
        webView.loadUrl("file:///android_asset/custom_skeleton.html");
        // polymer experimentation
        // webView.loadUrl("file:///android_asset/skeleton2.html");
        //webView.loadUrl("file:///android_asset/polymer.html");
        //webView.loadUrl("file:///android_asset/empty.html");
        //webView.loadUrl("file:///android_asset/vulcanized-simple.html");
        //webView.loadUrl("file:///android_asset/vulcanized-paper.html");
    }

    public void loadData() {
        // data-based loading of webview
        BufferedReader r = null;
        try {
            StringBuffer data = new StringBuffer();
            InputStream in = this.getClassLoader().getResourceAsStream("file:///android_asset/polymer.html");
            r = new BufferedReader(new InputStreamReader(in));
            String line;
            while ((line = r.readLine()) != null) {
                data.append(line);
            }
            webView.loadData(data.toString(), "text/html", "UTF-8");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (r != null) {
                try {
                    r.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private WebViewClient createWebViewClientThatHandlesFileLinksForCharts() {
        WebViewClient webViewClient = new WebViewClient() {

            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                Uri uri = Uri.parse(url);
                if (uri.getScheme().startsWith("http")) {
                    return true; // throw away http requests - we don't want 3rd party javascript sending url requests due to security issues.
                }

                String inputName = uri.getQueryParameter("inputName");
                if (inputName == null) {
                    return true;
                }
                JSONArray results = new JSONArray();
                for (Event event : experiment.getEvents()) {
                    JSONArray eventJson = new JSONArray();
                    DateTime responseTime = event.getResponseTime();
                    if (responseTime == null) {
                        continue; // missed signal;
                    }
                    eventJson.put(responseTime.getMillis());
                    // in this case we are looking for one input from the responses that we are charting.
                    for (Output response : event.getResponses()) {
                        if (response.getAnswer().equals(inputName)) {
                            Input2 input = ExperimentHelper.getInputWithName(experiment.getExperimentDAO(),
                                    inputName, null);
                            if (input.isNumeric()) {
                                eventJson.put(response.getDisplayOfAnswer(input));
                                results.put(eventJson);
                                continue;
                            }
                        }
                    }

                }
                env.put("data", results.toString());
                env.put("inputName", inputName);

                view.loadUrl(stripQuery(url));
                return true;
            }

        };
        return webViewClient;
    }

    public static String stripQuery(String url) {
        String urlWithoutQuery = url;
        int indexOfQuery = url.indexOf('?');
        if (indexOfQuery != -1) {
            urlWithoutQuery = urlWithoutQuery.substring(0, indexOfQuery);
        }
        return urlWithoutQuery;
    }

    private void setWebChromeClientThatHandlesAlertsAsDialogs() {
        webView.setWebChromeClient(new WebChromeClient() {
            @Override
            public boolean onJsAlert(WebView view, String url, String message, JsResult result) {

                new AlertDialog.Builder(view.getContext()).setMessage(message).setTitle("Alert!")
                        .setCancelable(true).setPositiveButton(R.string.ok, new Dialog.OnClickListener() {
                            public void onClick(DialogInterface dialog, int which) {
                                dialog.dismiss();
                            }
                        }).create().show();
                result.confirm();
                return true;
            }

            public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {

                if (showDialog == false) {
                    result.confirm();
                    return true;
                } else {
                    new AlertDialog.Builder(view.getContext()).setMessage(message).setTitle("Confirm")
                            .setCancelable(true).setPositiveButton(R.string.yes, new Dialog.OnClickListener() {
                                public void onClick(DialogInterface dialog, int which) {
                                    showDialog = false;
                                    dialog.dismiss();
                                    result.confirm();
                                }
                            }).setNegativeButton(R.string.cancel_button, new Dialog.OnClickListener() {
                                public void onClick(DialogInterface dialog, int which) {
                                    dialog.dismiss();
                                    result.cancel();
                                }
                            }).create().show();
                    return true;
                }

                // return super.onJsConfirm(view, url, message, result);
            }

            @Override
            public void onConsoleMessage(String message, int lineNumber, String sourceID) {
                Log.d(PacoConstants.TAG, message + " -- From line " + lineNumber + " of " + sourceID);
            }

            @Override
            public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
                Log.d(PacoConstants.TAG, consoleMessage.message() + " -- From line " + consoleMessage.lineNumber()
                        + " of " + consoleMessage.sourceId());
                return true;
            }

        });
    }

    public static String convertExperimentResultsToJsonString(final Experiment experiment) {
        List<Event> events = experiment.getEvents();
        return convertEventsToJsonString(experiment, events);
    }

    public static String convertLastEventToJsonString(final Experiment experiment) {
        List<Event> events = experiment.getEvents();
        if (events.isEmpty()) {
            return "[]";
        }
        return convertEventsToJsonString(experiment, events.subList(0, 1));
    }

    private static String convertEventsToJsonString(final Experiment experiment, List<Event> events) {
        return FeedbackActivity.convertEventsToJsonString(events);
    }

    static String getTextOfInputForOutput(ExperimentGroup experiment, Output output) {
        for (Input2 input : experiment.getInputs()) {

            if (input.getName().equals(output.getName())) {
                if (!input.isInvisible()) {
                    return input.getText();
                } else {
                    return input.getResponseType();
                }
            }
        }
        return output.getName();
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if ((keyCode == KeyEvent.KEYCODE_BACK) && webView.canGoBack()) {
            webView.goBack();
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }

    /// saving and external service callouts
    private void save() {
        try {
            if (notificationHolderId == null) {
                // workaround the bug with re-launching and stale scheduleTime.
                // How - if there isn't a notificationHolder waiting, then this is not a response
                // to a notification.
                scheduledTime = 0L;
            }
            Event event = EventUtil.createEvent(getExperiment(), experimentGroup.getName(), actionTriggerId,
                    actionId, actionTriggerSpecId, scheduledTime);
            gatherResponses(event);
            experimentProviderUtil.insertEvent(event);

            deleteNotification();

            Bundle extras = getIntent().getExtras();
            if (extras != null) {
                extras.clear();
            }
            updateAlarms();
            notifySyncService();
            showFeedback();
            finish();
        } catch (IllegalStateException ise) {
            new AlertDialog.Builder(this).setTitle(R.string.required_answers_missing).setMessage(ise.getMessage())
                    .show();
        }
    }

    private void updateAlarms() {
        startService(new Intent(this, BeeperService.class));
    }

    private void deleteNotification() {
        if (notificationHolderId != null && notificationHolderId.longValue() != 0l) {
            experimentProviderUtil.deleteNotification(notificationHolderId);
        }
        if (shouldExpireNotificationHolder) {
            NotificationManager notificationManager = (NotificationManager) getSystemService(
                    Context.NOTIFICATION_SERVICE);
            notificationManager.cancel(new Long(notificationHolderId).intValue()); //This should cancel the notification even if it's non-dismissible
            shouldExpireNotificationHolder = false;
        }
    }

    private void notifySyncService() {
        startService(new Intent(this, SyncService.class));
    }

    void showFeedback() {
        Intent intent = new Intent(this, FeedbackActivity.class);
        final Bundle extras = getIntent().getExtras();

        if (!extras.containsKey(Experiment.EXPERIMENT_SERVER_ID_EXTRA_KEY)) {
            extras.putLong(Experiment.EXPERIMENT_SERVER_ID_EXTRA_KEY, experiment.getExperimentDAO().getId());
        }
        if (!extras.containsKey(Experiment.EXPERIMENT_GROUP_NAME_EXTRA_KEY)) {
            extras.putString(Experiment.EXPERIMENT_GROUP_NAME_EXTRA_KEY, experimentGroup.getName());
        }
        intent.putExtras(extras);
        startActivity(intent);
    }

    private void gatherResponses(Event event) throws IllegalStateException {
        com.google.android.apps.paco.questioncondparser.Environment interpreter = updateInterpreter(null);
        ExpressionEvaluator main = new ExpressionEvaluator(interpreter);
        for (InputLayout inputView : inputs) {
            Input2 input = inputView.getInput();
            try {
                if (input.getConditional() != null && input.getConditional() == true
                        && !main.parse(input.getConditionExpression())) {
                    continue;
                }
            } catch (IllegalArgumentException iae) {
                Log.e(PacoConstants.TAG, "Parsing problem: " + iae.getMessage());
                continue;
            }
            Output responseForInput = new Output();
            String answer = inputView.getValueAsString();
            if (input.getRequired() && (answer == null || answer.length() == 0
                    || answer.equals("-1") /*||
                                           (input.getResponseType().equals(Input.LIST) && answer.equals("0"))*/)) {
                throw new IllegalStateException(getString(R.string.must_answer) + input.getText());
            }
            responseForInput.setAnswer(answer);
            responseForInput.setName(input.getName());
            event.addResponse(responseForInput);
        }
    }

    private void displayNoExperimentMessage() {
        inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        mainLayout = (LinearLayout) inflater.inflate(R.layout.could_not_load_experiment, null);
        setContentView(mainLayout);

    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        //return optionsMenu.init(menu);
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        return optionsMenu.onOptionsItemSelected(item);
    }

    public void onChange(InputLayout input) {
        synchronized (updateLock) {
            com.google.android.apps.paco.questioncondparser.Environment interpreter = updateInterpreter(input);
            ExpressionEvaluator main = new ExpressionEvaluator(interpreter);
            for (InputLayout inputLayout : inputs) {
                inputLayout.checkConditionalExpression(main);
            }
        }
    }

    private com.google.android.apps.paco.questioncondparser.Environment updateInterpreter(InputLayout input) {
        com.google.android.apps.paco.questioncondparser.Environment interpreter = null;
        // todo make interpreter a field to optimize updates.
        if (interpreter == null) {
            interpreter = new com.google.android.apps.paco.questioncondparser.Environment();

            for (InputLayout inputLayout : inputs) {
                interpreter.addInput(createBindingFromInputView(inputLayout));
            }
        }
        return interpreter;
    }

    private Binding createBindingFromInputView(InputLayout inputLayout) {
        String inputName = inputLayout.getInputName();
        Class responseType = inputLayout.getResponseType();
        Object value = inputLayout.getValue();
        Binding binding = new Binding(inputName, responseType, value);
        return binding;
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            if (requestCode == RESULT_SPEECH) {
                handleSpeechRecognitionActivityResult(resultCode, data);
            } else if (requestCode == RESULT_CAMERA) {
                cameraPictureTaken();
            } else if (requestCode == RESULT_GALLERY) {
                Uri selectedImage = data.getData();
                String[] filePathColumn = { MediaStore.Images.Media.DATA };

                try {
                    Cursor cursor = getContentResolver().query(selectedImage, filePathColumn, null, null, null);
                    cursor.moveToFirst();

                    int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
                    String filePath = cursor.getString(columnIndex);
                    cursor.close();
                    galleryPicturePicked(filePath);
                } catch (Exception e) {
                    Log.i(PacoConstants.TAG, "Exception in gallery picking: " + e.getMessage());
                    e.printStackTrace();
                }
            }
        }
    }

    private void handleSpeechRecognitionActivityResult(int resultCode, Intent data) {
        if (resultCode == Activity.RESULT_OK && null != data) {
            ArrayList<String> guesses = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
            notifySpeechRecognitionListeners(guesses);
        }
    }

    private void notifySpeechRecognitionListeners(ArrayList<String> guesses) {
        for (SpeechRecognitionListener listener : speechRecognitionListeners) {
            listener.speechRetrieved(guesses);
        }
    }

    public void removeSpeechRecognitionListener(SpeechRecognitionListener listener) {
        speechRecognitionListeners.remove(listener);
    }

    public void startSpeechRecognition(SpeechRecognitionListener listener) {
        speechRecognitionListeners.add(listener);
        Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, Locale.getDefault().getDisplayName());

        try {
            startActivityForResult(intent, RESULT_SPEECH);
        } catch (ActivityNotFoundException a) {
            Toast t = Toast.makeText(getApplicationContext(),
                    R.string.oops_your_device_doesn_t_support_speech_to_text, Toast.LENGTH_SHORT);
            t.show();
        }
    }

    private class JavascriptExecutorListener {
        private Experiment experiment;

        public JavascriptExecutorListener(Experiment experiment) {
            this.experiment = experiment;
        }

        @JavascriptInterface
        public void done() {
            deleteNotification();

            Bundle extras = getIntent().getExtras();
            if (extras != null) {
                extras.clear();
            }
            updateAlarms();
            notifySyncService();

            if (experimentGroup.getFeedback().getType() != FeedbackDAO.FEEDBACK_TYPE_HIDE_FEEDBACK) {
                showFeedback();
            }
            finish();
        }
    }

    //start duplicate from inputlayout for photo service

    public void renderCameraOrGalleryChooser() {
        String title = getString(R.string.please_choose_the_source_of_your_image);
        Dialog chooserDialog = new AlertDialog.Builder(this).setTitle(title)
                .setNegativeButton(getString(R.string.camera), new Dialog.OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        startCameraForResult();
                    }
                }).setPositiveButton(getString(R.string.gallery), new Dialog.OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        startGalleryPicker();

                    }

                }).create();
        chooserDialog.show();
    }

    private void startGalleryPicker() {
        Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
        photoPickerIntent.setType("image/*");
        startActivityForResult(photoPickerIntent, RESULT_GALLERY);
    }

    void galleryPicturePicked(String filepath) {
        if (!Strings.isNullOrEmpty(filepath)) {
            photoFile = new File(filepath);
            setBitmapToHtmlInput(decodeFileAndScaleToThumb(photoFile));
        } else if (Strings.isNullOrEmpty(filepath)) {
            photoFile = null;
        } // otherwise leave as it was previously
    }

    private void startCameraForResult() {
        try {
            Intent i = new Intent("android.media.action.IMAGE_CAPTURE");
            String dateString = createTimeStamp();
            photoFile = getOutputMediaFile(MEDIA_TYPE_IMAGE);
            i.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, Uri.fromFile(photoFile));
            startActivityForResult(i, RESULT_CAMERA);
        } catch (Exception e) {
            e.printStackTrace();
            new AlertDialog.Builder(this).setCancelable(true).setTitle(R.string.cannot_open_camera_warning)
                    .setMessage("Error: \n" + e.getMessage()).setNegativeButton(R.string.ok, null).create().show();
        }
    }

    public void cameraPictureTaken() {
        setBitmapToHtmlInput(decodeFileAndScaleToThumb(photoFile));
    }

    private void setBitmapToHtmlInput(Bitmap bitmap) {
        String encodedBitmap = base64Encode(bitmap);
        encodedBitmap = encodedBitmap.replaceAll("\n", ""); // todo figure out why it is adding newlines and why this isn't a problem for uploaded but is for rendering.
        //String fakeBitmap = "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";

        webView.loadUrl("javascript:paco.photoService.photoResult('" + encodedBitmap + "')");
    }

    private String base64Encode(Bitmap bitmap) {
        return encodeBitmap(bitmap);
    }

    private String getPhotoValue() {
        if (photoFile != null) {
            Bitmap bitmap = decodeFile(photoFile);
            if (bitmap == null) {
                return "";
            }

            return encodeBitmap(bitmap);
        }
        return "";
    }

    private String encodeBitmap(Bitmap bitmap) {
        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
        BufferedOutputStream out = new BufferedOutputStream(bytesOut);
        bitmap.compress(CompressFormat.JPEG, 50, out);
        return Base64.encodeToString(bytesOut.toByteArray(), Base64.DEFAULT);
    }

    private Bitmap decodeFile(File f) {
        return decodeBitmapFromFileWithMaxDimension(f, IMAGE_MAX_SIZE);
    }

    private Bitmap decodeFileAndScaleToThumb(File f) {
        return decodeBitmapFromFileWithMaxDimension(f, IMAGE_MAX_SIZE);
    }

    private Bitmap decodeBitmapFromFileWithMaxDimension(File f, int maxDimension) {
        Bitmap b = null;
        try {
            BitmapFactory.Options o = new BitmapFactory.Options();
            o.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(new FileInputStream(f), null, o);
            int scale = 1;
            if (o.outHeight > maxDimension || o.outWidth > maxDimension) {
                int longestDimension = Math.max(o.outHeight, o.outWidth);
                scale = (int) Math.pow(2,
                        (int) Math.round(Math.log(maxDimension / (double) longestDimension) / Math.log(0.5)));
            }

            // Decode with inSampleSize
            BitmapFactory.Options o2 = new BitmapFactory.Options();
            o2.inSampleSize = scale;
            b = BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
        } catch (FileNotFoundException e) {
            Toast.makeText(this, R.string.missing_image_warning, Toast.LENGTH_LONG);
        }
        return b;
    }

    public static final int MEDIA_TYPE_IMAGE = 1;
    public static final int MEDIA_TYPE_VIDEO = 2;

    /** Create a file Uri for saving an image or video */
    private Uri getOutputMediaFileUri(int type) {
        return Uri.fromFile(getOutputMediaFile(type));
    }

    /** Create a File for saving an image or video */
    private File getOutputMediaFile(int type) {
        // To be safe, you should check that the SDCard is mounted
        // using Environment.getExternalStorageState() before doing this.

        File mediaStorageDir = new File(getExternalFilesDir(android.os.Environment.DIRECTORY_PICTURES), "PacoApp");
        // This location works best if you want the created images to be shared
        // between applications and persist after your app has been uninstalled.

        // Create the storage directory if it does not exist
        if (!mediaStorageDir.exists()) {
            if (!mediaStorageDir.mkdirs()) {
                Log.d(PacoConstants.TAG, "failed to create directory");
                return null;
            }
        }

        // Create a media file name
        String timeStamp = createTimeStamp();
        File mediaFile;
        if (type == MEDIA_TYPE_IMAGE) {
            mediaFile = new File(mediaStorageDir.getPath() + File.separator + "IMG_" + timeStamp + ".jpg");
        } else if (type == MEDIA_TYPE_VIDEO) {
            mediaFile = new File(mediaStorageDir.getPath() + File.separator + "VID_" + timeStamp + ".mp4");
        } else {
            return null;
        }

        return mediaFile;
    }

    private String createTimeStamp() {
        SimpleDateFormat df = new SimpleDateFormat("MMddyyyyhhmmsss");
        return (df.format(new Date()));
    }

    // end duplicate from inputlayout
    public void setExperimentGroup(ExperimentGroup group) {
        this.experimentGroup = group;

    }

    public Experiment getExperiment() {
        return experiment;
    }

    public void setExperiment(Experiment experiment) {
        this.experiment = experiment;

    }

    public void stopExperiment() {
        deleteExperiment();
        finish();
    }

    public void deleteExperiment() {
        NotificationCreator nc = NotificationCreator.create(this);
        nc.timeoutNotificationsForExperiment(experiment.getExperimentDAO().getId());

        createStopEvent(experiment);

        experimentProviderUtil.deleteExperiment(experiment.getId());
        if (ExperimentHelper.shouldWatchProcesses(experiment.getExperimentDAO())) {
            BroadcastTriggerReceiver.initPollingAndLoggingPreference(this);
        }

        new AndroidEsmSignalStore(this).deleteAllSignalsForSurvey(experiment.getExperimentDAO().getId());

        startService(new Intent(this, BeeperService.class));
        startService(new Intent(this, ExperimentExpirationManagerService.class));
    }

    /**
     * Creates a pacot for stopping an experiment
     *
     * @param experiment
     */
    private void createStopEvent(Experiment experiment) {
        Event event = new Event();
        event.setExperimentId(experiment.getId());
        event.setServerExperimentId(experiment.getExperimentDAO().getId());
        event.setExperimentName(experiment.getExperimentDAO().getTitle());
        event.setExperimentVersion(experiment.getExperimentDAO().getVersion());
        event.setResponseTime(new DateTime());

        Output responseForInput = new Output();
        responseForInput.setAnswer("false");
        responseForInput.setName("joined");
        event.addResponse(responseForInput);

        experimentProviderUtil.insertEvent(event);
        startService(new Intent(this, SyncService.class));
    }

}