androidkotlincountdowntimer

CountDowntimer exceeds the maximum duration in Android


I was making a simple fitness app which holds countdowntimer. but the problem is that countdowntimer shows negative values.

it happens when it gets paused.

      progressBarExercise.progress = (workOutTimerDuration?.toInt() ?: 30) - exerciseProgress

Here is the problem, exerciseProgress becomes greater than workOutTimerDuration. so it gets negative values. I don't know how it's happening.


Solution

  • The problem is with how you're "resuming" your timer after you unpause. Calling exerciseTimer.start() restarts it with the same duration and tick parameters you used to create it. From the source code:

    // constructor stores the duration and tick length
    public CountDownTimer(long millisInFuture, long countDownInterval) {
        mMillisInFuture = millisInFuture;
        mCountdownInterval = countDownInterval;
    }
    
    // onStart uses those lengths to restart the whole thing, starting from -now-
    public synchronized final CountDownTimer start() {
        mCancelled = false;
        ...
        // this is when it ends, i.e. when onFinish() gets called. now + duration
        mStopTimeInFuture = SystemClock.elapsedRealtime() + mMillisInFuture;
        ...
    }
    

    So even though you're saving and restoring workOutTimerDuration and exerciseProgress when you pause and unpause, you're not maintaining the state of the timer itself. You're effectively restarting it from the beginning, with the full duration.

    And this is a problem, because you're using the timer to update exerciseProgress on every tick. The timer doesn't stop when exerciseProgress reaches workOutTimerDuration, it stops when the timer runs for its full length.

    That's fine when you start the timer with exerciseProgress at zero - but when you unpause, you're restoring whatever exerciseProgress was when you paused it. Now you're starting the timer with exerciseProgress > 0, so it's going to end up larger than workOutTimerDuration and give you a negative value when you subtract it.


    So you have two approaches really. One is to make the timer cancel() itself once exerciseProgress hits a limit, so that value is what causes onFinish to get called, not the duration of the timer. That way you're not relying on the length of the timer at all, just the tick behaviour. The downside is that you lose accuracy - if you pause halfway through a tick, another tick will immediately fire when you unpause, so it kinda skips ahead in that sense.

    The other approach is to store the state of the timer itself, so you can create one whose duration is the remaining time. You could do this by creating a new CountdownTimer with the remaining time as the duration - this runs into the same situation with the early tick though (which may or may not be a problem, maybe that kind of precision doesn't matter here!). Otherwise you'll have to create your own timer (e.g. posting Runnables, using a looping coroutine, etc.) which will give you more control over initialising it and controlling when the ticks happen. You could also use this kind of logic:

    class MyTimer(val durationMillis: Long, val tickLength: Long = 1000L) {
        
        val timerEnd = System.currentTimeMillis() + durationMillis
    
        val remainingMillis
            get() = (timerEnd - System.currentTimeMillis()).coerceAtLeast(0L)
        val nextTick
            get() = remainingMillis % tickLength
        val ticksRemaining
            get() = remainingMillis / tickLength
    }
    

    which gives you the ability to hit the next tick at the right time (schedule an update nextTick millis in the future) even if you restart partway through one, and you can also see how many ticks are remaining and use that to calculate your progress, instead of having to maintain a separate value which could potentially go out of sync (like your exerciseProgress is doing). This way, everything's derived from the durationMillis value, and that's the only state you have to store.