pro.dbro.bart.UsherService.java Source code

Java tutorial

Introduction

Here is the source code for pro.dbro.bart.UsherService.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 java.util.Date;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Binder;
import android.os.CountDownTimer;
import android.os.IBinder;
import android.os.Vibrator;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

// TODO: Look at Intent to send on notification click to ENSURE stop service label is created in TheActivity

public class UsherService extends Service {
    private NotificationManager mNM;

    private PendingIntent contentIntent;

    private Context c;

    private Notification notification; // keep an instance of the notification to update time text

    private int currentLeg; // keep track of which leg of the route we're currently on
    private boolean didBoard; // keep track of whether we've boarded the current leg, or are waiting for it to arrive
    private CountDownTimer timer; // keep track of current countdown for cancelling if new request comes
    // else we can get errors related to a timer expecting previous route
    private CountDownTimer reminderTimer; // timer separated from actual timer by REMINDER_PADDING_MS
    private route usherRoute; // the route to guide along. Updated with etd data

    private final long EVENT_PADDING_MS = 60 * 1000; // ms before an event (board, disembark) the usher should notify and issue next direction

    private final long REMINDER_PADDING_MS = 180 * 1000; // ms before an event (board, disembark) the usher should issue a reminder

    private final long UPDATE_INTERVAL_MS = 20 * 1000; // ms interval for updating notification

    // Unique Identification Number for the Notification.
    // We use it on Notification start, and to cancel it.
    private int NOTIFICATION = R.string.local_service_started;

