From 3d3a30a563f3362c130d4f30b7f37886e7867777 Mon Sep 17 00:00:00 2001 From: Doug Keen Date: Fri, 28 Sep 2012 10:10:54 -0700 Subject: [PATCH] Changed architecture of alarms and notifications (now the alarm is owned by the Departure itself, and the Departure generates the Notification) Notification now shows even if you don't set an alarm --- .../dougkeen/bart/BartRunnerApplication.java | 9 ++ .../activities/TrainAlertDialogFragment.java | 10 +- .../activities/ViewDeparturesActivity.java | 24 ++- src/com/dougkeen/bart/model/Departure.java | 132 ++++++++++++++++- .../receivers/AlarmBroadcastReceiver.java | 22 ++- .../bart/services/NotificationService.java | 137 +++--------------- 6 files changed, 192 insertions(+), 142 deletions(-) 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;