Created ChronometerNotificationService and ChronometerNotificationThread. Modify TimerNotificationService to extend from ChronometerNotificationService.
This commit is contained in:
parent
02fbee2f7b
commit
31e0a71d9f
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ import android.os.IBinder;
|
|||||||
import android.support.annotation.DrawableRes;
|
import android.support.annotation.DrawableRes;
|
||||||
import android.support.v4.app.NotificationCompat;
|
import android.support.v4.app.NotificationCompat;
|
||||||
|
|
||||||
|
import com.philliphsu.clock2.ChronometerNotificationThread;
|
||||||
import com.philliphsu.clock2.MainActivity;
|
import com.philliphsu.clock2.MainActivity;
|
||||||
import com.philliphsu.clock2.R;
|
import com.philliphsu.clock2.R;
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ public class StopwatchNotificationService extends Service {
|
|||||||
private NotificationCompat.Builder mNoteBuilder;
|
private NotificationCompat.Builder mNoteBuilder;
|
||||||
private NotificationManager mNotificationManager;
|
private NotificationManager mNotificationManager;
|
||||||
private AsyncLapsTableUpdateHandler mLapsTableUpdateHandler;
|
private AsyncLapsTableUpdateHandler mLapsTableUpdateHandler;
|
||||||
|
private ChronometerNotificationThread mThread;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
@ -33,13 +35,7 @@ public class StopwatchNotificationService extends Service {
|
|||||||
mNoteBuilder = new NotificationCompat.Builder(this)
|
mNoteBuilder = new NotificationCompat.Builder(this)
|
||||||
.setSmallIcon(R.drawable.ic_stopwatch_24dp)
|
.setSmallIcon(R.drawable.ic_stopwatch_24dp)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
// TODO: The chronometer takes the place of the 'when' timestamp
|
// .setUsesChronometer(true) // No way to pause/resume this native chronometer.
|
||||||
// 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)
|
|
||||||
.setContentTitle(getString(R.string.stopwatch));
|
.setContentTitle(getString(R.string.stopwatch));
|
||||||
Intent intent = new Intent(this, MainActivity.class);
|
Intent intent = new Intent(this, MainActivity.class);
|
||||||
intent.putExtra(null/*TODO:MainActivity.EXTRA_SHOW_PAGE*/, 2/*TODO:MainActivity.INDEX_STOPWATCH*/);
|
intent.putExtra(null/*TODO:MainActivity.EXTRA_SHOW_PAGE*/, 2/*TODO:MainActivity.INDEX_STOPWATCH*/);
|
||||||
@ -58,6 +54,7 @@ public class StopwatchNotificationService extends Service {
|
|||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
|
quitThread();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -107,4 +104,14 @@ public class StopwatchNotificationService extends Service {
|
|||||||
intent, 0/*no flags*/);
|
intent, 0/*no flags*/);
|
||||||
mNoteBuilder.addAction(icon, actionTitle, pi);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.philliphsu.clock2.timers;
|
|||||||
|
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
@ -80,16 +81,21 @@ public final class ChronometerDelegate {
|
|||||||
return mFormat;
|
return mFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CharSequence formatElapsedTime(long now, Resources resources) {
|
public CharSequence formatElapsedTime(long now, @Nullable Resources resources) {
|
||||||
mNow = now;
|
mNow = now;
|
||||||
long seconds = mCountDown ? mBase - now : now - mBase;
|
long seconds = mCountDown ? mBase - now : now - mBase;
|
||||||
boolean negative = false;
|
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;
|
seconds = -seconds;
|
||||||
negative = true;
|
negative = true;
|
||||||
}
|
}
|
||||||
String text = DateUtils.formatElapsedTime(mRecycle, seconds / 1000);
|
String text = DateUtils.formatElapsedTime(mRecycle, seconds / 1000);
|
||||||
if (negative) {
|
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);
|
text = resources.getString(R.string.negative_duration, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,22 +1,15 @@
|
|||||||
package com.philliphsu.clock2.timers;
|
package com.philliphsu.clock2.timers;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
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.os.SystemClock;
|
||||||
import android.support.annotation.DrawableRes;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.NotificationCompat;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.philliphsu.clock2.AsyncTimersTableUpdateHandler;
|
import com.philliphsu.clock2.AsyncTimersTableUpdateHandler;
|
||||||
|
import com.philliphsu.clock2.ChronometerNotificationService;
|
||||||
import com.philliphsu.clock2.R;
|
import com.philliphsu.clock2.R;
|
||||||
import com.philliphsu.clock2.Timer;
|
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
|
* a single task and immediately destroy itself, which means we lose all of
|
||||||
* our instance state.
|
* our instance state.
|
||||||
*/
|
*/
|
||||||
public class TimerNotificationService extends Service {
|
public class TimerNotificationService extends ChronometerNotificationService {
|
||||||
private static final String TAG = "TimerNotifService";
|
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_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";
|
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 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
|
* 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
|
* you want to cancel
|
||||||
*/
|
*/
|
||||||
public static void cancelNotification(Context context, long timerId) { // TODO: remove long param
|
public static void cancelNotification(Context context, long timerId) { // TODO: remove long param
|
||||||
NotificationManager nm = (NotificationManager)
|
// TODO: We do this in onDestroy() for a single notification.
|
||||||
context.getSystemService(Context.NOTIFICATION_SERVICE);
|
// Multiples will probably need something like this.
|
||||||
nm.cancel(TAG, (int) timerId);
|
// 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));
|
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
|
@Override
|
||||||
public void onCreate() {
|
protected int getSmallIcon() {
|
||||||
super.onCreate();
|
return R.drawable.ic_timer_24dp;
|
||||||
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
}
|
||||||
|
|
||||||
// Create base note
|
@Nullable
|
||||||
mNoteBuilder = new NotificationCompat.Builder(this)
|
@Override
|
||||||
.setSmallIcon(R.drawable.ic_timer_24dp)
|
protected PendingIntent getContentIntent() {
|
||||||
.setShowWhen(false)
|
|
||||||
.setOngoing(true);
|
|
||||||
// TODO: Set content intent so that when clicked, we launch
|
// TODO: Set content intent so that when clicked, we launch
|
||||||
// TimersFragment and scroll to the given timer id. The following
|
// TimersFragment and scroll to the given timer id. The following
|
||||||
// is merely pseudocode.
|
// is merely pseudocode.
|
||||||
@ -99,22 +81,28 @@ public class TimerNotificationService extends Service {
|
|||||||
// // per notification when retrieving the PendingIntent..
|
// // per notification when retrieving the PendingIntent..
|
||||||
// contentIntent,
|
// contentIntent,
|
||||||
// 0/*Shouldn't need a flag..*/));
|
// 0/*Shouldn't need a flag..*/));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
mCountdownDelegate.init();
|
@Override
|
||||||
mCountdownDelegate.setCountDown(true);
|
protected boolean isCountDown() {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
super.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
|
@Override
|
||||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
protected void handleDefaultAction(Intent intent, int flags, long startId) {
|
||||||
if (intent != null) {
|
|
||||||
final String action = intent.getAction();
|
|
||||||
if (action == null) {
|
|
||||||
if ((mTimer = intent.getParcelableExtra(EXTRA_TIMER)) == null) {
|
if ((mTimer = intent.getParcelableExtra(EXTRA_TIMER)) == null) {
|
||||||
throw new IllegalStateException("Cannot start TimerNotificationService without a Timer");
|
throw new IllegalStateException("Cannot start TimerNotificationService without a Timer");
|
||||||
}
|
}
|
||||||
@ -125,150 +113,54 @@ public class TimerNotificationService extends Service {
|
|||||||
if (title.isEmpty()) {
|
if (title.isEmpty()) {
|
||||||
title = getString(R.string.timer);
|
title = getString(R.string.timer);
|
||||||
}
|
}
|
||||||
mNoteBuilder.setContentTitle(title);
|
setContentTitle(title);
|
||||||
syncNotificationWithTimerState(mTimer.isRunning());
|
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.
|
@Override
|
||||||
// Update the text immediately, because there's no harm in doing so.
|
protected void handleStartPauseAction(Intent intent, int flags, long startId) {
|
||||||
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();
|
mController.startPause();
|
||||||
syncNotificationWithTimerState(mTimer.isRunning());
|
syncNotificationWithTimerState(mTimer.isRunning());
|
||||||
} else if (ACTION_STOP.equals(action)) {
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void handleStopAction(Intent intent, int flags, long startId) {
|
||||||
mController.stop();
|
mController.stop();
|
||||||
stopSelf();
|
stopSelf();
|
||||||
// We leave removing the notification up to AsyncTimersTableUpdateHandler
|
// We leave removing the notification up to AsyncTimersTableUpdateHandler
|
||||||
// when it calls cancelAlarm() from onPostAsyncUpdate().
|
// when it calls cancelAlarm() from onPostAsyncUpdate().
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return super.onStartCommand(intent, flags, startId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
@Override
|
||||||
public IBinder onBind(Intent intent) {
|
protected void handleAction(@NonNull String action, Intent intent, int flags, long startId) {
|
||||||
return null;
|
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) {
|
private void syncNotificationWithTimerState(boolean running) {
|
||||||
// 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.
|
||||||
// TODO: The source indicates mActions is hidden, so how are we able to access it?
|
clearActions();
|
||||||
// Will it remain accessible for all SDK versions? If not, we would have to rebuild
|
final int timerId = mTimer.getIntId();
|
||||||
// the entire notification with a new local Builder instance.
|
addAction(ACTION_ADD_ONE_MINUTE,
|
||||||
mNoteBuilder.mActions.clear();
|
R.drawable.ic_add_24dp,
|
||||||
addAction(ACTION_ADD_ONE_MINUTE, R.drawable.ic_add_24dp, getString(R.string.minute));
|
getString(R.string.minute),
|
||||||
addAction(ACTION_START_PAUSE,
|
timerId);
|
||||||
running ? R.drawable.ic_pause_24dp : R.drawable.ic_start_24dp,
|
addStartPauseAction(running, timerId);
|
||||||
getString(running ? R.string.pause : R.string.resume));
|
addStopAction(timerId);
|
||||||
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();
|
|
||||||
|
|
||||||
|
quitCurrentThread();
|
||||||
if (running) {
|
if (running) {
|
||||||
// An instance of Thread cannot be started more than once. You must create
|
startNewThread(timerId, SystemClock.elapsedRealtime() + mTimer.timeRemaining());
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user