9

From my observation the android CountDownTimer countDownInterval between ticks happens to be not accurate, the countDownInterval is regularly a few milliseconds longer than specified. The countDownInterval in my specific app is 1000ms, just counting down a certain amount of time with one second steps.

Due to this prolonged ticks I end up having less ticks then wanted when the the countdowntimer runs long enough which screws up the displayed countdown of the time (a 2 second step happens on the UI level when enough additional ms have summed up)

Looking into the source of CountDownTimer it seems possible to twist it so it corrects this unwanted inaccuracy yet I was wondering if there is already a better CountDownTimer available in the java/android world.

Thanks buddies for any pointer ...

dorjeduck
  • 7,624
  • 11
  • 52
  • 66

6 Answers6

18

Rewrite

As you said, you also noticed that the next time in onTick() is calculated from the time the previous onTick() ran, which introduces a tiny error on every tick. I changed the CountDownTimer source code to call each onTick() at the specified intervals from the start time.

I build this upon the CountDownTimer framework, so cut & paste the source code into your project and give the class a unique name. (I called mine MoreAccurateTimer.) Now make a few changes:

  1. Add a new class variable:

    private long mNextTime;
    
  2. Change start():

    public synchronized final MoreAccurateTimer start() {
        if (mMillisInFuture <= 0) {
            onFinish();
            return this;
        }
    
        mNextTime = SystemClock.uptimeMillis();
        mStopTimeInFuture = mNextTime + mMillisInFuture;
    
        mNextTime += mCountdownInterval;
        mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG), mNextTime);
        return this;
    }
    
  3. Change the Handler's handlerMessage():

    @Override
    public void handleMessage(Message msg) {
        synchronized (MoreAccurateTimer.this) {
            final long millisLeft = mStopTimeInFuture - SystemClock.uptimeMillis();
    
            if (millisLeft <= 0) {
                onFinish();
            } else {
                onTick(millisLeft);
    
                // Calculate next tick by adding the countdown interval from the original start time
                // If user's onTick() took too long, skip the intervals that were already missed
                long currentTime = SystemClock.uptimeMillis();
                do {
                    mNextTime += mCountdownInterval;
                } while (currentTime > mNextTime);
    
                // Make sure this interval doesn't exceed the stop time
                if(mNextTime < mStopTimeInFuture)
                    sendMessageAtTime(obtainMessage(MSG), mNextTime);
                else
                    sendMessageAtTime(obtainMessage(MSG), mStopTimeInFuture);
            }
        }
    }
    
Sam
  • 86,580
  • 20
  • 181
  • 179
  • Interesting idea, will digest it and post here what i came up with for further discussion.. – dorjeduck Oct 06 '12 at 18:18
  • 1
    you mentioned other problems with CountDownTimer - was it the missing last tick due to ms delay? already had to adjust the class for that issue ... – dorjeduck Oct 06 '12 at 18:25
  • Yes. Assume it's counting down from three, the output was often: 3, 2, (_long pause_) 0... That was infuriating. – Sam Oct 06 '12 at 18:28
  • yep - it is build in if you look into the source code (no tick if the remaining time is even 1ms smaller than the given intervall) had a discussion here on stackoverflow before, some people say it makes sense yet for me it is poor design (within an overall very well done android ...) – dorjeduck Oct 06 '12 at 18:33
  • thanks a lot - i just posted my solution i came up with which seems to do. it doesnt include my modifications i made to get the last tick as i didnt want to mix things up here. I will have a look at your solution now, thanks for sharing. – dorjeduck Oct 06 '12 at 19:44
  • more clean and straight forward than what i came up with - may google use it for future releases ... ;) Thanks a lot – dorjeduck Oct 06 '12 at 20:04
  • and I learned about the sendMessageAtTime method which does the trick in your clean solution ;-) – dorjeduck Oct 06 '12 at 20:17
  • 1
    Thanks for your kind words. This version prevents that cumulative error (just like yours), but I also did away with the quirk that would sometimes skip the last tick. – Sam Oct 06 '12 at 22:09
  • what exactly is `mStopTime` here? where is it instantiated? i'm trying to incorporate this to my own timer class. – mango Nov 17 '12 at 03:12
  • I can't find where `mStopTime` is defined in your code or in the original `CountDownTimer`. – borges Nov 25 '12 at 22:04
  • hey thanks for letting me know. by process of elimination i decided to test it with that regardless. very nice implementation you've done! it's been working wonders for me. – mango Nov 25 '12 at 23:47
  • @Sam This implementation (and the stock, copied and pasted CountDownTimer source) raise an warning in Android Studio: "This Handler class should be private or leaks might occur." Can that warning be ignored? – Alex Johnson May 26 '16 at 01:42
  • @Sam thanks for this awesome code..one issue though, the timer is starting 2 seconds late..for ex. when "long millisInFuture"=15 timer starts from 13 and when "long millisInFuture"=30 timer starts from 28..please help – Nike15 Oct 20 '16 at 06:41
  • @Sam one more issue, it is skipping seconds in UI..for ex 33,32,30 and 25, 24, 22 & so on – Nike15 Oct 20 '16 at 06:44
