diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4ff4f20..9431a9c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -125,6 +125,11 @@
android:enabled="true"
android:exported="false">
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/philliphsu/clock2/BaseChronometer.java b/app/src/main/java/com/philliphsu/clock2/BaseChronometer.java
new file mode 100644
index 0000000..f57577b
--- /dev/null
+++ b/app/src/main/java/com/philliphsu/clock2/BaseChronometer.java
@@ -0,0 +1,367 @@
+package com.philliphsu.clock2;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+
+import com.philliphsu.clock2.timers.ChronometerDelegate;
+
+/**
+ * Created by Phillip Hsu on 9/9/2016.
+ *
+ * Based on the framework's Chronometer class. Can be configured as a countdown
+ * chronometer and can also show centiseconds.
+ */
+public class BaseChronometer extends TextView {
+ private static final String TAG = "BaseChronometer";
+
+ /**
+ * A callback that notifies when the chronometer has incremented on its own.
+ */
+ public interface OnChronometerTickListener {
+ /**
+ * Notification that the chronometer has changed.
+ */
+ void onChronometerTick(BaseChronometer chronometer);
+ }
+
+ private boolean mVisible;
+ private boolean mStarted;
+ private boolean mRunning;
+ private OnChronometerTickListener mOnChronometerTickListener;
+ private final ChronometerDelegate mDelegate = new ChronometerDelegate();
+
+ private static final int TICK_WHAT = 2;
+
+ /**
+ * Initialize this Chronometer object.
+ * Sets the base to the current time.
+ */
+ public BaseChronometer(Context context) {
+ this(context, null, 0);
+ }
+
+ /**
+ * Initialize with standard view layout information.
+ * Sets the base to the current time.
+ */
+ public BaseChronometer(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ /**
+ * Initialize with standard view layout information and style.
+ * Sets the base to the current time.
+ */
+ public BaseChronometer(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+// final TypedArray a = context.obtainStyledAttributes(
+// attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, 0);
+// setFormat(a.getString(com.android.internal.R.styleable.Chronometer_format));
+// a.recycle();
+
+ init();
+ }
+
+ @TargetApi(21)
+ public BaseChronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+// final TypedArray a = context.obtainStyledAttributes(
+// attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, defStyleRes);
+// setFormat(a.getString(com.android.internal.R.styleable.Chronometer_format));
+// a.recycle();
+
+ init();
+ }
+
+ private void init() {
+ mDelegate.init();
+ updateText(SystemClock.elapsedRealtime());
+ }
+
+ /**
+ * Set this view to count down to the base instead of counting up from it.
+ *
+ * @param countDown whether this view should count down
+ *
+ * @see #setBase(long)
+ */
+ public void setCountDown(boolean countDown) {
+ mDelegate.setCountDown(countDown);
+ }
+
+ /**
+ * @return whether this view counts down
+ *
+ * @see #setCountDown(boolean)
+ */
+ public boolean isCountDown() {
+ return mDelegate.isCountDown();
+ }
+
+ /**
+ * Set this view to show centiseconds and to apply a size span on the centiseconds text.
+ * NOTE: Calling this method will reset the chronometer, so the visibility
+ * of the centiseconds text can be updated. You should call this method
+ * before you {@link #start()} this chronometer, because it makes no sense to show the
+ * centiseconds any time after the start of ticking.
+ *
+ * @param showCentiseconds whether this view should show centiseconds
+ * @param applySizeSpan whether a size span should be applied to the centiseconds text
+ */
+ public void setShowCentiseconds(boolean showCentiseconds, boolean applySizeSpan) {
+ mDelegate.setShowCentiseconds(showCentiseconds, applySizeSpan);
+ init(); // Clear and update the text again
+ }
+
+ /**
+ * @return whether this view shows centiseconds
+ *
+ * @see #setShowCentiseconds(boolean, boolean)
+ */
+ public boolean showsCentiseconds() {
+ return mDelegate.showsCentiseconds();
+ }
+
+ /**
+ * @return whether this view is currently running
+ *
+ * @see #start()
+ * @see #stop()
+ */
+ public boolean isRunning() {
+ return mRunning;
+ }
+
+ /**
+ * Set the time that the count-up timer is in reference to.
+ *
+ * @param base Use the {@link SystemClock#elapsedRealtime} time base.
+ */
+// @android.view.RemotableViewMethod
+ public void setBase(long base) {
+ mDelegate.setBase(base);
+ dispatchChronometerTick();
+ updateText(SystemClock.elapsedRealtime());
+ }
+
+ /**
+ * Return the base time as set through {@link #setBase}.
+ */
+ public long getBase() {
+ return mDelegate.getBase();
+ }
+
+ /**
+ * If {@link #isCountDown()}, equivalent to {@link #setBase(long)
+ * setBase(SystemClock.elapsedRealtime() + duration)}.
+ *
+ * Otherwise, equivalent to {@link #setBase(long)
+ * setBase(SystemClock.elapsedRealtime() - duration)}.
+ */
+ public void setDuration(long duration) {
+ setBase(SystemClock.elapsedRealtime() + (isCountDown() ? duration : -duration));
+ }
+
+ /**
+ * Sets the format string used for display. The Chronometer will display
+ * this string, with the first "%s" replaced by the current timer value in
+ * "MM:SS" or "H:MM:SS" form.
+ *
+ * If the format string is null, or if you never call setFormat(), the
+ * Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS"
+ * form.
+ *
+ * @param format the format string.
+ */
+// @android.view.RemotableViewMethod
+ public void setFormat(String format) {
+ mDelegate.setFormat(format);
+ }
+
+ /**
+ * Returns the current format string as set through {@link #setFormat}.
+ */
+ public String getFormat() {
+ return mDelegate.getFormat();
+ }
+
+ /**
+ * Sets the listener to be called when the chronometer changes.
+ *
+ * @param listener The listener.
+ */
+ public void setOnChronometerTickListener(OnChronometerTickListener listener) {
+ mOnChronometerTickListener = listener;
+ }
+
+ /**
+ * @return The listener (may be null) that is listening for chronometer change
+ * events.
+ */
+ public OnChronometerTickListener getOnChronometerTickListener() {
+ return mOnChronometerTickListener;
+ }
+
+ /**
+ * Start counting up. This does not affect the base as set from {@link #setBase}, just
+ * the view display.
+ *
+ * Chronometer works by regularly scheduling messages to the handler, even when the
+ * Widget is not visible. To make sure resource leaks do not occur, the user should
+ * make sure that each start() call has a reciprocal call to {@link #stop}.
+ */
+ public void start() {
+ mStarted = true;
+ updateRunning();
+ }
+
+ /**
+ * Stop counting up. This does not affect the base as set from {@link #setBase}, just
+ * the view display.
+ *
+ * This stops the messages to the handler, effectively releasing resources that would
+ * be held as the chronometer is running, via {@link #start}.
+ */
+ public void stop() {
+ mStarted = false;
+ updateRunning();
+ }
+
+ /**
+ * The same as calling {@link #start} or {@link #stop}.
+ * @hide pending API council approval
+ */
+// @android.view.RemotableViewMethod
+ public void setStarted(boolean started) {
+ mStarted = started;
+ updateRunning();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mVisible = false;
+ updateRunning();
+ }
+
+ @Override
+ protected void onWindowVisibilityChanged(int visibility) {
+ Log.d(TAG, "onWindowVisibilityChanged()");
+ super.onWindowVisibilityChanged(visibility);
+ mVisible = visibility == VISIBLE;
+ updateRunning();
+ }
+
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ updateRunning();
+ }
+
+ private synchronized void updateText(long now) {
+ setText(mDelegate.formatElapsedTime(now));
+ }
+
+ private void updateRunning() {
+ boolean running = mVisible && mStarted;
+ if (running != mRunning) {
+ if (running) {
+ Log.d(TAG, "Running");
+ updateText(SystemClock.elapsedRealtime());
+ dispatchChronometerTick();
+ mHandler.sendMessageDelayed(Message.obtain(mHandler, TICK_WHAT), 10);
+ } else {
+ Log.d(TAG, "Not running anymore");
+ mHandler.removeMessages(TICK_WHAT);
+ }
+ mRunning = running;
+ }
+ }
+
+ private Handler mHandler = new Handler() {
+ public void handleMessage(Message m) {
+ if (mRunning) {
+ updateText(SystemClock.elapsedRealtime());
+ dispatchChronometerTick();
+ sendMessageDelayed(Message.obtain(this, TICK_WHAT), 10);
+ }
+ }
+ };
+
+ void dispatchChronometerTick() {
+ if (mOnChronometerTickListener != null) {
+ mOnChronometerTickListener.onChronometerTick(this);
+ }
+ }
+
+ private static final int MIN_IN_SEC = 60;
+ private static final int HOUR_IN_SEC = MIN_IN_SEC*60;
+// private static String formatDuration(long ms) {
+// final Resources res = Resources.getSystem();
+// final StringBuilder text = new StringBuilder();
+//
+// int duration = (int) (ms / DateUtils.SECOND_IN_MILLIS);
+// if (duration < 0) {
+// duration = -duration;
+// }
+//
+// int h = 0;
+// int m = 0;
+//
+// if (duration >= HOUR_IN_SEC) {
+// h = duration / HOUR_IN_SEC;
+// duration -= h * HOUR_IN_SEC;
+// }
+// if (duration >= MIN_IN_SEC) {
+// m = duration / MIN_IN_SEC;
+// duration -= m * MIN_IN_SEC;
+// }
+// int s = duration;
+//
+// try {
+// if (h > 0) {
+// text.append(res.getQuantityString(
+// // TODO: Copy the resource into our own project
+// com.android.internal.R.plurals.duration_hours, h, h));
+// }
+// if (m > 0) {
+// if (text.length() > 0) {
+// text.append(' ');
+// }
+// text.append(res.getQuantityString(
+// // TODO: Copy the resource into our own project
+// com.android.internal.R.plurals.duration_minutes, m, m));
+// }
+//
+// if (text.length() > 0) {
+// text.append(' ');
+// }
+// text.append(res.getQuantityString(
+// // TODO: Copy the resource into our own project
+// com.android.internal.R.plurals.duration_seconds, s, s));
+// } catch (Resources.NotFoundException e) {
+// // Ignore; plurals throws an exception for an untranslated quantity for a given locale.
+// return null;
+// }
+// return text.toString();
+// }
+//
+// @Override
+// public CharSequence getContentDescription() {
+// return formatDuration(mNow - mBase);
+// }
+//
+// @Override
+// public CharSequence getAccessibilityClassName() {
+// return BaseChronometer.class.getName();
+// }
+}
diff --git a/app/src/main/java/com/philliphsu/clock2/stopwatch/ChronometerWithMillis.java b/app/src/main/java/com/philliphsu/clock2/stopwatch/ChronometerWithMillis.java
index f8ca53f..8133c49 100644
--- a/app/src/main/java/com/philliphsu/clock2/stopwatch/ChronometerWithMillis.java
+++ b/app/src/main/java/com/philliphsu/clock2/stopwatch/ChronometerWithMillis.java
@@ -18,21 +18,9 @@ package com.philliphsu.clock2.stopwatch;
import android.annotation.TargetApi;
import android.content.Context;
-import android.os.Handler;
-import android.os.Message;
-import android.os.SystemClock;
-import android.text.SpannableString;
-import android.text.Spanned;
-import android.text.TextUtils;
-import android.text.format.DateUtils;
-import android.text.style.RelativeSizeSpan;
import android.util.AttributeSet;
-import android.util.Log;
-import android.widget.TextView;
-import java.util.Formatter;
-import java.util.IllegalFormatException;
-import java.util.Locale;
+import com.philliphsu.clock2.BaseChronometer;
/**
* Created by Phillip Hsu on 8/9/2016.
@@ -40,348 +28,29 @@ import java.util.Locale;
* A modified version of the framework's Chronometer widget that shows
* up to hundredths of a second.
*/
-public class ChronometerWithMillis extends TextView {
+public class ChronometerWithMillis extends BaseChronometer {
private static final String TAG = "ChronometerWithMillis";
- /**
- * A callback that notifies when the chronometer has incremented on its own.
- */
- public interface OnChronometerTickListener {
-
- /**
- * Notification that the chronometer has changed.
- */
- void onChronometerTick(ChronometerWithMillis chronometer);
-
- }
-
- private long mBase;
- private long mNow; // the currently displayed time
- private boolean mVisible;
- private boolean mStarted;
- private boolean mRunning;
- private boolean mLogged;
- private String mFormat;
- private Formatter mFormatter;
- private Locale mFormatterLocale;
- private Object[] mFormatterArgs = new Object[1];
- private StringBuilder mFormatBuilder;
- private OnChronometerTickListener mOnChronometerTickListener;
- private StringBuilder mRecycle = new StringBuilder(8);
- private boolean mApplySizeSpan;
-
- private static final RelativeSizeSpan SIZE_SPAN = new RelativeSizeSpan(0.5f);
-
- private static final int TICK_WHAT = 2;
-
- /**
- * Initialize this Chronometer object.
- * Sets the base to the current time.
- */
public ChronometerWithMillis(Context context) {
this(context, null, 0);
}
- /**
- * Initialize with standard view layout information.
- * Sets the base to the current time.
- */
public ChronometerWithMillis(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
- /**
- * Initialize with standard view layout information and style.
- * Sets the base to the current time.
- */
public ChronometerWithMillis(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
-
-// final TypedArray a = context.obtainStyledAttributes(
-// attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, 0);
-// setFormat(a.getString(com.android.internal.R.styleable.Chronometer_format));
-// a.recycle();
-
init();
}
@TargetApi(21)
public ChronometerWithMillis(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
-
-// final TypedArray a = context.obtainStyledAttributes(
-// attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, defStyleRes);
-// setFormat(a.getString(com.android.internal.R.styleable.Chronometer_format));
-// a.recycle();
-
init();
}
private void init() {
- mBase = SystemClock.elapsedRealtime();
- updateText(mBase);
+ setShowCentiseconds(true, false);
}
-
- /**
- * Set the time that the count-up timer is in reference to.
- *
- * @param base Use the {@link SystemClock#elapsedRealtime} time base.
- */
-// @android.view.RemotableViewMethod
- public void setBase(long base) {
- mBase = base;
- dispatchChronometerTick();
- updateText(SystemClock.elapsedRealtime());
- }
-
- /**
- * Return the base time as set through {@link #setBase}.
- */
- public long getBase() {
- return mBase;
- }
-
- /**
- * Equivalent to {@link #setBase(long) setBase(SystemClock.elapsedRealtime() - elapsed)}.
- */
- public void setElapsed(long elapsed) {
- setBase(SystemClock.elapsedRealtime() - elapsed);
- }
-
- /**
- * Sets the format string used for display. The Chronometer will display
- * this string, with the first "%s" replaced by the current timer value in
- * "MM:SS" or "H:MM:SS" form.
- *
- * If the format string is null, or if you never call setFormat(), the
- * Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS"
- * form.
- *
- * @param format the format string.
- */
-// @android.view.RemotableViewMethod
- public void setFormat(String format) {
- mFormat = format;
- if (format != null && mFormatBuilder == null) {
- mFormatBuilder = new StringBuilder(format.length() * 2);
- }
- }
-
- /**
- * Returns the current format string as set through {@link #setFormat}.
- */
- public String getFormat() {
- return mFormat;
- }
-
- /**
- * Sets the listener to be called when the chronometer changes.
- *
- * @param listener The listener.
- */
- public void setOnChronometerTickListener(OnChronometerTickListener listener) {
- mOnChronometerTickListener = listener;
- }
-
- /**
- * @return The listener (may be null) that is listening for chronometer change
- * events.
- */
- public OnChronometerTickListener getOnChronometerTickListener() {
- return mOnChronometerTickListener;
- }
-
- /**
- * Start counting up. This does not affect the base as set from {@link #setBase}, just
- * the view display.
- *
- * Chronometer works by regularly scheduling messages to the handler, even when the
- * Widget is not visible. To make sure resource leaks do not occur, the user should
- * make sure that each start() call has a reciprocal call to {@link #stop}.
- */
- public void start() {
- mStarted = true;
- updateRunning();
- }
-
- /**
- * Stop counting up. This does not affect the base as set from {@link #setBase}, just
- * the view display.
- *
- * This stops the messages to the handler, effectively releasing resources that would
- * be held as the chronometer is running, via {@link #start}.
- */
- public void stop() {
- mStarted = false;
- updateRunning();
- }
-
- /**
- * The same as calling {@link #start} or {@link #stop}.
- * @hide pending API council approval
- */
-// @android.view.RemotableViewMethod
- public void setStarted(boolean started) {
- mStarted = started;
- updateRunning();
- }
-
- public boolean isRunning() {
- return mRunning;
- }
-
- public void setApplySizeSpan(boolean applySizeSpan) {
- mApplySizeSpan = applySizeSpan;
- init(); // update text again
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mVisible = false;
- updateRunning();
- }
-
- @Override
- protected void onWindowVisibilityChanged(int visibility) {
- Log.d(TAG, "onWindowVisibilityChanged()");
- super.onWindowVisibilityChanged(visibility);
- mVisible = visibility == VISIBLE;
- updateRunning();
- }
-
- private synchronized void updateText(long now) {
- mNow = now;
- long millis = now - mBase;
- String text = DateUtils.formatElapsedTime(mRecycle, millis / 1000/*needs to be in seconds*/);
-
- Locale loc = Locale.getDefault();
- if (mFormat != null) {
- if (mFormatter == null || !loc.equals(mFormatterLocale)) {
- mFormatterLocale = loc;
- mFormatter = new Formatter(mFormatBuilder, loc);
- }
- mFormatBuilder.setLength(0);
- mFormatterArgs[0] = text;
- try {
- mFormatter.format(mFormat, mFormatterArgs);
- text = mFormatBuilder.toString();
- } catch (IllegalFormatException ex) {
- if (!mLogged) {
- Log.w(TAG, "Illegal format string: " + mFormat);
- mLogged = true;
- }
- }
- }
- long centiseconds = (millis % 1000) / 10;
- String centisecondsText = String.format(loc,
- // TODO: Different locales use different decimal marks.
- // The two most common are . and ,
- // Consider removing the . and just let the size span
- // represent this as fractional seconds?
- // ...or figure out how to get the correct mark for the
- // current locale.
- // It looks like Google's Clock app strictly uses .
- ".%02d", // The . before % is not a format specifier
- centiseconds);
- if (mApplySizeSpan) {
- SpannableString span = new SpannableString(centisecondsText);
- span.setSpan(SIZE_SPAN, 0, centisecondsText.length(),
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- setText(TextUtils.concat(text, span), BufferType.SPANNABLE);
- } else {
- setText(text.concat(centisecondsText));
- }
- }
-
- private void updateRunning() {
- boolean running = mVisible && mStarted;
- if (running != mRunning) {
- if (running) {
- Log.d(TAG, "Running");
- updateText(SystemClock.elapsedRealtime());
- dispatchChronometerTick();
- mHandler.sendMessageDelayed(Message.obtain(mHandler, TICK_WHAT), 10);
- } else {
- Log.d(TAG, "Not running anymore");
- mHandler.removeMessages(TICK_WHAT);
- }
- mRunning = running;
- }
- }
-
- private Handler mHandler = new Handler() {
- public void handleMessage(Message m) {
- if (mRunning) {
- updateText(SystemClock.elapsedRealtime());
- dispatchChronometerTick();
- sendMessageDelayed(Message.obtain(this, TICK_WHAT), 10);
- }
- }
- };
-
- void dispatchChronometerTick() {
- if (mOnChronometerTickListener != null) {
- mOnChronometerTickListener.onChronometerTick(this);
- }
- }
-
- private static final int MIN_IN_SEC = 60;
- private static final int HOUR_IN_SEC = MIN_IN_SEC*60;
-// private static String formatDuration(long ms) {
-// final Resources res = Resources.getSystem();
-// final StringBuilder text = new StringBuilder();
-//
-// int duration = (int) (ms / DateUtils.SECOND_IN_MILLIS);
-// if (duration < 0) {
-// duration = -duration;
-// }
-//
-// int h = 0;
-// int m = 0;
-//
-// if (duration >= HOUR_IN_SEC) {
-// h = duration / HOUR_IN_SEC;
-// duration -= h * HOUR_IN_SEC;
-// }
-// if (duration >= MIN_IN_SEC) {
-// m = duration / MIN_IN_SEC;
-// duration -= m * MIN_IN_SEC;
-// }
-// int s = duration;
-//
-// try {
-// if (h > 0) {
-// text.append(res.getQuantityString(
-// com.android.internal.R.plurals.duration_hours, h, h));
-// }
-// if (m > 0) {
-// if (text.length() > 0) {
-// text.append(' ');
-// }
-// text.append(res.getQuantityString(
-// com.android.internal.R.plurals.duration_minutes, m, m));
-// }
-//
-// if (text.length() > 0) {
-// text.append(' ');
-// }
-// text.append(res.getQuantityString(
-// com.android.internal.R.plurals.duration_seconds, s, s));
-// } catch (Resources.NotFoundException e) {
-// // Ignore; plurals throws an exception for an untranslated quantity for a given locale.
-// return null;
-// }
-// return text.toString();
-// }
-//
-// @Override
-// public CharSequence getContentDescription() {
-// return formatDuration(mNow - mBase);
-// }
-//
-// @Override
-// public CharSequence getAccessibilityClassName() {
-// return ChronometerWithMillis.class.getName();
-// }
}
diff --git a/app/src/main/java/com/philliphsu/clock2/stopwatch/LapViewHolder.java b/app/src/main/java/com/philliphsu/clock2/stopwatch/LapViewHolder.java
index 9605f6a..64fb305 100644
--- a/app/src/main/java/com/philliphsu/clock2/stopwatch/LapViewHolder.java
+++ b/app/src/main/java/com/philliphsu/clock2/stopwatch/LapViewHolder.java
@@ -46,7 +46,7 @@ public class LapViewHolder extends BaseViewHolder {
// We're going to forget about the + sign in front of the text. I think
// the 'Elapsed' header column is sufficient to convey what this timer means.
// (Don't want to figure out a solution)
- mElapsedTime.setElapsed(lap.elapsed());
+ mElapsedTime.setDuration(lap.elapsed());
if (lap.isRunning()) {
mElapsedTime.start();
}
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 4b1dd91..a9e966a 100644
--- a/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchFragment.java
+++ b/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchFragment.java
@@ -2,6 +2,7 @@ package com.philliphsu.clock2.stopwatch;
import android.animation.Animator;
import android.animation.ObjectAnimator;
+import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
@@ -82,7 +83,7 @@ public class StopwatchFragment extends RecyclerViewFragment<
Log.d(TAG, "onCreateView()");
View view = super.onCreateView(inflater, container, savedInstanceState);
- mChronometer.setApplySizeSpan(true);
+ mChronometer.setShowCentiseconds(true, true);
if (mStartTime > 0) {
long base = mStartTime;
if (mPauseTime > 0) {
@@ -252,6 +253,7 @@ public class StopwatchFragment extends RecyclerViewFragment<
// if (mProgressAnimator != null) {
// mProgressAnimator.resume();
// }
+ getActivity().startService(new Intent(getActivity(), StopwatchNotificationService.class));
}
savePrefs();
// TOneverDO: Precede savePrefs(), or else we don't save false to KEY_CHRONOMETER_RUNNING
diff --git a/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchNotificationService.java b/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchNotificationService.java
new file mode 100644
index 0000000..891d531
--- /dev/null
+++ b/app/src/main/java/com/philliphsu/clock2/stopwatch/StopwatchNotificationService.java
@@ -0,0 +1,110 @@
+package com.philliphsu.clock2.stopwatch;
+
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.support.annotation.DrawableRes;
+import android.support.v4.app.NotificationCompat;
+
+import com.philliphsu.clock2.MainActivity;
+import com.philliphsu.clock2.R;
+
+public class StopwatchNotificationService extends Service {
+ private static final String ACTION_ADD_LAP = "com.philliphsu.clock2.stopwatch.action.ADD_LAP";
+ private static final String ACTION_START_PAUSE = "com.philliphsu.clock2.stopwatch.action.START_PAUSE";
+ private static final String ACTION_STOP = "com.philliphsu.clock2.stopwatch.action.STOP";
+
+ private NotificationCompat.Builder mNoteBuilder;
+ private NotificationManager mNotificationManager;
+ private AsyncLapsTableUpdateHandler mLapsTableUpdateHandler;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ mLapsTableUpdateHandler = new AsyncLapsTableUpdateHandler(this, null);
+
+ // Create base note
+ // TODO: I think we can make this a foreground service so even
+ // if the process is killed, this service remains alive.
+ mNoteBuilder = new NotificationCompat.Builder(this)
+ .setSmallIcon(R.drawable.ic_stopwatch_24dp)
+ .setOngoing(true)
+ // TODO: The chronometer takes the place of the 'when' timestamp
+ // at its usual location. If you don't like this location,
+ // we can write a thread that posts a new notification every second
+ // that updates the content text.
+ // TODO: We would have to write our own chronometer logic if there
+ // is no way to pause/resume the native chronometer.
+ .setUsesChronometer(true)
+ .setContentTitle(getString(R.string.stopwatch));
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.putExtra(null/*TODO:MainActivity.EXTRA_SHOW_PAGE*/, 2/*TODO:MainActivity.INDEX_STOPWATCH*/);
+ mNoteBuilder.setContentIntent(PendingIntent.getActivity(this, 0, intent, 0));
+
+ // TODO: Move adding these actions to the default case
+ // TODO: Change fillColor to white, to accommodate API < 21.
+ // Apparently, notifications on 21+ are automatically
+ // tinted to gray to contrast against the native notification background color.
+ addAction(ACTION_ADD_LAP, R.drawable.ic_add_lap_24dp, getString(R.string.lap));
+ // TODO: Set icon and title according to state of stopwatch
+ addAction(ACTION_START_PAUSE, R.drawable.ic_pause_24dp, getString(R.string.pause));
+ addAction(ACTION_STOP, R.drawable.ic_stop_24dp, getString(R.string.stop));
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent != null) {
+ final String action = intent.getAction();
+ if (action == null) {
+ // TODO: Read the stopwatch's start time in shared prefs.
+ mNoteBuilder.setWhen(System.currentTimeMillis());
+ // TODO: Lap # content text
+ mNoteBuilder.setContentText("Lap 1");
+ // Use class name as tag instead of defining our own tag constant, because
+ // the latter is limited to 23 (?) chars if you also want to use it as
+ // a log tag.
+ mNotificationManager.notify(getClass().getName(), 0, mNoteBuilder.build());
+ } else {
+ switch (action) {
+ case ACTION_ADD_LAP:
+// mLapsTableUpdateHandler.asyncInsert(null/*TODO*/);
+ break;
+ case ACTION_START_PAUSE:
+ break;
+ case ACTION_STOP:
+ // Cancels all of the notifications issued by *this instance* of the manager,
+ // not those of any other instances (in this app or otherwise).
+ // TODO: We could cancel by (tag, id) if we cared.
+ mNotificationManager.cancelAll();
+ break;
+ }
+ }
+ }
+ return super.onStartCommand(intent, flags, startId);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ /**
+ * Builds and adds the specified action to the notification's mNoteBuilder.
+ */
+ private void addAction(String action, @DrawableRes int icon, String actionTitle) {
+ Intent intent = new Intent(this, StopwatchNotificationService.class)
+ .setAction(action);
+ PendingIntent pi = PendingIntent.getService(this, 0/*no requestCode*/,
+ intent, 0/*no flags*/);
+ mNoteBuilder.addAction(icon, actionTitle, pi);
+ }
+}
diff --git a/app/src/main/java/com/philliphsu/clock2/timers/ChronometerDelegate.java b/app/src/main/java/com/philliphsu/clock2/timers/ChronometerDelegate.java
new file mode 100644
index 0000000..a57a704
--- /dev/null
+++ b/app/src/main/java/com/philliphsu/clock2/timers/ChronometerDelegate.java
@@ -0,0 +1,127 @@
+package com.philliphsu.clock2.timers;
+
+import android.os.SystemClock;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.text.style.RelativeSizeSpan;
+import android.util.Log;
+
+import java.util.Formatter;
+import java.util.IllegalFormatException;
+import java.util.Locale;
+
+/**
+ * Created by Phillip Hsu on 9/7/2016.
+ *
+ * A helper class for BaseChronometer that handles formatting the text.
+ * Can also be used independent of Chronometer to format elapsed times and return the result
+ * as a CharSequence.
+ */
+public final class ChronometerDelegate {
+ private static final String TAG = "ChronometerDelegate";
+
+ private static final RelativeSizeSpan SIZE_SPAN = new RelativeSizeSpan(0.5f);
+
+ private long mBase;
+ private long mNow; // the currently displayed time
+ private boolean mLogged;
+ private String mFormat;
+ private Formatter mFormatter;
+ private Locale mFormatterLocale;
+ private Object[] mFormatterArgs = new Object[1];
+ private StringBuilder mFormatBuilder;
+ private StringBuilder mRecycle = new StringBuilder(8);
+ private boolean mCountDown;
+ private boolean mShowCentiseconds;
+ private boolean mApplySizeSpanOnCentiseconds;
+
+ public void init() {
+ mBase = SystemClock.elapsedRealtime();
+ }
+
+ public void setCountDown(boolean countDown) {
+ mCountDown = countDown;
+ }
+
+ public boolean isCountDown() {
+ return mCountDown;
+ }
+
+ public void setShowCentiseconds(boolean showCentiseconds, boolean applySizeSpan) {
+ mShowCentiseconds = showCentiseconds;
+ mApplySizeSpanOnCentiseconds = applySizeSpan;
+ }
+
+ public boolean showsCentiseconds() {
+ return mShowCentiseconds;
+ }
+
+ public void setBase(long base) {
+ mBase = base;
+ }
+
+ public long getBase() {
+ return mBase;
+ }
+
+ public void setFormat(String format) {
+ mFormat = format;
+ if (format != null && mFormatBuilder == null) {
+ mFormatBuilder = new StringBuilder(format.length() * 2);
+ }
+ }
+
+ public String getFormat() {
+ return mFormat;
+ }
+
+ public CharSequence formatElapsedTime(long now) {
+ mNow = now;
+ long millis = mCountDown ? mBase - now : now - mBase;
+ String text = DateUtils.formatElapsedTime(mRecycle, millis / 1000);
+
+ Locale loc = Locale.getDefault();
+ if (mFormat != null) {
+ if (mFormatter == null || !loc.equals(mFormatterLocale)) {
+ mFormatterLocale = loc;
+ mFormatter = new Formatter(mFormatBuilder, loc);
+ }
+ mFormatBuilder.setLength(0);
+ mFormatterArgs[0] = text;
+ try {
+ mFormatter.format(mFormat, mFormatterArgs);
+ text = mFormatBuilder.toString();
+ } catch (IllegalFormatException ex) {
+ if (!mLogged) {
+ Log.w(TAG, "Illegal format string: " + mFormat);
+ mLogged = true;
+ }
+ }
+ }
+ if (mShowCentiseconds) {
+ long centiseconds = (millis % 1000) / 10;
+ String centisecondsText = String.format(loc,
+ // TODO: Different locales use different decimal marks.
+ // The two most common are . and ,
+ // Consider removing the . and just let the size span
+ // represent this as fractional seconds?
+ // ...or figure out how to get the correct mark for the
+ // current locale.
+ // It looks like Google's Clock app strictly uses .
+ ".%02d", // The . before % is not a format specifier
+ centiseconds);
+ if (mApplySizeSpanOnCentiseconds) {
+ SpannableString span = new SpannableString(centisecondsText);
+ span.setSpan(SIZE_SPAN, 0, centisecondsText.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ return TextUtils.concat(text, span);
+ } else {
+ return text.concat(centisecondsText);
+ }
+ }
+
+ return text;
+ }
+}
diff --git a/app/src/main/java/com/philliphsu/clock2/timers/CountdownChronometer.java b/app/src/main/java/com/philliphsu/clock2/timers/CountdownChronometer.java
index 62f367e..0cf3e0b 100644
--- a/app/src/main/java/com/philliphsu/clock2/timers/CountdownChronometer.java
+++ b/app/src/main/java/com/philliphsu/clock2/timers/CountdownChronometer.java
@@ -18,12 +18,9 @@ package com.philliphsu.clock2.timers;
import android.annotation.TargetApi;
import android.content.Context;
-import android.os.Handler;
-import android.os.Message;
-import android.os.SystemClock;
import android.util.AttributeSet;
-import android.util.Log;
-import android.widget.TextView;
+
+import com.philliphsu.clock2.BaseChronometer;
/**
* Created by Phillip Hsu on 7/25/2016.
@@ -32,285 +29,29 @@ import android.widget.TextView;
* towards the base time. The ability to count down was added to Chronometer
* in API 24.
*/
-public class CountdownChronometer extends TextView {
+public class CountdownChronometer extends BaseChronometer {
private static final String TAG = "CountdownChronometer";
- /**
- * A callback that notifies when the chronometer has incremented on its own.
- */
- public interface OnChronometerTickListener {
-
- /**
- * Notification that the chronometer has changed.
- */
- void onChronometerTick(CountdownChronometer chronometer);
-
- }
-
- private boolean mVisible;
- private boolean mStarted;
- private boolean mRunning;
- private final CountdownDelegate mDelegate = new CountdownDelegate();
- private OnChronometerTickListener mOnChronometerTickListener;
-
- private static final int TICK_WHAT = 2;
-
- /**
- * Initialize this Chronometer object.
- * Sets the base to the current time.
- */
public CountdownChronometer(Context context) {
this(context, null, 0);
}
- /**
- * Initialize with standard view layout information.
- * Sets the base to the current time.
- */
public CountdownChronometer(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
- /**
- * Initialize with standard view layout information and style.
- * Sets the base to the current time.
- */
public CountdownChronometer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
-
-// final TypedArray a = context.obtainStyledAttributes(
-// attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, 0);
-// setFormat(a.getString(com.android.internal.R.styleable.Chronometer_format));
-// a.recycle();
-
init();
}
@TargetApi(21)
public CountdownChronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
-
-// final TypedArray a = context.obtainStyledAttributes(
-// attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, defStyleRes);
-// setFormat(a.getString(com.android.internal.R.styleable.Chronometer_format));
-// a.recycle();
-
init();
}
private void init() {
- mDelegate.init();
- updateText(SystemClock.elapsedRealtime());
+ setCountDown(true);
}
-
- /**
- * Set the time that the count-up timer is in reference to.
- *
- * @param base Use the {@link SystemClock#elapsedRealtime} time base.
- */
-// @android.view.RemotableViewMethod
- public void setBase(long base) {
- mDelegate.setBase(base);
- dispatchChronometerTick();
- updateText(SystemClock.elapsedRealtime());
- }
-
- /**
- * Return the base time as set through {@link #setBase}.
- */
- public long getBase() {
- return mDelegate.getBase();
- }
-
- /**
- * Equivalent to {@link #setBase(long) setBase(SystemClock.elapsedRealtime() + duration)}.
- */
- public void setDuration(long duration) {
- setBase(SystemClock.elapsedRealtime() + duration);
- }
-
- /**
- * Sets the format string used for display. The Chronometer will display
- * this string, with the first "%s" replaced by the current timer value in
- * "MM:SS" or "H:MM:SS" form.
- *
- * If the format string is null, or if you never call setFormat(), the
- * Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS"
- * form.
- *
- * @param format the format string.
- */
-// @android.view.RemotableViewMethod
- public void setFormat(String format) {
- mDelegate.setFormat(format);
- }
-
- /**
- * Returns the current format string as set through {@link #setFormat}.
- */
- public String getFormat() {
- return mDelegate.getFormat();
- }
-
- /**
- * Sets the listener to be called when the chronometer changes.
- *
- * @param listener The listener.
- */
- public void setOnChronometerTickListener(OnChronometerTickListener listener) {
- mOnChronometerTickListener = listener;
- }
-
- /**
- * @return The listener (may be null) that is listening for chronometer change
- * events.
- */
- public OnChronometerTickListener getOnChronometerTickListener() {
- return mOnChronometerTickListener;
- }
-
- /**
- * Start counting up. This does not affect the base as set from {@link #setBase}, just
- * the view display.
- *
- * Chronometer works by regularly scheduling messages to the handler, even when the
- * Widget is not visible. To make sure resource leaks do not occur, the user should
- * make sure that each start() call has a reciprocal call to {@link #stop}.
- */
- public void start() {
- mStarted = true;
- updateRunning();
- }
-
- /**
- * Stop counting up. This does not affect the base as set from {@link #setBase}, just
- * the view display.
- *
- * This stops the messages to the handler, effectively releasing resources that would
- * be held as the chronometer is running, via {@link #start}.
- */
- public void stop() {
- mStarted = false;
- updateRunning();
- }
-
- /**
- * The same as calling {@link #start} or {@link #stop}.
- * @hide pending API council approval
- */
-// @android.view.RemotableViewMethod
- public void setStarted(boolean started) {
- mStarted = started;
- updateRunning();
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mVisible = false;
- updateRunning();
- }
-
- @Override
- protected void onWindowVisibilityChanged(int visibility) {
- Log.d(TAG, "onWindowVisibilityChanged()");
- super.onWindowVisibilityChanged(visibility);
- mVisible = visibility == VISIBLE;
- updateRunning();
- }
-
- private synchronized void updateText(long now) {
- setText(mDelegate.formatElapsedTime(now));
- }
-
- private void updateRunning() {
- boolean running = mVisible && mStarted;
- if (running != mRunning) {
- if (running) {
- Log.d(TAG, "Running");
- updateText(SystemClock.elapsedRealtime());
- dispatchChronometerTick();
- mHandler.sendMessageDelayed(Message.obtain(mHandler, TICK_WHAT), 1000);
- } else {
- Log.d(TAG, "Not running anymore");
- mHandler.removeMessages(TICK_WHAT);
- }
- mRunning = running;
- }
- }
-
- private Handler mHandler = new Handler() {
- public void handleMessage(Message m) {
- if (mRunning) {
- updateText(SystemClock.elapsedRealtime());
- dispatchChronometerTick();
- sendMessageDelayed(Message.obtain(this, TICK_WHAT), 1000);
- }
- }
- };
-
- void dispatchChronometerTick() {
- if (mOnChronometerTickListener != null) {
- mOnChronometerTickListener.onChronometerTick(this);
- }
- }
-
- private static final int MIN_IN_SEC = 60;
- private static final int HOUR_IN_SEC = MIN_IN_SEC*60;
-// private static String formatDuration(long ms) {
-// final Resources res = Resources.getSystem();
-// final StringBuilder text = new StringBuilder();
-//
-// int duration = (int) (ms / DateUtils.SECOND_IN_MILLIS);
-// if (duration < 0) {
-// duration = -duration;
-// }
-//
-// int h = 0;
-// int m = 0;
-//
-// if (duration >= HOUR_IN_SEC) {
-// h = duration / HOUR_IN_SEC;
-// duration -= h * HOUR_IN_SEC;
-// }
-// if (duration >= MIN_IN_SEC) {
-// m = duration / MIN_IN_SEC;
-// duration -= m * MIN_IN_SEC;
-// }
-// int s = duration;
-//
-// try {
-// if (h > 0) {
-// text.append(res.getQuantityString(
-// com.android.internal.R.plurals.duration_hours, h, h));
-// }
-// if (m > 0) {
-// if (text.length() > 0) {
-// text.append(' ');
-// }
-// text.append(res.getQuantityString(
-// com.android.internal.R.plurals.duration_minutes, m, m));
-// }
-//
-// if (text.length() > 0) {
-// text.append(' ');
-// }
-// text.append(res.getQuantityString(
-// com.android.internal.R.plurals.duration_seconds, s, s));
-// } catch (Resources.NotFoundException e) {
-// // Ignore; plurals throws an exception for an untranslated quantity for a given locale.
-// return null;
-// }
-// return text.toString();
-// }
-//
-// @Override
-// public CharSequence getContentDescription() {
-// return formatDuration(mNow - mBase);
-// }
-//
-// @Override
-// public CharSequence getAccessibilityClassName() {
-// return CountdownChronometer.class.getName();
-// }
}
diff --git a/app/src/main/java/com/philliphsu/clock2/timers/CountdownDelegate.java b/app/src/main/java/com/philliphsu/clock2/timers/CountdownDelegate.java
deleted file mode 100644
index 5ccbb03..0000000
--- a/app/src/main/java/com/philliphsu/clock2/timers/CountdownDelegate.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package com.philliphsu.clock2.timers;
-
-import android.os.SystemClock;
-import android.text.format.DateUtils;
-import android.util.Log;
-
-import java.util.Formatter;
-import java.util.IllegalFormatException;
-import java.util.Locale;
-
-/**
- * Created by Phillip Hsu on 9/7/2016.
- *
- * A helper class for CountdownChronometer that handles formatting the countdown text.
- * TODO: A similar delegate class can also be made for ChronometerWithMillis. However, try to
- * use a common base class between this and ChronometerWithMillis.
- */
-final class CountdownDelegate {
- private static final String TAG = "CountdownDelegate";
-
- private long mBase;
- private long mNow; // the currently displayed time
- private boolean mLogged;
- private String mFormat;
- private Formatter mFormatter;
- private Locale mFormatterLocale;
- private Object[] mFormatterArgs = new Object[1];
- private StringBuilder mFormatBuilder;
- private StringBuilder mRecycle = new StringBuilder(8);
-
- void init() {
- mBase = SystemClock.elapsedRealtime();
- }
-
- void setBase(long base) {
- mBase = base;
- }
-
- long getBase() {
- return mBase;
- }
-
- void setFormat(String format) {
- mFormat = format;
- if (format != null && mFormatBuilder == null) {
- mFormatBuilder = new StringBuilder(format.length() * 2);
- }
- }
-
- String getFormat() {
- return mFormat;
- }
-
- String formatElapsedTime(long now) {
- mNow = now;
- long seconds = mBase - now;
- seconds /= 1000;
- String text = DateUtils.formatElapsedTime(mRecycle, seconds);
-
- if (mFormat != null) {
- Locale loc = Locale.getDefault();
- if (mFormatter == null || !loc.equals(mFormatterLocale)) {
- mFormatterLocale = loc;
- mFormatter = new Formatter(mFormatBuilder, loc);
- }
- mFormatBuilder.setLength(0);
- mFormatterArgs[0] = text;
- try {
- mFormatter.format(mFormat, mFormatterArgs);
- text = mFormatBuilder.toString();
- } catch (IllegalFormatException ex) {
- if (!mLogged) {
- Log.w(TAG, "Illegal format string: " + mFormat);
- mLogged = true;
- }
- }
- }
-
- return text;
- }
-}
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 62a532c..1d0f356 100644
--- a/app/src/main/java/com/philliphsu/clock2/timers/TimerNotificationService.java
+++ b/app/src/main/java/com/philliphsu/clock2/timers/TimerNotificationService.java
@@ -40,7 +40,7 @@ public class TimerNotificationService extends Service {
private TimerController mController;
private NotificationCompat.Builder mNoteBuilder;
private NotificationManager mNotificationManager;
- private final CountdownDelegate mCountdownDelegate = new CountdownDelegate();
+ private final ChronometerDelegate mCountdownDelegate = new ChronometerDelegate();
private MyHandlerThread mThread; // TODO: I think we may need a list of threads.
/**
@@ -210,7 +210,7 @@ public class TimerNotificationService extends Service {
}
private void updateNotification() {
- String text = mCountdownDelegate.formatElapsedTime(SystemClock.elapsedRealtime());
+ CharSequence text = mCountdownDelegate.formatElapsedTime(SystemClock.elapsedRealtime());
mNoteBuilder.setContentText(text);
mNotificationManager.notify(TAG, mTimer.getIntId(), mNoteBuilder.build());
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 63e6de2..7e5e01d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -203,4 +203,7 @@
No alarms added
No timers added
+
+ Stopwatch
+ Lap