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

Commit d39042c7 authored by Matías Hernández's avatar Matías Hernández
Browse files

Reduce tick frequency for Chronometer with adaptive format

Update every minute instead of every second.

Fixes: 440594215
Test: atest ChronometerTest
Flag: android.app.api_metric_style
Change-Id: I47fe11d05039fbaa30af7a453dbcee1ed8f1c631
parent e98a5dc9
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,