4

So this is what I came up with. It is a small modification of the original CountDownTimer. What it adds is a variable mTickCounter which counts the amount of ticks called. This variable is used together with the new variable mStartTime to see how accurate we are with our ticks. Based on this input the delay to the next tick is adjusted ... It seems to do what I was looking for yet I am sure this can be improved upon.

Look for

// ************AccurateCountdownTimer***************

in the source code to find the modifications I have added to the original class.

package com.dorjeduck.xyz;

import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;

/**
 * Schedule a countdown until a time in the future, with regular notifications
 * on intervals along the way.
 * 
 * Example of showing a 30 second countdown in a text field:
 * 
 * <pre class="prettyprint">
 * new CountDownTimer(30000, 1000) {
 * 
 *  public void onTick(long millisUntilFinished) {
 *      mTextField.setText(&quot;seconds remaining: &quot; + millisUntilFinished / 1000);
 *  }
 * 
 *  public void onFinish() {
 *      mTextField.setText(&quot;done!&quot;);
 *  }
 * }.start();
 * </pre>
 * 
 * The calls to {@link #onTick(long)} are synchronized to this object so that
 * one call to {@link #onTick(long)} won't ever occur before the previous
 * callback is complete. This is only relevant when the implementation of
 * {@link #onTick(long)} takes an amount of time to execute that is significant
 * compared to the countdown interval.
 */
public abstract class AccurateCountDownTimer {

    /**
     * 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;

    // ************AccurateCountdownTimer***************
    private int mTickCounter;
    private long mStartTime;

    // ************AccurateCountdownTimer***************

    /**
     * @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 AccurateCountDownTimer(long millisInFuture, long countDownInterval) {
        mMillisInFuture = millisInFuture;
        mCountdownInterval = countDownInterval;

        // ************AccurateCountdownTimer***************
        mTickCounter = 0;
        // ************AccurateCountdownTimer***************
    }

    /**
     * Cancel the countdown.
     */
    public final void cancel() {
        mHandler.removeMessages(MSG);
    }

    /**
     * Start the countdown.
     */
    public synchronized final AccurateCountDownTimer start() {
        if (mMillisInFuture <= 0) {
            onFinish();
            return this;
        }

        // ************AccurateCountdownTimer***************
        mStartTime = SystemClock.elapsedRealtime();
        mStopTimeInFuture = mStartTime + mMillisInFuture;
        // ************AccurateCountdownTimer***************

        mHandler.sendMessage(mHandler.obtainMessage(MSG));
        return this;
    }

