Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e91b68ac authored by Matías Hernández's avatar Matías Hernández Committed by Android (Google) Code Review
Browse files

Merge "Reduce tick frequency for Chronometer with adaptive format" into main

parents a81b98f1 d39042c7
Loading
Loading
Loading
Loading
+28 −8
Original line number Diff line number Diff line
@@ -16,6 +16,9 @@

package android.widget;

import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;

import static java.util.Objects.requireNonNull;

import android.annotation.ElapsedRealtimeLong;
@@ -70,6 +73,13 @@ import java.util.function.LongSupplier;
public class Chronometer extends TextView {
    private static final String TAG = "Chronometer";

    /**
     * In adaptive format, when displaying an elapsed/remaining duration greater than or equal to
     * this number of minutes, seconds will not be shown (which also means the chronometer will tick
     * on the minute instead of on the second).
     */
    private static final int ADAPTIVE_MINUTES_WITHOUT_SECONDS = 3;

    /**
     * A callback that notifies when the chronometer has incremented on its own.
     */
@@ -433,7 +443,7 @@ public class Chronometer extends TextView {
            }
        } else if (minutes.getNumber().intValue() != 0) {
            partsList.add(minutes);
            if (minutes.getNumber().intValue() < 3) {
            if (minutes.getNumber().intValue() < ADAPTIVE_MINUTES_WITHOUT_SECONDS) {
              partsList.add(seconds);
            }
        }
@@ -467,7 +477,7 @@ public class Chronometer extends TextView {
            if (running) {
                updateText(mElapsedRealtimeClock.getAsLong());
                dispatchChronometerTick();
                postTickOnNextSecond();
                postTickOnNextChange();
            } else {
                removeCallbacks(mTickRunnable);
            }
@@ -481,22 +491,32 @@ public class Chronometer extends TextView {
            if (mRunning) {
                updateText(mElapsedRealtimeClock.getAsLong());
                dispatchChronometerTick();
                postTickOnNextSecond();
                postTickOnNextChange();
            }
        }
    };

    private void postTickOnNextSecond() {
    private void postTickOnNextChange() {
        long nowMillis = mNow;

        // In adaptive format, ticks are every 1 minute instead of 1 second, if the time elapsed
        // or remaining is >= 3 minutes. Thus for time > 3 minutes the tick will be "on the minute"
        // and for lower than that it's "on the second".
        long periodInMillis = mUseAdaptiveFormat
                && Math.abs(nowMillis - mBase) > ADAPTIVE_MINUTES_WITHOUT_SECONDS * MINUTE_IN_MILLIS
                        ? MINUTE_IN_MILLIS
                        : SECOND_IN_MILLIS;

        long delayMillis;
        if (mCountDown) {
            delayMillis = (mBase - nowMillis) % 1000;
            delayMillis = (mBase - nowMillis) % periodInMillis;
            if (delayMillis <= 0) {
                delayMillis += 1000;
                delayMillis += periodInMillis;
            }
        } else {
            delayMillis = 1000 - (Math.abs(nowMillis - mBase) % 1000);
            delayMillis = periodInMillis - (Math.abs(nowMillis - mBase) % periodInMillis);
        }

        // Aim for 3 milliseconds into the next second so we don't update exactly on the second
        delayMillis += 3;
        postDelayed(mTickRunnable, delayMillis);
@@ -511,7 +531,7 @@ public class Chronometer extends TextView {
    private static final int MIN_IN_SEC = 60;
    private static final int HOUR_IN_SEC = MIN_IN_SEC*60;
    private static String formatDuration(long ms) {
        int duration = (int) (ms / DateUtils.SECOND_IN_MILLIS);
        int duration = (int) (ms / SECOND_IN_MILLIS);
        if (duration < 0) {
            duration = -duration;
        }
+91 −0
Original line number Diff line number Diff line
@@ -17,9 +17,16 @@
package android.widget;


import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertArrayEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;

import static java.time.temporal.ChronoUnit.MINUTES;
import static java.time.temporal.ChronoUnit.SECONDS;
@@ -41,6 +48,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;

/**
@@ -307,6 +315,89 @@ public class ChronometerTest extends ActivityInstrumentationTestCase2<Chronomete
        assertThat(chronometer.getText().toString()).isEqualTo("1m 0s");
    }

    @UiThreadTest
    public void testScheduledTicks() {
        long base = SystemClock.elapsedRealtime();

        // Non-adaptive: Always on the next second, regardless of stopwatch or countdown.
        verifyNextTickScheduledIn(/* adaptive= */ false, /* countdown= */ false, base,
                /* now= */ base + 200,
                /* expectedDelay= */ 800);
        verifyNextTickScheduledIn(/* adaptive= */ false, /* countdown= */ false, base,
                /* now= */ base + 5 * SECOND_IN_MILLIS,
                /* expectedDelay= */ 1000);
        verifyNextTickScheduledIn(/* adaptive= */ false, /* countdown= */ false, base,
                /* now= */ base + 30 * SECOND_IN_MILLIS + 300,
                /* expectedDelay= */ 700);
        verifyNextTickScheduledIn(/* adaptive= */ false, /* countdown= */ false, base,
                /* now= */ base + 45 * MINUTE_IN_MILLIS + 900,
                /* expectedDelay= */ 100);
        verifyNextTickScheduledIn(/* adaptive= */ false, /* countdown= */ true, base,
                /* now= */ base - 200,
                /* expectedDelay= */ 200);
        verifyNextTickScheduledIn(/* adaptive= */ false, /* countdown= */ true, base,
                /* now= */ base - 10 * SECOND_IN_MILLIS,
                /* expectedDelay= */ 1000);
        verifyNextTickScheduledIn(/* adaptive= */ false, /* countdown= */ true, base,
                /* now= */ base + 200, // overrun countdown
                /* expectedDelay= */ 800);
        verifyNextTickScheduledIn(/* adaptive= */ false, /* countdown= */ true, base,
                /* now= */ base + 4 * SECOND_IN_MILLIS + 300, // overrun countdown
                /* expectedDelay= */ 700);

        // Adaptive stopwatch, 2:20 elapsed (< 3 minutes) -> on the second.
        verifyNextTickScheduledIn(/* adaptive= */ true, /* countdown= */ false, base,
                /* now= */ base + 2 * MINUTE_IN_MILLIS + 20 * SECOND_IN_MILLIS + 100,
                /* expectedDelay= */ 900);

        // Adaptive stopwatch, more than than 3 minutes elapsed -> on the next minute.
        verifyNextTickScheduledIn(/* adaptive= */ true, /* countdown= */ false, base,
                /* now= */ base + 5 * MINUTE_IN_MILLIS + 10 * SECOND_IN_MILLIS + 300,
                /* expectedDelay= */ 49 * SECOND_IN_MILLIS + 700);

        // Adaptive timer, more than than 3 minutes remaining -> on the next minute.
        verifyNextTickScheduledIn(/* adaptive= */ true, /* countdown= */ true, base,
                /* now= */ base - 4 * MINUTE_IN_MILLIS - 10 * SECOND_IN_MILLIS - 300,
                /* expectedDelay= */ 10 * SECOND_IN_MILLIS + 300);

        // Adaptive timer, slightly more than than 3 minutes remaining -> on the next minute.
        verifyNextTickScheduledIn(/* adaptive= */ true, /* countdown= */ true, base,
                /* now= */ base - 3 * MINUTE_IN_MILLIS - 2 * SECOND_IN_MILLIS - 100,
                /* expectedDelay= */ 2 * SECOND_IN_MILLIS + 100);

        // Adaptive timer, barely a few ms more than than 3 minutes remaining -> on the next minute.
        verifyNextTickScheduledIn(/* adaptive= */ true, /* countdown= */ true, base,
                /* now= */ base - 3 * MINUTE_IN_MILLIS - 1,
                /* expectedDelay= */ 1);

        // Adaptive timer, less than than 3 minutes remaining -> on the next second.
        verifyNextTickScheduledIn(/* adaptive= */ true, /* countdown= */ true, base,
                /* now= */ base - 2 * MINUTE_IN_MILLIS - 8 * SECOND_IN_MILLIS - 400,
                /* expectedDelay= */ 400);
    }

    private void verifyNextTickScheduledIn(boolean adaptive, boolean countdown, long base, long now,
            long expectedDelay) {
        AtomicLong elapsedRealtime = new AtomicLong(0);

        // Need to spy() because it's not possible to replace the looper used by postDelayed() :(
        Chronometer chronometer = spy(
                new Chronometer(mActivity, () -> elapsedRealtime.get(),
                        () -> Instant.ofEpochMilli(0), null, 0, 0));
        mActivity.setContentView(chronometer);

        elapsedRealtime.set(now);
        chronometer.setCountDown(countdown);
        chronometer.setBase(base);
        chronometer.setUseAdaptiveFormat(adaptive);

        chronometer.start();

        // Chronometer adds a small delay to prevent transitions *exactly* on the time, but for
        // testing it's better to hide this.
        verify(chronometer).postDelayed(any(), eq(expectedDelay + 3));
    }

    private void testChronometerTicks(
            Instant clockSystemNow,
            List<String> expectedTicks,