Removed unnecessary classes. Implement handling start/pause action in StopwatchNotificationService.

This commit is contained in:
Phillip Hsu 2016-09-12 01:36:04 -07:00
parent d6eeac1db1
commit 81e8e0b1ad
8 changed files with 123 additions and 264 deletions

View File

@ -135,7 +135,7 @@ public abstract class ChronometerNotificationService extends Service {
// 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();
mThread.updateNotification(false/*updateText*/);
// 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();
@ -214,8 +214,8 @@ public abstract class ChronometerNotificationService extends Service {
return mDelegate.getBase();
}
protected final void updateNotification() {
mThread.updateNotification();
protected final void updateNotification(boolean updateText) {
mThread.updateNotification(updateText);
}
protected final void setContentTitle(CharSequence title) {

View File

@ -75,7 +75,7 @@ public class ChronometerNotificationThread extends HandlerThread {
mHandler = new Handler() {
@Override
public void handleMessage(Message m) {
updateNotification();
updateNotification(true);
sendMessageDelayed(Message.obtain(this, MSG_WHAT), 1000);
}
};
@ -83,9 +83,16 @@ public class ChronometerNotificationThread extends HandlerThread {
mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_WHAT), 1000);
}
public void updateNotification() {
CharSequence text = mDelegate.formatElapsedTime(SystemClock.elapsedRealtime(), mResources);
mNoteBuilder.setContentText(text);
/**
* @param updateText whether the new notification should update its chronometer.
* Use {@code false} if you are updating everything else about the notification,
* e.g. you just want to refresh the actions due to a start/pause state change.
*/
public void updateNotification(boolean updateText) {
if (updateText) {
CharSequence text = mDelegate.formatElapsedTime(SystemClock.elapsedRealtime(), mResources);
mNoteBuilder.setContentText(text);
}
mNotificationManager.notify(mNoteTag, mNoteId, mNoteBuilder.build());
}

View File

@ -1,92 +0,0 @@
package com.philliphsu.clock2.stopwatch;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
/**
* Created by Phillip Hsu on 9/11/2016.
*/
public abstract class BaseStopwatchController { // TODO: Extend this for use in StopwatchFragment and StopwatchNotificationSErvice
// TODO: EIther expose these to subclasses or write an API for them to call
// to write to prefs.
private static final String KEY_START_TIME = "start_time";
private static final String KEY_PAUSE_TIME = "pause_time";
private static final String KEY_RUNNING = "running";
private final AsyncLapsTableUpdateHandler mUpdateHandler;
private final SharedPreferences mPrefs;
private Stopwatch mStopwatch;
private Lap mCurrentLap;
private Lap mPreviousLap;
public BaseStopwatchController(@NonNull AsyncLapsTableUpdateHandler updateHandler,
@NonNull SharedPreferences prefs) {
mUpdateHandler = updateHandler;
mPrefs = prefs;
long startTime = mPrefs.getLong(KEY_START_TIME, 0);
long pauseTime = mPrefs.getLong(KEY_PAUSE_TIME, 0);
mStopwatch = new Stopwatch(startTime, pauseTime);
}
public void run() {
if (!mStopwatch.hasStarted()) {
// addNewLap() won't call through unless chronometer is running, which
// we can't start until we compute mStartTime
mCurrentLap = new Lap();
mUpdateHandler.asyncInsert(mCurrentLap);
}
mStopwatch.run();
if (!mCurrentLap.isRunning()) {
mCurrentLap.resume();
mUpdateHandler.asyncUpdate(mCurrentLap.getId(), mCurrentLap);
}
savePrefs();
}
public void pause() {
mStopwatch.pause();
mCurrentLap.pause();
mUpdateHandler.asyncUpdate(mCurrentLap.getId(), mCurrentLap);
savePrefs();
}
public void stop() {
mStopwatch.stop();
mCurrentLap = null;
mPreviousLap = null;
mUpdateHandler.asyncClear(); // Clear laps
savePrefs();
}
public void addNewLap(String currentLapTotalText) {
if (mCurrentLap != null) {
mCurrentLap.end(currentLapTotalText);
}
mPreviousLap = mCurrentLap;
mCurrentLap = new Lap();
if (mPreviousLap != null) {
// if (getAdapter().getItemCount() == 0) {
// mUpdateHandler.asyncInsert(mPreviousLap);
// } else {
mUpdateHandler.asyncUpdate(mPreviousLap.getId(), mPreviousLap);
// }
}
mUpdateHandler.asyncInsert(mCurrentLap);
}
public final Stopwatch getStopwatch() {
return mStopwatch;
}
public boolean isStopwatchRunning() {
return mPrefs.getBoolean(KEY_RUNNING, false);
}
private void savePrefs() {
mPrefs.edit().putLong(KEY_START_TIME, mStopwatch.getStartTime())
.putLong(KEY_PAUSE_TIME, mStopwatch.getPauseTime())
.putBoolean(KEY_RUNNING, mStopwatch.isRunning())
.apply();
}
}

View File

@ -1,55 +0,0 @@
package com.philliphsu.clock2.stopwatch;
import android.os.SystemClock;
/**
* Created by Phillip Hsu on 9/11/2016.
*/
public final class Stopwatch {
private static final String TAG = "Stopwatch";
private long mStartTime;
private long mPauseTime;
public Stopwatch(long startTime, long pauseTime) {
mStartTime = startTime;
mPauseTime = pauseTime;
}
public synchronized void pause() {
if (!isRunning())
throw new IllegalStateException("This stopwatch cannot be paused because it is not running");
if (mPauseTime > 0)
throw new IllegalStateException("This stopwatch is already paused");
mPauseTime = SystemClock.elapsedRealtime();
}
public synchronized void run() {
if (isRunning())
throw new IllegalStateException("This stopwatch is already running");
mStartTime += SystemClock.elapsedRealtime() - mPauseTime;
mPauseTime = 0;
}
public synchronized void stop() {
mStartTime = 0;
mPauseTime = 0;
}
public long getStartTime() {
return mStartTime;
}
public long getPauseTime() {
return mPauseTime;
}
public boolean isRunning() {
return hasStarted() && mPauseTime == 0;
}
public boolean hasStarted() {
// Not required to be presently running to have been started
return mStartTime > 0;
}
}

View File

@ -6,6 +6,7 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton;
@ -36,17 +37,22 @@ public class StopwatchFragment extends RecyclerViewFragment<
LapsAdapter> {
private static final String TAG = "StopwatchFragment";
// Exposed for StopwatchNotificationService
static final String KEY_START_TIME = "start_time";
static final String KEY_PAUSE_TIME = "pause_time";
static final String KEY_CHRONOMETER_RUNNING = "chronometer_running";
private long mStartTime;
private long mPauseTime;
private Lap mCurrentLap;
private Lap mPreviousLap;
private AsyncLapsTableUpdateHandler mUpdateHandler;
private ObjectAnimator mProgressAnimator;
private SharedPreferences mPrefs;
private WeakReference<FloatingActionButton> mActivityFab;
private Drawable mStartDrawable;
private Drawable mPauseDrawable;
// TODO: Actual subclass
private BaseStopwatchController mController;
// For read-only purposes within this Fragment.
// Actual changes are persisted by the controller.
private Lap mCurrentLap;
private Lap mPreviousLap;
@Bind(R.id.chronometer) ChronometerWithMillis mChronometer;
@Bind(R.id.new_lap) FloatingActionButton mNewLapButton;
@ -61,6 +67,12 @@ public class StopwatchFragment extends RecyclerViewFragment<
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mUpdateHandler = new AsyncLapsTableUpdateHandler(getActivity(), null/*we shouldn't need a scroll handler*/);
mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
mStartTime = mPrefs.getLong(KEY_START_TIME, 0);
mPauseTime = mPrefs.getLong(KEY_PAUSE_TIME, 0);
Log.d(TAG, "mStartTime = " + mStartTime
+ ", mPauseTime = " + mPauseTime);
// TODO: Will these be kept alive after onDestroyView()? If not, we should move these to
// onCreateView() or any other callback that is guaranteed to be called.
mStartDrawable = ContextCompat.getDrawable(getActivity(), R.drawable.ic_start_24dp);
@ -73,13 +85,21 @@ public class StopwatchFragment extends RecyclerViewFragment<
Log.d(TAG, "onCreateView()");
View view = super.onCreateView(inflater, container, savedInstanceState);
AsyncLapsTableUpdateHandler updateHandler = new AsyncLapsTableUpdateHandler(
getActivity(), null/*we shouldn't need a scroll handler*/);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
// This can't be initialized until the layout is inflated, because we need the
// mChronometer reference for the controller.
mController = new StopwatchViewController(updateHandler, prefs, mChronometer);
mChronometer.setShowCentiseconds(true, true);
if (mStartTime > 0) {
long base = mStartTime;
if (mPauseTime > 0) {
base += SystemClock.elapsedRealtime() - mPauseTime;
// We're not done pausing yet, so don't reset mPauseTime.
}
mChronometer.setBase(base);
}
if (isStopwatchRunning()) {
mChronometer.start();
// Note: mChronometer.isRunning() will return false at this point and
// in other upcoming lifecycle methods because it is not yet visible
// (i.e. mVisible == false).
}
// The primary reason we call this is to show the mini FABs after rotate,
// if the stopwatch is running. If the stopwatch is stopped, then this
// would have hidden the mini FABs, if not for us already setting its
@ -138,6 +158,9 @@ public class StopwatchFragment extends RecyclerViewFragment<
if (mProgressAnimator != null) {
mProgressAnimator.removeAllListeners();
}
Log.d(TAG, "onDestroyView()");
Log.d(TAG, "mStartTime = " + mStartTime
+ ", mPauseTime = " + mPauseTime);
}
@Override
@ -180,8 +203,7 @@ public class StopwatchFragment extends RecyclerViewFragment<
//
// NOTE: If we just recreated ourselves due to rotation, mChronometer.isRunning() == false,
// because it is not yet visible (i.e. mVisible == false).
if (mController.isStopwatchRunning()) {
// TODO: I think we should just pass in the two laps as local params
if (isStopwatchRunning()) {
startNewProgressBarAnimator();
} else {
// I verified the bar was visible already without this, so we probably don't need this,
@ -198,7 +220,10 @@ public class StopwatchFragment extends RecyclerViewFragment<
@Override
public void onFabClick() {
if (mChronometer.isRunning()) {
mController.pause();
mPauseTime = SystemClock.elapsedRealtime();
mChronometer.stop();
mCurrentLap.pause();
mUpdateHandler.asyncUpdate(mCurrentLap.getId(), mCurrentLap);
// No issues controlling the animator here, because onLoadFinished() can't
// call through to startNewProgressBarAnimator(), because by that point
// the chronometer won't be running.
@ -209,7 +234,20 @@ public class StopwatchFragment extends RecyclerViewFragment<
mProgressAnimator.cancel();
}
} else {
mController.run();
if (mStartTime == 0) {
// addNewLap() won't call through unless chronometer is running, which
// we can't start until we compute mStartTime
mCurrentLap = new Lap();
mUpdateHandler.asyncInsert(mCurrentLap);
}
mStartTime += SystemClock.elapsedRealtime() - mPauseTime;
mPauseTime = 0;
mChronometer.setBase(mStartTime);
mChronometer.start();
if (!mCurrentLap.isRunning()) {
mCurrentLap.resume();
mUpdateHandler.asyncUpdate(mCurrentLap.getId(), mCurrentLap);
}
// This animator instance will end up having end() called on it. When
// the table update prompts us to requery, onLoadFinished will be called as a result.
// There, it calls startNewProgressAnimator() to end this animation and starts an
@ -219,6 +257,7 @@ public class StopwatchFragment extends RecyclerViewFragment<
// }
getActivity().startService(new Intent(getActivity(), StopwatchNotificationService.class));
}
savePrefs();
// TOneverDO: Precede savePrefs(), or else we don't save false to KEY_CHRONOMETER_RUNNING
/// and updateFab will update the wrong icon.
updateAllFabs();
@ -241,7 +280,23 @@ public class StopwatchFragment extends RecyclerViewFragment<
@OnClick(R.id.new_lap)
void addNewLap() {
mController.addNewLap(mChronometer.getText().toString());
if (!mChronometer.isRunning()) {
Log.d(TAG, "Cannot add new lap");
return;
}
if (mCurrentLap != null) {
mCurrentLap.end(mChronometer.getText().toString());
}
mPreviousLap = mCurrentLap;
mCurrentLap = new Lap();
if (mPreviousLap != null) {
// if (getAdapter().getItemCount() == 0) {
// mUpdateHandler.asyncInsert(mPreviousLap);
// } else {
mUpdateHandler.asyncUpdate(mPreviousLap.getId(), mPreviousLap);
// }
}
mUpdateHandler.asyncInsert(mCurrentLap);
// This would end up being called twice: here, and in onLoadFinished(), because the
// table updates will prompt us to requery.
// startNewProgressBarAnimator();
@ -249,13 +304,17 @@ public class StopwatchFragment extends RecyclerViewFragment<
@OnClick(R.id.stop)
void stop() {
mChronometer.stop();
mChronometer.setBase(SystemClock.elapsedRealtime());
// ----------------------------------------------------------------------
// TOneverDO: Precede these with mProgressAnimator.end(), otherwise our
// Animator.onAnimationEnd() callback won't hide SeekBar in time.
mController.stop();
mStartTime = 0;
mPauseTime = 0;
// ----------------------------------------------------------------------
mCurrentLap = null;
mPreviousLap = null;
mUpdateHandler.asyncClear(); // Clear laps
// No issues controlling the animator here, because onLoadFinished() can't
// call through to startNewProgressBarAnimator(), because by that point
// the chronometer won't be running.
@ -263,6 +322,7 @@ public class StopwatchFragment extends RecyclerViewFragment<
mProgressAnimator.end();
}
mProgressAnimator = null;
savePrefs();
// TOneverDO: Precede savePrefs(), or else we don't save false to KEY_CHRONOMETER_RUNNING
/// and updateFab will update the wrong icon.
updateAllFabs();
@ -282,14 +342,14 @@ public class StopwatchFragment extends RecyclerViewFragment<
}
private void updateMiniFabs() {
boolean started = mController.getStopwatch().hasStarted();
boolean started = mStartTime > 0;
int vis = started ? View.VISIBLE : View.INVISIBLE;
mNewLapButton.setVisibility(vis);
mStopButton.setVisibility(vis);
}
private void updateFab() {
mActivityFab.get().setImageDrawable(mController.isStopwatchRunning() ? mPauseDrawable : mStartDrawable);
mActivityFab.get().setImageDrawable(isStopwatchRunning() ? mPauseDrawable : mStartDrawable);
}
private void startNewProgressBarAnimator() {
@ -354,6 +414,21 @@ public class StopwatchFragment extends RecyclerViewFragment<
return mPreviousLap.elapsed() - mCurrentLap.elapsed();
}
private void savePrefs() {
mPrefs.edit().putLong(KEY_START_TIME, mStartTime)
.putLong(KEY_PAUSE_TIME, mPauseTime)
.putBoolean(KEY_CHRONOMETER_RUNNING, mChronometer.isRunning())
.apply();
}
/**
* @return the state of the stopwatch when we're in a resumed and visible state,
* or when we're going through a rotation
*/
private boolean isStopwatchRunning() {
return mChronometer.isRunning() || mPrefs.getBoolean(KEY_CHRONOMETER_RUNNING, false);
}
// ======================= DO NOT IMPLEMENT ============================
@Override

View File

@ -68,18 +68,20 @@ public class StopwatchNotificationService extends ChronometerNotificationService
@Override
protected void handleStartPauseAction(Intent intent, int flags, long startId) {
// TODO: Tell StopwatchFragment to start/pause itself.. perhaps with an Intent?
boolean running = mPrefs.getBoolean("KEY_RUNNING", false);
syncNotificationWithStopwatchState(!running);
boolean running = mPrefs.getBoolean(StopwatchFragment.KEY_CHRONOMETER_RUNNING, false);
SharedPreferences.Editor editor = mPrefs.edit();
editor.putBoolean("KEY_RUNNING", !running);
editor.putBoolean(StopwatchFragment.KEY_CHRONOMETER_RUNNING, !running);
if (running) {
editor.putLong("key_pause_time", SystemClock.elapsedRealtime());
editor.putLong(StopwatchFragment.KEY_PAUSE_TIME, SystemClock.elapsedRealtime());
} else {
long startTime = mPrefs.getLong("key_start_time", 0);
long pauseTime = mPrefs.getLong("key_pause_time", 0);
editor.putLong("key_start_time", startTime + SystemClock.elapsedRealtime() - pauseTime);
long startTime = mPrefs.getLong(StopwatchFragment.KEY_START_TIME, 0);
long pauseTime = mPrefs.getLong(StopwatchFragment.KEY_PAUSE_TIME, 0);
startTime += SystemClock.elapsedRealtime() - pauseTime;
editor.putLong(StopwatchFragment.KEY_START_TIME, startTime);
editor.putLong(StopwatchFragment.KEY_PAUSE_TIME, 0);
}
editor.apply();
syncNotificationWithStopwatchState(!running);
}
@Override
@ -110,8 +112,8 @@ public class StopwatchNotificationService extends ChronometerNotificationService
quitCurrentThread();
if (running) {
// TODO: Read the stopwatch's start time in shared prefs.
startNewThread(0, SystemClock.elapsedRealtime());
long startTime = mPrefs.getLong(StopwatchFragment.KEY_START_TIME, SystemClock.elapsedRealtime());
startNewThread(0, startTime);
}
}
}

View File

@ -1,78 +0,0 @@
package com.philliphsu.clock2.stopwatch;
import android.content.SharedPreferences;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.util.Log;
/**
* Created by Phillip Hsu on 9/11/2016.
*/
public final class StopwatchViewController extends BaseStopwatchController {
private static final String TAG = "StopwatchViewController";
private final ChronometerWithMillis mChronometer;
public StopwatchViewController(@NonNull AsyncLapsTableUpdateHandler updateHandler,
@NonNull SharedPreferences prefs,
@NonNull ChronometerWithMillis chronometer) {
super(updateHandler, prefs);
mChronometer = chronometer;
mChronometer.setShowCentiseconds(true, true);
final Stopwatch sw = getStopwatch();
if (sw.getStartTime() > 0) {
long base = sw.getStartTime();
if (sw.getPauseTime() > 0) {
base += SystemClock.elapsedRealtime() - sw.getPauseTime();
// We're not done pausing yet, so don't reset mPauseTime.
}
chronometer.setBase(base);
}
if (isStopwatchRunning()) {
chronometer.start();
// Note: mChronometer.isRunning() will return false at this point and
// in other upcoming lifecycle methods because it is not yet visible
// (i.e. mVisible == false).
}
}
@Override
public void run() {
super.run();
mChronometer.setBase(getStopwatch().getStartTime());
mChronometer.start();
}
@Override
public void pause() {
// Keep this call and the call to Stopwatch.pause() close together to minimize the time delta.
mChronometer.stop();
super.pause();
}
@Override
public void stop() {
mChronometer.stop();
mChronometer.setBase(SystemClock.elapsedRealtime());
super.stop();
}
@Override
public void addNewLap(String currentLapTotalText) {
if (!mChronometer.isRunning()) {
Log.d(TAG, "Cannot add new lap");
return;
}
super.addNewLap(currentLapTotalText);
}
/**
* @return the state of the stopwatch when we're in a resumed and visible state,
* or when we're going through a rotation
*/
@Override
public boolean isStopwatchRunning() {
return mChronometer.isRunning() || super.isStopwatchRunning();
}
}

View File

@ -136,7 +136,7 @@ public class TimerNotificationService extends ChronometerNotificationService {
// 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();
updateNotification(true);
mController.addOneMinute();
} else {
throw new IllegalArgumentException("TimerNotificationService cannot handle action " + action);