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
This commit is contained in:
Doug Keen 2012-09-28 10:10:54 -07:00
parent 9e4b4cf8d2
commit 3d3a30a563
6 changed files with 192 additions and 142 deletions

View File

@ -8,7 +8,9 @@ import java.io.InputStream;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import android.app.AlarmManager;
import android.app.Application; import android.app.Application;
import android.content.Context;
import android.media.MediaPlayer; import android.media.MediaPlayer;
import android.os.Parcel; import android.os.Parcel;
import android.util.Log; import android.util.Log;
@ -88,6 +90,13 @@ public class BartRunnerApplication extends Application {
public void setBoardedDeparture(Departure boardedDeparture) { public void setBoardedDeparture(Departure boardedDeparture) {
if (!ObjectUtils.equals(boardedDeparture, mBoardedDeparture) if (!ObjectUtils.equals(boardedDeparture, mBoardedDeparture)
|| ObjectUtils.compare(mBoardedDeparture, boardedDeparture) != 0) { || 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; this.mBoardedDeparture = boardedDeparture;
if (mBoardedDeparture != null) { if (mBoardedDeparture != null) {

View File

@ -4,7 +4,6 @@ import net.simonvt.widget.NumberPicker;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor; import android.content.SharedPreferences.Editor;
import android.os.Bundle; import android.os.Bundle;
@ -14,7 +13,6 @@ import android.support.v4.app.FragmentActivity;
import com.WazaBe.HoloEverywhere.AlertDialog; import com.WazaBe.HoloEverywhere.AlertDialog;
import com.dougkeen.bart.BartRunnerApplication; import com.dougkeen.bart.BartRunnerApplication;
import com.dougkeen.bart.R; import com.dougkeen.bart.R;
import com.dougkeen.bart.services.NotificationService;
public class TrainAlertDialogFragment extends DialogFragment { public class TrainAlertDialogFragment extends DialogFragment {
@ -85,10 +83,10 @@ public class TrainAlertDialogFragment extends DialogFragment {
alertLeadTime); alertLeadTime);
editor.commit(); editor.commit();
Intent intent = new Intent(getActivity(), ((BartRunnerApplication) getActivity()
NotificationService.class); .getApplication())
intent.putExtra("alertLeadTime", alertLeadTime); .getBoardedDeparture().setUpAlarm(
getActivity().startService(intent); alertLeadTime);
} }
}) })
.setNegativeButton(R.string.skip_alert, .setNegativeButton(R.string.skip_alert,

View File

@ -399,7 +399,7 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements
return true; return true;
} else if (itemId == R.id.cancel_alarm_button) { } else if (itemId == R.id.cancel_alarm_button) {
Intent intent = new Intent(this, NotificationService.class); Intent intent = new Intent(this, NotificationService.class);
intent.putExtra("cancelAlarm", true); intent.putExtra("cancelNotifications", true);
startService(intent); startService(intent);
return true; return true;
} else if (itemId == R.id.view_on_bart_site_button) { } else if (itemId == R.id.view_on_bart_site_button) {
@ -424,14 +424,20 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements
private void refreshBoardedDeparture() { private void refreshBoardedDeparture() {
final Departure boardedDeparture = ((BartRunnerApplication) getApplication()) final Departure boardedDeparture = ((BartRunnerApplication) getApplication())
.getBoardedDeparture(); .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() == null
|| !boardedDeparture.getStationPair().equals(getStationPair())) { || !boardedDeparture.getStationPair().equals(getStationPair());
findViewById(R.id.yourTrainSection).setVisibility(View.GONE);
if (boardedDepartureDoesNotApply) {
if (currentVisibility != View.GONE) {
yourTrainSection.setVisibility(View.GONE);
}
return; return;
} }
findViewById(R.id.yourTrainSection).setVisibility(View.VISIBLE);
((TextView) findViewById(R.id.yourTrainDestinationText)) ((TextView) findViewById(R.id.yourTrainDestinationText))
.setText(boardedDeparture.getTrainDestination().toString()); .setText(boardedDeparture.getTrainDestination().toString());
@ -482,6 +488,10 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements
.getEstimatedArrivalMinutesLeftText(ViewDeparturesActivity.this); .getEstimatedArrivalMinutesLeftText(ViewDeparturesActivity.this);
} }
}); });
if (currentVisibility != View.VISIBLE) {
yourTrainSection.setVisibility(View.VISIBLE);
}
} }
private void startDepartureActionMode() { private void startDepartureActionMode() {
@ -512,8 +522,8 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements
application.setBoardedDeparture(mSelectedDeparture); application.setBoardedDeparture(mSelectedDeparture);
refreshBoardedDeparture(); refreshBoardedDeparture();
// Stop the notification service // Start the notification service
stopService(new Intent(ViewDeparturesActivity.this, startService(new Intent(ViewDeparturesActivity.this,
NotificationService.class)); NotificationService.class));
// Don't prompt for alert if train is about to leave // Don't prompt for alert if train is about to leave

View File

@ -3,12 +3,23 @@ package com.dougkeen.bart.model;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; 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.Context;
import android.content.Intent;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Builder;
import android.text.format.DateFormat; import android.text.format.DateFormat;
import android.util.Log; import android.util.Log;
import com.dougkeen.bart.R;
import com.dougkeen.bart.services.NotificationService;
public class Departure implements Parcelable, Comparable<Departure> { public class Departure implements Parcelable, Comparable<Departure> {
private static final int MINIMUM_MERGE_OVERLAP_MILLIS = 10000; private static final int MINIMUM_MERGE_OVERLAP_MILLIS = 10000;
@ -56,6 +67,9 @@ public class Departure implements Parcelable, Comparable<Departure> {
private long arrivalTimeOverride; private long arrivalTimeOverride;
private int alarmLeadTimeMinutes;
private boolean alarmPending;
public Station getOrigin() { public Station getOrigin() {
return origin; return origin;
} }
@ -273,14 +287,14 @@ public class Departure implements Parcelable, Comparable<Departure> {
if (minutesLeft < 0) { if (minutesLeft < 0) {
return "Arrived at destination"; return "Arrived at destination";
} else if (minutesLeft == 0) { } else if (minutesLeft == 0) {
return "Arrives around " + getEstimatedArrivalTimeText(context) return "Arrives ~" + getEstimatedArrivalTimeText(context)
+ " (<1 min)"; + " (<1 min)";
} else if (minutesLeft == 1) { } else if (minutesLeft == 1) {
return "Arrives around " + getEstimatedArrivalTimeText(context) return "Arrives ~" + getEstimatedArrivalTimeText(context)
+ " (1 min)"; + " (1 min)";
} else { } else {
return "Arrives around " + getEstimatedArrivalTimeText(context) return "Arrives ~" + getEstimatedArrivalTimeText(context) + " ("
+ " (" + minutesLeft + " mins)"; + minutesLeft + " mins)";
} }
} }
@ -460,6 +474,112 @@ public class Departure implements Parcelable, Comparable<Departure> {
} }
} }
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 @Override
public String toString() { public String toString() {
java.text.DateFormat format = SimpleDateFormat.getTimeInstance(); java.text.DateFormat format = SimpleDateFormat.getTimeInstance();
@ -533,4 +653,8 @@ public class Departure implements Parcelable, Comparable<Departure> {
return new Departure[size]; return new Departure[size];
} }
}; };
public void notifyAlarmHasBeenHandled() {
this.alarmPending = false;
}
} }

