Android Open Source - BART The Activity






From Project

Back to project page BART.

License

The source code is released under:

GNU General Public License

If you think the Android project BART listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
 *  Copyright (C) 2012  David Brodsky//from w  ww. j a  va  2s .  com
 *  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();
  }
    
}




Java Source Code List

pro.dbro.bart.BART.java
pro.dbro.bart.BartLinearLayout.java
pro.dbro.bart.BartRouteParser.java
pro.dbro.bart.BartStationEtdParser.java
pro.dbro.bart.DeviceLocation.java
pro.dbro.bart.LocalPersistence.java
pro.dbro.bart.MapActivity.java
pro.dbro.bart.RequestTask.java
pro.dbro.bart.StationSuggestion.java
pro.dbro.bart.TextPlusIconArrayAdapter.java
pro.dbro.bart.TheActivity.java
pro.dbro.bart.UsherService.java
pro.dbro.bart.ViewCountDownTimer.java
pro.dbro.bart.etdResponse.java
pro.dbro.bart.etd.java
pro.dbro.bart.leg.java
pro.dbro.bart.routeResponse.java
pro.dbro.bart.route.java