diff --git a/app/src/main/java/com/philliphsu/clock2/ChronometerNotificationService.java b/app/src/main/java/com/philliphsu/clock2/ChronometerNotificationService.java index ba82e63..9c1f102 100644 --- a/app/src/main/java/com/philliphsu/clock2/ChronometerNotificationService.java +++ b/app/src/main/java/com/philliphsu/clock2/ChronometerNotificationService.java @@ -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) { diff --git a/app/src/main/java/com/philliphsu/clock2/ChronometerNotificationThread.java b/app/src/main/java/com/philliphsu/clock2/ChronometerNotificationThread.java index 694416d..fb5eb0b 100644 --- a/app/src/main/java/com/philliphsu/clock2/ChronometerNotificationThread.java +++ b/app/src/main/java/com/philliphsu/clock2/ChronometerNotificationThread.java @@ -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()); } diff --git a/app/src/main/java/com/philliphsu/clock2/stopwatch/BaseStopwatchController.java b/app/src/main/java/com/philliphsu/clock2/stopwatch/BaseStopwatchController.java deleted file mode 100644 index 1584428..0000000 --- a/app/src/main/java/com/philliphsu/clock2/stopwatch/BaseStopwatchController.java +++ /dev/null @@ -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(); - } -} diff --git a/app/src/main/java/com/philliphsu/clock2/stopwatch/Stopwatch.java b/app/src/main/java/com/philliphsu/clock2/stopwatch/Stopwatch.java deleted file mode 100644 index 6c93b73..0000000 --- a/app/src/main/java/com/philliphsu/clock2/stopwatch/Stopwatch.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchFragment.java b/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchFragment.java index 036b066..6468463 100644 --- a/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchFragment.java @@ -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 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 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 3175b0e..7c6a61d 100644 --- a/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchNotificationService.java +++ b/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchNotificationService.java @@ -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); } } } diff --git a/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchViewController.java b/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchViewController.java deleted file mode 100644 index 9786d0e..0000000 --- a/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchViewController.java +++ /dev/null @@ -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(); - } -} 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 df0fe67..c7e2692 100644 --- a/app/src/main/java/com/philliphsu/clock2/timers/TimerNotificationService.java +++ b/app/src/main/java/com/philliphsu/clock2/timers/TimerNotificationService.java @@ -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);