diff --git a/cancelAlarm.svg b/cancelAlarm.svg new file mode 100644 index 0000000..56c465f --- /dev/null +++ b/cancelAlarm.svg @@ -0,0 +1,438 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/res/drawable-hdpi/ic_action_cancel_alarm.png b/res/drawable-hdpi/ic_action_cancel_alarm.png new file mode 100644 index 0000000..bd87e85 Binary files /dev/null and b/res/drawable-hdpi/ic_action_cancel_alarm.png differ diff --git a/res/drawable-ldpi/ic_action_cancel_alarm.png b/res/drawable-ldpi/ic_action_cancel_alarm.png new file mode 100644 index 0000000..2781dec Binary files /dev/null and b/res/drawable-ldpi/ic_action_cancel_alarm.png differ diff --git a/res/drawable-mdpi/ic_action_cancel_alarm.png b/res/drawable-mdpi/ic_action_cancel_alarm.png new file mode 100644 index 0000000..5d1ebba Binary files /dev/null and b/res/drawable-mdpi/ic_action_cancel_alarm.png differ diff --git a/res/drawable-xhdpi/ic_action_cancel_alarm.png b/res/drawable-xhdpi/ic_action_cancel_alarm.png new file mode 100644 index 0000000..c1761e0 Binary files /dev/null and b/res/drawable-xhdpi/ic_action_cancel_alarm.png differ diff --git a/res/layout/departures.xml b/res/layout/departures.xml index baa2a05..ac8598f 100644 --- a/res/layout/departures.xml +++ b/res/layout/departures.xml @@ -97,7 +97,8 @@ android:layout_height="wrap_content" android:indeterminate="true" android:visibility="invisible" /> - + diff --git a/src/com/dougkeen/bart/AlarmBroadcastReceiver.java b/src/com/dougkeen/bart/AlarmBroadcastReceiver.java index b1b369f..85bbb58 100644 --- a/src/com/dougkeen/bart/AlarmBroadcastReceiver.java +++ b/src/com/dougkeen/bart/AlarmBroadcastReceiver.java @@ -19,6 +19,8 @@ public class AlarmBroadcastReceiver extends BroadcastReceiver { targetIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(targetIntent); + + application.setAlarmPending(false); } } diff --git a/src/com/dougkeen/bart/BartRunnerApplication.java b/src/com/dougkeen/bart/BartRunnerApplication.java index 175d81e..e7d795b 100644 --- a/src/com/dougkeen/bart/BartRunnerApplication.java +++ b/src/com/dougkeen/bart/BartRunnerApplication.java @@ -4,10 +4,13 @@ import android.app.Application; import android.media.MediaPlayer; import com.dougkeen.bart.model.Departure; +import com.dougkeen.util.Observable; public class BartRunnerApplication extends Application { private Departure mBoardedDeparture; + private Observable mAlarmPending = new Observable(false); + private boolean mPlayAlarmRingtone; private boolean mAlarmSounding; @@ -46,4 +49,16 @@ public class BartRunnerApplication extends Application { this.mAlarmMediaPlayer = alarmMediaPlayer; } + public boolean isAlarmPending() { + return mAlarmPending.getValue(); + } + + public Observable getAlarmPendingObservable() { + return mAlarmPending; + } + + public void setAlarmPending(boolean alarmPending) { + this.mAlarmPending.setValue(alarmPending); + } + } \ No newline at end of file diff --git a/src/com/dougkeen/bart/EtdService.java b/src/com/dougkeen/bart/EtdService.java index fea62d7..14b131f 100644 --- a/src/com/dougkeen/bart/EtdService.java +++ b/src/com/dougkeen/bart/EtdService.java @@ -67,10 +67,11 @@ public class EtdService extends Service { public void unregisterListener(EtdServiceListener listener) { StationPair stationPair = getStationPairFromListener(listener); - if (stationPair == null) - return; - - if (mServiceEngineMap.containsKey(stationPair)) { + if (stationPair == null) { + for (EtdServiceEngine engine : mServiceEngineMap.values()) { + engine.unregisterListener(listener); + } + } else if (mServiceEngineMap.containsKey(stationPair)) { mServiceEngineMap.get(stationPair).unregisterListener(listener); } } @@ -93,7 +94,6 @@ public class EtdService extends Service { } public class EtdServiceBinder extends Binder { - public EtdService getService() { return EtdService.this; } @@ -210,9 +210,9 @@ public class EtdService extends Service { mIgnoreDepartureDirection) { @Override public void onResult(RealTimeDepartures result) { - Log.d(Constants.TAG, "Processing data from server"); + Log.v(Constants.TAG, "Processing data from server"); processLatestDepartures(result); - Log.d(Constants.TAG, "Done processing data from server"); + Log.v(Constants.TAG, "Done processing data from server"); notifyListenersOfRequestEnd(); mPendingEtdRequest = false; } @@ -227,7 +227,7 @@ public class EtdService extends Service { } }; mGetDeparturesTask = task; - Log.d(Constants.TAG, "Fetching data from server"); + Log.v(Constants.TAG, "Fetching data from server"); task.execute(new StationPair(mStationPair.getOrigin(), mStationPair .getDestination())); notifyListenersOfRequestStart(); @@ -244,10 +244,10 @@ public class EtdService extends Service { GetScheduleInformationTask task = new GetScheduleInformationTask() { @Override public void onResult(ScheduleInformation result) { - Log.d(Constants.TAG, "Processing data from server"); + Log.v(Constants.TAG, "Processing data from server"); mLatestScheduleInfo = result; applyScheduleInformation(result); - Log.d(Constants.TAG, "Done processing data from server"); + Log.v(Constants.TAG, "Done processing data from server"); } @Override @@ -299,8 +299,8 @@ public class EtdService extends Service { ScheduleItem trip = mLatestScheduleInfo.getTrips().get(i); // Definitely not a match if they have different // destinations - if (!departure.getTrainDestination().abbreviation.equals(trip - .getTrainHeadStation())) { + if (!departure.getTrainDestination().abbreviation + .equals(trip.getTrainHeadStation())) { continue; } @@ -582,7 +582,7 @@ public class EtdService extends Service { } }, millisUntilExecute); mNextFetchClockTime = requestedFetchTime; - Log.d(Constants.TAG, "Scheduled another departure fetch in " + Log.i(Constants.TAG, "Scheduled another departure fetch in " + millisUntilExecute / 1000 + "s"); } } @@ -593,7 +593,7 @@ public class EtdService extends Service { fetchLatestSchedule(); } }, millisUntilExecute); - Log.d(Constants.TAG, "Scheduled another schedule fetch in " + Log.i(Constants.TAG, "Scheduled another schedule fetch in " + millisUntilExecute / 1000 + "s"); } diff --git a/src/com/dougkeen/bart/NotificationService.java b/src/com/dougkeen/bart/NotificationService.java index 530c757..17840f3 100644 --- a/src/com/dougkeen/bart/NotificationService.java +++ b/src/com/dougkeen/bart/NotificationService.java @@ -2,6 +2,8 @@ package com.dougkeen.bart; import java.util.List; +import org.apache.commons.lang3.time.DateFormatUtils; + import android.app.AlarmManager; import android.app.NotificationManager; import android.app.PendingIntent; @@ -19,6 +21,8 @@ import android.os.Message; import android.os.SystemClock; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.Builder; +import android.util.Log; +import android.util.TimeFormatException; import com.dougkeen.bart.EtdService.EtdServiceBinder; import com.dougkeen.bart.EtdService.EtdServiceListener; @@ -103,6 +107,7 @@ public class NotificationService extends Service implements EtdServiceListener { @Override public int onStartCommand(Intent intent, int flags, int startId) { + mHasShutDown = false; onStart(intent, startId); return START_STICKY; } @@ -119,8 +124,9 @@ public class NotificationService extends Service implements EtdServiceListener { protected void onHandleIntent(Intent intent) { final Departure boardedDeparture = ((BartRunnerApplication) getApplication()) .getBoardedDeparture(); - if (boardedDeparture == null) { - // Nothing to notify about + if (boardedDeparture == null + || intent.getBooleanExtra("cancelAlarm", false)) { + // Nothing to notify about, or we want to cancel the alarm shutDown(false); return; } @@ -139,14 +145,14 @@ public class NotificationService extends Service implements EtdServiceListener { mEtdService.registerListener(this); } - updateNotification(); - Intent targetIntent = new Intent(Intent.ACTION_VIEW, mStationPair.getUri()); targetIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); mNotificationIntent = PendingIntent.getActivity(this, 0, targetIntent, PendingIntent.FLAG_UPDATE_CURRENT); + updateNotification(); + setAlarm(); pollDepartureStatus(); @@ -168,14 +174,20 @@ public class NotificationService extends Service implements EtdServiceListener { 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, @@ -189,6 +201,7 @@ public class NotificationService extends Service implements EtdServiceListener { } private void cancelAlarm() { + ((BartRunnerApplication) getApplication()).setAlarmPending(false); mAlarmManager.cancel(mAlarmPendingIntent); } @@ -280,7 +293,9 @@ public class NotificationService extends Service implements EtdServiceListener { private void updateNotification() { if (mHasShutDown) { - mNotificationManager.cancel(DEPARTURE_NOTIFICATION_ID); + if (mEtdService != null) { + mEtdService.unregisterListener(this); + } return; } @@ -292,16 +307,35 @@ public class NotificationService extends Service implements EtdServiceListener { : (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) - .setContentText(minutesText + " until departure") - .setSubText( - "Alert " + mAlertLeadTime + " minutes before departure") .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()); } @@ -318,11 +352,7 @@ public class NotificationService extends Service implements EtdServiceListener { return 10000000; // Arbitrarily large number } - if (secondsToAlarm > 10 * 60) { - return 60 * 1000; - } else if (secondsToAlarm > 5 * 60) { - return 60 * 1000; - } else if (secondsToAlarm > 3 * 60) { + if (secondsToAlarm > 3 * 60) { return 30 * 1000; } else { return 10 * 1000; diff --git a/src/com/dougkeen/bart/TrainAlertDialogFragment.java b/src/com/dougkeen/bart/TrainAlertDialogFragment.java index 5b9c353..644478d 100644 --- a/src/com/dougkeen/bart/TrainAlertDialogFragment.java +++ b/src/com/dougkeen/bart/TrainAlertDialogFragment.java @@ -82,8 +82,7 @@ public class TrainAlertDialogFragment extends DialogFragment { alertLeadTime); editor.commit(); - Intent intent = new Intent(getActivity() - .getApplicationContext(), + Intent intent = new Intent(getActivity(), NotificationService.class); intent.putExtra("alertLeadTime", alertLeadTime); getActivity().startService(intent); diff --git a/src/com/dougkeen/bart/ViewDeparturesActivity.java b/src/com/dougkeen/bart/ViewDeparturesActivity.java index 2920b80..c9818f3 100644 --- a/src/com/dougkeen/bart/ViewDeparturesActivity.java +++ b/src/com/dougkeen/bart/ViewDeparturesActivity.java @@ -49,6 +49,7 @@ import com.dougkeen.bart.model.Departure; import com.dougkeen.bart.model.Station; import com.dougkeen.bart.model.StationPair; import com.dougkeen.bart.model.TextProvider; +import com.dougkeen.util.Observer; public class ViewDeparturesActivity extends SherlockFragmentActivity implements EtdServiceListener { @@ -291,6 +292,8 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements } }; + private Observer mAlarmPendingObserver; + protected DepartureArrayAdapter getListAdapter() { return mDeparturesAdapter; } @@ -305,6 +308,10 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements super.onStop(); if (mEtdService != null) mEtdService.unregisterListener(this); + if (mAlarmPendingObserver != null) + ((BartRunnerApplication) getApplication()) + .getAlarmPendingObservable().unregisterObserver( + mAlarmPendingObserver); if (mBound) unbindService(mConnection); Ticker.getInstance().stopTicking(); @@ -355,6 +362,25 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getSupportMenuInflater(); inflater.inflate(R.menu.route_menu, menu); + final MenuItem cancelAlarmButton = menu + .findItem(R.id.cancel_alarm_button); + final BartRunnerApplication application = (BartRunnerApplication) getApplication(); + if (application.isAlarmPending()) { + cancelAlarmButton.setVisible(true); + } + mAlarmPendingObserver = new Observer() { + @Override + public void onUpdate(final Boolean newValue) { + runOnUiThread(new Runnable() { + @Override + public void run() { + cancelAlarmButton.setVisible(newValue); + } + }); + } + }; + application.getAlarmPendingObservable().registerObserver( + mAlarmPendingObserver); return true; } @@ -367,6 +393,11 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); return true; + } else if (itemId == R.id.cancel_alarm_button) { + Intent intent = new Intent(this, NotificationService.class); + intent.putExtra("cancelAlarm", true); + startService(intent); + return true; } else if (itemId == R.id.view_on_bart_site_button) { startActivity(new Intent( Intent.ACTION_VIEW, @@ -477,7 +508,7 @@ public class ViewDeparturesActivity extends SherlockFragmentActivity implements refreshBoardedDeparture(); // Stop the notification service - stopService(new Intent(getApplicationContext(), + stopService(new Intent(ViewDeparturesActivity.this, NotificationService.class)); // Don't prompt for alert if train is about to leave diff --git a/src/com/dougkeen/util/Observable.java b/src/com/dougkeen/util/Observable.java new file mode 100644 index 0000000..4dd97bd --- /dev/null +++ b/src/com/dougkeen/util/Observable.java @@ -0,0 +1,46 @@ +package com.dougkeen.util; + +import java.util.WeakHashMap; + +import org.apache.commons.lang3.ObjectUtils; + +public class Observable { + private T value; + private WeakHashMap, Boolean> listeners = new WeakHashMap, Boolean>(); + + public Observable() { + super(); + } + + public Observable(T value) { + super(); + this.value = value; + } + + public T getValue() { + return value; + } + + public void setValue(T value) { + if (!ObjectUtils.equals(this.value, value)) { + this.value = value; + notifyOfChange(value); + } + } + + public void registerObserver(Observer observer) { + listeners.put(observer, true); + } + + public void unregisterObserver(Observer observer) { + listeners.remove(observer); + } + + protected void notifyOfChange(T value) { + for (Observer listener : listeners.keySet()) { + if (listener != null) { + listener.onUpdate(value); + } + } + } +} diff --git a/src/com/dougkeen/util/Observer.java b/src/com/dougkeen/util/Observer.java new file mode 100644 index 0000000..0645024 --- /dev/null +++ b/src/com/dougkeen/util/Observer.java @@ -0,0 +1,5 @@ +package com.dougkeen.util; + +public interface Observer { + void onUpdate(final T newValue); +}