diff --git a/src/com/dougkeen/bart/BartRunnerApplication.java b/src/com/dougkeen/bart/BartRunnerApplication.java index b6591d1..791dfae 100644 --- a/src/com/dougkeen/bart/BartRunnerApplication.java +++ b/src/com/dougkeen/bart/BartRunnerApplication.java @@ -8,7 +8,9 @@ import java.io.InputStream; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.ObjectUtils; +import android.app.AlarmManager; import android.app.Application; +import android.content.Context; import android.media.MediaPlayer; import android.os.Parcel; import android.util.Log; @@ -88,6 +90,13 @@ public class BartRunnerApplication extends Application { public void setBoardedDeparture(Departure boardedDeparture) { if (!ObjectUtils.equals(boardedDeparture, mBoardedDeparture) || ObjectUtils.compare(mBoardedDeparture, boardedDeparture) != 0) { + // Cancel any pending alarms for the current departure + if (this.mBoardedDeparture != null + && this.mBoardedDeparture.isAlarmPending()) { + this.mBoardedDeparture.cancelAlarm(this, + (AlarmManager) getSystemService(Context.ALARM_SERVICE)); + } + this.mBoardedDeparture = boardedDeparture; if (mBoardedDeparture != null) { diff --git a/src/com/dougkeen/bart/activities/TrainAlertDialogFragment.java b/src/com/dougkeen/bart/activities/TrainAlertDialogFragment.java index 428b0b2..4105560 100644 --- a/src/com/dougkeen/bart/activities/TrainAlertDialogFragment.java +++ b/src/com/dougkeen/bart/activities/TrainAlertDialogFragment.java @@ -4,7 +4,6 @@ import net.simonvt.widget.NumberPicker; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; -import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.os.Bundle; @@ -14,7 +13,6 @@ import android.support.v4.app.FragmentActivity; import com.WazaBe.HoloEverywhere.AlertDialog; import com.dougkeen.bart.BartRunnerApplication; import com.dougkeen.bart.R; -import com.dougkeen.bart.services.NotificationService; public class TrainAlertDialogFragment extends DialogFragment { @@ -85,10 +83,10 @@ public class TrainAlertDialogFragment extends DialogFragment { alertLeadTime); editor.commit(); - Intent intent = new Intent(getActivity(), - NotificationService.class); - intent.putExtra("alertLeadTime", alertLeadTime); - getActivity().startService(intent); + ((BartRunnerApplication) getActivity() + .getApplication()) + .getBoardedDeparture().setUpAlarm( + alertLeadTime); } }) .setNegativeButton(R.string.skip_alert, diff --git a/src/com/dougkeen/bart/activities/ViewDeparturesActivity.java b/src/com/dougkeen/bart/activities/ViewDeparturesActivity.java index 0cea643..519ab56 100644 --- a/src/com/dougkeen/bart/activities/ViewDeparturesActivity.java +++ b/src/com/dougkeen/bart/activities/ViewDeparturesActivity.java @@ -399,7 +399,7 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements return true; } else if (itemId == R.id.cancel_alarm_button) { Intent intent = new Intent(this, NotificationService.class); - intent.putExtra("cancelAlarm", true); + intent.putExtra("cancelNotifications", true); startService(intent); return true; } else if (itemId == R.id.view_on_bart_site_button) { @@ -424,14 +424,20 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements private void refreshBoardedDeparture() { final Departure boardedDeparture = ((BartRunnerApplication) getApplication()) .getBoardedDeparture(); - if (boardedDeparture == null + final View yourTrainSection = findViewById(R.id.yourTrainSection); + int currentVisibility = yourTrainSection.getVisibility(); + + final boolean boardedDepartureDoesNotApply = boardedDeparture == null || boardedDeparture.getStationPair() == null - || !boardedDeparture.getStationPair().equals(getStationPair())) { - findViewById(R.id.yourTrainSection).setVisibility(View.GONE); + || !boardedDeparture.getStationPair().equals(getStationPair()); + + if (boardedDepartureDoesNotApply) { + if (currentVisibility != View.GONE) { + yourTrainSection.setVisibility(View.GONE); + } return; } - findViewById(R.id.yourTrainSection).setVisibility(View.VISIBLE); ((TextView) findViewById(R.id.yourTrainDestinationText)) .setText(boardedDeparture.getTrainDestination().toString()); @@ -482,6 +488,10 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements .getEstimatedArrivalMinutesLeftText(ViewDeparturesActivity.this); } }); + + if (currentVisibility != View.VISIBLE) { + yourTrainSection.setVisibility(View.VISIBLE); + } } private void startDepartureActionMode() { @@ -512,8 +522,8 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements application.setBoardedDeparture(mSelectedDeparture); refreshBoardedDeparture(); - // Stop the notification service - stopService(new Intent(ViewDeparturesActivity.this, + // Start the notification service + startService(new Intent(ViewDeparturesActivity.this, NotificationService.class)); // Don't prompt for alert if train is about to leave diff --git a/src/com/dougkeen/bart/model/Departure.java b/src/com/dougkeen/bart/model/Departure.java index 4c84ae0..4d1ebf5 100644 --- a/src/com/dougkeen/bart/model/Departure.java +++ b/src/com/dougkeen/bart/model/Departure.java @@ -3,12 +3,23 @@ package com.dougkeen.bart.model; import java.text.SimpleDateFormat; import java.util.Date; +import org.apache.commons.lang3.time.DateFormatUtils; + +import android.app.AlarmManager; +import android.app.Notification; +import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; import android.os.Parcel; import android.os.Parcelable; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationCompat.Builder; import android.text.format.DateFormat; import android.util.Log; +import com.dougkeen.bart.R; +import com.dougkeen.bart.services.NotificationService; + public class Departure implements Parcelable, Comparable { private static final int MINIMUM_MERGE_OVERLAP_MILLIS = 10000; @@ -56,6 +67,9 @@ public class Departure implements Parcelable, Comparable { private long arrivalTimeOverride; + private int alarmLeadTimeMinutes; + private boolean alarmPending; + public Station getOrigin() { return origin; } @@ -273,14 +287,14 @@ public class Departure implements Parcelable, Comparable { if (minutesLeft < 0) { return "Arrived at destination"; } else if (minutesLeft == 0) { - return "Arrives around " + getEstimatedArrivalTimeText(context) + return "Arrives ~" + getEstimatedArrivalTimeText(context) + " (<1 min)"; } else if (minutesLeft == 1) { - return "Arrives around " + getEstimatedArrivalTimeText(context) + return "Arrives ~" + getEstimatedArrivalTimeText(context) + " (1 min)"; } else { - return "Arrives around " + getEstimatedArrivalTimeText(context) - + " (" + minutesLeft + " mins)"; + return "Arrives ~" + getEstimatedArrivalTimeText(context) + " (" + + minutesLeft + " mins)"; } } @@ -460,6 +474,112 @@ public class Departure implements Parcelable, Comparable { } } + public int getAlarmLeadTimeMinutes() { + return alarmLeadTimeMinutes; + } + + public boolean isAlarmPending() { + return alarmPending; + } + + private PendingIntent getAlarmIntent(Context context) { + return PendingIntent.getBroadcast(context, 0, new Intent( + Constants.ACTION_ALARM, getStationPair().getUri()), + PendingIntent.FLAG_UPDATE_CURRENT); + } + + private long getAlarmClockTime() { + return getMeanEstimate() - alarmLeadTimeMinutes * 60 * 1000; + } + + public int getSecondsUntilAlarm() { + return getMeanSecondsLeft() - getAlarmLeadTimeMinutes() * 60; + } + + public void setUpAlarm(int leadTimeMinutes) { + this.alarmLeadTimeMinutes = leadTimeMinutes; + this.alarmPending = true; + } + + public void updateAlarm(Context context, AlarmManager alarmManager) { + if (alarmManager == null) + return; + + if (isAlarmPending() && getAlarmLeadTimeMinutes() > 0) { + final PendingIntent alarmIntent = getAlarmIntent(context); + alarmManager.cancel(alarmIntent); + + long alertTime = getAlarmClockTime(); + + alarmManager.set(AlarmManager.RTC_WAKEUP, alertTime, alarmIntent); + + if (Log.isLoggable(Constants.TAG, Log.VERBOSE)) + Log.v(Constants.TAG, + "Scheduling alarm for " + + DateFormatUtils.format(alertTime, "h:mm:ss")); + } + } + + public void cancelAlarm(Context context, AlarmManager alarmManager) { + alarmManager.cancel(getAlarmIntent(context)); + this.alarmPending = false; + } + + private PendingIntent notificationIntent; + + private PendingIntent getNotificationIntent(Context context) { + if (notificationIntent == null) { + Intent targetIntent = new Intent(Intent.ACTION_VIEW, + getStationPair().getUri()); + targetIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + notificationIntent = PendingIntent.getActivity(context, 0, + targetIntent, PendingIntent.FLAG_UPDATE_CURRENT); + } + return notificationIntent; + } + + public Notification createNotification(Context context) { + final int halfMinutes = (getMeanSecondsLeft() + 15) / 30; + float minutes = halfMinutes / 2f; + final String minutesText = (minutes < 1) ? "Less than one minute" + : (String.format("~%.1f minute", minutes) + ((minutes != 1.0) ? "s" + : "")); + + final Intent cancelAlarmIntent = new Intent(context, + NotificationService.class); + cancelAlarmIntent.putExtra("cancelNotifications", true); + Builder notificationBuilder = new NotificationCompat.Builder(context) + .setOngoing(true) + .setSmallIcon(R.drawable.ic_stat_notification) + .setContentTitle( + getOrigin().shortName + " to " + + getPassengerDestination().shortName) + .setContentIntent(getNotificationIntent(context)).setWhen(0); + if (android.os.Build.VERSION.SDK_INT >= 16) { + notificationBuilder.setPriority(NotificationCompat.PRIORITY_HIGH) + .setContentText(minutesText + " until departure"); + if (isAlarmPending()) { + notificationBuilder.addAction( + R.drawable.ic_action_cancel_alarm, + "Cancel alarm", + PendingIntent.getService(context, 0, cancelAlarmIntent, + PendingIntent.FLAG_UPDATE_CURRENT)).setSubText( + "Alert " + getAlarmLeadTimeMinutes() + + " minutes before departure"); + } + } else if (isAlarmPending()) { + notificationBuilder.setContentText(minutesText + + " to departure (alarm at " + getAlarmLeadTimeMinutes() + + " min" + ((getAlarmLeadTimeMinutes() == 1) ? "" : "s") + + ")"); + } else { + notificationBuilder + .setContentText(minutesText + " until departure"); + } + + return notificationBuilder.build(); + } + @Override public String toString() { java.text.DateFormat format = SimpleDateFormat.getTimeInstance(); @@ -533,4 +653,8 @@ public class Departure implements Parcelable, Comparable { return new Departure[size]; } }; + + public void notifyAlarmHasBeenHandled() { + this.alarmPending = false; + } } \ No newline at end of file diff --git a/src/com/dougkeen/bart/receivers/AlarmBroadcastReceiver.java b/src/com/dougkeen/bart/receivers/AlarmBroadcastReceiver.java index ef9b5b1..0ecd8f3 100644 --- a/src/com/dougkeen/bart/receivers/AlarmBroadcastReceiver.java +++ b/src/com/dougkeen/bart/receivers/AlarmBroadcastReceiver.java @@ -1,29 +1,35 @@ package com.dougkeen.bart.receivers; -import com.dougkeen.bart.BartRunnerApplication; -import com.dougkeen.util.WakeLocker; - import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import com.dougkeen.bart.BartRunnerApplication; +import com.dougkeen.bart.model.Departure; +import com.dougkeen.util.WakeLocker; + public class AlarmBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - WakeLocker.acquire(context); - BartRunnerApplication application = (BartRunnerApplication) context .getApplicationContext(); + final Departure boardedDeparture = application.getBoardedDeparture(); + if (boardedDeparture == null) { + // Nothing to notify about + return; + } + + WakeLocker.acquire(context); + application.setPlayAlarmRingtone(true); - Intent targetIntent = new Intent(Intent.ACTION_VIEW, application - .getBoardedDeparture().getStationPair().getUri()); + Intent targetIntent = new Intent(Intent.ACTION_VIEW, boardedDeparture.getStationPair().getUri()); targetIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(targetIntent); - application.setAlarmPending(false); + boardedDeparture.notifyAlarmHasBeenHandled(); } } diff --git a/src/com/dougkeen/bart/services/NotificationService.java b/src/com/dougkeen/bart/services/NotificationService.java index 1b1d5d0..911c806 100644 --- a/src/com/dougkeen/bart/services/NotificationService.java +++ b/src/com/dougkeen/bart/services/NotificationService.java @@ -45,8 +45,6 @@ public class NotificationService extends Service implements EtdServiceListener { private NotificationManager mNotificationManager; private AlarmManager mAlarmManager; private PendingIntent mNotificationIntent; - private PendingIntent mAlarmPendingIntent; - private int mAlertLeadTime; private Handler mHandler; private boolean mHasShutDown = false; @@ -126,17 +124,20 @@ public class NotificationService extends Service implements EtdServiceListener { protected void onHandleIntent(Intent intent) { final Departure boardedDeparture = ((BartRunnerApplication) getApplication()) .getBoardedDeparture(); - if (boardedDeparture == null - || intent.getBooleanExtra("cancelAlarm", false)) { - // Nothing to notify about, or we want to cancel the alarm + if (boardedDeparture == null) { + // Nothing to notify about + return; + } + if (intent.getBooleanExtra("cancelNotifications", false)) { + // We want to cancel the alarm/notification + boardedDeparture + .cancelAlarm(getApplicationContext(), mAlarmManager); shutDown(false); return; } - Bundle bundle = intent.getExtras(); StationPair oldStationPair = mStationPair; mStationPair = boardedDeparture.getStationPair(); - mAlertLeadTime = bundle.getInt("alertLeadTime"); if (mEtdService != null && mStationPair != null && !mStationPair.equals(oldStationPair)) { @@ -155,57 +156,15 @@ public class NotificationService extends Service implements EtdServiceListener { updateNotification(); - setAlarm(); - pollDepartureStatus(); } - private void refreshAlarmPendingIntent() { - final Intent alarmIntent = new Intent(Constants.ACTION_ALARM, - getStationPair().getUri()); - final Departure boardedDeparture = ((BartRunnerApplication) getApplication()) + private void updateAlarm() { + Departure boardedDeparture = ((BartRunnerApplication) getApplication()) .getBoardedDeparture(); - alarmIntent.putExtra("departure", (Parcelable) boardedDeparture); - mAlarmPendingIntent = PendingIntent.getBroadcast(this, 0, alarmIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - } - - private void setAlarm() { - cancelAlarm(); - - if (mAlertLeadTime > 0) { - long alertTime = getAlarmClockTime(); - if (alertTime > System.currentTimeMillis()) { - if (Log.isLoggable(Constants.TAG, Log.VERBOSE)) - Log.v(Constants.TAG, "Scheduling alarm for " - + DateFormatUtils.format(alertTime, "h:mm:ss")); - refreshAlarmPendingIntent(); - mAlarmManager.set(AlarmManager.RTC_WAKEUP, alertTime, - mAlarmPendingIntent); - ((BartRunnerApplication) getApplication()) - .setAlarmPending(true); - } - } - } - - private void triggerAlarmImmediately() { - Log.v(Constants.TAG, "Setting off alarm immediately"); - cancelAlarm(); - refreshAlarmPendingIntent(); - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, - SystemClock.elapsedRealtime() + 100, mAlarmPendingIntent); - } - - private long getAlarmClockTime() { - final Departure boardedDeparture = ((BartRunnerApplication) getApplication()) - .getBoardedDeparture(); - return boardedDeparture.getMeanEstimate() - mAlertLeadTime * 60 * 1000; - } - - private void cancelAlarm() { - ((BartRunnerApplication) getApplication()).setAlarmPending(false); - if (mAlarmManager != null) { - mAlarmManager.cancel(mAlarmPendingIntent); + if (boardedDeparture != null) { + boardedDeparture + .updateAlarm(getApplicationContext(), mAlarmManager); } } @@ -219,20 +178,8 @@ public class NotificationService extends Service implements EtdServiceListener { .getMeanSecondsLeft() || boardedDeparture .getUncertaintySeconds() != departure .getUncertaintySeconds())) { - long initialAlertClockTime = getAlarmClockTime(); - boardedDeparture.mergeEstimate(departure); - - final long now = System.currentTimeMillis(); - final long newAlarmClockTime = getAlarmClockTime(); - if (initialAlertClockTime > now && newAlarmClockTime <= now) { - // Alert time was changed to the past - triggerAlarmImmediately(); - } else if (newAlarmClockTime > now) { - // Alert time is still in the future - setAlarm(); - } - + updateAlarm(); break; } } @@ -268,10 +215,7 @@ public class NotificationService extends Service implements EtdServiceListener { shutDown(false); } - // Departure must have changed... fire the alarm - if (getAlarmClockTime() < System.currentTimeMillis()) { - triggerAlarmImmediately(); - } + boardedDeparture.updateAlarm(getApplicationContext(), mAlarmManager); updateNotification(); @@ -298,7 +242,6 @@ public class NotificationService extends Service implements EtdServiceListener { if (mNotificationManager != null) { mNotificationManager.cancel(DEPARTURE_NOTIFICATION_ID); } - cancelAlarm(); if (!isBeingDestroyed) stopSelf(); } @@ -314,58 +257,18 @@ public class NotificationService extends Service implements EtdServiceListener { final Departure boardedDeparture = ((BartRunnerApplication) getApplication()) .getBoardedDeparture(); - final int halfMinutes = (boardedDeparture.getMeanSecondsLeft() + 15) / 30; - float minutes = halfMinutes / 2f; - final String minutesText = (minutes < 1) ? "Less than one minute" - : (String.format("~%.1f minute", minutes) + ((minutes != 1.0) ? "s" - : "")); - - final Intent cancelAlarmIntent = new Intent(getApplicationContext(), - NotificationService.class); - cancelAlarmIntent.putExtra("cancelAlarm", true); - Builder notificationBuilder = new NotificationCompat.Builder(this) - .setOngoing(true) - .setSmallIcon(R.drawable.ic_stat_notification) - .setContentTitle( - mStationPair.getOrigin().shortName + " to " - + mStationPair.getDestination().shortName) - .setContentIntent(mNotificationIntent).setWhen(0); - if (android.os.Build.VERSION.SDK_INT >= 16) { - notificationBuilder - .setPriority(NotificationCompat.PRIORITY_HIGH) - .addAction( - R.drawable.ic_action_cancel_alarm, - "Cancel alarm", - PendingIntent.getService(getApplicationContext(), - 0, cancelAlarmIntent, - PendingIntent.FLAG_UPDATE_CURRENT)) - .setContentText(minutesText + " until departure") - .setSubText( - "Alert " + mAlertLeadTime - + " minutes before departure"); - } else { - notificationBuilder.setContentText(minutesText - + " to departure (alarm at " + mAlertLeadTime + " min" - + ((mAlertLeadTime == 1) ? "" : "s") + ")"); + if (boardedDeparture != null) { + mNotificationManager.notify(DEPARTURE_NOTIFICATION_ID, + boardedDeparture + .createNotification(getApplicationContext())); } - - mNotificationManager.notify(DEPARTURE_NOTIFICATION_ID, - notificationBuilder.build()); } private int getPollIntervalMillis() { final Departure boardedDeparture = ((BartRunnerApplication) getApplication()) .getBoardedDeparture(); - final int secondsToAlarm = boardedDeparture.getMeanSecondsLeft() - - mAlertLeadTime * 60; - if (secondsToAlarm < -20) { - /* Alarm should have already gone off by now */ - shutDown(false); - return 10000000; // Arbitrarily large number - } - - if (secondsToAlarm > 3 * 60) { + if (boardedDeparture.getSecondsUntilAlarm() > 3 * 60) { return 15 * 1000; } else { return 6 * 1000;