    /**
     * 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 (AccurateCountDownTimer.this) {
                final long millisLeft = mStopTimeInFuture
                        - SystemClock.elapsedRealtime();

                if (millisLeft <= 0) {
                    onFinish();
                } else if (millisLeft < mCountdownInterval) {
                    // no tick, just delay until done
                    sendMessageDelayed(obtainMessage(MSG), millisLeft);
                } else {
                    long lastTickStart = SystemClock.elapsedRealtime();
                    onTick(millisLeft);

                    // ************AccurateCountdownTimer***************
                    long now = SystemClock.elapsedRealtime();
                    long extraDelay = now - mStartTime - mTickCounter
                            * mCountdownInterval;
                    mTickCounter++;
                    long delay = lastTickStart + mCountdownInterval - now
                            - extraDelay;

                    // ************AccurateCountdownTimer***************

                    // take into account user's onTick taking time to execute

                    // special case: user's onTick took more than interval to
                    // complete, skip to next interval
                    while (delay < 0)
                        delay += mCountdownInterval;

                    sendMessageDelayed(obtainMessage(MSG), delay);
                }
            }
        }
    };
}
dorjeduck
  • 7,624
  • 11
  • 52
  • 66
  • (Upvote) Looks like we implemented the same logic, we must be on the right path. – Sam Oct 06 '12 at 19:45
  • I think both of our solution do yet yours seems more clean and straight forward, thanks a lot for sharing. – dorjeduck Oct 06 '12 at 20:02
2

In most systems the timers are never perfectly accurate. The system only executes the timer when it doesn't have anything else to do. If the CPU is busy with a background process or a different thread then it won't get around to calling the timer till it has finished.

You might have better luck changing the interval to something smaller like 100ms and then only redrawing the screen if something has changed. With this approach the timer isn't directly causing anything to occur, it's just redrawing the screen periodically.

Nathan Villaescusa
  • 17,331
  • 4
  • 53
  • 56
  • Thx Nathan, I understand that we cant expect 100% accuracy. Yet I am wondering why classes like the CountDownTimer have no accuracy adjustment logic build in, it seems straight forward to count the ticks, check what time has been gone since start and adjust the next intervall accordingly. Well I will just give it a try and implement it myself yet I just hope and expect that his has been done already, it seems so straight forward to me. – dorjeduck Oct 06 '12 at 17:59
  • I just dont like this making the intervall smaller solutions, it seems not satisfying at all as you will have shorter ticks and sometimes longer ticks (which are made of two ticks) in the UI user experience .. – dorjeduck Oct 06 '12 at 18:02
  • I see, you just want to know how long it has been since the last tick. You can always do long System.currentTimeMillis() after each tick and compare it to the time of the previous tick. – Nathan Villaescusa Oct 06 '12 at 18:03
  • not really, my problem is that the ticks are ofter a bit longer, never shorter, which sums up - so i just want to count down in seconds steps on UI level, 87, 86, 85 ... and so on. with the summing up i get occasionally 87, 86, 84, ... internally the complete duration is accurate yet the UI display is not proper – dorjeduck Oct 06 '12 at 18:08
  • 2
    if i would see a countdown timer with a missing second on UI level i would uninstall the app ;-) – dorjeduck Oct 06 '12 at 18:09
  • In that case I would just call System.currentTimeMillis() when the countdown starts, and every 30ms or so I would compare the current time to the start time and if the result was different than what is displayed in the countdown label I would modify the label. This way the user always sees the correct countdown. – Nathan Villaescusa Oct 06 '12 at 18:10
  • thx - if I really need to start coding myself I will first try to add the adjustment idea I mentioned to the CountdownTimerClass. If will post it here if it feels ok to do, checking each 100 ms or so might be too much of a system demanding approach - but it might be the most proper way to go, I will investigate. Thanks again – dorjeduck Oct 06 '12 at 18:16
  • The amount of time between ticks will determine how accurate the label is. If you choose 30ms then the countdown the user sees can only ever be ~30ms off. The amount of time between frames in a movie is 41.6ms btw, so anything below that won't be noticeable. Plus this would make it trivial to show the milliseconds in the countdown. – Nathan Villaescusa Oct 06 '12 at 18:29
  • i will post my idea soon, i just check every tick how much i am over and make the next delay with that ms shorter - no need for extra check - this will adjust the ticks and no delays will sum up - i dont mind of a tick is 4ms longer, it is only the summing up which makes problems – dorjeduck Oct 06 '12 at 18:36
  • posted my idea yet Sam's solution is cleaner so if interested go for his one – dorjeduck Oct 06 '12 at 20:20
0

I know it's old but it's something that you will always need. I wrote a class called TimedTaskExecutor because I needed to run a specific command every x milliseconds for a unknown amount of time. One of my top priorities was to make x as accurate as can be. I tried with AsyncTask, Handlers and CountdownTimer but all gave me bad results. Here is the class:

package com.example.myapp;

import java.util.concurrent.TimeUnit;

/*
 * MUST RUN IN BACKGROUND THREAD
 */
public class TimedTaskExecutor {
// ------------------------------ FIELDS ------------------------------

/**
 *
 */
private double intervalInMilliseconds;

/**
 *
 */
private IVoidEmptyCallback callback;

/**
 *
 */
private long sleepInterval;

// --------------------------- CONSTRUCTORS ---------------------------

/**
 * @param intervalInMilliseconds
 * @param callback
 */
public TimedTaskExecutor(double intervalInMilliseconds, IVoidEmptyCallback callback, long sleepInterval) {
    this.intervalInMilliseconds = intervalInMilliseconds;
    this.callback = callback;
    this.sleepInterval = sleepInterval;
}

// --------------------- GETTER / SETTER METHODS ---------------------

/**
 * @return
 */
private IVoidEmptyCallback getCallback() {
    return callback;
}

/**
 * @return
 */
private double getIntervalInMilliseconds() {
    return intervalInMilliseconds;
}

/**
 * @return
 */
private long getSleepInterval() {
    return sleepInterval;
}

// -------------------------- OTHER METHODS --------------------------

/**
 *
 */
public void run(ICallback<Boolean> isRunningChecker) {

    long nanosInterval = (long) (getIntervalInMilliseconds() * 1000000);

    Long previousNanos = null;

    while (isRunningChecker.callback()) {

        long nanos = TimeUnit.NANOSECONDS.toNanos(System.nanoTime());

        if (previousNanos == null || (double) (nanos - previousNanos) >= nanosInterval) {

            getCallback().callback();

            if (previousNanos != null) {

                // Removing the difference
                previousNanos = nanos - (nanos - previousNanos - nanosInterval);

            } else {

                previousNanos = nanos;

            }

        }

        if (getSleepInterval() > 0) {

            try {

                Thread.sleep(getSleepInterval());

            } catch (InterruptedException ignore) {
            }

        }

    }

}

// -------------------------- INNER CLASSES --------------------------

/**
 *
 */
public interface IVoidEmptyCallback {
    /**
     *
     */
    public void callback();
}

/**
 * @param <T>
 */
public interface ICallback<T> {
    /**
     * @return
     */
    public T callback();
}

}

Here is an example of how to use it:

private boolean running;

Handler handler = new Handler();

handler.postDelayed(
    new Runnable() {
        /**
         *
         */
        @Override
        public void run() {
            running = false;
        }
    },
    5000
);

HandlerThread handlerThread = new HandlerThread("For background");
handlerThread.start();

Handler background = new Handler(handlerThread.getLooper());

background.post(
    new Runnable() {
        /**
         *
         */
        @Override
        public void run() {

            new TimedTaskExecutor(
                    10, // Run tick every 10 milliseconds
                    // The callback for each tick
                    new TimedTaskExecutor.IVoidEmptyCallback() {
                        /**
                         *
                         */
                        private int counter = 1;

                        /**
                         *
                         */
                        @Override
                        public void callback() {
                            // You can use the handler to post runnables to the UI
                            Log.d("runTimedTask", String.valueOf(counter++));
                        }
                    },
                    // sleep interval in order to allow the CPU to rest
                    2
            ).run(
                    // A callback to check when to stop
                    new TimedTaskExecutor.ICallback<Boolean>() {
                        /**
                         *
                         * @return
                         */
                        @Override
                        public Boolean callback() {
                            return running;
                        }
                    }
            );

        }
    }
 );

Running this code will produce 500 calls with more or less accurate x. (lowering the sleep factor make it more accurate)

