IntersectGlasses
IntersectGlasses

Reputation: 53

CountDownTimer onTick() only gets called once

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

Answers (1)

Sagar
Sagar

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

Related Questions