Implement multiple threads and chronometers. Check that StopwatchNotificationService still works.

This commit is contained in:
Phillip Hsu 2016-09-17 20:13:51 -07:00
parent 6c1f10da8d
commit ced47b7fbe
3 changed files with 105 additions and 90 deletions

View File

@ -22,20 +22,18 @@ 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_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 ACTION_STOP = "com.philliphsu.clock2.timers.action.STOP";
public static final String EXTRA_ID = "com.philliphsu.clock2.extra.ID";
// TODO: I think we'll need a collection of builders too. However, we can have a common immutable // TODO: I think we'll need a collection of builders too. However, we can have a common immutable
// builder instance with attributes that all timer notifications will have. // builder instance with attributes that all timer notifications will have.
private NotificationCompat.Builder mNoteBuilder; // private NotificationCompat.Builder mNoteBuilder;
private NotificationManager mNotificationManager; private NotificationManager mNotificationManager;
@Deprecated
private ChronometerNotificationThread mThread;
@Deprecated
private final ChronometerDelegate mDelegate = new ChronometerDelegate();
/** /**
* The default capacity of an array map is 0. * The default capacity of an array map is 0.
* The minimum amount by which the capacity of a ArrayMap will increase * The minimum amount by which the capacity of a ArrayMap will increase
* is currently {@link SimpleArrayMap#BASE_SIZE 4}. * is currently {@link SimpleArrayMap#BASE_SIZE 4}.
*/ */
private final SimpleArrayMap<Long, NotificationCompat.Builder> mNoteBuilders = new SimpleArrayMap<>();
private final SimpleArrayMap<Long, ChronometerNotificationThread> mThreads = new SimpleArrayMap<>(); private final SimpleArrayMap<Long, ChronometerNotificationThread> mThreads = new SimpleArrayMap<>();
private final SimpleArrayMap<Long, ChronometerDelegate> mDelegates = new SimpleArrayMap<>(); private final SimpleArrayMap<Long, ChronometerDelegate> mDelegates = new SimpleArrayMap<>();
@ -94,25 +92,32 @@ public abstract class ChronometerNotificationService extends Service {
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// Create base note
mNoteBuilder = new NotificationCompat.Builder(this)
.setSmallIcon(getSmallIcon())
.setShowWhen(false)
.setOngoing(true)
.setContentIntent(getContentIntent());
if (isForeground()) { if (isForeground()) {
registerNewNoteBuilder(getNoteId());
registerNewChronometer(getNoteId()); registerNewChronometer(getNoteId());
startForeground(getNoteId(), mNoteBuilder.build()); // IGNORE THE LINT WARNING ABOUT UNNECESSARY BOXING. Because getNoteId() returns an int,
// it gets boxed to an Integer. A Long and an Integer are never interchangeable, even
// if they wrap the same integer value.
startForeground(getNoteId(), mNoteBuilders.get(Long.valueOf(getNoteId())).build());
} }
} }
private void registerNewChronometer(long id) { protected final void registerNewChronometer(long id) {
ChronometerDelegate delegate = new ChronometerDelegate(); ChronometerDelegate delegate = new ChronometerDelegate();
delegate.init(); delegate.init();
delegate.setCountDown(isCountDown()); delegate.setCountDown(isCountDown());
mDelegates.put(id, delegate); mDelegates.put(id, delegate);
} }
protected final void registerNewNoteBuilder(long id) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon(getSmallIcon())
.setShowWhen(false)
.setOngoing(true)
.setContentIntent(getContentIntent());
mNoteBuilders.put(id, builder);
}
// Didn't work! // Didn't work!
// @Override // @Override
// public void onTrimMemory(int level) { // public void onTrimMemory(int level) {
@ -132,8 +137,10 @@ public abstract class ChronometerNotificationService extends Service {
@Override @Override
public void onDestroy() { public void onDestroy() {
// TODO: Quit all threads by iterating through the collection for (int i = 0; i < mThreads.size(); i++) {
quitThread(); // TOneverDO: quitCurrentThread() because that posts the notification again // TOneverDO: quitCurrentThread() because that posts the notification again
quitThread(mThreads.keyAt(i));
}
} }
@CallSuper @CallSuper
@ -171,17 +178,18 @@ public abstract class ChronometerNotificationService extends Service {
* If there is a thread currently running, then this will push any notification updates * 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 * you might have configured in the Builder and then call the thread's {@link
* ChronometerNotificationThread#quit() quit()}. * ChronometerNotificationThread#quit() quit()}.
* @param id the id associated with the thread to quit
*/ */
// TODO: rename method to quitThread(long id) public void quitCurrentThread(long id) {
public void quitCurrentThread() { ChronometerNotificationThread thread = mThreads.get(id);
if (mThread != null) { if (thread != null) {
// Display any notification updates associated with the current state // Display any notification updates associated with the current state
// of the chronometer. If we relied on the HandlerThread to do this for us, // of the chronometer. If we relied on the HandlerThread to do this for us,
// the message delivery would be delayed. // the message delivery would be delayed.
mThread.updateNotification(false/*updateText*/); thread.updateNotification(/*TODO:pass in id*/false/*updateText*/);
// If the chronometer has been set to not run, the effect is obvious. // If the chronometer has been set to not run, the effect is obvious.
// Otherwise, we're preparing for the start of a new thread. // Otherwise, we're preparing for the start of a new thread.
quitThread(); quitThread(id);
} }
} }
@ -189,116 +197,130 @@ public abstract class ChronometerNotificationService extends Service {
* Instantiates a new HandlerThread and calls its {@link Thread#start() start()}. * Instantiates a new HandlerThread and calls its {@link Thread#start() start()}.
* The calling thread will be blocked until the HandlerThread created here finishes * The calling thread will be blocked until the HandlerThread created here finishes
* initializing its looper. * initializing its looper.
* @param id
* @param base the new base time of the chronometer * @param base the new base time of the chronometer
*/ */
// TODO: Change sig to (long id, long base) public void startNewThread(long id, long base) {
public void startNewThread(long base) {
// An instance of Thread cannot be started more than once. You must create // 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. // a new instance if you want to start the Thread's work again.
mThread = new ChronometerNotificationThread( ChronometerNotificationThread thread = new ChronometerNotificationThread(
mDelegate, mDelegates.get(id),
mNotificationManager, mNotificationManager,
mNoteBuilder, mNoteBuilders.get(id),
getResources(), getResources(),
getNoteId()); getNoteId());
mThreads.put(id, thread);
// Initializes this thread as a looper. HandlerThread.run() will be executed // Initializes this thread as a looper. HandlerThread.run() will be executed
// in this thread. // in this thread.
// This gives you a chance to create handlers that then reference this looper, // This gives you a chance to create handlers that then reference this looper,
// before actually starting the loop. // before actually starting the loop.
mThread.start(); thread.start();
// If this thread has been started, this method will block *the calling thread* // 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 // until the looper has been initialized. This ensures the handler thread is
// fully initialized before we proceed. // fully initialized before we proceed.
mThread.getLooper(); thread.getLooper();
// ------------------------------------------------------------------------------- // -------------------------------------------------------------------------------
// TOneverDO: Set base BEFORE the thread is ready to begin working, or else when // 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 // the thread actually begins working, it will initially show that some time has
// passed. // passed.
mDelegate.setBase(base); ChronometerDelegate delegate = mDelegates.get(id);
delegate.setBase(base);
// ------------------------------------------------------------------------------- // -------------------------------------------------------------------------------
} }
/** /**
* Helper method to add the start/pause action to the notification's builder. * Helper method to add the start/pause action to the notification's builder.
* @param running whether the chronometer is running * @param running whether the chronometer is running
* @param requestCode Used to create the PendingIntent that is fired when this action is clicked. * @param id The id of the notification that the action should be added to.
* Will be used as an integer request code to create the PendingIntent that
* is fired when this action is clicked.
*/ */
protected final void addStartPauseAction(boolean running, int requestCode/*TODO: long id. as a request code, cast down.*/) { protected final void addStartPauseAction(boolean running, long id) {
// TODO: Add this to the correct Builder, associated with the provided long id.
addAction(ACTION_START_PAUSE, addAction(ACTION_START_PAUSE,
running ? R.drawable.ic_pause_24dp : R.drawable.ic_start_24dp, running ? R.drawable.ic_pause_24dp : R.drawable.ic_start_24dp,
getString(running ? R.string.pause : R.string.resume), getString(running ? R.string.pause : R.string.resume),
requestCode); id);
} }
/** /**
* Helper method to add the stop action to the notification's builder. * 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. * @param id The id of the notification that the action should be added to.
* Will be used as an integer request code to create the PendingIntent that
* is fired when this action is clicked.
*/ */
protected final void addStopAction(int requestCode/*TODO: long id. as a request code, cast down.*/) { protected final void addStopAction(long id) {
addAction(ACTION_STOP, R.drawable.ic_stop_24dp, getString(R.string.stop), requestCode); addAction(ACTION_STOP, R.drawable.ic_stop_24dp, getString(R.string.stop), id);
} }
/** /**
* Clear the notification builder's set actions. * Clear the notification builder's set actions.
* @param id the id associated with the builder whose actions should be cleared
*/ */
protected final void clearActions(/*TODO: long id*/) { protected final void clearActions(long id) {
// TODO: Clear the actions from the correct builder.
// TODO: The source indicates mActions is hidden, so how are we able to access it? // 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 // Will it remain accessible for all SDK versions? If not, we would have to rebuild
// the entire notification with a new local Builder instance. // the entire notification with a new local Builder instance.
mNoteBuilder.mActions.clear(); mNoteBuilders.get(id).mActions.clear();
} }
// TODO: We'll need to change the signatures of all these to have a long id param. /**
protected final void setBase(long base) { * @param id The id associated with the chronometer that you wish to modify.
mDelegate.setBase(base); */
protected final void setBase(long id, long base) {
mDelegates.get(id).setBase(base);
} }
protected final long getBase() { /**
return mDelegate.getBase(); * @param id The id associated with the chronometer that you wish to modify.
*/
protected final long getBase(long id) {
return mDelegates.get(id).getBase();
} }
protected final void updateNotification(boolean updateText) { /**
mThread.updateNotification(updateText); * @param id The id associated with the thread that should update the notification.
*/
protected final void updateNotification(long id, boolean updateText) {
mThreads.get(id).updateNotification(updateText);
} }
protected final void setContentTitle(CharSequence title) { /**
mNoteBuilder.setContentTitle(title); * @param id The id associated with the builder that should update its content title.
*/
protected final void setContentTitle(long id, CharSequence title) {
mNoteBuilders.get(id).setContentTitle(title);
} }
//////////////////////////////////////////////////////////////////////////////////////
/** /**
* Adds the specified action to the notification's Builder. * Adds the specified action to the notification's Builder.
* @param id The id of the notification that the action should be added to.
* Will be used as an integer request code to create the PendingIntent that
* is fired when this action is clicked.
*/ */
protected final void addAction(String action, @DrawableRes int icon, String actionTitle, int requestCode/*TODO: long id. as a request code, cast down.*/) { protected final void addAction(String action, @DrawableRes int icon, String actionTitle, long id) {
Intent intent = new Intent(this, getClass()) Intent intent = new Intent(this, getClass())
.setAction(action); .setAction(action)
// TODO: We can put the requestCode as an extra to this intent, and then retrieve that extra .putExtra(EXTRA_ID, id);
// in onStartCommand() to figure out which of the multiple timers should we apply this action to.
// .putExtra(EXTRA_TIMER, mTimer);
PendingIntent pi = PendingIntent.getService( PendingIntent pi = PendingIntent.getService(
this, requestCode, intent, 0/*no flags*/); this, (int) id, intent, 0/*no flags*/);
mNoteBuilder.addAction(icon, actionTitle, pi); mNoteBuilders.get(id).addAction(icon, actionTitle, pi);
} }
/** /**
* Cancels the foreground notification. * Cancels the notification associated with the ID.
*/ */
// TODO: change sig to long id protected final void cancelNotification(long id/*TODO: change to int noteId?*/) {
protected final void cancelNotification() { mNotificationManager.cancel((int) id);
mNotificationManager.cancel(getNoteId());
} }
/** /**
* Causes the handler thread's looper to terminate without processing * Causes the handler thread's looper to terminate without processing
* any more messages in the message queue. * any more messages in the message queue.
*/ */
// TODO: change sig to long id private void quitThread(long id) {
private void quitThread() { ChronometerNotificationThread thread = mThreads.get(id);
if (mThread != null && mThread.isAlive()) { if (thread != null && thread.isAlive()) {
mThread.quit(); thread.quit();
} }
} }
} }

