From facdf05602cc57808fa7166480537912d381b382 Mon Sep 17 00:00:00 2001 From: Phillip Hsu Date: Wed, 14 Sep 2016 22:12:13 -0700 Subject: [PATCH] Moved lap inserting and updating to StopwatchNotificationService --- .../AsyncLapsTableUpdateHandler.java | 17 +++- .../clock2/stopwatch/StopwatchFragment.java | 91 +++++-------------- .../StopwatchNotificationService.java | 58 ++++++++++-- app/src/main/res/values/strings.xml | 2 + 4 files changed, 86 insertions(+), 82 deletions(-) diff --git a/app/src/main/java/com/philliphsu/clock2/stopwatch/AsyncLapsTableUpdateHandler.java b/app/src/main/java/com/philliphsu/clock2/stopwatch/AsyncLapsTableUpdateHandler.java index 4ce8092..fed3edd 100644 --- a/app/src/main/java/com/philliphsu/clock2/stopwatch/AsyncLapsTableUpdateHandler.java +++ b/app/src/main/java/com/philliphsu/clock2/stopwatch/AsyncLapsTableUpdateHandler.java @@ -1,6 +1,7 @@ package com.philliphsu.clock2.stopwatch; import android.content.Context; +import android.content.Intent; import com.philliphsu.clock2.AsyncDatabaseTableUpdateHandler; import com.philliphsu.clock2.alarms.ScrollHandler; @@ -19,6 +20,17 @@ public class AsyncLapsTableUpdateHandler extends AsyncDatabaseTableUpdateHandler return new LapsTableManager(context); } + @Override + protected void onPostAsyncInsert(Long result, Lap item) { + if (result > 1) { + // Update the notification's title with this lap number + Intent intent = new Intent(getContext(), StopwatchNotificationService.class) + .setAction(StopwatchNotificationService.ACTION_UPDATE_LAP_TITLE) + .putExtra(StopwatchNotificationService.EXTRA_LAP_NUMBER, result.intValue()); + getContext().startService(intent); + } + } + // ===================== DO NOT IMPLEMENT ========================= @Override @@ -26,11 +38,6 @@ public class AsyncLapsTableUpdateHandler extends AsyncDatabaseTableUpdateHandler // Leave blank. } - @Override - protected void onPostAsyncInsert(Long result, Lap item) { - // Leave blank. - } - @Override protected void onPostAsyncUpdate(Long result, Lap item) { // Leave blank. 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 4b7a0bb..dbf7af3 100644 --- a/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchFragment.java @@ -46,10 +46,7 @@ public class StopwatchFragment extends RecyclerViewFragment< // TODO: See if we can remove these? Since we save prefs in the notif. service. private long mStartTime; private long mPauseTime; - private Lap mCurrentLap; - private Lap mPreviousLap; - private AsyncLapsTableUpdateHandler mUpdateHandler; private ObjectAnimator mProgressAnimator; private SharedPreferences mPrefs; private WeakReference mActivityFab; @@ -69,7 +66,6 @@ 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); @@ -79,6 +75,8 @@ public class StopwatchFragment extends RecyclerViewFragment< // onCreateView() or any other callback that is guaranteed to be called. mStartDrawable = ContextCompat.getDrawable(getActivity(), R.drawable.ic_start_24dp); mPauseDrawable = ContextCompat.getDrawable(getActivity(), R.drawable.ic_pause_24dp); + + // TODO: Load the current lap here } @Nullable @@ -184,15 +182,17 @@ public class StopwatchFragment extends RecyclerViewFragment< // TODO: Will manipulating the cursor's position here affect the current // position in the adapter? Should we make a defensive copy and manipulate // that copy instead? + Lap currentLap = null; + Lap previousLap = null; if (data.moveToFirst()) { - mCurrentLap = data.getItem(); + currentLap = data.getItem(); // Log.d(TAG, "Current lap ID = " + mCurrentLap.getId()); } if (data.moveToNext()) { - mPreviousLap = data.getItem(); + previousLap = data.getItem(); // Log.d(TAG, "Previous lap ID = " + mPreviousLap.getId()); } - if (mCurrentLap != null && mPreviousLap != null) { + if (currentLap != null && previousLap != null) { // We really only want to start a new animator when the NEWLY RETRIEVED current // and previous laps are different (i.e. different laps, NOT merely different instances) // from the CURRENT current and previous laps, as referenced by mCurrentLap and mPreviousLap. @@ -208,13 +208,13 @@ 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 (isStopwatchRunning()) { - startNewProgressBarAnimator(); + startNewProgressBarAnimator(currentLap, previousLap); } else { // I verified the bar was visible already without this, so we probably don't need this, // but it's just a safety measure.. // ACTUALLY NOT A SAFETY MEASURE! // mSeekBar.setVisibility(View.VISIBLE); - ProgressBarUtils.setProgress(mSeekBar, getCurrentLapProgressRatio()); + ProgressBarUtils.setProgress(mSeekBar, getCurrentLapProgressRatio(currentLap, previousLap)); } } else { mSeekBar.setVisibility(View.INVISIBLE); @@ -228,10 +228,6 @@ public class StopwatchFragment extends RecyclerViewFragment< final Intent serviceIntent = new Intent(getActivity(), StopwatchNotificationService.class); 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); setMiniFabsVisible(true); // Handle the default action, i.e. post the notification for the first time. getActivity().startService(serviceIntent); @@ -258,41 +254,14 @@ public class StopwatchFragment extends RecyclerViewFragment< @OnClick(R.id.new_lap) void addNewLap() { - 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(); - // TODO: Start service with ACTION_ADD_LAP + Intent serviceIntent = new Intent(getActivity(), StopwatchNotificationService.class) + .setAction(StopwatchNotificationService.ACTION_ADD_LAP); + getActivity().startService(serviceIntent); } @OnClick(R.id.stop) void stop() { // Remove the notification. This will also write to prefs and clear the laps table. - // - // The service will make changes to shared prefs, which will fire our - // OnSharedPrefChangeListener, which will call this stop() method. As such, this - // stop() method will be called twice: first from the click, and the second from - // the OnSharedPrefChange callback. - // TODO: Make similar changes as you did with onFabClick() so that the above method calls - // are made in the OnSharedPrefChange callback. You may find it helpful to extract - // the above method calls into private helper methods, just as you did for - // onFabClick(). Intent stop = new Intent(getActivity(), StopwatchNotificationService.class) .setAction(StopwatchNotificationService.ACTION_STOP); getActivity().startService(stop); @@ -311,8 +280,6 @@ public class StopwatchFragment extends RecyclerViewFragment< // stop() will also make a call to updateRunning(), but the running state has not // changed from the time we left the app. 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. @@ -330,10 +297,6 @@ public class StopwatchFragment extends RecyclerViewFragment< 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 @@ -353,8 +316,6 @@ public class StopwatchFragment extends RecyclerViewFragment< mStartTime = 0; mPauseTime = 0; // ---------------------------------------------------------------------- - mCurrentLap = null; - mPreviousLap = null; // No issues controlling the animator here, because onLoadFinished() can't // call through to startNewProgressBarAnimator(), because by that point // the chronometer won't be running. @@ -376,8 +337,8 @@ public class StopwatchFragment extends RecyclerViewFragment< mActivityFab.get().setImageDrawable(running ? mPauseDrawable : mStartDrawable); } - private void startNewProgressBarAnimator() { - final long timeRemaining = remainingTimeBetweenLaps(); + private void startNewProgressBarAnimator(Lap currentLap, Lap previousLap) { + final long timeRemaining = remainingTimeBetweenLaps(currentLap, previousLap); if (timeRemaining <= 0) { mSeekBar.setVisibility(View.INVISIBLE); return; @@ -388,8 +349,8 @@ public class StopwatchFragment extends RecyclerViewFragment< // This can't go in the onAnimationStart() callback because the listener is added // AFTER ProgressBarUtils.startNewAnimator() starts the animation. mSeekBar.setVisibility(View.VISIBLE); - mProgressAnimator = ProgressBarUtils.startNewAnimator( - mSeekBar, getCurrentLapProgressRatio(), timeRemaining); + mProgressAnimator = ProgressBarUtils.startNewAnimator(mSeekBar, + getCurrentLapProgressRatio(currentLap, previousLap), timeRemaining); mProgressAnimator.addListener(new Animator.AnimatorListener() { private boolean cancelled; @@ -424,26 +385,18 @@ public class StopwatchFragment extends RecyclerViewFragment< }); } - private double getCurrentLapProgressRatio() { - if (mPreviousLap == null) + private double getCurrentLapProgressRatio(Lap currentLap, Lap previousLap) { + if (previousLap == null) return 0; // The cast is necessary, or else we'd have integer division between two longs and we'd // always get zero since the numerator will always be less than the denominator. - return remainingTimeBetweenLaps() / (double) mPreviousLap.elapsed(); + return remainingTimeBetweenLaps(currentLap, previousLap) / (double) previousLap.elapsed(); } - private long remainingTimeBetweenLaps() { - if (mCurrentLap == null || mPreviousLap == null) + private long remainingTimeBetweenLaps(Lap currentLap, Lap previousLap) { + if (currentLap == null || previousLap == null) return 0; - return mPreviousLap.elapsed() - mCurrentLap.elapsed(); - } - - // TODO: Delete. - private void savePrefs() { - mPrefs.edit().putLong(KEY_START_TIME, mStartTime) - .putLong(KEY_PAUSE_TIME, mPauseTime) - .putBoolean(KEY_CHRONOMETER_RUNNING, mChronometer.isRunning()) - .apply(); + return previousLap.elapsed() - currentLap.elapsed(); } /** 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 455ca3e..4d2cd32 100644 --- a/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchNotificationService.java +++ b/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchNotificationService.java @@ -7,22 +7,35 @@ import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.util.Log; import com.philliphsu.clock2.ChronometerNotificationService; import com.philliphsu.clock2.MainActivity; import com.philliphsu.clock2.R; +import com.philliphsu.clock2.timers.ChronometerDelegate; public class StopwatchNotificationService extends ChronometerNotificationService { - private static final String ACTION_ADD_LAP = "com.philliphsu.clock2.stopwatch.action.ADD_LAP"; + private static final String TAG = "StopwatchNotifService"; - private AsyncLapsTableUpdateHandler mLapsTableUpdateHandler; + public static final String ACTION_ADD_LAP = "com.philliphsu.clock2.stopwatch.action.ADD_LAP"; + public static final String ACTION_UPDATE_LAP_TITLE = "com.philliphsu.clock2.stopwatch.action.UPDATE_LAP_TITLE"; + + public static final String EXTRA_LAP_NUMBER = "com.philliphsu.clock2.stopwatch.extra.LAP_NUMBER"; + + private AsyncLapsTableUpdateHandler mUpdateHandler; private SharedPreferences mPrefs; + private final ChronometerDelegate mDelegate = new ChronometerDelegate(); + private Lap mCurrentLap; @Override public void onCreate() { super.onCreate(); - mLapsTableUpdateHandler = new AsyncLapsTableUpdateHandler(this, null); + mUpdateHandler = new AsyncLapsTableUpdateHandler(this, null); mPrefs = PreferenceManager.getDefaultSharedPreferences(this); + // TODO: I'm afraid the base time here will be off by a considerable amount from the base time + // set in StopwatchFragment. + mDelegate.init(); + mDelegate.setShowCentiseconds(true, false); // TODO: I think we can make this a foreground service so even // if the process is killed, this service remains alive. } @@ -58,27 +71,38 @@ public class StopwatchNotificationService extends ChronometerNotificationService @Override protected void handleDefaultAction(Intent intent, int flags, long startId) { + // TODO: Why do we need this check? Won't KEY_START_TIME always have a value of 0 here? + if (mPrefs.getLong(StopwatchFragment.KEY_START_TIME, 0) == 0) { + mCurrentLap = new Lap(); + mUpdateHandler.asyncInsert(mCurrentLap); + } // TODO: String resource [Stopwatch: Lap %1$s]. If no laps, just [Stopwatch] setContentTitle(getString(R.string.stopwatch)); syncNotificationWithStopwatchState(true/*always true*/); // We don't need to write anything to SharedPrefs because if we're here, StopwatchFragment - // already wrote the necessary values to file. + // will start this service again with ACTION_START_PAUSE, which will do the writing. } @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(StopwatchFragment.KEY_CHRONOMETER_RUNNING, false); SharedPreferences.Editor editor = mPrefs.edit(); editor.putBoolean(StopwatchFragment.KEY_CHRONOMETER_RUNNING, !running); if (running) { editor.putLong(StopwatchFragment.KEY_PAUSE_TIME, SystemClock.elapsedRealtime()); + mCurrentLap.pause(); + mUpdateHandler.asyncUpdate(mCurrentLap.getId(), mCurrentLap); } else { 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); + // TODO: Why do we need this check? Won't this lap always be paused here? + if (!mCurrentLap.isRunning()) { + mCurrentLap.resume(); + mUpdateHandler.asyncUpdate(mCurrentLap.getId(), mCurrentLap); + } } editor.apply(); syncNotificationWithStopwatchState(!running); @@ -86,7 +110,6 @@ public class StopwatchNotificationService extends ChronometerNotificationService @Override protected void handleStopAction(Intent intent, int flags, long startId) { - // TODO: Tell StopwatchFragment to stop itself.. perhaps with an Intent? mPrefs.edit() .putLong(StopwatchFragment.KEY_START_TIME, 0) .putLong(StopwatchFragment.KEY_PAUSE_TIME, 0) @@ -100,14 +123,33 @@ public class StopwatchNotificationService extends ChronometerNotificationService // We can either clear the laps table here, as we've done already, or do as the TODO above // says and tell StopwatchFragment to stop itself. The latter would also stop the // chronometer view if the fragment is still in view (i.e. app is still open). - mLapsTableUpdateHandler.asyncClear(); + mCurrentLap = null; + mUpdateHandler.asyncClear(); stopSelf(); } @Override protected void handleAction(@NonNull String action, Intent intent, int flags, long startId) { if (ACTION_ADD_LAP.equals(action)) { - mLapsTableUpdateHandler.asyncInsert(null/*TODO*/); + if (mPrefs.getBoolean(StopwatchFragment.KEY_CHRONOMETER_RUNNING, false)) { + String timestamp = mDelegate.formatElapsedTime(SystemClock.elapsedRealtime(), + null/*Resources not needed here*/).toString(); + mCurrentLap.end(timestamp); + mUpdateHandler.asyncUpdate(mCurrentLap.getId(), mCurrentLap); + + Lap newLap = new Lap(); + mUpdateHandler.asyncInsert(newLap); + mCurrentLap = newLap; + } + } else if (ACTION_UPDATE_LAP_TITLE.equals(action)) { + int lapNumber = intent.getIntExtra(EXTRA_LAP_NUMBER, 0); + if (lapNumber == 0) { + Log.w(TAG, "Lap number was not passed in with intent"); + } + // Unfortunately, the ID is only assigned when retrieving a Lap instance from + // its cursor; the value here will always be 0. + setContentTitle(getString(R.string.stopwatch_and_lap_number, lapNumber)); + updateNotification(true); } else { throw new IllegalArgumentException("StopwatchNotificationService cannot handle action " + action); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5e77aac..969d3b3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -206,6 +206,8 @@ Stopwatch Lap + + Stopwatch \u002d Lap %1$d