pro.dbro.bart.TheActivity.java Source code

Java tutorial

Introduction

Here is the source code for pro.dbro.bart.TheActivity.java

Source

/*
 *  Copyright (C) 2012  David Brodsky
 *   This file is part of Open BART.
 *
 *  Open BART is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Open BART is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Open BART.  If not, see <http://www.gnu.org/licenses/>.
*/

package pro.dbro.bart;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.content.*;
import android.content.res.Resources;
import android.location.Location;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.support.v4.content.LocalBroadcastManager;
import android.text.Editable;
import android.text.Html;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.*;
import android.view.View.OnClickListener;
import android.view.View.OnFocusChangeListener;
import android.view.View.OnLongClickListener;
import android.view.View.OnTouchListener;
import android.view.inputmethod.InputMethodManager;
import android.widget.*;
import android.widget.AdapterView.OnItemClickListener;
import com.crittercism.app.Crittercism;
import pro.dbro.bart.DeviceLocation.LocationResult;

import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;

public class TheActivity extends Activity {

    static Context c;
    TableLayout tableLayout;
    LinearLayout tableContainerLayout;
    static String lastRequest = "";
    Resources res;
    AutoCompleteTextView destinationTextView;
    AutoCompleteTextView originTextView;
    TextView fareTv;
    TextView stopServiceTv;
    LinearLayout infoLayout;

    ArrayList timerViews = new ArrayList();
    static ViewCountDownTimer timer;
    long maxTimer = 0;

    ArrayList<StationSuggestion> stationSuggestions;
    private final int STATION_SUGGESTION_SIZE = 3;

    // route that the usher service should access
    public static route usherRoute;
    // real time info for current station of interest in route
    // set on completion of etdresponse
    // freshness of response is available in currentEtdResponse.Date
    public static etdResponse currentEtdResponse;

    // time in ms to allow a currentEtdResponse to be considered 'fresh'
    private final long CURRENT_ETD_RESPONSE_FRESH_MS = 60 * 1000;

    // determines whether UI is automatically updated after api request by handleResponse(response)
    // set to false in events where a routeResponse is displayed BEFORE an etdresponse was cached
    // in currentEtdResponse.
    // etdResponse has the real-time station info, while routeResponse is based on the BART schedule
    // private boolean updateUIOnResponse = true;

    private SharedPreferences prefs;
    private SharedPreferences.Editor editor;

    // Location 
    Location currentLocation;
    double currentLat;
    double currentLon;
    boolean hasLocation = false;
    // set when first location received
    String localStation = "";

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //TESTING: enable crittercism
        Crittercism.init(getApplicationContext(), SECRETS.CRITTERCISM_SECRET);

        if (Build.VERSION.SDK_INT < 11) {
            //If API 14+, The ActionBar will be hidden with this call
            this.requestWindowFeature(Window.FEATURE_NO_TITLE);
        }
        setContentView(R.layout.main);
        tableLayout = (TableLayout) findViewById(R.id.tableLayout);
        tableContainerLayout = (LinearLayout) findViewById(R.id.tableContainerLayout);
        c = this;
        res = getResources();
        prefs = getSharedPreferences("PREFS", 0);
        editor = prefs.edit();

        if (prefs.getBoolean("first_timer", true)) {
            TextView greetingTv = (TextView) View.inflate(c, R.layout.tabletext, null);
            greetingTv.setText(Html.fromHtml(getString(R.string.greeting)));
            greetingTv.setTextSize(18);
            greetingTv.setPadding(0, 0, 0, 0);
            greetingTv.setMovementMethod(LinkMovementMethod.getInstance());
            new AlertDialog.Builder(c).setTitle("Welcome to Open BART").setIcon(R.drawable.ic_launcher)
                    .setView(greetingTv).setPositiveButton("Okay!", null).show();

            editor.putBoolean("first_timer", false);
            editor.commit();
        }
        // LocalBroadCast Stuff
        LocalBroadcastManager.getInstance(this).registerReceiver(serviceStateMessageReceiver,
                new IntentFilter("service_status_change"));

        // infoLayout is at the bottom of the screen
        // currently contains the stop service label 
        infoLayout = (LinearLayout) findViewById(R.id.infoLayout);

        // Assign the stationSuggestions Set
        stationSuggestions = new ArrayList();