View File

@ -30,7 +30,7 @@ public class StopwatchNotificationService extends ChronometerNotificationService
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
setContentTitle(getString(R.string.stopwatch)); setContentTitle(getNoteId(), getString(R.string.stopwatch));
mUpdateHandler = new AsyncLapsTableUpdateHandler(this, null); mUpdateHandler = new AsyncLapsTableUpdateHandler(this, null);
mPrefs = PreferenceManager.getDefaultSharedPreferences(this); mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mDelegate.init(); mDelegate.init();
@ -72,11 +72,6 @@ public class StopwatchNotificationService extends ChronometerNotificationService
return super.onStartCommand(intent, flags, startId); return super.onStartCommand(intent, flags, startId);
} }
@Override
public void onDestroy() {
super.onDestroy();
}
@Override @Override
protected int getSmallIcon() { protected int getSmallIcon() {
return R.drawable.ic_stopwatch_24dp; return R.drawable.ic_stopwatch_24dp;
@ -180,26 +175,23 @@ public class StopwatchNotificationService extends ChronometerNotificationService
if (lapNumber == 0) { if (lapNumber == 0) {
Log.w(TAG, "Lap number was not passed in with intent"); Log.w(TAG, "Lap number was not passed in with intent");
} }
// Unfortunately, the ID is only assigned when retrieving a Lap instance from setContentTitle(getNoteId(), getString(R.string.stopwatch_and_lap_number, lapNumber));
// its cursor; the value here will always be 0. updateNotification(getNoteId(), true);
setContentTitle(getString(R.string.stopwatch_and_lap_number, lapNumber));
updateNotification(true);
} else { } else {
throw new IllegalArgumentException("StopwatchNotificationService cannot handle action " + action); throw new IllegalArgumentException("StopwatchNotificationService cannot handle action " + action);
} }
} }
private void syncNotificationWithStopwatchState(boolean running) { private void syncNotificationWithStopwatchState(boolean running) {
clearActions(); clearActions(getNoteId());
// No request code needed, so use 0. addAction(ACTION_ADD_LAP, R.drawable.ic_add_lap_24dp, getString(R.string.lap), getNoteId());
addAction(ACTION_ADD_LAP, R.drawable.ic_add_lap_24dp, getString(R.string.lap), 0); addStartPauseAction(running, getNoteId());
addStartPauseAction(running, 0); addStopAction(getNoteId());
addStopAction(0);
quitCurrentThread(); quitCurrentThread(getNoteId());
if (running) { if (running) {
long startTime = mPrefs.getLong(StopwatchFragment.KEY_START_TIME, SystemClock.elapsedRealtime()); long startTime = mPrefs.getLong(StopwatchFragment.KEY_START_TIME, SystemClock.elapsedRealtime());
startNewThread(startTime); startNewThread(getNoteId(), startTime);
} }
} }
} }

