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.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) {

View File

@ -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,

View File

@ -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

View File

@ -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<Departure> {
private static final int MINIMUM_MERGE_OVERLAP_MILLIS = 10000;
@ -56,6 +67,9 @@ public class Departure implements Parcelable, Comparable<Departure> {
private long arrivalTimeOverride;
private int alarmLeadTimeMinutes;
private boolean alarmPending;
public Station getOrigin() {
return origin;
}
@ -273,14 +287,14 @@ public class Departure implements Parcelable, Comparable<Departure> {
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<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
public String toString() {
java.text.DateFormat format = SimpleDateFormat.getTimeInstance();
@ -533,4 +653,8 @@ public class Departure implements Parcelable, Comparable<Departure> {
return new Departure[size];
}
};
public void notifyAlarmHasBeenHandled() {
this.alarmPending = false;
}
}

View File

@ -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();
}
}

View File

@ -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;