        // Assign the bart station list to the autocompletetextviews 
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_dropdown_item_1line,
                BART.STATIONS);
        originTextView = (AutoCompleteTextView) findViewById(R.id.originTv);
        // Set tag for array adapter switch
        originTextView.setTag(R.id.TextInputShowingSuggestions, "false");

        fareTv = (TextView) findViewById(R.id.fareTv);
        stopServiceTv = (TextView) findViewById(R.id.stopServiceTv);

        destinationTextView = (AutoCompleteTextView) findViewById(R.id.destinationTv);
        destinationTextView.setTag(R.id.TextInputShowingSuggestions, "false");
        destinationTextView.setAdapter(adapter);
        originTextView.setAdapter(adapter);

        // Retrieve TextView inputs from saved preferences
        if (prefs.contains("origin") && prefs.contains("destination")) {
            //state= originTextView,destinationTextView
            String origin = prefs.getString("origin", "");
            String destination = prefs.getString("destination", "");
            if (origin.compareTo("") != 0)
                originTextView.setThreshold(200); // disable auto-complete until new text entered
            if (destination.compareTo("") != 0)
                destinationTextView.setThreshold(200); // disable auto-complete until new text entered

            originTextView.setText(origin);
            destinationTextView.setText(destination);
            validateInputAndDoRequest();
        }

        // Retrieve station suggestions from file storage
        try {
            ArrayList<StationSuggestion> storedSuggestions = (ArrayList<StationSuggestion>) LocalPersistence
                    .readObjectFromFile(c, res.getResourceEntryName(R.string.StationSuggestionFileName));
            // If stored StationSuggestions are found, apply them
            if (storedSuggestions != null) {
                stationSuggestions = storedSuggestions;
                Log.d("stationSuggestions", "Loaded");
            } else
                Log.d("stationSuggestions", "Not Found");

        } catch (Throwable t) {
            // don't sweat it
        }

        ImageButton map = (ImageButton) findViewById(R.id.map);
        map.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View arg0) {
                // TODO Auto-generated method stub
                Intent intent = new Intent(c, MapActivity.class);
                startActivity(intent);
            }

        });

        ImageButton reverse = (ImageButton) findViewById(R.id.reverse);
        reverse.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View arg0) {
                Editable originTempText = originTextView.getText();
                originTextView.setText(destinationTextView.getText());
                destinationTextView.setText(originTempText);

                validateInputAndDoRequest();
            }
        });

        // Handles restoring TextView input when focus lost, if no new input entered
        // previous input is stored in the target View Tag attribute
        // Assumes the target view is a TextView
        // TODO:This works but starts autocomplete when the view loses focus after clicking outside the autocomplete listview
        OnFocusChangeListener inputOnFocusChangeListener = new OnFocusChangeListener() {
            @Override
            public void onFocusChange(View inputTextView, boolean hasFocus) {
                if (inputTextView.getTag(R.id.TextInputMemory) != null && !hasFocus
                        && ((TextView) inputTextView).getText().toString().compareTo("") == 0) {
                    //Log.v("InputTextViewTagGet","orig: "+ inputTextView.getTag());
                    ((TextView) inputTextView).setText(inputTextView.getTag(R.id.TextInputMemory).toString());
                }
            }
        };

        originTextView.setOnFocusChangeListener(inputOnFocusChangeListener);
        destinationTextView.setOnFocusChangeListener(inputOnFocusChangeListener);

        // When the TextView is clicked, store current text in TextView's Tag property, clear displayed text 
        // and enable Auto-Completing after first character entered
        OnTouchListener inputOnTouchListener = new OnTouchListener() {
            @Override
            public boolean onTouch(View inputTextView, MotionEvent me) {
                // Only perform this logic on finger-down
                if (me.getAction() == me.ACTION_DOWN) {
                    inputTextView.setTag(R.id.TextInputMemory, ((TextView) inputTextView).getText().toString());
                    Log.d("adapterSwitch", "suggestions");
                    ((AutoCompleteTextView) inputTextView).setThreshold(1);
                    ((TextView) inputTextView).setText("");

                    // TESTING 
                    // set tag to be retrieved on input entered to set adapter back to station list
                    // The key of a tag must be a unique ID resource
                    inputTextView.setTag(R.id.TextInputShowingSuggestions, "true");
                    ArrayList<StationSuggestion> prunedSuggestions = new ArrayList<StationSuggestion>();
                    // copy suggestions

                    for (int x = 0; x < stationSuggestions.size(); x++) {
                        prunedSuggestions.add(stationSuggestions.get(x));
                    }

                    // Check for and remove other text input's value from stationSuggestions
                    if (inputTextView.equals(findViewById(R.id.originTv))) {
                        // If the originTv is clicked, remove the destinationTv's value from prunedSuggestions
                        if (prunedSuggestions.contains(new StationSuggestion(
                                ((TextView) findViewById(R.id.destinationTv)).getText().toString(), "recent"))) {
                            prunedSuggestions.remove(new StationSuggestion(
                                    ((TextView) findViewById(R.id.destinationTv)).getText().toString(), "recent"));
                        }
                    } else if (inputTextView.equals(findViewById(R.id.destinationTv))) {
                        // If the originTv is clicked, remove the destinationTv's value from prunedSuggestions
                        if (prunedSuggestions.contains(new StationSuggestion(
                                ((TextView) findViewById(R.id.originTv)).getText().toString(), "recent"))) {
                            prunedSuggestions.remove(new StationSuggestion(
                                    ((TextView) findViewById(R.id.originTv)).getText().toString(), "recent"));
                        }
                    }

                    //if(stationSuggestions.contains(new StationSuggestion(((TextView)inputTextView).getText().toString(),"recent")))

                    // if available, add localStation to prunedSuggestions
                    if (localStation.compareTo("") != 0) {
                        if (BART.REVERSE_STATION_MAP.get(localStation) != null) {
                            // If a valid localStation (based on DeviceLocation) is available: 
                            // remove localStations from recent suggestions (if it exists there)
                            // and add as nearby station
                            prunedSuggestions.remove(
                                    new StationSuggestion(BART.REVERSE_STATION_MAP.get(localStation), "recent"));
                            prunedSuggestions.add(
                                    new StationSuggestion(BART.REVERSE_STATION_MAP.get(localStation), "nearby"));
                        }
                    }

                    // TESTING: Set Custom ArrayAdapter to hold recent/nearby stations
                    TextPlusIconArrayAdapter adapter = new TextPlusIconArrayAdapter(c, prunedSuggestions);
                    ((AutoCompleteTextView) inputTextView).setAdapter(adapter);
                    // force drop-down to appear, overriding requirement that at least one char is entered
                    ((AutoCompleteTextView) inputTextView).showDropDown();

                    // ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
                    // android.R.layout.simple_dropdown_item_1line, BART.STATIONS);
                }
                // Allow Android to handle additional actions - i.e: TextView takes focus
                return false;
            }
        };

        originTextView.setOnTouchListener(inputOnTouchListener);
        destinationTextView.setOnTouchListener(inputOnTouchListener);

        // Autocomplete ListView item select listener

        originTextView.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View arg1, int position, long arg3) {
                Log.d("OriginTextView", "item clicked");
                AutoCompleteTextView originTextView = (AutoCompleteTextView) findViewById(R.id.originTv);
                originTextView.setThreshold(200);
                //hideSoftKeyboard(arg1);
                // calling hideSoftKeyboard with arg1 doesn't work with stationSuggestion adapter
                hideSoftKeyboard(findViewById(R.id.inputLinearLayout));

                // Add selected station to stationSuggestions ArrayList if it doesn't exist
                if (!stationSuggestions
                        .contains((new StationSuggestion(originTextView.getText().toString(), "recent")))) {
                    stationSuggestions.add(0, new StationSuggestion(originTextView.getText().toString(), "recent"));
                    // if the stationSuggestion arraylist is over the max size, remove the last item
                    if (stationSuggestions.size() > STATION_SUGGESTION_SIZE) {
                        stationSuggestions.remove(stationSuggestions.size() - 1);
                    }
                }
                // Else, increment click count for that recent
                else {
                    stationSuggestions
                            .get(stationSuggestions.indexOf(
                                    (new StationSuggestion(originTextView.getText().toString(), "recent"))))
                            .addHit();
                }
                validateInputAndDoRequest();

            }
        });

        destinationTextView.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View arg1, int position, long arg3) {
                Log.d("DestinationTextView", "item clicked");

                // Actv not available as arg1
                AutoCompleteTextView destinationTextView = (AutoCompleteTextView) findViewById(R.id.destinationTv);
                destinationTextView.setThreshold(200);
                //hideSoftKeyboard(arg1);
                hideSoftKeyboard(findViewById(R.id.inputLinearLayout));

                // Add selected station to stationSuggestions set
                if (!stationSuggestions
                        .contains((new StationSuggestion(destinationTextView.getText().toString(), "recent")))) {
                    Log.d("DestinationTextView", "adding station");
                    stationSuggestions.add(0,
                            new StationSuggestion(destinationTextView.getText().toString(), "recent"));
                    if (stationSuggestions.size() > STATION_SUGGESTION_SIZE) {
                        stationSuggestions.remove(stationSuggestions.size() - 1);
                    }
                }
                // If station exists in StationSuggestions, increment hit
                else {
                    stationSuggestions
                            .get(stationSuggestions.indexOf(
                                    (new StationSuggestion(destinationTextView.getText().toString(), "recent"))))
                            .addHit();
                    //Log.d("DestinationTextView",String.valueOf(stationSuggestions.get(stationSuggestions.indexOf((new StationSuggestion(destinationTextView.getText().toString(),"recent")))).hits));
                }

                //If a valid origin station is not entered, return
                if (BART.STATION_MAP.get(originTextView.getText().toString()) == null)
                    return;
                validateInputAndDoRequest();
                //lastRequest = "etd";
                //String url = "http://api.bart.gov/api/etd.aspx?cmd=etd&orig="+originStation+"&key=MW9S-E7SL-26DU-VV8V";
                // TEMP: For testing route function
                //lastRequest = "route";
                //bartApiRequest();
            }
        });

        //OnKeyListener only gets physical device keyboard events (except the softkeyboard delete key. hmmm)
        originTextView.addTextChangedListener(new TextWatcher() {
            public void afterTextChanged(Editable s) {
                //Log.d("seachScreen", "afterTextChanged"); 
            }

            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                //Log.d("seachScreen", "beforeTextChanged"); 
            }

            public void onTextChanged(CharSequence s, int start, int before, int count) {

                ArrayAdapter<String> adapter = new ArrayAdapter<String>(c,
                        android.R.layout.simple_dropdown_item_1line, BART.STATIONS);
                if (((String) ((TextView) findViewById(R.id.originTv)).getTag(R.id.TextInputShowingSuggestions))
                        .compareTo("true") == 0) {
                    ((TextView) findViewById(R.id.originTv)).setTag(R.id.TextInputShowingSuggestions, "false");
                    ((AutoCompleteTextView) findViewById(R.id.originTv)).setAdapter(adapter);
                }

                Log.d("seachScreen", s.toString());
            }

        });
        destinationTextView.addTextChangedListener(new TextWatcher() {
            public void afterTextChanged(Editable s) {
                //Log.d("seachScreen", "afterTextChanged"); 
            }

            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                //Log.d("seachScreen", "beforeTextChanged"); 
            }

            public void onTextChanged(CharSequence s, int start, int before, int count) {
                ArrayAdapter<String> adapter = new ArrayAdapter<String>(c,
                        android.R.layout.simple_dropdown_item_1line, BART.STATIONS);
                if (((String) ((TextView) findViewById(R.id.destinationTv))
                        .getTag(R.id.TextInputShowingSuggestions)).compareTo("true") == 0) {
                    ((TextView) findViewById(R.id.destinationTv)).setTag(R.id.TextInputShowingSuggestions, "false");
                    ((AutoCompleteTextView) findViewById(R.id.destinationTv)).setAdapter(adapter);
                }
                Log.d("seachScreen", s.toString());
            }

        });

    } // End OnCreate
      // Initialize settings menu

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        //Use setting-button context menu OR Action bar
        if (Build.VERSION.SDK_INT < 11) {
            MenuItem mi = menu.add(0, 0, 0, "About");
            mi.setIcon(R.drawable.about);
        } else {
            MenuInflater inflater = getMenuInflater();
            inflater.inflate(R.layout.actionitem, menu);
            //return true;
        }
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        //settings context menu ID pre API 11 and action bar item post API 11
        if (item.getItemId() == 0 || item.getItemId() == R.id.menu_about) {
            showInfoDialog();
            return true;
        }
        return false;
    }

    public void onInfoClick(View v) {
        showInfoDialog();
    }

    public void showInfoDialog() {
        TextView aboutTv = (TextView) View.inflate(c, R.layout.tabletext, null);
        aboutTv.setText(Html.fromHtml(res.getStringArray(R.array.aboutDialog)[1]));
        aboutTv.setPadding(10, 0, 10, 0);
        aboutTv.setTextSize(18);
        aboutTv.setMovementMethod(LinkMovementMethod.getInstance());
        new AlertDialog.Builder(c).setTitle(res.getStringArray(R.array.aboutDialog)[0])
                .setIcon(R.drawable.ic_launcher).setView(aboutTv).setPositiveButton("Okay!", null).show();
    }

    //CALLED-BY: originTextView and destinationTextView item-select listeners
    //CALLS: HTTP requester: RequestTask
    public void bartApiRequest(String request, boolean updateUI) {
        String url = BART.API_ROOT;
        if (request.compareTo("etd") == 0) {
            url += "etd.aspx?cmd=etd&orig=" + BART.STATION_MAP.get(originTextView.getText().toString());
        } else if (request.compareTo("route") == 0) {
            url += "sched.aspx?cmd=depart&a=4&b=0&orig=" + BART.STATION_MAP.get(originTextView.getText().toString())
                    + "&dest=" + BART.STATION_MAP.get(destinationTextView.getText().toString());
        }
        url += "&key=" + BART.API_KEY;
        Log.d("BART API", url);
        Crittercism.leaveBreadcrumb("BART API: " + url);
        new RequestTask(request, updateUI).execute(url);
        // Set loading indicator
        // I find this jarring when network latency is low
        // TODO: set a countdown timer and only indicate loading after a threshold
        //fareTv.setVisibility(0);
        //fareTv.setText("Loading...");
    }

    public static void hideSoftKeyboard(View view) {
        InputMethodManager imm = (InputMethodManager) c.getSystemService(Context.INPUT_METHOD_SERVICE);
        imm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0);
    }

    //CALLED-BY: HTTP requester: RequestTask
    //CALLS: Bart API XML response parsers
    public void parseBart(String response, String request, boolean updateUI) {
        // Clear loading indicator
        //fareTv.setText("");
        //fareTv.setVisibility(View.GONE);

        // if the response was not initiated by the user (updateUI is false)
        // fail silently
        if (response == "error") {
            if (updateUI) {
                new AlertDialog.Builder(c).setTitle(res.getStringArray(R.array.networkErrorDialog)[0])
                        .setMessage(res.getStringArray(R.array.networkErrorDialog)[1])
                        .setPositiveButton("Bummer", null).show();
            }
        } else if (request.compareTo("etd") == 0)
            new BartStationEtdParser(updateUI).execute(response);
        else if (request.compareTo("route") == 0)
            new BartRouteParser(updateUI).execute(response);
    }

    //CALLED-BY: Bart API XML response parsers: BartRouteParser, BartEtdParser
    //CALLS: the appropriate method to update the UI if updateUI is true
    //       else cache the response (if it includes realtime info)
    public void handleResponse(Object response, boolean updateUI) {
        if (updateUI) {
            //If special messages exist from a previous request, remove them
            if (tableContainerLayout.getChildCount() > 1)
                tableContainerLayout.removeViews(1, tableContainerLayout.getChildCount() - 1);
            if (response instanceof etdResponse) {
                currentEtdResponse = (etdResponse) response;
                //Log.v("ETD_CACHE","ETD SAVED");
                displayEtdResponse((etdResponse) response);
            } else if (response instanceof routeResponse) {
                //Log.v("ETD_CACHE","ETD ROUTE DISPLAY");
                // BartRouteParser removes routes that have bunk date info
                // If all routes removed, alert user
                if (((routeResponse) response).routes.size() == 0) {
                    showErrorDialog("");
                } else {
                    // Check that routeResponse routes are in the future. 
                    // BART API may return routes from earlier in the night when called after service has stopped
                    displayRouteResponse(updateRouteResponseWithEtd(
                            (routeResponse) removeExpiredRoutes((routeResponse) response)));
                }
            }
        } else {
            // if response is not being displayed cache it if it's real-time info
            if (response instanceof etdResponse) {
                currentEtdResponse = (etdResponse) response;
                sendEtdResponseToService();
                //Log.v("ETD_CACHE","ETD SAVED");
            }
        }
    }

    //CALLED-BY: handleResponse() if updateUIOnResponse is true
    //Updates the UI with data from a routeResponse
    public void displayRouteResponse(routeResponse routeResponse) {
        // Log.d("displayRouteResponse","Is this real?: "+routeResponse.toString());
        // Previously, if the device's locale wasn't in Pacific Standard Time
        // Responses with all expired routes could present, causing a looping refresh cycle
        // This is now remedied by coercing response dates into PST
        boolean expiredResponse = false;
        if (routeResponse.routes.size() == 0) {
            Log.d("displayRouteResponse", "no routes to display");
            expiredResponse = true;
        }

        if (timer != null)
            timer.cancel(); // cancel previous timer
        timerViews = new ArrayList(); // release old ETA text views
        maxTimer = 0;
        try {
            tableLayout.removeAllViews();
            //Log.v("DATE",new Date().toString());
            long now = new Date().getTime();

            if (!expiredResponse) {
                fareTv.setVisibility(0);
                fareTv.setText("$" + routeResponse.routes.get(0).fare);
                for (int x = 0; x < routeResponse.routes.size(); x++) {
                    route thisRoute = routeResponse.routes.get(x);

                    TableRow tr = (TableRow) View.inflate(c, R.layout.tablerow, null);
                    tr.setPadding(0, 20, 0, 0);
                    LinearLayout legLayout = (LinearLayout) View.inflate(c, R.layout.routelinearlayout, null);

                    for (int y = 0; y < thisRoute.legs.size(); y++) {
                        TextView trainTv = (TextView) View.inflate(c, R.layout.tabletext, null);
                        trainTv.setPadding(0, 0, 0, 0);
                        trainTv.setTextSize(20);
                        trainTv.setGravity(3); // set left gravity
                        // If route has multiple legs, generate "Transfer At [station name]" and "To [train name] " rows for each leg after the first
                        if (y > 0) {
                            trainTv.setText("transfer at " + BART.REVERSE_STATION_MAP
                                    .get(((leg) thisRoute.legs.get(y - 1)).disembarkStation.toLowerCase()));
                            trainTv.setPadding(0, 0, 0, 0);
                            legLayout.addView(trainTv);
                            trainTv.setTextSize(14);
                            trainTv = (TextView) View.inflate(c, R.layout.tabletext, null);
                            trainTv.setPadding(0, 0, 0, 0);
                            trainTv.setTextSize(20);
                            trainTv.setGravity(3); // set left gravity
                            trainTv.setText("to " + BART.REVERSE_STATION_MAP
                                    .get(((leg) thisRoute.legs.get(y)).trainHeadStation.toLowerCase()));
                        } else {
                            // For first route leg, display "Take [train name]" row
                            trainTv.setText("take "
                                    + BART.REVERSE_STATION_MAP.get(((leg) thisRoute.legs.get(y)).trainHeadStation));
                        }

                        legLayout.addView(trainTv);

                    }

                    if (thisRoute.legs.size() == 1) {
                        legLayout.setPadding(0, 10, 0, 0); // Address detination train and ETA not aligning 
                    }

                    tr.addView(legLayout);

                    // Prepare ETA TextView
                    TextView arrivalTimeTv = (TextView) View.inflate(c, R.layout.tabletext, null);
                    arrivalTimeTv.setPadding(10, 0, 0, 0);

                    //Log.v("DEPART_DATE",thisRoute.departureDate.toString());

                    // Don't report a train that may JUST be leaving with a negative ETA
                    long eta;
                    if (thisRoute.departureDate.getTime() - now <= 0) {
                        eta = 0;
                    } else {
                        eta = thisRoute.departureDate.getTime() - now;
                    }

                    if (eta > maxTimer) {
                        maxTimer = eta;
                    }
                    // Set timeTv Tag to departure date for interpretation by ViewCountDownTimer
                    arrivalTimeTv.setTag(thisRoute.departureDate.getTime());

                    // Print arrival as time, not eta if greater than BART.ETA_THRESHOLD_MS
                    if (thisRoute.departureDate.getTime() - now > BART.ETA_IN_MINUTES_THRESHOLD_MS) {
                        SimpleDateFormat sdf = new SimpleDateFormat("h:mm a");
                        arrivalTimeTv.setText(sdf.format(thisRoute.departureDate));
                        arrivalTimeTv.setTextSize(20);
                    }
                    // Display ETA as minutes until arrival
                    else {
                        arrivalTimeTv.setTextSize(36);
                        // Display eta less than 1m as "<1"
                        if (eta < 60 * 1000)
                            arrivalTimeTv.setText("<1"); // TODO - remove this? Does countdown tick on start
                        else
                            arrivalTimeTv.setText(String.valueOf(eta / (1000 * 60))); // TODO - remove this? Does countdown tick on start
                        // Add the timerView to the list of views to be passed to the ViewCountDownTimer
                        timerViews.add(arrivalTimeTv);
                    }

                    //new ViewCountDownTimer(arrivalTimeTv, eta, 60*1000).start();
                    tr.addView(arrivalTimeTv);
                    // Set the Row View (containing train names and times) Tag to the route it represents
                    tr.setTag(thisRoute);
                    tableLayout.addView(tr);
                    tr.setOnLongClickListener(new OnLongClickListener() {

                        @Override
                        public boolean onLongClick(View arg0) {
                            Log.d("RouteViewTag", ((route) arg0.getTag()).toString());
                            usherRoute = (route) arg0.getTag();
                            TextView guidanceTv = (TextView) View.inflate(c, R.layout.tabletext, null);
                            guidanceTv.setText(Html.fromHtml(getString(R.string.service_prompt)));
                            guidanceTv.setTextSize(18);
                            guidanceTv.setPadding(0, 0, 0, 0);
                            new AlertDialog.Builder(c).setTitle("Route Guidance").setIcon(R.drawable.ic_launcher)
                                    .setView(guidanceTv).setPositiveButton(R.string.service_start_button,
                                            new DialogInterface.OnClickListener() {

                                                public void onClick(DialogInterface dialog, int which) {
                                                    Intent i = new Intent(c, UsherService.class);
                                                    //i.putExtra("departure", ((leg)usherRoute.legs.get(0)).boardStation);
                                                    //Log.v("SERVICE","Starting");
                                                    if (usherServiceIsRunning()) {
                                                        stopService(i);
                                                    }
                                                    startService(i);
                                                }

                                            })
                                    .setNeutralButton("Cancel", null).show();
                            return true; // consumed the long click
                        }

                    });
                    tr.setOnClickListener(new OnClickListener() {

                        @Override
                        public void onClick(View arg0) {
                            int index = tableLayout.indexOfChild(arg0); // index of clicked view. Expanded view will always be +1
                            route thisRoute = (route) arg0.getTag();
                            if (!thisRoute.isExpanded) { // if route not expanded
                                thisRoute.isExpanded = true;
                                LinearLayout routeDetail = (LinearLayout) View.inflate(c, R.layout.routedetail,
                                        null);
                                TextView arrivalTv = (TextView) View.inflate(c, R.layout.tabletext, null);
                                SimpleDateFormat curFormater = new SimpleDateFormat("h:mm a");
                                //arrivalTv.setTextColor(0xFFC9C7C8);
                                arrivalTv.setText("arrives " + curFormater.format(thisRoute.arrivalDate));
                                arrivalTv.setTextSize(20);
                                routeDetail.addView(arrivalTv);
                                ImageView bikeIv = (ImageView) View.inflate(c, R.layout.bikeimage, null);

                                if (!thisRoute.bikes) {
                                    bikeIv.setImageResource(R.drawable.no_bicycle);
                                }
                                routeDetail.addView(bikeIv);
                                tableLayout.addView(routeDetail, index + 1);
                            } else {
                                thisRoute.isExpanded = false;
                                tableLayout.removeViewAt(index + 1);
                            }

                        }
                    });
                } // end route iteration
            } // end expiredResponse check
              // expiredResponse == True
              // If a late-night routeResponse includes the next morning's routes, they will be
              // presented with HH:MM ETAs, instead of minutes
              // Else if a late-night routeResponse includes routes from earlier in the evening
              // We will display "This route has stopped for tonight"
            else {
                String message = "This route has stopped for tonight";
                TextView specialScheduleTextView = (TextView) View.inflate(c, R.layout.tabletext, null);
                specialScheduleTextView.setText(message);
                specialScheduleTextView.setPadding(0, 0, 0, 0);
                tableLayout.addView(specialScheduleTextView);
            }
            if (routeResponse.specialSchedule != null) {
                ImageView specialSchedule = (ImageView) View.inflate(c, R.layout.specialschedulelayout, null);
                specialSchedule.setTag(routeResponse.specialSchedule);
                specialSchedule.setOnClickListener(new OnClickListener() {

                    @Override
                    public void onClick(View arg0) {
                        TextView specialScheduleTv = (TextView) View.inflate(c, R.layout.tabletext, null);
                        specialScheduleTv.setPadding(0, 0, 0, 0);
                        specialScheduleTv.setText(Html.fromHtml(arg0.getTag().toString()));
                        specialScheduleTv.setTextSize(16);
                        specialScheduleTv.setMovementMethod(LinkMovementMethod.getInstance());
                        new AlertDialog.Builder(c).setTitle("Route Alerts").setIcon(R.drawable.warning)
                                .setView(specialScheduleTv).setPositiveButton("Okay!", null).show();

                    }

                });
                tableLayout.addView(specialSchedule);
            }
            // Don't set timer if response is expired
            if (!expiredResponse) {
                timer = new ViewCountDownTimer(timerViews, "route", maxTimer, 30 * 1000);
                timer.start();
            }
        } catch (Throwable t) {
            Log.d("displayRouteResponseError", t.getStackTrace().toString());
        }
    }

    // Update route times with ETAs from cached etd response
    private routeResponse updateRouteResponseWithEtd(routeResponse input) {
        int numRoutes = input.routes.size();
        /***** Preliminary Argument Checks *****/
        // If response has no routes (due to filtering by removeExpiredRoutes), return
        if (numRoutes == 0)
            return input;

        // If there is no cached etdResponse to update with, return
        //TODO: Confirm that currentEtdResponse has all ready been verified fresh
        if (currentEtdResponse == null)
            return input;

        // If etdResponse indicates a closed station, return
        if (currentEtdResponse.message != null) {
            if (currentEtdResponse.message.contains("No data matched your criteria."))
                return input;
        }

        /***** End Preliminary Argument Checks *****/

        // BUGFIX: Using Date().getTime() could possibly return a time different than BART's API Locale
        // Bart doesn't provide timezone info in their date responses, so consider whether to coerce their responses to PST
        // In this instance, we can simply use the time returned with the etd response
        //long now = new Date().getTime();
        long now = input.date.getTime();
        int numEtds = currentEtdResponse.etds.size();
        int lastLeg;
        HashMap<Integer, Integer> routeToEtd = new HashMap<Integer, Integer>();
        //find proper destination etds in currentEtdResponse
        //match times in routeResponse to times in proper etds

        // ASSUMPTION: etds and routes are sorted by time, increasing

        // For each route
        for (int x = 0; x < numRoutes; x++) {
            lastLeg = ((route) input.routes.get(x)).legs.size() - 1;
            // For each possible etd match
            for (int y = 0; y < numEtds; y++) {
                // DEBUG
                try {
                    //Check that destination train is listed in terminal-station format. Ex: "Fremont" CounterEx: 'SFO/Milbrae'
                    if (!BART.STATION_MAP.containsKey(((etd) currentEtdResponse.etds.get(y)).destination)) {
                        // If this is not a known silly-named train terminal station
                        if (!BART.KNOWN_SILLY_TRAINS
                                .containsKey(((etd) currentEtdResponse.etds.get(y)).destination)) {
                            // Let's try and guess what it is
                            boolean station_guessed = false;
                            for (int z = 0; z < BART.STATIONS.length; z++) {

                                // Can we match a station name within the silly-train name?
                                // haystack.indexOf(needle1);
                                if ((((etd) currentEtdResponse.etds.get(y)).destination)
                                        .indexOf(BART.STATIONS[z]) != -1) {
                                    // Set the etd destination to the guessed real station name
                                    ((etd) currentEtdResponse.etds.get(y)).destination = BART.STATIONS[z];
                                    station_guessed = true;
                                }
                            }
                            if (!station_guessed) {
                                break; //We have to give up on updating routes based on this utterly silly-named etd
                            }
                        } else {
                            // Set the etd destination station to the real station name
                            ((etd) currentEtdResponse.etds.get(y)).destination = BART.KNOWN_SILLY_TRAINS
                                    .get(((etd) currentEtdResponse.etds.get(y)).destination);
                            //break;
                        }
                    } // end STATION_MAP silly-name train check and replace

                    // Comparing BART station abbreviations
                    if (BART.STATION_MAP.get(((etd) currentEtdResponse.etds.get(y)).destination)
                            .compareTo(((leg) ((route) input.routes.get(x)).legs.get(0)).trainHeadStation) == 0) {
                        //If matching etd is not all ready matched to a route, match it to this one
                        if (!routeToEtd.containsKey(x) && !routeToEtd.containsValue(y)) {
                            routeToEtd.put(x, y);
                            //Log.v("routeToEtd","Route: " + String.valueOf(x)+ " Etd: " + String.valueOf(y));
                        } else {
                            //if the etd is all ready claimed by a route, go to next etd
                            continue;
                        }
                    } else if (BART.STATION_MAP.get(((etd) currentEtdResponse.etds.get(y)).destination).compareTo(
                            ((leg) ((route) input.routes.get(x)).legs.get(lastLeg)).trainHeadStation) == 0) {
                        if (!routeToEtd.containsKey(x) && !routeToEtd.containsValue(y)) {
                            routeToEtd.put(x, y);
                            //Log.v("routeToEtd","Route: " + String.valueOf(x)+ " Etd: " + String.valueOf(y));
                        } else {
                            //if the etd is all ready claimed by a route, go to next etd
                            continue;
                        }
                    }

                } catch (Throwable T) {
                    // Likely, a train with destination listed as a
                    // special tuple and not an actual station name
                    // was encountered 
                    //Log.v("WTF", "Find me");
                }
            } // end etd for loop

        } // end route for loop

        Integer[] routesToUpdate = (Integer[]) ((routeToEtd.keySet()).toArray(new Integer[0]));
        for (int x = 0; x < routeToEtd.size(); x++) {
            //Log.v("routeToEtd","Update Route: " + String.valueOf(routesToUpdate[x])+ " w/Etd: " + String.valueOf(routeToEtd.get(x)));
            // etd ETA - route ETA (ms)
            //Log.v("updateRR", "etd: "+ new Date((now + ((etd)currentEtdResponse.etds.get(routeToEtd.get(routesToUpdate[x]))).minutesToArrival*60*1000)).toString()+" route: "+ new Date(((route)input.routes.get(routesToUpdate[x])).departureDate.getTime()).toString());
            long timeCorrection = (now
                    + ((etd) currentEtdResponse.etds.get(routeToEtd.get(routesToUpdate[x]))).minutesToArrival * 60
                            * 1000)
                    - ((route) input.routes.get(routesToUpdate[x])).departureDate.getTime();
            //Log.v("updateRRCorrection",String.valueOf(timeCorrection/(1000*60))+"m");
            // Adjust the arrival date based on the difference in departure dates
            ((route) input.routes.get(routesToUpdate[x])).arrivalDate
                    .setTime(((route) input.routes.get(routesToUpdate[x])).arrivalDate.getTime() + timeCorrection);
            // Adjust departure date similarly
            ((route) input.routes.get(routesToUpdate[x])).departureDate.setTime(
                    ((route) input.routes.get(routesToUpdate[x])).departureDate.getTime() + timeCorrection);
            //((route)input.routes.get(routesToUpdate[x])).departureDate = new Date(now + ((etd)currentEtdResponse.etds.get(routeToEtd.get(routesToUpdate[x]))).minutesToArrival*60*1000);

            // Update all leg times
            for (int y = 0; y < input.routes.get(routesToUpdate[x]).legs.size(); y++) {
                // Adjust leg's board time
                ((leg) ((route) input.routes.get(routesToUpdate[x])).legs.get(y)).boardTime.setTime(
                        ((leg) ((route) input.routes.get(routesToUpdate[x])).legs.get(y)).boardTime.getTime()
                                + timeCorrection);
                // Adjust leg's disembark time
                ((leg) ((route) input.routes.get(routesToUpdate[x])).legs.get(y)).disembarkTime.setTime(
                        ((leg) ((route) input.routes.get(routesToUpdate[x])).legs.get(y)).disembarkTime.getTime()
                                + timeCorrection);
            }
        }
        input.sortRoutes();
        return input;

        // OLD method of updating, for humor

        // for every first leg train of each route
        //ArrayList routesToUpdate = new ArrayList();
        /*
        for(int y=0;y<numRoutes;y++){
         // if the etd train matches the first leg of this route, update it's departureTime with etd value
         // OR if the etd train matches the last leg of this route, update with first leg
         lastLeg = ((route)input.routes.get(y)).legs.size()-1;
         if (STATION_MAP.get(((etd)currentEtdResponse.etds.get(x)).destination).compareTo(((leg)((route)input.routes.get(y)).legs.get(0)).trainHeadStation) == 0 ){
            routesToUpdate.add(y);
            if (!etdsToUpdateWith.contains(x))
               etdsToUpdateWith.add(x);
         }
         else if (STATION_MAP.get(((etd)currentEtdResponse.etds.get(x)).destination).compareTo(((leg)((route)input.routes.get(y)).legs.get(lastLeg)).trainHeadStation) == 0 ){
            routesToUpdate.add(y);
            if (!etdsToUpdateWith.contains(x))
               etdsToUpdateWith.add(x);
         }
        }
        for(int y=0;y<routesToUpdate.size();y++){
         if(y==etdsToUpdateWith.size())
            break;
         //TODO: verify boardTime is what routeResponse timer views are set by
         ((route)input.routes.get((Integer) routesToUpdate.get(y))).departureDate = new Date(now + ((etd)currentEtdResponse.etds.get((Integer) etdsToUpdateWith.get(y))).minutesToArrival*60*1000);
         //TODO: evaluate whether the first leg boardTime also needs to be updated. I think it does for UsherService
         ((leg)((route)input.routes.get((Integer) routesToUpdate.get(y))).legs.get(0)).boardTime = new Date(now + ((etd)currentEtdResponse.etds.get((Integer) etdsToUpdateWith.get(y))).minutesToArrival*60*1000);
        }
        }*/

    }

    //CALLED-BY: handleResponse() if updateUIOnResponse is true
    //Updates the UI with data from a etdResponse
    public void displayEtdResponse(etdResponse etdResponse) {
        if (timer != null)
            timer.cancel(); // cancel previous timer
        long now = new Date().getTime();
        timerViews = new ArrayList(); // release old ETA text views
        maxTimer = 0; // reset maxTimer
        fareTv.setText("");
        fareTv.setVisibility(View.GONE);
        tableLayout.removeAllViews();
        String lastDestination = "";

        // Display the alert ImageView and create a click listener to display alert html
        if (etdResponse.message != null) {

            // If the response message matches the response for a closed station, 
            // Display "Closed for tonight" and time of next train, if available.
            if (etdResponse.message.contains("No data matched your criteria.")) {
                String message = "This station is closed for tonight";
                TextView specialScheduleTextView = (TextView) View.inflate(c, R.layout.tabletext, null);
                specialScheduleTextView.setPadding(0, 0, 0, 0);
                if (etdResponse.etds != null && etdResponse.etds.size() > 0) {
                    Date nextTrain = new Date(etdResponse.date.getTime()
                            + ((etd) etdResponse.etds.get(0)).minutesToArrival * 60 * 1000);
                    SimpleDateFormat sdf = new SimpleDateFormat("KK:MM a");
                    message += ". Next train at " + sdf.format(nextTrain);
                }
                specialScheduleTextView.setText(message);
                tableLayout.addView(specialScheduleTextView);
            } else {
                // Create an imageview that spawns an alertDialog with BART message
                ImageView specialScheduleImageView = (ImageView) View.inflate(c, R.layout.specialschedulelayout,
                        null);
                // Tag the specialScheduleImageView with the message html
                specialScheduleImageView.setTag(Html.fromHtml(etdResponse.message));

                // Set the OnClickListener for the specialScheduleImageView to display the tagged message html
                specialScheduleImageView.setOnClickListener(new OnClickListener() {

                    @Override
                    public void onClick(View arg0) {
                        TextView specialScheduleTv = (TextView) View.inflate(c, R.layout.tabletext, null);
                        specialScheduleTv.setPadding(0, 0, 0, 0);
                        specialScheduleTv.setText(Html.fromHtml(arg0.getTag().toString()));
                        specialScheduleTv.setTextSize(16);
                        specialScheduleTv.setMovementMethod(LinkMovementMethod.getInstance());
                        new AlertDialog.Builder(c).setTitle("Station Alerts").setIcon(R.drawable.warning)
                                .setView(specialScheduleTv).setPositiveButton("Bummer", null).show();

                    }

                });
                tableLayout.addView(specialScheduleImageView);
            }

        }

        TableRow tr = (TableRow) View.inflate(c, R.layout.tablerow_right, null);
        LinearLayout destinationRow = (LinearLayout) View.inflate(c, R.layout.destination_row, null);
        //TextView timeTv =(TextView) View.inflate(c, R.layout.tabletext, null);
        int numAlt = 0;
        for (int x = 0; x < etdResponse.etds.size(); x++) {
            if (etdResponse.etds.get(x) == null)
                break;
            etd thisEtd = (etd) etdResponse.etds.get(x);
            if (thisEtd.destination != lastDestination) { // new train destination
                numAlt = 0;
                tr = (TableRow) View.inflate(c, R.layout.tablerow_right, null);
                tr.setPadding(0, 0, 10, 0);
                destinationRow = (LinearLayout) View.inflate(c, R.layout.destination_row, null);
                TextView destinationTv = (TextView) View.inflate(c, R.layout.destinationlayout, null);
                if (x == 0)
                    destinationTv.setPadding(0, 0, 0, 0);
                //bullet.setWidth(200);
                //destinationTv.setPadding(0, 0, 0, 0);
                destinationTv.setTextSize(28);
                destinationTv.setText(thisEtd.destination);
                TextView timeTv = (TextView) View.inflate(c, R.layout.tabletext, null);
                // Display eta less than 1m as "<1"
                if (thisEtd.minutesToArrival == 0)
                    timeTv.setText("<1");
                else
                    timeTv.setText(String.valueOf(thisEtd.minutesToArrival));
                timeTv.setSingleLine(false);
                timeTv.setTextSize(36);
                //timeTv.setPadding(30, 0, 0, 0);
                long counterTime = thisEtd.minutesToArrival * 60 * 1000;
                if (counterTime > maxTimer) {
                    maxTimer = counterTime;
                }
                timeTv.setTag(counterTime + now);
                timerViews.add(timeTv);
                //new ViewCountDownTimer(timeTv, counterTime, 60*1000).start();
                //text.setWidth(120);
                destinationRow.addView(destinationTv);
                //tr.addView(destinationTv);
                tr.addView(timeTv);
                tr.setTag(thisEtd);
                tableLayout.addView(destinationRow);
                tableLayout.addView(tr);
                tr.setOnClickListener(new OnClickListener() {

                    @Override
                    public void onClick(View arg0) {
                        int index = tableLayout.indexOfChild(arg0); // index of clicked view. Expanded view will always be +1
                        etd thisEtd = (etd) arg0.getTag();
                        if (!thisEtd.isExpanded) { // if route not expanded
                            thisEtd.isExpanded = true;
                            LinearLayout routeDetail = (LinearLayout) View.inflate(c, R.layout.routedetail, null);
                            TextView platformTv = (TextView) View.inflate(c, R.layout.tabletext, null);
                            platformTv.setPadding(0, 0, 0, 0);
                            platformTv.setText("platform " + thisEtd.platform);
                            platformTv.setTextSize(20);
                            routeDetail.addView(platformTv);
                            ImageView bikeIv = (ImageView) View.inflate(c, R.layout.bikeimage, null);
                            if (!thisEtd.bikes)
                                bikeIv.setImageResource(R.drawable.no_bicycle);

                            routeDetail.addView(bikeIv);
                            tableLayout.addView(routeDetail, index + 1);
                        } else {
                            thisEtd.isExpanded = false;
                            tableLayout.removeViewAt(index + 1);
                        }

                    }
                });
            } else { // append next trains arrival time to existing destination display
                     //timeTv.append(String.valueOf(", "+thisEtd.minutesToArrival));
                numAlt++;
                TextView nextTimeTv = (TextView) View.inflate(c, R.layout.tabletext, null);
                //nextTimeTv.setTextSize(36-(5*numAlt));
                nextTimeTv.setTextSize(36);
                nextTimeTv.setText(String.valueOf(thisEtd.minutesToArrival));
                //nextTimeTv.setPadding(30, 0, 0, 0);
                if (numAlt == 1) //0xFFF06D2F  C9C7C8
                    nextTimeTv.setTextColor(0xFFC9C7C8);
                else if (numAlt == 2)
                    nextTimeTv.setTextColor(0xFFA8A7A7);
                long counterTime = thisEtd.minutesToArrival * 60 * 1000;
                nextTimeTv.setTag(counterTime + now);
                if (counterTime > maxTimer) {
                    maxTimer = counterTime;
                }
                timerViews.add(nextTimeTv);

                //new ViewCountDownTimer(nextTimeTv, counterTime, 60*1000).start();
                tr.addView(nextTimeTv);
            }
            lastDestination = thisEtd.destination;
        } // end for
          //scrolly.scrollTo(0, 0);
          // Avoid spamming bart.gov. Only re-ping if etd response is valid for at least 3m
        if (maxTimer > 1000 * 60 * 3) {
            timer = new ViewCountDownTimer(timerViews, "etd", maxTimer, 30 * 1000);
            timer.start();
        }
    }

    // Validates text input values (originTextView, destinationTextView) are valid stations
    // And performs requests as needed. Handles caching of etdResponse for merge into routeResponse
    private void validateInputAndDoRequest() {
        long now = new Date().getTime();
        if (BART.STATION_MAP.get(originTextView.getText().toString()) != null) {
            if (BART.STATION_MAP.get(destinationTextView.getText().toString()) != null) {
                // If origin and destination stations are equal, cancel
                if (destinationTextView.getText().toString().compareTo(originTextView.getText().toString()) == 0)
                    return;
                //if an etd response is cached, is fresh, and is for the route departure station:
                //temp testing
                if (currentEtdResponse != null) {
                    long timeCheck = (now - currentEtdResponse.date.getTime());
                    boolean stationCheck = (currentEtdResponse.station
                            .compareTo(originTextView.getText().toString()) == 0);

                    //Log.v("CACHE_CHECK",String.valueOf(timeCheck) + " " + String.valueOf(stationCheck)+ " " + currentEtdResponse.date.toString());
                }
                if (currentEtdResponse != null
                        && (now - currentEtdResponse.date.getTime() < CURRENT_ETD_RESPONSE_FRESH_MS)
                        && (currentEtdResponse.station.compareTo(originTextView.getText().toString()) == 0)) {

                    //Log.v("ETD_CACHE","Cache found");
                    bartApiRequest("route", true);
                }
                // if an appropriate etd cache is not available, fetch it now
                else {
                    //("ETD_CACHE","Cache ETD and display ROUTE");
                    bartApiRequest("etd", false);
                    bartApiRequest("route", true);
                }
            } else {
                bartApiRequest("etd", true);
            }
        }
    }

    @Override
    public void onPause() {
        //Log.v("onPause","pausin for a cause");
        super.onPause();

        //Save station suggestions
        LocalPersistence.writeObjectToFile(c, stationSuggestions,
                res.getResourceEntryName(R.string.StationSuggestionFileName));
        // Save text input state
        editor.putString("origin", originTextView.getText().toString());
        editor.putString("destination", destinationTextView.getText().toString());
        editor.commit();
    }

    // Called when message received
    private BroadcastReceiver serviceStateMessageReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            // Get extra data included in the Intent
            int status = intent.getIntExtra("status", -1);
            if (status == 0) { // service stopped
                Log.d("TheActivity-BroadcastReceived", "service stopped");
                stopServiceTv.setVisibility(View.GONE);
            } else if (status == 1) { // service started
                Log.d("TheActivity-BroadcastReceived", "service started");
                stopServiceTv.setVisibility(0);
                stopServiceTv.setOnClickListener(new OnClickListener() {

                    @Override
                    public void onClick(View v) {
                        Intent i = new Intent(c, UsherService.class);
                        //i.putExtra("departure", ((leg)usherRoute.legs.get(0)).boardStation);
                        //Log.v("SERVICE","Stopping");
                        stopService(i);
                        v.setVisibility(View.GONE);
                    }
                });
            } else if (status == 2) {//temporarily test this as avenue for countdowntimer to signal views need refreshing
                Log.d("TheActivity-BroadcastReceived", "countdown timer expired");
                // Change this to validateInputAndDoRequest
                validateInputAndDoRequest();
                //bartApiRequest(intent.getStringExtra("request"), true);
            } else if (status == 3) {// Sent by RequestTask upon completion
                Log.d("TheActivity-BroadcastReceived", "requestTask complete");
                parseBart(intent.getStringExtra("result"), intent.getStringExtra("request"),
                        intent.getBooleanExtra("updateUI", true));
            } else if (status == 4) { // Sent by BartRouteParser / BartStationEtdParser upon completion
                Log.d("TheActivity-BroadcastReceived", "Bart parser complete");
                // I'm amazed that the result's Class (etdResponse, routeResponse) can be introspected from the Serializable!
                // Watch how handleResponse operates as intended!

                // TODO: Address infinite looping here when response result returns all 0m trains
                // i.e: after BART service has ended for a station
                handleResponse(intent.getSerializableExtra("result"), intent.getBooleanExtra("updateUI", true));
            } else if (status == 13) { // Error from BartStationParser
                showErrorDialog(intent.getStringExtra("message"));
            }

        }
    };

    @SuppressLint("NewApi")
    @Override
    protected void onResume() {

        // If a timer is active, force it to refresh all on-screen estimates
        if (timer != null) {
            long msUntilTimerExpiry = timer.expiryTime - new Date().getTime();
            if (msUntilTimerExpiry > 0) {
                timer.onTick(msUntilTimerExpiry);
            }
        }
        // Else if a timer is not active, check if a request can be made
        // on the current input
        else {
            validateInputAndDoRequest();
        }
        if (usherServiceIsRunning()) {
            stopServiceTv.setVisibility(0);
            stopServiceTv.setOnClickListener(new OnClickListener() {

                @Override
                public void onClick(View v) {
                    Intent i = new Intent(c, UsherService.class);
                    //i.putExtra("departure", ((leg)usherRoute.legs.get(0)).boardStation);
                    //Log.v("SERVICE","Stopping");
                    stopService(i);
                    v.setVisibility(View.GONE);
                }
            });
        }

        // Update user location, if none exists OR enough time has elapsed since last update
        if (currentLocation == null
                || (currentLocation.getTime() + DeviceLocation.LOCATION_FRESH_MS < new Date().getTime())) {
            Log.d("RefreshLocation", "Bagooosh!");
            getDeviceLocation();
        }

        super.onResume();
    }

    @Override
    protected void onDestroy() {
        // Unregister since the activity is about to be closed.
        LocalBroadcastManager.getInstance(this).unregisterReceiver(serviceStateMessageReceiver);
        super.onDestroy();
    }

    // Called in onResume() to ensure stop service button available as necessary
    private boolean usherServiceIsRunning() {
        ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
        for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
            if ("pro.dbro.bart.UsherService".equals(service.service.getClassName())) {
                return true;
            }
        }
        return false;
    }

    //Sends message to service with etd data
    private void sendEtdResponseToService() { // 0 = service stopped , 1 = service started, 2 = refresh view with call to bartApiRequest(), 3 = 
        int status = 5; // hardcode status for calling UsherService with new etdResponse
        //Log.d("sender", "Sending AsyncTask message");
        Intent intent = new Intent("service_status_change");
        // You can also include some extra data.
        intent.putExtra("status", status);
        intent.putExtra("etdResponse", (Serializable) currentEtdResponse);
        LocalBroadcastManager.getInstance(TheActivity.c).sendBroadcast(intent);
    }

    // Registers with LocationService to update appropriate class variables
    // with LocationResult when it's available
    private void getDeviceLocation() {
        DeviceLocation deviceLocation = new DeviceLocation();
        LocationResult locationResult = new LocationResult() {
            @Override
            public void gotLocation(final Location location) {
                //Got the location!

                currentLocation = location;
                if (location != null) {
                    currentLat = location.getLatitude();
                    currentLon = location.getLongitude();
                    localStation = BART.findNearestStation(currentLat, currentLon);
                    Log.d("RefreshLocation", "station: " + localStation + " accuracy: "
                            + String.valueOf(location.getAccuracy()) + " meters");
                }
                hasLocation = true;
            };
        };
        deviceLocation.getLocation(this, locationResult);
    }

    // Remove all routes returned in a RouteResponse that occur before now
    // and all routes that occur more than BART.ETA_DISPLAY_THRESHOLD_MS out
    // the latter rule accounts for a bug in BART's feed occurring after business hours
    private routeResponse removeExpiredRoutes(routeResponse response) {
        long MINIMUM_TIME_MS = 1000 * 60;
        Log.d("preRemoveExpiredRoutes", response.toString());
        Date now = new Date();
        ArrayList indexesToRemove = new ArrayList(response.routes.size());
        // Fun Fact: Hand-written iteration of ArrayList is 3x faster than the Java enhanced for-loop syntax
        // See http://developer.android.com/guide/practices/design/performance.html#foreach
        for (int x = 0; x < response.routes.size(); x++) {
            // If a returned route departs before the current time, remove it
            if (((route) response.routes.get(x)).departureDate.getTime() - now.getTime() < MINIMUM_TIME_MS) {
                indexesToRemove.add(x);
            }
            // If a returned route occurs more than BART.ETA_DISPLAY_THRESHOLD_MS out, remove it
            else if (((route) response.routes.get(x)).departureDate.getTime()
                    - now.getTime() > BART.ETA_DISPLAY_THRESHOLD_MS) {
                indexesToRemove.add(x);
            }
        }
        // Remove indexesToRemove from response.routes by descending index
        for (int x = indexesToRemove.size() - 1; x >= 0; x--) {
            response.routes.remove(Integer.parseInt(indexesToRemove.get(x).toString()));
        }
        Log.d("postRemoveExpiredRoutes", response.toString());
        return response;
    }

    // Displays an error dialog with a generic error if message is an empty string
    private void showErrorDialog(String message) {
        TextView crashTv = (TextView) View.inflate(c, R.layout.tabletext, null);
        if (message.compareTo("") == 0)
            crashTv.setText(Html.fromHtml(res.getStringArray(R.array.crashCatchDialog)[1]));
        else
            crashTv.setText(message);
        crashTv.setTextSize(18);
        crashTv.setPadding(0, 0, 0, 0);
        crashTv.setMovementMethod(LinkMovementMethod.getInstance());
        new AlertDialog.Builder(c).setTitle(res.getStringArray(R.array.crashCatchDialog)[0]).setView(crashTv)
                .setIcon(R.drawable.sad_mac).setPositiveButton("Bummer", null).show();
    }

}