View File

@ -108,7 +108,7 @@ public class TimerNotificationService extends ChronometerNotificationService {
// our thread has enough leeway to sneak in a final call to post the notification before it // our thread has enough leeway to sneak in a final call to post the notification before it
// is actually quit(). // is actually quit().
// As such, try cancelling the notification with this (tag, id) pair again. // As such, try cancelling the notification with this (tag, id) pair again.
cancelNotification(); cancelNotification(getNoteId());
} }
@Override @Override
@ -126,7 +126,7 @@ public class TimerNotificationService extends ChronometerNotificationService {
if (title.isEmpty()) { if (title.isEmpty()) {
title = getString(R.string.timer); title = getString(R.string.timer);
} }
setContentTitle(title); setContentTitle(mTimer.getId(), title);
syncNotificationWithTimerState(mTimer.isRunning()); syncNotificationWithTimerState(mTimer.isRunning());
} }
@ -150,8 +150,9 @@ public class TimerNotificationService extends ChronometerNotificationService {
// While the notification's countdown would automatically be extended by one minute, // While the notification's countdown would automatically be extended by one minute,
// there is a noticeable delay before the minute gets added on. // there is a noticeable delay before the minute gets added on.
// Update the text immediately, because there's no harm in doing so. // Update the text immediately, because there's no harm in doing so.
setBase(getBase() + 60000); long id = intent.getLongExtra(EXTRA_ID, -1);
updateNotification(true); setBase(id, getBase(id) + 60000);
updateNotification(id, true);
mController.addOneMinute(); mController.addOneMinute();
} else { } else {
throw new IllegalArgumentException("TimerNotificationService cannot handle action " + action); throw new IllegalArgumentException("TimerNotificationService cannot handle action " + action);
@ -162,8 +163,8 @@ public class TimerNotificationService extends ChronometerNotificationService {
// The actions from the last time we configured the Builder are still here. // 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 // We have to retain the relative ordering of the actions while updating
// just the start/pause action, so clear them and set them again. // just the start/pause action, so clear them and set them again.
clearActions(); final long timerId = mTimer.getId();
final int timerId = mTimer.getIntId(); clearActions(timerId);
addAction(ACTION_ADD_ONE_MINUTE, addAction(ACTION_ADD_ONE_MINUTE,
R.drawable.ic_add_24dp, R.drawable.ic_add_24dp,
getString(R.string.minute), getString(R.string.minute),
@ -171,9 +172,9 @@ public class TimerNotificationService extends ChronometerNotificationService {
addStartPauseAction(running, timerId); addStartPauseAction(running, timerId);
addStopAction(timerId); addStopAction(timerId);
quitCurrentThread(); quitCurrentThread(getNoteId());
if (running) { if (running) {
startNewThread(SystemClock.elapsedRealtime() + mTimer.timeRemaining()); startNewThread(getNoteId(), SystemClock.elapsedRealtime() + mTimer.timeRemaining());
} }
} }
} }