android countdowntimer tick is not accurate
Asked Answered
P

4

10

I am using a countdown timer for audio notification... and it's not accurate from the start...

using initial parameters

private final long startCountDown; 
private final long intervalCountDown;
    ...
    startCountDown = 180 * 1000;   // 3 mns  - to be set from Preferences later
intervalCountDown = 60 * 1000;   // 1 mns - to be set from Preferences later
    ...
    public void onTick(long millisUntilFinished) {
       Log.d(TAG, "notify countDown: " + millisUntilFinished + " msecs");
    }


    countDownTimer = new SwimCountDownTimer(startCountDown,intervalCountDown);
    ....

public void startCountDown() {
    Log.d(TAG, "start countDown for " + startCountDown + " msecs" );
    countDownTimer.start();
}

I can see in the log that the initial countdown is correctly set to 180000 but the next one should be 120000 and it's set to 119945 !!!

04-27 14:50:42.146: I/SWIMMER(8670): notify countDown: 180000 msecs
04-27 14:51:42.206: I/SWIMMER(8670): notify countDown: 119945 msecs

This is quite annoying as the audio notifier is expecting to say only '2 minutes" and not "1 minute and fifty nine seconds" ...; why the interval is not right ... ? I can tricj it in setting myself the text to speech string ... but is there any way to get correct data ?

thanks for suggestions

Phosphor answered 27/4, 2014 at 13:8 Comment(3)
Is 55 milliseconds such a big problem? It's 1/20th of a second off. Most people would not be able to discern the difference.Bit
If it's consistently slow by about 55 milliseconds, and never greater than 120000ms, then that's probably how long it takes to call on onTick method. Just round it up.Caitlin
thanks ... problem is not accuracy , but reusing the value for audio notification, so I guess rounding is the solution as Greg mentioned... 120000ms is "2 minutes" audio message... 119945 is not ...Phosphor
S
21

I know it's an old question- but I've also encountered the problem, and thought I would share my solution.

Apperantly CountDownTimer isn't very accurate, so I've decided to implement a more percise countdown timer, using java.util.Timer:

public abstract class PreciseCountdown extends Timer {
    private long totalTime, interval, delay;
    private TimerTask task;
    private long startTime = -1;
    private boolean restart = false, wasCancelled = false, wasStarted = false;

    public PreciseCountdown(long totalTime, long interval) {
        this(totalTime, interval, 0);
    }

    public PreciseCountdown(long totalTime, long interval, long delay) {
        super("PreciseCountdown", true);
        this.delay = delay;
        this.interval = interval;
        this.totalTime = totalTime;
        this.task = getTask(totalTime);
    }

    public void start() {
        wasStarted = true;
        this.scheduleAtFixedRate(task, delay, interval);
    }

    public void restart() {
        if(!wasStarted) {
            start();
        }
        else if(wasCancelled) {
            wasCancelled = false;
            this.task = getTask(totalTime);
            start();
        }
        else{
            this.restart = true;
        }
    }

    public void stop() {
        this.wasCancelled = true;
        this.task.cancel();
    }

    // Call this when there's no further use for this timer
    public void dispose(){
        cancel();
        purge();
    }

    private TimerTask getTask(final long totalTime) {
        return new TimerTask() {

            @Override
            public void run() {
                long timeLeft;
                if (startTime < 0 || restart) {
                    startTime = scheduledExecutionTime();
                    timeLeft = totalTime;
                    restart = false;
                } else {
                    timeLeft = totalTime - (scheduledExecutionTime() - startTime);

                    if (timeLeft <= 0) {
                        this.cancel();
                        startTime = -1;
                        onFinished();
                        return;
                    }
                }

                onTick(timeLeft);
            }
        };
    }

    public abstract void onTick(long timeLeft);
    public abstract void onFinished();
}

Usage example would be:

this.countDown = new PreciseCountdown(totalTime, interval, delay) {
    @Override
    public void onTick(long timeLeft) {
        // update..
        // note that this runs on a different thread, so to update any GUI components you need to use Activity.runOnUiThread()
    }

    @Override
    public void onFinished() {
        onTick(0); // when the timer finishes onTick isn't called
        // count down is finished
    }
};

to start the countdown, simply call countDown.start(). countDown.stop() stops the countDown, which could be restarted using countDown.restart().

Hope this is any help for anyone in the future.

Selinaselinda answered 13/9, 2015 at 20:14 Comment(2)
Man, you are amazing) You might be the reason I ll get my new job as I was doing a test assignment )))Possession
Thankyou very much! you save my life!Martino
C
2

This is an extension on what Noam Gal posted. I added extra functionality where you can pause and resume the timer. This was very helpful in my case.

public abstract class PreciseCountdownTimer extends Timer {

