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;
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.

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.
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;
@ -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();
}
/**

View File

@ -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);
}

View File

@ -206,6 +206,8 @@
<string name="stopwatch">Stopwatch</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 -->
<!-- The representation of a time duration when negative. An example is -1:14. This can be used with a countdown timer for example.-->