  • Edit: It seems that in Nexus 5 with Lollipop you should use 0 in the sleep factor.
Rotem
  • 2,306
  • 3
  • 26
  • 44
0

As Nathan Villaescusa wrote earlier good approach is to decrease "ticking" interval and then fire ACTIONS needed every second by checking if this is actually new second.

Guys, take a look on this approach:

new CountDownTimer(5000, 100) {

     int n = 5;
     Toast toast = null;

     @Override
     public void onTick(long l) {

         if(l < n*1000) {
             //
             // new seconds. YOUR ACTIONS
             //
             if(toast != null) toast.cancel();

             toast = Toast.makeText(MainActivity.this, "" + n, Toast.LENGTH_SHORT);
             toast.show();

             // this one is important!!!
             n-=1;
         }

     }

     @Override
     public void onFinish() {
         if(toast != null) toast.cancel();
         Toast.makeText(MainActivity.this, "START", Toast.LENGTH_SHORT).show();
     }
}.start();

Hope this helps to someone ;p

Andrij
  • 218
  • 2
  • 8
-2

I wrote one lib to prevent this phenomenon.
https://github.com/imknown/NoDelayCountDownTimer

Core codes for usage:

private long howLongLeftInMilliSecond = NoDelayCountDownTimer.SIXTY_SECONDS;

private NoDelayCountDownTimer noDelayCountDownTimer;
private TextView noDelayCountDownTimerTv;

NoDelayCountDownTimer noDelayCountDownTimer = new NoDelayCountDownTimerInjector<TextView>(noDelayCountDownTimerTv, howLongLeftInMilliSecond).inject(new NoDelayCountDownTimerInjector.ICountDownTimerCallback() {
    @Override
    public void onTick(long howLongLeft, String howLongSecondLeftInStringFormat) {
        String result = getString(R.string.no_delay_count_down_timer, howLongSecondLeftInStringFormat);

        noDelayCountDownTimerTv.setText(result);
    }

    @Override
    public void onFinish() {
        noDelayCountDownTimerTv.setText(R.string.finishing_counting_down);
    }
});

Main base logic cods:

private Handler mHandler = new Handler(new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {

        synchronized (NoDelayCountDownTimer.this) {
            if (mCancelled) {
                return true;
            }

            final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();

            if (millisLeft <= 0 || millisLeft < mCountdownInterval) {
                onFinish();
            } else {
                long lastTickStart = SystemClock.elapsedRealtime();
                onTick(millisLeft);

                // take into account user's onTick taking time to execute
                long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();

                // special case: user's onTick took more than interval to complete, skip to next interval
                while (delay < 0) {
                    delay += mCountdownInterval;
                }

                mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG), delay);
            }
        }

        return true;
    }
});

record

imknown
  • 68
  • 1
  • 11
  • 2
    Be careful when advertising your own work here. I suggest you [edit] this answer, to explain how you solved this in your library. Visitors should be able to reproduce it without _requiring_ the library. You can then keep the link for reference. – S.L. Barth is on codidact.com Apr 20 '16 at 08:25