    private long totalTime, interval, delay;
    private TimerTask task;
    private long startTime = -1;
    private long timeLeft;
    private boolean restart = false;
    private boolean wasCancelled = false;
    private boolean wasStarted = false;

    public PreciseCountdownTimer(long totalTime, long interval) {
        this(totalTime, interval, 0);
    }


    public PreciseCountdownTimer(long totalTime, long interval, long delay ) {
        super("PreciseCountdownTimer", true);
        this.delay = delay;
        this.interval = interval;
        this.totalTime = totalTime;
        this.task = buildTask(totalTime);
    }

    private TimerTask buildTask(final long totalTime) {
        return new TimerTask() {

            @Override
            public void run() {
                if (startTime < 0 || restart) {
                    startTime = scheduledExecutionTime();
                    timeLeft = totalTime;
                    restart = false;
                } else {
                    timeLeft = totalTime - (scheduledExecutionTime() - startTime);

                    if (timeLeft <= 0) {
                        this.cancel();
                        wasCancelled = true;
                        startTime = -1;
                        onFinished();
                        return;
                    }
                }

                onTick(timeLeft);
            }
        };
    }

    public void start() {
        wasStarted = true;
        this.scheduleAtFixedRate(task, delay, interval);
    }

    public void stop() {
        this.wasCancelled = true;
        this.task.cancel();
    }

    public void restart() {
        if (!wasStarted) {
            start();
        } else if (wasCancelled) {
            wasCancelled = false;
            this.task = buildTask(totalTime);
            start();
        } else {
            this.restart = true;
        }
    }

    public void pause(){
        wasCancelled = true;
        this.task.cancel();
        onPaused();
    }

    public void resume(){
        wasCancelled = false;
        this.task = buildTask(timeLeft);
        this.startTime = - 1;
        start();
        onResumed();
    }

    // Call this when there's no further use for this timer
    public void dispose() {
        this.cancel();
        this.purge();
    }

    public abstract void onTick(long timeLeft);

    public abstract void onFinished();

    public abstract void onPaused();

    public abstract void onResumed();
}

Usage example would be almost exactly the same:

this.timer = new PreciseCountdownTimer(totalTime, interval, delay) {
            @Override
            public void onTick(long timeLeft) {
                // note that this runs on a different thread, so to update any GUI components you need to use Activity.runOnUiThread()
            }

            @Override
            public void onFinished() {
                onTick(0); // when the timer finishes onTick isn't called
                // count down is finished
            }

            @Override
            public void onPaused() {
                // runs after the timer has been paused
            }

            @Override
            public void onResumed() {
                // runs after the timer has been resumed
            }
        };

Enjoy and have fun :D

Colossians answered 18/2, 2021 at 10:53 Comment(0)
D
0

That's true and I observed the same behaviour (logging millisUntilFinished):

9999        // 1 ms lag
8997        // 3 ms lag
7995        // 5 ms lag
6993        // 7 ms lag
5991        // 9 ms lag
4987        // 13 ms lag
3985        // 15 ms lag
2979        // 21 ms lag
1975        // 25 ms lag
971         // 29 ms lag

The reason is that it's implementation doesn't take into account the time a message stays in thread's message queue and the time needed for synchronization.

I prepared the fixed version (repo, class source).

It prints the following sequence:

9999        // 1 ms lag
8999        // 1 ms lag
7999        // 1 ms lag
6997        // 3 ms lag
5997        // 3 ms lag
4998        // 2 ms lag
3997        // 3 ms lag
2998        // 2 ms lag
1997        // 3 ms lag
997         // 3 ms lag

Small lag is still here, but the most important thing is that it doesn't accumulate.

To install it add in your root build.gradle at the end of repositories:

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

Add the dependency:

dependencies {
    implementation 'com.github.cardinalby:accurate-count-down-timer:1.0'
}
Diverse answered 31/10, 2022 at 20:18 Comment(0)
F
-1

Instead of using millisUntilFinished, you can use a variable to hold the remaining time and in every onTick, minus the variable with the interval. In this way, remainingTime is always accurate.

private class MyTimer(
    countDownTime: Long, 
    interval: Long
) : CountDownTimer(countDownTime, interval) {

    private var remainingTime = countDownTime

    override fun onFinish() {
    }

    override fun onTick(millisUntilFinished: Long) {
        // consume remainingTime here and then minus interval
        remainingTime -= interval
    }
}
Forgive answered 22/5, 2019 at 2:46 Comment(1)
millisUntilFinished is correct (corresponds to the remaining millis at the moment when onTick() is called). The problem is that onTick() is called at the not accurate time and your approach doesn't address it.Diverse

© 2022 - 2024 — McMap. All rights reserved.