CountDownTimer.java

// SPDX-License-Identifier: Apache-2.0
package com.stkouyu.util;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;

/**
 * Countdown on the main thread that can be paused: {@link #start()} starts or resumes,
 * {@link #stop()} pauses, {@link #cancel()} ends it without {@link #onFinish(long)}.
 * {@code onTick(millisUntilFinished)} runs every interval, {@code onFinish(elapsedMillis)} once at
 * the end.
 */
public abstract class CountDownTimer {
    private static final int MSG = 1;

    private final long millisInFuture;
    private final long interval;
    private long startedAt = -1;
    private long pausedElapsed;
    private boolean running;
    private boolean cancelled;
    private boolean finished;

    private final Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            step();
        }
    };

    public CountDownTimer(long millisInFuture, long countDownInterval) {
        this.millisInFuture = millisInFuture;
        this.interval = countDownInterval > 0 ? countDownInterval : 100;
    }

    public final synchronized void cancel() {
        cancelled = true;
        running = false;
        handler.removeMessages(MSG);
    }

    public final synchronized boolean isCancelled() {
        return cancelled;
    }

    /** Pauses; {@link #start()} resumes. */
    public final synchronized CountDownTimer stop() {
        if (running) {
            pausedElapsed = elapsedLocked();
            running = false;
            handler.removeMessages(MSG);
        }
        return this;
    }

    /** Starts, or resumes after {@link #stop()}. */
    public final synchronized CountDownTimer start() {
        if (running || finished) {
            return this;
        }
        cancelled = false;
        if (millisInFuture <= 0) {
            finished = true;
            handler.post(new Runnable() {
                @Override
                public void run() {
                    onFinish(0);
                }
            });
            return this;
        }
        startedAt = SystemClock.uptimeMillis() - pausedElapsed;
        running = true;
        handler.sendMessageDelayed(handler.obtainMessage(MSG), Math.min(interval, millisInFuture - pausedElapsed));
        return this;
    }

    /** Elapsed counting time in ms (paused time excluded). */
    public final synchronized long getNowTime() {
        return elapsedLocked();
    }

    private long elapsedLocked() {
        if (!running) {
            return pausedElapsed;
        }
        return SystemClock.uptimeMillis() - startedAt;
    }

    private void step() {
        long left;
        long elapsed;
        synchronized (this) {
            if (!running || cancelled) {
                return;
            }
            elapsed = elapsedLocked();
            left = millisInFuture - elapsed;
            if (left <= 0) {
                running = false;
                finished = true;
            } else {
                handler.sendMessageDelayed(handler.obtainMessage(MSG), Math.min(interval, left));
            }
        }
        if (left <= 0) {
            onFinish(Math.min(elapsed, millisInFuture));
        } else {
            onTick(left);
        }
    }

    public abstract void onTick(long millisUntilFinished);

    public abstract void onFinish(long elapsedMillis);
}