    /**
     * Class for clients to access.  Because we know this service always
     * runs in the same process as its clients, we don't need to deal with
     * IPC.
     */
    public class LocalBinder extends Binder {
        UsherService getService() {
            return UsherService.this;
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        c = this;
        mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        //Log.v("Usher","OnCreate");
        // LocalBroadCast Stuff
        LocalBroadcastManager.getInstance(this).registerReceiver(serviceDataMessageReceiver,
                new IntentFilter("service_status_change"));
        // Display a notification about us starting.  We put an icon in the status bar.
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //TODO: Check if service-start should be sent OnCreate
        sendMessage(1); // send service-start message
        usherRoute = TheActivity.usherRoute;
        Log.i("UsherService", "Received start id " + startId + ": " + intent + " route: " + usherRoute.toString());
        if (timer != null) {
            timer.cancel();
        }
        if (reminderTimer != null) {
            reminderTimer.cancel();
        }

        // We want this service to continue running until it is explicitly
        // stopped, so return sticky.
        showNotification();
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        sendMessage(0); // send service-stopped message
        Log.d("onDestroy", "called");

        if (timer != null)
            timer.cancel();
        if (reminderTimer != null)
            reminderTimer.cancel();
        // Cancel the persistent notification.
        mNM.cancel(NOTIFICATION);

        // Tell the user we stopped.
        //Toast.makeText(this, R.string.local_service_stopped, Toast.LENGTH_SHORT).show();

    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    // This is the object that receives interactions from clients.  See
    // RemoteService for a more complete example.
    private final IBinder mBinder = new LocalBinder();

    /**
     * Initialize first guidance timer and send initial notification
     */
    private void showNotification() {
        // In this sample, we'll use the same text for the ticker and the expanded notification
        //CharSequence text = getText(R.string.local_service_started);
        String destinationStation = "";
        try {
            destinationStation = ((leg) usherRoute.legs.get(usherRoute.legs.size() - 1)).disembarkStation;
        } catch (Throwable T) {
            Log.d("usherRoute null err", "catch it, bro");
        }
        currentLeg = 0;
        didBoard = false;
        CharSequence tickerText = "Guiding to " + BART.REVERSE_STATION_MAP.get(destinationStation.toLowerCase());

        // Set the info for the views that show in the notification panel.
        // BUGFIX: new Date() is not guaranteed to return in BART's locale
        Date now = new Date();
        Log.d("UsherRouteEta", ((leg) usherRoute.legs.get(0)).boardTime.toString());
        long minutesUntilNext = ((((leg) usherRoute.legs.get(0)).boardTime.getTime() - now.getTime()));
        //minutesUntilNext is, for this brief moment, actually milliseconds. 
        makeLegCountdownTimer(minutesUntilNext);
        // back to minutes
        minutesUntilNext = (minutesUntilNext) / (1000 * 60);
        //catch negative time state
        if (minutesUntilNext <= 0) {
            //Log.v("Negative ETA", "Catch me");
        }

        CharSequence currentStepText = "At "
                + BART.REVERSE_STATION_MAP.get(((leg) usherRoute.legs.get(0)).boardStation.toLowerCase());
        // display 0m as "<1m"
        CharSequence nextStepText = "";
        if (minutesUntilNext == 0) {
            nextStepText = "Board "
                    + BART.REVERSE_STATION_MAP.get(((leg) usherRoute.legs.get(0)).trainHeadStation.toLowerCase())
                    + " train in <1m";
        } else {
            nextStepText = "Board "
                    + BART.REVERSE_STATION_MAP.get(((leg) usherRoute.legs.get(0)).trainHeadStation.toLowerCase())
                    + " train in " + String.valueOf(minutesUntilNext) + "m";
        }
        // The PendingIntent to launch our activity if the user selects this notification
        setContentIntent();

        // Create notification
        NotificationCompat.Builder builder = new NotificationCompat.Builder(c);

        builder.setContentIntent(contentIntent).setSmallIcon(R.drawable.ic_launcher_notification)
                //.setLargeIcon(BitmapFactory.decodeResource(res, R.drawable.some_big_img))
                .setTicker(tickerText).setWhen(0)
                //.setAutoCancel(true)
                .setContentTitle(currentStepText).setContentText(nextStepText).setOngoing(true);

        notification = builder.getNotification();

        mNM.notify(NOTIFICATION, notification);
    }

    //if newNotification is true, generate new notification with scroller text
    //else, simply update menu item text
    private void updateNotification(boolean newNotification) {
        Date now = new Date();
        CharSequence nextStepText = "";
        CharSequence currentStepText = "";
        if (didBoard) {
            // Notification text set for next disembark (Transfer or final destination)
            currentStepText = "On " + BART.REVERSE_STATION_MAP
                    .get(((leg) usherRoute.legs.get(currentLeg)).trainHeadStation.toLowerCase()) + " train";
            long minutesUntilNext = ((((leg) usherRoute.legs.get(currentLeg)).disembarkTime.getTime()
                    - now.getTime()) / (1000 * 60));
            if (minutesUntilNext < 0) {
                //Log.v("Negative ETA", "Catch me");
            }
            CharSequence actionText = "";
            // If last leg, begin message with "Get off at", else "Transfer at"
            if (currentLeg + 1 == usherRoute.legs.size()) {
                actionText = "Get off at ";
            } else {
                actionText = "Transfer at ";
            }
            CharSequence timeText = "";
            // If minutesUntilNext == 0, display as "<1m"
            if (minutesUntilNext == 0) {
                timeText = "<1m";
            } else {
                timeText = String.valueOf(minutesUntilNext) + "m";
            }
            // Construct notification text (nextStepText) from actionText, usherRoute next station, and timeText
            nextStepText = actionText
                    + BART.REVERSE_STATION_MAP
                            .get(((leg) usherRoute.legs.get(currentLeg)).disembarkStation.toLowerCase())
                    + " in " + timeText;

        } else {
            // Notification text set for next boarding
            long minutesUntilNext = ((((leg) usherRoute.legs.get(currentLeg)).boardTime.getTime() - now.getTime())
                    / (1000 * 60));
            if (minutesUntilNext < 0) {
                //Log.v("Negative ETA", "Catch me");
            }
            nextStepText = "Board " + BART.REVERSE_STATION_MAP
                    .get(((leg) usherRoute.legs.get(currentLeg)).trainHeadStation.toLowerCase()) + " train in ";
            // Display 0m eta as "<1m"
            if (minutesUntilNext == 0) {
                nextStepText = nextStepText + "<1m";
            } else {
                nextStepText = nextStepText + String.valueOf(minutesUntilNext) + "m";
            }
            currentStepText = "At " + BART.REVERSE_STATION_MAP
                    .get(((leg) usherRoute.legs.get(currentLeg)).boardStation.toLowerCase());
        }

        NotificationCompat.Builder builder = new NotificationCompat.Builder(c);
        if (contentIntent == null) {
            Log.d("ContentIntent", "was null");
            setContentIntent();
        }
        builder.setContentIntent(contentIntent).setSmallIcon(R.drawable.ic_launcher_notification)
                //.setLargeIcon(BitmapFactory.decodeResource(res, R.drawable.some_big_img))
                .setWhen(0).setContentTitle(currentStepText).setContentText(nextStepText).setOngoing(true);

        if (newNotification) {
            builder.setTicker(nextStepText);
        }

        notification = builder.getNotification();
        mNM.notify(NOTIFICATION, notification);

    }

    private void makeLegCountdownTimer(long msUntilNext) {
        //make sure we don't leak any timers
        if (timer != null)
            timer.cancel();

        // Make a new timer to expire EVENT_PADDING_MS before event
        timer = new CountDownTimer(msUntilNext - EVENT_PADDING_MS, UPDATE_INTERVAL_MS) {
            //new CountDownTimer(5000, 1000){

            @Override
            public void onFinish() {
                // TODO Auto-generated method stub
                Vibrator v = (Vibrator) getSystemService(c.VIBRATOR_SERVICE);
                long[] vPattern = { 0, 400, 300, 400, 300, 400, 300, 400 };
                v.vibrate(vPattern, -1);
                //if(didBoard) // if we've boarded, we're handling the last leg
                //   currentLeg ++;
                didBoard = !didBoard;

                // Our next move is departing the final leg train
                if ((usherRoute.legs.size() == currentLeg + 1) && !didBoard) {
                    NotificationCompat.Builder builder = new NotificationCompat.Builder(c);
                    if (contentIntent == null) {
                        Log.d("ContentIntent", "was null");
                        setContentIntent();
                    }
                    builder.setContentIntent(contentIntent).setSmallIcon(R.drawable.ic_launcher_notification)
                            //.setLargeIcon(BitmapFactory.decodeResource(res, R.drawable.some_big_img))
                            .setWhen(0).setTicker("This is your stop! Take care!");

                    notification = builder.getNotification();
                    mNM.notify(NOTIFICATION, notification);
                    //TheActivity.removeStopServiceText();
                    stopSelf();
                }
                // Our next move is departing the current leg's train
                else if (didBoard) { //Set timer for this leg's disembark time
                    Date now = new Date();
                    long msUntilNext = ((((leg) usherRoute.legs.get(currentLeg)).disembarkTime.getTime()
                            - now.getTime()));
                    //Log.v("UsherState","leg: "+ String.valueOf(currentLeg)+ " / "+String.valueOf(usherRoute.legs.size())+" Boarded. Next: "+String.valueOf(msUntilNext)+"ms");
                    updateNotification(true);
                    makeLegCountdownTimer(msUntilNext); // this cancels current timer
                }
                // Our next move is boarding the next leg's train
                else {
                    currentLeg++;
                    Date now = new Date();
                    long msUntilNext = ((((leg) usherRoute.legs.get(currentLeg)).boardTime.getTime()
                            - now.getTime()));
                    //Log.v("UsherState","leg: "+ String.valueOf(currentLeg)+ " / "+String.valueOf(usherRoute.legs.size())+" Waiting. Next: "+String.valueOf(msUntilNext)+"ms");
                    updateNotification(true);
                    makeLegCountdownTimer(msUntilNext);
                }
            }

            @Override
            public void onTick(long arg0) {
                updateNotification(false);
            }

        }.start();
        //timer.start();
        // Set Reminder timer REMINDER_PADDING_MS ms before event
        if (msUntilNext > (REMINDER_PADDING_MS + 30 * 1000)) { // if next event is more than 30 seconds + REMINDER_PADDING_MS out, set reminder
            //avoid leaking timer
            if (reminderTimer != null)
                reminderTimer.cancel();
            reminderTimer = new CountDownTimer(msUntilNext - REMINDER_PADDING_MS,
                    msUntilNext - REMINDER_PADDING_MS) {

                @Override
                public void onFinish() {
                    // TODO: Is this a good time for updating?
                    //requestDataUpdate();
                    updateNotification(true);
                    Vibrator v = (Vibrator) getSystemService(c.VIBRATOR_SERVICE);
                    long[] vPattern = { 0, 400, 300, 400 };
                    v.vibrate(vPattern, -1);

                }

                @Override
                public void onTick(long millisUntilFinished) {
                    // TODO Auto-generated method stub

                }
            }.start();
        }
    }

    private void sendMessage(int status) { // 0 = service stopped , 1 = service started
        Log.d("sender", "Broadcasting message");
        Intent intent = new Intent("service_status_change");
        // You can also include some extra data.
        intent.putExtra("status", status);
        LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
    }

    private BroadcastReceiver serviceDataMessageReceiver = 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 == 5) { // service stopped
                //update timers with fresh data
                updateTimersWithEtdResponse((etdResponse) intent.getSerializableExtra("etdResponse"));
            }
        }
    };

    private void requestDataUpdate() {
        String curStation;
        if (didBoard) {
            curStation = ((leg) usherRoute.legs.get(currentLeg)).disembarkStation.toLowerCase();
        } else {
            curStation = ((leg) usherRoute.legs.get(currentLeg)).boardStation.toLowerCase();
        }

        new RequestTask("etd", false)
                .execute(BART.API_ROOT + "etd.aspx?cmd=etd&orig=" + curStation + "&key=" + BART.API_KEY);
    }
    // TODO: I've gotten NullPointerException pointed here ?
    //04-11 18:24:42.420: E/AndroidRuntime(6698): java.lang.NullPointerException
    //04-11 18:24:42.420: E/AndroidRuntime(6698):    at pro.dbro.bart.UsherService.updateTimersWithEtdResponse(UsherService.java:319)

    private void updateTimersWithEtdResponse(etdResponse response) {
        leg curLeg = (leg) usherRoute.legs.get(currentLeg);
        long curTargetTime; // time until next move according to schedule
        int etd = -1;
        /*
        for(int y=0;y<response.etds.size();y++){
          // DEBUG
          try{
         //Check that destination train is listed in terminal-station format. Ex: "Fremont" CounterEx: 'SFO/Milbrae'
         if (!BART.STATION_MAP.containsKey(((etd)response.etds.get(y)).destination)){
            // If this is not a known silly-named train terminal station
            if (!BART.KNOWN_SILLY_TRAINS.containsKey(((etd)response.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)response.etds.get(y)).destination).indexOf(BART.STATIONS[z]) != -1){
                     // Set the etd destination to the guessed real station name
                     ((etd)response.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)response.etds.get(y)).destination = KNOWN_SILLY_TRAINS.get(((etd)response.etds.get(y)).destination);
               //break;
            }      
         } // end STATION_MAP silly-name train check and replace
             
            // Comparing BART station abbreviations
         if (BART.STATION_MAP.get(((etd)response.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);
            }
            else{
               //if the etd is all ready claimed by a route, go to next etd
               break;
            }
         }
         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);
            }
            else{
               //if the etd is all ready claimed by a route, go to next etd
               break;
            }
         }
             
          }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
        */
        for (int x = 0; x < response.etds.size(); x++) {
            //find the etd of response which matches current train
            //check this logic
            //crash here
            /* old method
            if(curLeg.trainHeadStation.compareTo(BART.STATION_MAP.get(((etd)response.etds.get(x)).destination)) == 0){
               etd = x;
               break;
            }
            */
        }
        //something went wrong
        if (etd == -1)
            return;
        long etdTargetTime = ((etd) response.etds.get(etd)).minutesToArrival * 60 * 1000;

        // BUFIX: new Date() is not guaranteed to return time in BART's locale
        // instead, use date appended to etd response
        //Date now = new Date();
        Date now = response.date;
        if (didBoard) {
            curTargetTime = curLeg.disembarkTime.getTime(); // for debug only
            // update the usherRoute disembarkTime by adding minutesToArrival to new Date()
            curLeg.disembarkTime = new Date(
                    now.getTime() + ((etd) response.etds.get(etd)).minutesToArrival * 60 * 1000);
            makeLegCountdownTimer(curLeg.disembarkTime.getTime());
        } else {
            curTargetTime = curLeg.boardTime.getTime(); // for debug only
            // update the usherRoute boardTime by adding minutesToArrival to new Date()
            curLeg.boardTime = new Date(
                    now.getTime() + ((etd) response.etds.get(etd)).minutesToArrival * 60 * 1000);
            makeLegCountdownTimer(curLeg.boardTime.getTime());
        }

        //Log.v("USHER SYNC",String.valueOf((etdTargetTime-curTargetTime)/1000)); // s diff b/t current and etd

    }

    private void setContentIntent() {
        Intent i = new Intent(this, TheActivity.class);
        i.putExtra("Service", true);
        i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
        contentIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_CANCEL_CURRENT);
    }
}