View File

@ -1,29 +1,35 @@
package com.dougkeen.bart.receivers; package com.dougkeen.bart.receivers;
import com.dougkeen.bart.BartRunnerApplication;
import com.dougkeen.util.WakeLocker;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; 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 { public class AlarmBroadcastReceiver extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
WakeLocker.acquire(context);
BartRunnerApplication application = (BartRunnerApplication) context BartRunnerApplication application = (BartRunnerApplication) context
.getApplicationContext(); .getApplicationContext();
final Departure boardedDeparture = application.getBoardedDeparture();
if (boardedDeparture == null) {
// Nothing to notify about
return;
}
WakeLocker.acquire(context);
application.setPlayAlarmRingtone(true); application.setPlayAlarmRingtone(true);
Intent targetIntent = new Intent(Intent.ACTION_VIEW, application Intent targetIntent = new Intent(Intent.ACTION_VIEW, boardedDeparture.getStationPair().getUri());
.getBoardedDeparture().getStationPair().getUri());
targetIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); targetIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(targetIntent); context.startActivity(targetIntent);
application.setAlarmPending(false); boardedDeparture.notifyAlarmHasBeenHandled();
} }
} }

View File

@ -45,8 +45,6 @@ public class NotificationService extends Service implements EtdServiceListener {
private NotificationManager mNotificationManager; private NotificationManager mNotificationManager;
private AlarmManager mAlarmManager; private AlarmManager mAlarmManager;
private PendingIntent mNotificationIntent; private PendingIntent mNotificationIntent;
private PendingIntent mAlarmPendingIntent;
private int mAlertLeadTime;
private Handler mHandler; private Handler mHandler;
private boolean mHasShutDown = false; private boolean mHasShutDown = false;
@ -126,17 +124,20 @@ public class NotificationService extends Service implements EtdServiceListener {
protected void onHandleIntent(Intent intent) { protected void onHandleIntent(Intent intent) {
final Departure boardedDeparture = ((BartRunnerApplication) getApplication()) final Departure boardedDeparture = ((BartRunnerApplication) getApplication())
.getBoardedDeparture(); .getBoardedDeparture();
if (boardedDeparture == null if (boardedDeparture == null) {
|| intent.getBooleanExtra("cancelAlarm", false)) { // Nothing to notify about
// Nothing to notify about, or we want to cancel the alarm return;
}
if (intent.getBooleanExtra("cancelNotifications", false)) {
// We want to cancel the alarm/notification
boardedDeparture
.cancelAlarm(getApplicationContext(), mAlarmManager);
shutDown(false); shutDown(false);
return; return;
} }
Bundle bundle = intent.getExtras();
StationPair oldStationPair = mStationPair; StationPair oldStationPair = mStationPair;
mStationPair = boardedDeparture.getStationPair(); mStationPair = boardedDeparture.getStationPair();
mAlertLeadTime = bundle.getInt("alertLeadTime");
if (mEtdService != null && mStationPair != null if (mEtdService != null && mStationPair != null
&& !mStationPair.equals(oldStationPair)) { && !mStationPair.equals(oldStationPair)) {
@ -155,57 +156,15 @@ public class NotificationService extends Service implements EtdServiceListener {
updateNotification(); updateNotification();
setAlarm();
pollDepartureStatus(); pollDepartureStatus();
} }
private void refreshAlarmPendingIntent() { private void updateAlarm() {
final Intent alarmIntent = new Intent(Constants.ACTION_ALARM, Departure boardedDeparture = ((BartRunnerApplication) getApplication())
getStationPair().getUri());
final Departure boardedDeparture = ((BartRunnerApplication) getApplication())
.getBoardedDeparture(); .getBoardedDeparture();
alarmIntent.putExtra("departure", (Parcelable) boardedDeparture); if (boardedDeparture != null) {
mAlarmPendingIntent = PendingIntent.getBroadcast(this, 0, alarmIntent, boardedDeparture
PendingIntent.FLAG_UPDATE_CURRENT); .updateAlarm(getApplicationContext(), mAlarmManager);
}
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);
} }
} }
@ -219,20 +178,8 @@ public class NotificationService extends Service implements EtdServiceListener {
.getMeanSecondsLeft() || boardedDeparture .getMeanSecondsLeft() || boardedDeparture
.getUncertaintySeconds() != departure .getUncertaintySeconds() != departure
.getUncertaintySeconds())) { .getUncertaintySeconds())) {
long initialAlertClockTime = getAlarmClockTime();
boardedDeparture.mergeEstimate(departure); boardedDeparture.mergeEstimate(departure);
updateAlarm();
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();
}
break; break;
} }
} }
@ -268,10 +215,7 @@ public class NotificationService extends Service implements EtdServiceListener {
shutDown(false); shutDown(false);
} }
// Departure must have changed... fire the alarm boardedDeparture.updateAlarm(getApplicationContext(), mAlarmManager);
if (getAlarmClockTime() < System.currentTimeMillis()) {
triggerAlarmImmediately();
}
updateNotification(); updateNotification();
@ -298,7 +242,6 @@ public class NotificationService extends Service implements EtdServiceListener {
if (mNotificationManager != null) { if (mNotificationManager != null) {
mNotificationManager.cancel(DEPARTURE_NOTIFICATION_ID); mNotificationManager.cancel(DEPARTURE_NOTIFICATION_ID);
} }
cancelAlarm();
if (!isBeingDestroyed) if (!isBeingDestroyed)
stopSelf(); stopSelf();
} }
@ -314,58 +257,18 @@ public class NotificationService extends Service implements EtdServiceListener {
final Departure boardedDeparture = ((BartRunnerApplication) getApplication()) final Departure boardedDeparture = ((BartRunnerApplication) getApplication())
.getBoardedDeparture(); .getBoardedDeparture();
final int halfMinutes = (boardedDeparture.getMeanSecondsLeft() + 15) / 30; if (boardedDeparture != null) {
float minutes = halfMinutes / 2f; mNotificationManager.notify(DEPARTURE_NOTIFICATION_ID,
final String minutesText = (minutes < 1) ? "Less than one minute" boardedDeparture
: (String.format("~%.1f minute", minutes) + ((minutes != 1.0) ? "s" .createNotification(getApplicationContext()));
: ""));
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") + ")");
} }
mNotificationManager.notify(DEPARTURE_NOTIFICATION_ID,
notificationBuilder.build());
} }
private int getPollIntervalMillis() { private int getPollIntervalMillis() {
final Departure boardedDeparture = ((BartRunnerApplication) getApplication()) final Departure boardedDeparture = ((BartRunnerApplication) getApplication())
.getBoardedDeparture(); .getBoardedDeparture();
final int secondsToAlarm = boardedDeparture.getMeanSecondsLeft()
- mAlertLeadTime * 60;
if (secondsToAlarm < -20) { if (boardedDeparture.getSecondsUntilAlarm() > 3 * 60) {
/* Alarm should have already gone off by now */
shutDown(false);
return 10000000; // Arbitrarily large number
}
if (secondsToAlarm > 3 * 60) {
return 15 * 1000; return 15 * 1000;
} else { } else {
return 6 * 1000; return 6 * 1000;