Moved lap inserting and updating to StopwatchNotificationService

This commit is contained in:
Phillip Hsu 2016-09-14 22:12:13 -07:00
parent ad7335c6d6
commit facdf05602
4 changed files with 86 additions and 82 deletions

View File

@ -1,6 +1,7 @@
package com.philliphsu.clock2.stopwatch; package com.philliphsu.clock2.stopwatch;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import com.philliphsu.clock2.AsyncDatabaseTableUpdateHandler; import com.philliphsu.clock2.AsyncDatabaseTableUpdateHandler;
import com.philliphsu.clock2.alarms.ScrollHandler; import com.philliphsu.clock2.alarms.ScrollHandler;
@ -19,6 +20,17 @@ public class AsyncLapsTableUpdateHandler extends AsyncDatabaseTableUpdateHandler
return new LapsTableManager(context); 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 ========================= // ===================== DO NOT IMPLEMENT =========================
@Override @Override
@ -26,11 +38,6 @@ public class AsyncLapsTableUpdateHandler extends AsyncDatabaseTableUpdateHandler
// Leave blank. // Leave blank.
} }
@Override
protected void onPostAsyncInsert(Long result, Lap item) {
// Leave blank.
}
@Override @Override
protected void onPostAsyncUpdate(Long result, Lap item) { protected void onPostAsyncUpdate(Long result, Lap item) {
// Leave blank. // Leave blank.

View File

@ -46,10 +46,7 @@ public class StopwatchFragment extends RecyclerViewFragment<
// TODO: See if we can remove these? Since we save prefs in the notif. service. // TODO: See if we can remove these? Since we save prefs in the notif. service.
private long mStartTime; private long mStartTime;
private long mPauseTime; private long mPauseTime;
private Lap mCurrentLap;
private Lap mPreviousLap;
private AsyncLapsTableUpdateHandler mUpdateHandler;
private ObjectAnimator mProgressAnimator; private ObjectAnimator mProgressAnimator;
private SharedPreferences mPrefs; private SharedPreferences mPrefs;
private WeakReference<FloatingActionButton> mActivityFab; private WeakReference<FloatingActionButton> mActivityFab;
@ -69,7 +66,6 @@ public class StopwatchFragment extends RecyclerViewFragment<
@Override @Override
public void onCreate(@Nullable Bundle savedInstanceState) { public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
mUpdateHandler = new AsyncLapsTableUpdateHandler(getActivity(), null/*we shouldn't need a scroll handler*/);
mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
mStartTime = mPrefs.getLong(KEY_START_TIME, 0); mStartTime = mPrefs.getLong(KEY_START_TIME, 0);
mPauseTime = mPrefs.getLong(KEY_PAUSE_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. // onCreateView() or any other callback that is guaranteed to be called.
mStartDrawable = ContextCompat.getDrawable(getActivity(), R.drawable.ic_start_24dp); mStartDrawable = ContextCompat.getDrawable(getActivity(), R.drawable.ic_start_24dp);
mPauseDrawable = ContextCompat.getDrawable(getActivity(), R.drawable.ic_pause_24dp); mPauseDrawable = ContextCompat.getDrawable(getActivity(), R.drawable.ic_pause_24dp);
// TODO: Load the current lap here
} }
@Nullable @Nullable
@ -184,15 +182,17 @@ public class StopwatchFragment extends RecyclerViewFragment<
// TODO: Will manipulating the cursor's position here affect the current // TODO: Will manipulating the cursor's position here affect the current
// position in the adapter? Should we make a defensive copy and manipulate // position in the adapter? Should we make a defensive copy and manipulate
// that copy instead? // that copy instead?
Lap currentLap = null;
Lap previousLap = null;
if (data.moveToFirst()) { if (data.moveToFirst()) {
mCurrentLap = data.getItem(); currentLap = data.getItem();
// Log.d(TAG, "Current lap ID = " + mCurrentLap.getId()); // Log.d(TAG, "Current lap ID = " + mCurrentLap.getId());
} }
if (data.moveToNext()) { if (data.moveToNext()) {
mPreviousLap = data.getItem(); previousLap = data.getItem();
// Log.d(TAG, "Previous lap ID = " + mPreviousLap.getId()); // 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 // 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) // 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. // 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, // NOTE: If we just recreated ourselves due to rotation, mChronometer.isRunning() == false,
// because it is not yet visible (i.e. mVisible == false). // because it is not yet visible (i.e. mVisible == false).
if (isStopwatchRunning()) { if (isStopwatchRunning()) {
startNewProgressBarAnimator(); startNewProgressBarAnimator(currentLap, previousLap);
} else { } else {
// I verified the bar was visible already without this, so we probably don't need this, // I verified the bar was visible already without this, so we probably don't need this,
// but it's just a safety measure.. // but it's just a safety measure..
// ACTUALLY NOT A SAFETY MEASURE! // ACTUALLY NOT A SAFETY MEASURE!
// mSeekBar.setVisibility(View.VISIBLE); // mSeekBar.setVisibility(View.VISIBLE);
ProgressBarUtils.setProgress(mSeekBar, getCurrentLapProgressRatio()); ProgressBarUtils.setProgress(mSeekBar, getCurrentLapProgressRatio(currentLap, previousLap));
} }
} else { } else {
mSeekBar.setVisibility(View.INVISIBLE); mSeekBar.setVisibility(View.INVISIBLE);
@ -228,10 +228,6 @@ public class StopwatchFragment extends RecyclerViewFragment<
final Intent serviceIntent = new Intent(getActivity(), StopwatchNotificationService.class); final Intent serviceIntent = new Intent(getActivity(), StopwatchNotificationService.class);
if (mStartTime == 0) { 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); setMiniFabsVisible(true);
// Handle the default action, i.e. post the notification for the first time. // Handle the default action, i.e. post the notification for the first time.
getActivity().startService(serviceIntent); getActivity().startService(serviceIntent);
@ -258,41 +254,14 @@ public class StopwatchFragment extends RecyclerViewFragment<
@OnClick(R.id.new_lap) @OnClick(R.id.new_lap)
void addNewLap() { void addNewLap() {
if (!mChronometer.isRunning()) { Intent serviceIntent = new Intent(getActivity(), StopwatchNotificationService.class)
Log.d(TAG, "Cannot add new lap"); .setAction(StopwatchNotificationService.ACTION_ADD_LAP);
return; getActivity().startService(serviceIntent);
}
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
} }
@OnClick(R.id.stop) @OnClick(R.id.stop)
void stop() { void stop() {
// Remove the notification. This will also write to prefs and clear the laps table. // 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) Intent stop = new Intent(getActivity(), StopwatchNotificationService.class)
.setAction(StopwatchNotificationService.ACTION_STOP); .setAction(StopwatchNotificationService.ACTION_STOP);
getActivity().startService(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 // stop() will also make a call to updateRunning(), but the running state has not
// changed from the time we left the app. // changed from the time we left the app.
mChronometer.stop(); mChronometer.stop();
mCurrentLap.pause();
mUpdateHandler.asyncUpdate(mCurrentLap.getId(), mCurrentLap);
// No issues controlling the animator here, because onLoadFinished() can't // No issues controlling the animator here, because onLoadFinished() can't
// call through to startNewProgressBarAnimator(), because by that point // call through to startNewProgressBarAnimator(), because by that point
// the chronometer won't be running. // the chronometer won't be running.
@ -330,10 +297,6 @@ public class StopwatchFragment extends RecyclerViewFragment<
mPauseTime = 0; mPauseTime = 0;
mChronometer.setBase(mStartTime); mChronometer.setBase(mStartTime);
mChronometer.start(); mChronometer.start();
if (!mCurrentLap.isRunning()) {
mCurrentLap.resume();
mUpdateHandler.asyncUpdate(mCurrentLap.getId(), mCurrentLap);
}
// This animator instance will end up having end() called on it. When // 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. // 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 // There, it calls startNewProgressAnimator() to end this animation and starts an
@ -353,8 +316,6 @@ public class StopwatchFragment extends RecyclerViewFragment<
mStartTime = 0; mStartTime = 0;
mPauseTime = 0; mPauseTime = 0;
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
mCurrentLap = null;
mPreviousLap = null;
// No issues controlling the animator here, because onLoadFinished() can't // No issues controlling the animator here, because onLoadFinished() can't
// call through to startNewProgressBarAnimator(), because by that point // call through to startNewProgressBarAnimator(), because by that point
// the chronometer won't be running. // the chronometer won't be running.
@ -376,8 +337,8 @@ public class StopwatchFragment extends RecyclerViewFragment<
mActivityFab.get().setImageDrawable(running ? mPauseDrawable : mStartDrawable); mActivityFab.get().setImageDrawable(running ? mPauseDrawable : mStartDrawable);
} }
private void startNewProgressBarAnimator() { private void startNewProgressBarAnimator(Lap currentLap, Lap previousLap) {
final long timeRemaining = remainingTimeBetweenLaps(); final long timeRemaining = remainingTimeBetweenLaps(currentLap, previousLap);
if (timeRemaining <= 0) { if (timeRemaining <= 0) {
mSeekBar.setVisibility(View.INVISIBLE); mSeekBar.setVisibility(View.INVISIBLE);
return; return;
@ -388,8 +349,8 @@ public class StopwatchFragment extends RecyclerViewFragment<
// This can't go in the onAnimationStart() callback because the listener is added // This can't go in the onAnimationStart() callback because the listener is added
// AFTER ProgressBarUtils.startNewAnimator() starts the animation. // AFTER ProgressBarUtils.startNewAnimator() starts the animation.
mSeekBar.setVisibility(View.VISIBLE); mSeekBar.setVisibility(View.VISIBLE);
mProgressAnimator = ProgressBarUtils.startNewAnimator( mProgressAnimator = ProgressBarUtils.startNewAnimator(mSeekBar,
mSeekBar, getCurrentLapProgressRatio(), timeRemaining); getCurrentLapProgressRatio(currentLap, previousLap), timeRemaining);
mProgressAnimator.addListener(new Animator.AnimatorListener() { mProgressAnimator.addListener(new Animator.AnimatorListener() {
private boolean cancelled; private boolean cancelled;
@ -424,26 +385,18 @@ public class StopwatchFragment extends RecyclerViewFragment<
}); });
} }
private double getCurrentLapProgressRatio() { private double getCurrentLapProgressRatio(Lap currentLap, Lap previousLap) {
if (mPreviousLap == null) if (previousLap == null)
return 0; return 0;
// The cast is necessary, or else we'd have integer division between two longs and we'd // 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. // 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() { private long remainingTimeBetweenLaps(Lap currentLap, Lap previousLap) {
if (mCurrentLap == null || mPreviousLap == null) if (currentLap == null || previousLap == null)
return 0; return 0;
return mPreviousLap.elapsed() - mCurrentLap.elapsed(); return previousLap.elapsed() - currentLap.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();
} }
/** /**

View File

@ -7,22 +7,35 @@ import android.os.SystemClock;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log;
import com.philliphsu.clock2.ChronometerNotificationService; import com.philliphsu.clock2.ChronometerNotificationService;
import com.philliphsu.clock2.MainActivity; import com.philliphsu.clock2.MainActivity;
import com.philliphsu.clock2.R; import com.philliphsu.clock2.R;
import com.philliphsu.clock2.timers.ChronometerDelegate;
public class StopwatchNotificationService extends ChronometerNotificationService { 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 SharedPreferences mPrefs;
private final ChronometerDelegate mDelegate = new ChronometerDelegate();
private Lap mCurrentLap;
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
mLapsTableUpdateHandler = new AsyncLapsTableUpdateHandler(this, null); mUpdateHandler = new AsyncLapsTableUpdateHandler(this, null);
mPrefs = PreferenceManager.getDefaultSharedPreferences(this); 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 // TODO: I think we can make this a foreground service so even
// if the process is killed, this service remains alive. // if the process is killed, this service remains alive.
} }
@ -58,27 +71,38 @@ public class StopwatchNotificationService extends ChronometerNotificationService
@Override @Override
protected void handleDefaultAction(Intent intent, int flags, long startId) { 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] // TODO: String resource [Stopwatch: Lap %1$s]. If no laps, just [Stopwatch]
setContentTitle(getString(R.string.stopwatch)); setContentTitle(getString(R.string.stopwatch));
syncNotificationWithStopwatchState(true/*always true*/); syncNotificationWithStopwatchState(true/*always true*/);
// We don't need to write anything to SharedPrefs because if we're here, StopwatchFragment // 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 @Override
protected void handleStartPauseAction(Intent intent, int flags, long startId) { 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); boolean running = mPrefs.getBoolean(StopwatchFragment.KEY_CHRONOMETER_RUNNING, false);
SharedPreferences.Editor editor = mPrefs.edit(); SharedPreferences.Editor editor = mPrefs.edit();
editor.putBoolean(StopwatchFragment.KEY_CHRONOMETER_RUNNING, !running); editor.putBoolean(StopwatchFragment.KEY_CHRONOMETER_RUNNING, !running);
if (running) { if (running) {
editor.putLong(StopwatchFragment.KEY_PAUSE_TIME, SystemClock.elapsedRealtime()); editor.putLong(StopwatchFragment.KEY_PAUSE_TIME, SystemClock.elapsedRealtime());
mCurrentLap.pause();
mUpdateHandler.asyncUpdate(mCurrentLap.getId(), mCurrentLap);
} else { } else {
long startTime = mPrefs.getLong(StopwatchFragment.KEY_START_TIME, 0); long startTime = mPrefs.getLong(StopwatchFragment.KEY_START_TIME, 0);
long pauseTime = mPrefs.getLong(StopwatchFragment.KEY_PAUSE_TIME, 0); long pauseTime = mPrefs.getLong(StopwatchFragment.KEY_PAUSE_TIME, 0);
startTime += SystemClock.elapsedRealtime() - pauseTime; startTime += SystemClock.elapsedRealtime() - pauseTime;
editor.putLong(StopwatchFragment.KEY_START_TIME, startTime); editor.putLong(StopwatchFragment.KEY_START_TIME, startTime);
editor.putLong(StopwatchFragment.KEY_PAUSE_TIME, 0); 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(); editor.apply();
syncNotificationWithStopwatchState(!running); syncNotificationWithStopwatchState(!running);
@ -86,7 +110,6 @@ public class StopwatchNotificationService extends ChronometerNotificationService
@Override @Override
protected void handleStopAction(Intent intent, int flags, long startId) { protected void handleStopAction(Intent intent, int flags, long startId) {
// TODO: Tell StopwatchFragment to stop itself.. perhaps with an Intent?
mPrefs.edit() mPrefs.edit()
.putLong(StopwatchFragment.KEY_START_TIME, 0) .putLong(StopwatchFragment.KEY_START_TIME, 0)
.putLong(StopwatchFragment.KEY_PAUSE_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 // 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 // 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). // chronometer view if the fragment is still in view (i.e. app is still open).
mLapsTableUpdateHandler.asyncClear(); mCurrentLap = null;
mUpdateHandler.asyncClear();
stopSelf(); stopSelf();
} }
@Override @Override
protected void handleAction(@NonNull String action, Intent intent, int flags, long startId) { protected void handleAction(@NonNull String action, Intent intent, int flags, long startId) {
if (ACTION_ADD_LAP.equals(action)) { 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 { } else {
throw new IllegalArgumentException("StopwatchNotificationService cannot handle action " + action); throw new IllegalArgumentException("StopwatchNotificationService cannot handle action " + action);
} }

View File

@ -206,6 +206,8 @@
<string name="stopwatch">Stopwatch</string> <string name="stopwatch">Stopwatch</string>
<string name="lap">Lap</string> <string name="lap">Lap</string>
<!-- The notification title to use when there are laps in a stopwatch timing. An example is "Stopwatch - Lap 2". -->
<string name="stopwatch_and_lap_number">Stopwatch \u002d Lap %1$d</string>
<!-- https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/strings.xml --> <!-- https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/strings.xml -->
<!-- The representation of a time duration when negative. An example is -1:14. This can be used with a countdown timer for example.--> <!-- The representation of a time duration when negative. An example is -1:14. This can be used with a countdown timer for example.-->