Reputation: 53
I've constructed a custom CountDownTimer for my Android App out of this and this to add functionality (pause etc) and make it more accurate than the original CountDownTimer.
import android.os.SystemClock;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
public abstract class MoreAccurateCountDownTimer {
/**
* Millis since epoch when alarm should stop.
*/
private final long mMillisInFuture;
/**
* The interval in millis that the user receives callbacks
*/
private final long mCountdownInterval;
private long mStopTimeInFuture;
private long mPauseTime;
/**
* boolean representing if the timer was cancelled
*/
private boolean mCancelled = false;
private boolean mPaused = false;
private long mNextTime;
/**
* @param millisInFuture The number of millis in the future from the call
* to {@link #start()} until the countdown is done and {@link #onFinish()}
* is called.
* @param countDownInterval The interval along the way to receive
* {@link #onTick(long)} callbacks.
*/
public MoreAccurateCountDownTimer(long millisInFuture, long countDownInterval) {
mMillisInFuture = millisInFuture;
mCountdownInterval = countDownInterval;
}
/**
* Cancel the countdown.
*/
public synchronized final void cancel() {
mCancelled = true;
mHandler.removeMessages(MSG);
}
/**
* Start the countdown.
*/
public synchronized final MoreAccurateCountDownTimer start() {
mCancelled = false;
mPaused = false;
if (mMillisInFuture <= 0) {
onFinish();
return this;
}
mNextTime = SystemClock.elapsedRealtime();
mStopTimeInFuture = mNextTime + mMillisInFuture;
mNextTime += mCountdownInterval;
mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG), mNextTime);
return this;
}
public long pause() {
mPauseTime = mStopTimeInFuture - SystemClock.elapsedRealtime();
mPaused = true;
return mPauseTime;
}
public long resume() {
mStopTimeInFuture = mPauseTime + SystemClock.elapsedRealtime();
mPaused = false;
mHandler.sendMessage(mHandler.obtainMessage(MSG));
return mPauseTime;
}
/**
* Callback fired on regular interval.
* @param millisUntilFinished The amount of time until finished.
*/
public abstract void onTick(long millisUntilFinished);
/**
* Callback fired when the time is up.
*/
public abstract void onFinish();
private static final int MSG = 1;
// handles counting down
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
synchronized (MoreAccurateCountDownTimer.this) {
if (mCancelled) {
return;
}
if (!mPaused) {
final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();
if (millisLeft <= 0) {
onFinish();
} else {
onTick(millisLeft);
long currentTime = SystemClock.elapsedRealtime();
do {
mNextTime += mCountdownInterval;
} while (currentTime > mNextTime);
if (mNextTime < mStopTimeInFuture) {
sendMessageAtTime(obtainMessage(MSG), mNextTime);
} else {
sendMessageAtTime(obtainMessage(MSG), mStopTimeInFuture);
}
}
}
}
}
};
}
The Timer is updating a Text and a ProgressBar every second (counting down, who would've thought :) ) and can be started, paused and resumed through a button (a FAB).
All of this works great on multiple Emulators and it worked without a hitch on my old phone (Nexus 5) a while back (kind of an ongoing side-project).
Now I tested it on my new phone (Pixel 2) and it's not working.
It seems to call the onTick() just once per "action" (start, pause, resume), which leads to: (Example) -> I press start, wait for 5 seconds, press pause (nothing happened visually until now) and then resume. On the resume-click it updates the Text and ProgressBar with the -5 seconds and does nothing again (visually).
And here comes the really weird part: When I reboot my phone and then open the App before doing anything else, it works! If I put it aside for a few minutes after that (screen off) it's bugged again.
My thoughts so far:
- The Timer itself (the handler) seems to work, because WHEN it updates the UI on the resume-click, the missing seconds correspond to real-life-seconds :P
- I put Log's all over the Class to check if something isn't getting called in there, but everything is (just once per click and not on it's own as it should).
- I simulated idle/deep-sleep in the emulator, because I thought that it could explain why it works after a reboot. It doesn't
- I tried debugging it step-by-step, but nothing stood out to me
- Android Studio isn't showing me any error, exception etc.
Maybe I'm just blind to an obvious mistake, because I looked at it so much (I hope), and I really hope someone sees it right away :)
Any help is much appreciated!
Edit for clarification:
In the onClick() of my FAB I call countDownTimer1.start();
or pause/resume.
And the onTick() part looks like this:
MoreAccurateCountDownTimer countDownTimer1 = new MoreAccurateCountDownTimer(timeTimer * 1000 + 100, 1000) {
@Override
public void onTick(long millisUntilFinished) {
timeRemaining.setText(...etc...);
progressCircle1.setProgress((int) (millisUntilFinished / 1000));
And the onFinish() would call a second timer the same way.
Small edit2: The edited code runs in a Fragment, if that is of any importance.
Upvotes: 1
Views: 880
Reputation: 24937
Updated:
Based on documentation for sendMessageAtTime()
:
Enqueue a message into the message queue after all pending messages before the absolute time (in milliseconds) uptimeMillis. The time-base isuptimeMillis(). Time spent in deep sleep will add an additional delay to execution.
When I change all of your
SystemClock.elapsedRealtime()
TO
SystemClock.uptimeMillis()
And, in onResume()
do following:
mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(MSG))
intead of
mHandler.sendMessage(mHandler.obtainMessage(MSG));
It worked perfectly. Based on Documentation for handler.sendMessage()
:
Pushes a message onto the end of the message queue after all pending messages before the current time
Upvotes: 0