From 31e0a71d9f60bf5c6c5d806ceaeccd6609447f9f Mon Sep 17 00:00:00 2001 From: Phillip Hsu Date: Sun, 11 Sep 2016 01:05:03 -0700 Subject: [PATCH] Created ChronometerNotificationService and ChronometerNotificationThread. Modify TimerNotificationService to extend from ChronometerNotificationService. --- .../ChronometerNotificationService.java | 248 +++++++++++++++++ .../clock2/ChronometerNotificationThread.java | 99 +++++++ .../StopwatchNotificationService.java | 21 +- .../clock2/timers/ChronometerDelegate.java | 10 +- .../timers/TimerNotificationService.java | 260 +++++------------- 5 files changed, 445 insertions(+), 193 deletions(-) create mode 100644 app/src/main/java/com/philliphsu/clock2/ChronometerNotificationService.java create mode 100644 app/src/main/java/com/philliphsu/clock2/ChronometerNotificationThread.java diff --git a/app/src/main/java/com/philliphsu/clock2/ChronometerNotificationService.java b/app/src/main/java/com/philliphsu/clock2/ChronometerNotificationService.java new file mode 100644 index 0000000..07a6cc6 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/ChronometerNotificationService.java @@ -0,0 +1,248 @@ +package com.philliphsu.clock2; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.support.annotation.CallSuper; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.NotificationCompat; + +import com.philliphsu.clock2.timers.ChronometerDelegate; + +/** + * Created by Phillip Hsu on 9/10/2016. + */ +public abstract class ChronometerNotificationService extends Service { + public static final String ACTION_START_PAUSE = "com.philliphsu.clock2.timers.action.START_PAUSE"; + public static final String ACTION_STOP = "com.philliphsu.clock2.timers.action.STOP"; + + private NotificationCompat.Builder mNoteBuilder; + private NotificationManager mNotificationManager; + // TODO: I think we may need a list of threads. + private ChronometerNotificationThread mThread; + private final ChronometerDelegate mDelegate = new ChronometerDelegate(); + + /** + * @return the icon for the notification + */ + @DrawableRes + protected abstract int getSmallIcon(); + + /** + * @return an optional content intent that is fired when the notification is clicked + */ + @Nullable + protected abstract PendingIntent getContentIntent(); + + /** + * @return whether the chronometer should be counting down + */ + protected abstract boolean isCountDown(); + + /** + * @return a tag associated with the notification. The default implementation returns the + * name of this class. + */ + protected String getNoteTag() { + return getClass().getName(); + } + + /** + * The intent received in {@link #onStartCommand(Intent, int, int)} + * has no {@link Intent#getAction() action} set. At this point, you + * should configure the notification to be displayed. + * @param intent + * @param flags + * @param startId + */ + protected abstract void handleDefaultAction(Intent intent, int flags, long startId); + + protected abstract void handleStartPauseAction(Intent intent, int flags, long startId); + + protected abstract void handleStopAction(Intent intent, int flags, long startId); + + /** + * This will be called if the command in {@link #onStartCommand(Intent, int, int)} + * has an action that your subclass defines. + * @param action Your custom action. + */ + protected abstract void handleAction(@NonNull String action, Intent intent, int flags, long startId); + + @Override + public void onCreate() { + super.onCreate(); + mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + // Create base note + mNoteBuilder = new NotificationCompat.Builder(this) + .setSmallIcon(getSmallIcon()) + .setShowWhen(false) + .setOngoing(true) + .setContentIntent(getContentIntent()); + mDelegate.init(); + mDelegate.setCountDown(isCountDown()); + } + + @Override + public void onDestroy() { + super.onDestroy(); + quitThread(); + // TODO: Cancel all notifications pushed by this notification manager. + } + + @CallSuper + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null) { + final String action = intent.getAction(); + if (action != null) { + switch (action) { + case ACTION_START_PAUSE: + handleStartPauseAction(intent, flags, startId); + break; + case ACTION_STOP: + handleStopAction(intent, flags, startId); + break; + default: + // Defer to the subclass + handleAction(action, intent, flags, startId); + break; + } + } else { + handleDefaultAction(intent, flags, startId); + } + } + return super.onStartCommand(intent, flags, startId); + } + + @Nullable + @Override + public final IBinder onBind(Intent intent) { + return null; + } + + /** + * If there is a thread currently running, then this will push any notification updates + * you might have configured in the Builder and then call the thread's {@link + * ChronometerNotificationThread#quit() quit()}. + */ + public void quitCurrentThread() { + if (mThread != null) { + // Display any notification updates associated with the current state + // of the chronometer. If we relied on the HandlerThread to do this for us, + // the message delivery would be delayed. + mThread.updateNotification(); + // If the chronometer has been set to not run, the effect is obvious. + // Otherwise, we're preparing for the start of a new thread. + quitThread(); + } + } + + /** + * Instantiates a new HandlerThread and calls its {@link Thread#start() start()}. + * The calling thread will be blocked until the HandlerThread created here finishes + * initializing its looper. + * @param noteId the id with which the thread created here will be posting notifications. + * @param base the new base time of the chronometer + */ + public void startNewThread(int noteId, long base) { + // An instance of Thread cannot be started more than once. You must create + // a new instance if you want to start the Thread's work again. + mThread = new ChronometerNotificationThread( + mDelegate, + mNotificationManager, + mNoteBuilder, + getResources(), + getNoteTag(), + noteId); + // Initializes this thread as a looper. HandlerThread.run() will be executed + // in this thread. + // This gives you a chance to create handlers that then reference this looper, + // before actually starting the loop. + mThread.start(); + // If this thread has been started, this method will block *the calling thread* + // until the looper has been initialized. This ensures the handler thread is + // fully initialized before we proceed. + mThread.getLooper(); + // ------------------------------------------------------------------------------- + // TOneverDO: Set base BEFORE the thread is ready to begin working, or else when + // the thread actually begins working, it will initially show that some time has + // passed. + mDelegate.setBase(base); + // ------------------------------------------------------------------------------- + } + + /** + * Helper method to add the start/pause action to the notification's builder. + * @param running whether the chronometer is running + * @param requestCode Used to create the PendingIntent that is fired when this action is clicked. + */ + protected final void addStartPauseAction(boolean running, int requestCode) { + addAction(ACTION_START_PAUSE, + running ? R.drawable.ic_pause_24dp : R.drawable.ic_start_24dp, + getString(running ? R.string.pause : R.string.resume), + requestCode); + } + + /** + * Helper method to add the stop action to the notification's builder. + * @param requestCode Used to create the PendingIntent that is fired when this action is clicked. + */ + protected final void addStopAction(int requestCode) { + addAction(ACTION_STOP, R.drawable.ic_stop_24dp, getString(R.string.stop), requestCode); + } + + /** + * Clear the notification builder's set actions. + */ + protected final void clearActions() { + // TODO: The source indicates mActions is hidden, so how are we able to access it? + // Will it remain accessible for all SDK versions? If not, we would have to rebuild + // the entire notification with a new local Builder instance. + mNoteBuilder.mActions.clear(); + } + + protected final void setBase(long base) { + mDelegate.setBase(base); + } + + protected final long getBase() { + return mDelegate.getBase(); + } + + protected final void updateNotification() { + mThread.updateNotification(); + } + + protected final void setContentTitle(CharSequence title) { + mNoteBuilder.setContentTitle(title); + } + + /** + * Adds the specified action to the notification's Builder. + */ + protected final void addAction(String action, @DrawableRes int icon, String actionTitle, int requestCode) { + Intent intent = new Intent(this, getClass()) + .setAction(action); + // TODO: We can put the requestCode as an extra to this intent, and then retrieve that extra + // in onStartCommand() to figure out which of the multiple timers should we apply this action to. +// .putExtra(EXTRA_TIMER, mTimer); + PendingIntent pi = PendingIntent.getService( + this, requestCode, intent, 0/*no flags*/); + mNoteBuilder.addAction(icon, actionTitle, pi); + } + + /** + * Causes the handler thread's looper to terminate without processing + * any more messages in the message queue. + */ + private void quitThread() { + if (mThread != null && mThread.isAlive()) { + mThread.quit(); + } + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/ChronometerNotificationThread.java b/app/src/main/java/com/philliphsu/clock2/ChronometerNotificationThread.java new file mode 100644 index 0000000..694416d --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/ChronometerNotificationThread.java @@ -0,0 +1,99 @@ +package com.philliphsu.clock2; + +import android.annotation.SuppressLint; +import android.app.NotificationManager; +import android.content.res.Resources; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.NotificationCompat; + +import com.philliphsu.clock2.timers.ChronometerDelegate; + +/** + * Created by Phillip Hsu on 9/10/2016. + * + * A thread that updates a chronometer-based notification. While notifications + * have built-in support for using a chronometer, it lacks pause/resume functionality + * and the ability to choose between count up or count down. + */ +public class ChronometerNotificationThread extends HandlerThread { + private static final String TAG = "ChronomNotifThread"; + + private static final int MSG_WHAT = 2; + + private final ChronometerDelegate mDelegate; + private final NotificationManager mNotificationManager; + private final NotificationCompat.Builder mNoteBuilder; + private final Resources mResources; + private final String mNoteTag; + private final int mNoteId; + + private Handler mHandler; + + /** + * @param delegate Configured by the client service, including whether to be counting down or not. + * @param builder A preconfigured Builder from the client service whose content + * text will be updated and eventually built from. + * @param resources Required only if the ChronometerDelegate is configured to count down. + * Used to retrieve a String resource if/when the countdown reaches negative. + * TODO: Will the notification be cancelled fast enough before the countdown + * becomes negative? If so, this param is rendered useless. + * @param noteTag A tag associated with the client service, used for posting + * the notification. We require this because we want to avoid + * using a tag associated with this thread, or else the client + * service won't be able to manipulate the notifications that + * are posted from this thread. + */ + public ChronometerNotificationThread(@NonNull ChronometerDelegate delegate, + @NonNull NotificationManager manager, + @NonNull NotificationCompat.Builder builder, + @Nullable Resources resources, + @NonNull String noteTag, + int noteId) { + super(TAG); + mDelegate = delegate; + mNotificationManager = manager; + mNoteBuilder = builder; + mResources = resources; + mNoteTag = noteTag; + mNoteId = noteId; + } + + // There won't be a memory leak since our handler is using a looper that is not + // associated with the main thread. The full Lint warning confirmed this. + @SuppressLint("HandlerLeak") + @Override + protected void onLooperPrepared() { + // This is called after the looper has completed initializing, but before + // it starts looping through its message queue. Right now, there is no + // message queue, so this is the place to create it. + // By default, the constructor associates this handler with this thread's looper. + mHandler = new Handler() { + @Override + public void handleMessage(Message m) { + updateNotification(); + sendMessageDelayed(Message.obtain(this, MSG_WHAT), 1000); + } + }; + // Once the handler is initialized, we may immediately begin our work. + mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_WHAT), 1000); + } + + public void updateNotification() { + CharSequence text = mDelegate.formatElapsedTime(SystemClock.elapsedRealtime(), mResources); + mNoteBuilder.setContentText(text); + mNotificationManager.notify(mNoteTag, mNoteId, mNoteBuilder.build()); + } + + @Override + public boolean quit() { + // TODO: I think we can call removeCallbacksAndMessages(null) + // to remove ALL messages. + mHandler.removeMessages(MSG_WHAT); + return super.quit(); + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchNotificationService.java b/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchNotificationService.java index 891d531..bfd5c13 100644 --- a/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchNotificationService.java +++ b/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchNotificationService.java @@ -9,6 +9,7 @@ import android.os.IBinder; import android.support.annotation.DrawableRes; import android.support.v4.app.NotificationCompat; +import com.philliphsu.clock2.ChronometerNotificationThread; import com.philliphsu.clock2.MainActivity; import com.philliphsu.clock2.R; @@ -20,6 +21,7 @@ public class StopwatchNotificationService extends Service { private NotificationCompat.Builder mNoteBuilder; private NotificationManager mNotificationManager; private AsyncLapsTableUpdateHandler mLapsTableUpdateHandler; + private ChronometerNotificationThread mThread; @Override public void onCreate() { @@ -33,13 +35,7 @@ public class StopwatchNotificationService extends Service { mNoteBuilder = new NotificationCompat.Builder(this) .setSmallIcon(R.drawable.ic_stopwatch_24dp) .setOngoing(true) - // TODO: The chronometer takes the place of the 'when' timestamp - // at its usual location. If you don't like this location, - // we can write a thread that posts a new notification every second - // that updates the content text. - // TODO: We would have to write our own chronometer logic if there - // is no way to pause/resume the native chronometer. - .setUsesChronometer(true) +// .setUsesChronometer(true) // No way to pause/resume this native chronometer. .setContentTitle(getString(R.string.stopwatch)); Intent intent = new Intent(this, MainActivity.class); intent.putExtra(null/*TODO:MainActivity.EXTRA_SHOW_PAGE*/, 2/*TODO:MainActivity.INDEX_STOPWATCH*/); @@ -58,6 +54,7 @@ public class StopwatchNotificationService extends Service { @Override public void onDestroy() { super.onDestroy(); + quitThread(); } @Override @@ -107,4 +104,14 @@ public class StopwatchNotificationService extends Service { intent, 0/*no flags*/); mNoteBuilder.addAction(icon, actionTitle, pi); } + + /** + * Causes the handler thread's looper to terminate without processing + * any more messages in the message queue. + */ + private void quitThread() { + if (mThread != null && mThread.isAlive()) { + mThread.quit(); + } + } } diff --git a/app/src/main/java/com/philliphsu/clock2/timers/ChronometerDelegate.java b/app/src/main/java/com/philliphsu/clock2/timers/ChronometerDelegate.java index db30ed9..a69b0df 100644 --- a/app/src/main/java/com/philliphsu/clock2/timers/ChronometerDelegate.java +++ b/app/src/main/java/com/philliphsu/clock2/timers/ChronometerDelegate.java @@ -2,6 +2,7 @@ package com.philliphsu.clock2.timers; import android.content.res.Resources; import android.os.SystemClock; +import android.support.annotation.Nullable; import android.text.SpannableString; import android.text.Spanned; import android.text.TextUtils; @@ -80,16 +81,21 @@ public final class ChronometerDelegate { return mFormat; } - public CharSequence formatElapsedTime(long now, Resources resources) { + public CharSequence formatElapsedTime(long now, @Nullable Resources resources) { mNow = now; long seconds = mCountDown ? mBase - now : now - mBase; boolean negative = false; - if (seconds < 0) { + // Only modify how negative timers are displayed if we have a Resources. + // Otherwise, if we invert the sign of seconds and we don't have a Resources, + // the timer will be positive again which will confuse the user. + if (seconds < 0 && resources != null) { seconds = -seconds; negative = true; } String text = DateUtils.formatElapsedTime(mRecycle, seconds / 1000); if (negative) { + // The only way this can call through is if the previous null check on + // `resources` passed. text = resources.getString(R.string.negative_duration, text); } diff --git a/app/src/main/java/com/philliphsu/clock2/timers/TimerNotificationService.java b/app/src/main/java/com/philliphsu/clock2/timers/TimerNotificationService.java index 1c200b0..e30b295 100644 --- a/app/src/main/java/com/philliphsu/clock2/timers/TimerNotificationService.java +++ b/app/src/main/java/com/philliphsu/clock2/timers/TimerNotificationService.java @@ -1,22 +1,15 @@ package com.philliphsu.clock2.timers; -import android.annotation.SuppressLint; import android.app.NotificationManager; import android.app.PendingIntent; -import android.app.Service; import android.content.Context; import android.content.Intent; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.os.Message; import android.os.SystemClock; -import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.v4.app.NotificationCompat; -import android.util.Log; import com.philliphsu.clock2.AsyncTimersTableUpdateHandler; +import com.philliphsu.clock2.ChronometerNotificationService; import com.philliphsu.clock2.R; import com.philliphsu.clock2.Timer; @@ -27,21 +20,16 @@ import com.philliphsu.clock2.Timer; * a single task and immediately destroy itself, which means we lose all of * our instance state. */ -public class TimerNotificationService extends Service { +public class TimerNotificationService extends ChronometerNotificationService { private static final String TAG = "TimerNotifService"; public static final String ACTION_ADD_ONE_MINUTE = "com.philliphsu.clock2.timers.action.ADD_ONE_MINUTE"; - public static final String ACTION_START_PAUSE = "com.philliphsu.clock2.timers.action.START_PAUSE"; - public static final String ACTION_STOP = "com.philliphsu.clock2.timers.action.STOP"; public static final String EXTRA_TIMER = "com.philliphsu.clock2.timers.extra.TIMER"; - private Timer mTimer; // TODO: I think we may need a list of timers. + // TODO: I think we may need a list of timers. + private Timer mTimer; private TimerController mController; - private NotificationCompat.Builder mNoteBuilder; - private NotificationManager mNotificationManager; - private final ChronometerDelegate mCountdownDelegate = new ChronometerDelegate(); - private MyHandlerThread mThread; // TODO: I think we may need a list of threads. /** * Helper method to start this Service for its default action: to show @@ -62,29 +50,23 @@ public class TimerNotificationService extends Service { * you want to cancel */ public static void cancelNotification(Context context, long timerId) { // TODO: remove long param - NotificationManager nm = (NotificationManager) - context.getSystemService(Context.NOTIFICATION_SERVICE); - nm.cancel(TAG, (int) timerId); + // TODO: We do this in onDestroy() for a single notification. + // Multiples will probably need something like this. +// NotificationManager nm = (NotificationManager) +// context.getSystemService(Context.NOTIFICATION_SERVICE); +// nm.cancel(getNoteTag(), (int) timerId); + // TODO: We only do this for a single notification. Remove this for multiples. context.stopService(new Intent(context, TimerNotificationService.class)); - // After being cancelled due to time being up, sometimes the active timer notification posts again - // with a static 00:00 text, along with the Time's up notification. My theory is - // our thread has enough leeway to sneak in a final call to post the notification before it - // is actually quit(). - // TODO: Write an API in MyHandlerThread that removes its messages. Then, write and - // handle a service command that calls that API. Alternatively, we may not need the - // command because we could just call that API in your quitThread() helper method. } @Override - public void onCreate() { - super.onCreate(); - mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + protected int getSmallIcon() { + return R.drawable.ic_timer_24dp; + } - // Create base note - mNoteBuilder = new NotificationCompat.Builder(this) - .setSmallIcon(R.drawable.ic_timer_24dp) - .setShowWhen(false) - .setOngoing(true); + @Nullable + @Override + protected PendingIntent getContentIntent() { // TODO: Set content intent so that when clicked, we launch // TimersFragment and scroll to the given timer id. The following // is merely pseudocode. @@ -99,176 +81,86 @@ public class TimerNotificationService extends Service { // // per notification when retrieving the PendingIntent.. // contentIntent, // 0/*Shouldn't need a flag..*/)); + return null; + } - mCountdownDelegate.init(); - mCountdownDelegate.setCountDown(true); + @Override + protected boolean isCountDown() { + return true; } @Override public void onDestroy() { super.onDestroy(); - quitThread(); + // After being cancelled due to time being up, sometimes the active timer notification posts again + // with a static 00:00 text, along with the Time's up notification. My theory is + // our thread has enough leeway to sneak in a final call to post the notification before it + // is actually quit(). + // As such, try cancelling the notification with this (tag, id) pair again. + NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.cancel(getNoteTag(), mTimer.getIntId()); } @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (intent != null) { - final String action = intent.getAction(); - if (action == null) { - if ((mTimer = intent.getParcelableExtra(EXTRA_TIMER)) == null) { - throw new IllegalStateException("Cannot start TimerNotificationService without a Timer"); - } - mController = new TimerController(mTimer, new AsyncTimersTableUpdateHandler(this, null)); - // The note's title should change here every time, - // especially if the Timer's label was updated. - String title = mTimer.label(); - if (title.isEmpty()) { - title = getString(R.string.timer); - } - mNoteBuilder.setContentTitle(title); - syncNotificationWithTimerState(mTimer.isRunning()); - } else if (ACTION_ADD_ONE_MINUTE.equals(action)) { - // While the notification's countdown would automatically be extended by one minute, - // there is a noticeable delay before the minute gets added on. - // Update the text immediately, because there's no harm in doing so. - mCountdownDelegate.setBase(mCountdownDelegate.getBase() + 60000); - // Dispatch a one-time (non-looping) message so as not to conflate - // with the current set of looping messages. - mThread.sendMessage(MSG_DISPATCH_TICK); - mController.addOneMinute(); - } else if (ACTION_START_PAUSE.equals(action)) { - mController.startPause(); - syncNotificationWithTimerState(mTimer.isRunning()); - } else if (ACTION_STOP.equals(action)) { - mController.stop(); - stopSelf(); - // We leave removing the notification up to AsyncTimersTableUpdateHandler - // when it calls cancelAlarm() from onPostAsyncUpdate(). - } + protected void handleDefaultAction(Intent intent, int flags, long startId) { + if ((mTimer = intent.getParcelableExtra(EXTRA_TIMER)) == null) { + throw new IllegalStateException("Cannot start TimerNotificationService without a Timer"); } - return super.onStartCommand(intent, flags, startId); + mController = new TimerController(mTimer, new AsyncTimersTableUpdateHandler(this, null)); + // The note's title should change here every time, + // especially if the Timer's label was updated. + String title = mTimer.label(); + if (title.isEmpty()) { + title = getString(R.string.timer); + } + setContentTitle(title); + syncNotificationWithTimerState(mTimer.isRunning()); } - @Nullable @Override - public IBinder onBind(Intent intent) { - return null; + protected void handleStartPauseAction(Intent intent, int flags, long startId) { + mController.startPause(); + syncNotificationWithTimerState(mTimer.isRunning()); + } + + @Override + protected void handleStopAction(Intent intent, int flags, long startId) { + mController.stop(); + stopSelf(); + // We leave removing the notification up to AsyncTimersTableUpdateHandler + // when it calls cancelAlarm() from onPostAsyncUpdate(). + } + + @Override + protected void handleAction(@NonNull String action, Intent intent, int flags, long startId) { + if (ACTION_ADD_ONE_MINUTE.equals(action)) { + // While the notification's countdown would automatically be extended by one minute, + // there is a noticeable delay before the minute gets added on. + // Update the text immediately, because there's no harm in doing so. + setBase(getBase() + 60000); + updateNotification(); + mController.addOneMinute(); + } else { + throw new IllegalArgumentException("TimerNotificationService cannot handle action " + action); + } } private void syncNotificationWithTimerState(boolean running) { // The actions from the last time we configured the Builder are still here. // We have to retain the relative ordering of the actions while updating // just the start/pause action, so clear them and set them again. - // TODO: The source indicates mActions is hidden, so how are we able to access it? - // Will it remain accessible for all SDK versions? If not, we would have to rebuild - // the entire notification with a new local Builder instance. - mNoteBuilder.mActions.clear(); - addAction(ACTION_ADD_ONE_MINUTE, R.drawable.ic_add_24dp, getString(R.string.minute)); - addAction(ACTION_START_PAUSE, - running ? R.drawable.ic_pause_24dp : R.drawable.ic_start_24dp, - getString(running ? R.string.pause : R.string.resume)); - addAction(ACTION_STOP, R.drawable.ic_stop_24dp, getString(R.string.stop)); - - // Post the notification immediately, as the HandlerThread will delay its first - // message delivery. - updateNotification(); - // Quit any previously executed thread. If running == false, the effect is obvious; - // otherwise, we're preparing for the start of a new thread. - quitThread(); + clearActions(); + final int timerId = mTimer.getIntId(); + addAction(ACTION_ADD_ONE_MINUTE, + R.drawable.ic_add_24dp, + getString(R.string.minute), + timerId); + addStartPauseAction(running, timerId); + addStopAction(timerId); + quitCurrentThread(); if (running) { - // An instance of Thread cannot be started more than once. You must create - // a new instance if you want to start the Thread's work again. - mThread = new MyHandlerThread(); - // Initializes this thread as a looper. HandlerThread.run() will be executed - // in this thread. - // This gives you a chance to create handlers that then reference this looper, - // before actually starting the loop. - mThread.start(); - // If this thread has been started, this method will block *the calling thread* - // until the looper has been initialized. This ensures the handler thread is - // fully initialized before we proceed. - mThread.getLooper(); - Log.d(TAG, "Looper initialized"); - mCountdownDelegate.setBase(SystemClock.elapsedRealtime() + mTimer.timeRemaining()); - mThread.sendMessage(MSG_WHAT); - } - } - - /** - * Builds and adds the specified action to the notification's mNoteBuilder. - */ - private void addAction(String action, @DrawableRes int icon, String actionTitle) { - Intent intent = new Intent(this, TimerNotificationService.class) - .setAction(action); -// .putExtra(EXTRA_TIMER, mTimer); - PendingIntent pi = PendingIntent.getService(this, - mTimer.getIntId(), intent, 0/*no flags*/); - mNoteBuilder.addAction(icon, actionTitle, pi); - } - - /** - * Causes the handler thread's looper to terminate without processing - * any more messages in the message queue. - */ - private void quitThread() { - if (mThread != null && mThread.isAlive()) { - mThread.quit(); - } - } - - private void updateNotification() { - CharSequence text = mCountdownDelegate.formatElapsedTime(SystemClock.elapsedRealtime(), getResources()); - mNoteBuilder.setContentText(text); - mNotificationManager.notify(TAG, mTimer.getIntId(), mNoteBuilder.build()); - } - - private static final int MSG_WHAT = 2; - private static final int MSG_DISPATCH_TICK = 3; - - private class MyHandlerThread extends HandlerThread { - private Handler mHandler; - - public MyHandlerThread() { - super("MyHandlerThread"); - } - - // There won't be a memory leak since our handler is using a looper that is not - // associated with the main thread. The full Lint warning confirmed this. - @SuppressLint("HandlerLeak") - @Override - protected void onLooperPrepared() { - Log.d(TAG, "Looper fully prepared"); - // This is called after the looper has completed initializing, but before - // it starts looping through its message queue. Right now, there is no - // message queue, so this is the place to create it. - // By default, the constructor associates this handler with this thread's looper. - mHandler = new Handler() { - @Override - public void handleMessage(Message m) { - updateNotification(); - if (m.what != MSG_DISPATCH_TICK) { - sendMessageDelayed(Message.obtain(this, MSG_WHAT), 1000); - } - } - }; - } - - public void sendMessage(int what) { - // We've encountered NPEs because the handler was still - // uninitialized even at this point. I assume we cannot rely on any - // defined order in which different threads execute their code. - // Block the calling thread from proceeding until the handler thread - // completes the handler's initialization. - while (mHandler == null); - - Log.d(TAG, "Sending message"); - Message msg = Message.obtain(mHandler, what); - if (what == MSG_DISPATCH_TICK) { - mHandler.sendMessage(msg); - } else if (what == MSG_WHAT) { - mHandler.sendMessageDelayed(msg, 1000); - } + startNewThread(timerId, SystemClock.elapsedRealtime() + mTimer.timeRemaining()); } } } \ No newline at end of file