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

Commit 542fb046 authored by Ibrahim Yilmaz's avatar Ibrahim Yilmaz Committed by Android (Google) Code Review
Browse files

Merge "MetricStyle: Add Adaptive Formatting to Chronometer" into main

parents 75aa2493 51081dbb
Loading
Loading
Loading
Loading
+4 −2
Original line number Original line Diff line number Diff line
@@ -11854,7 +11854,10 @@ public class Notification implements Parcelable
                        contentView.setViewVisibility(metricView.chronometerId(), View.VISIBLE);
                        contentView.setViewVisibility(metricView.chronometerId(), View.VISIBLE);
                        contentView.setChronometerCountDown(
                        contentView.setChronometerCountDown(
                                metricView.chronometerId(), timeDifference.isTimer());
                                metricView.chronometerId(), timeDifference.isTimer());
                        contentView.setBoolean(metricView.chronometerId(),
                                "setUseAdaptiveFormat",
                                timeDifference.getFormat()
                                        == Metric.TimeDifference.FORMAT_ADAPTIVE);
                        if (timeDifference.getZeroTime() != null) {
                        if (timeDifference.getZeroTime() != null) {
                            contentView.setChronometer(metricView.chronometerId(),
                            contentView.setChronometer(metricView.chronometerId(),
                                    timeDifference.getZeroTime(), /* format= */ null,
                                    timeDifference.getZeroTime(), /* format= */ null,
@@ -11871,7 +11874,6 @@ public class Notification implements Parcelable
                                    "No zeroTime or pausedDuration for running TimeDifference in "
                                    "No zeroTime or pausedDuration for running TimeDifference in "
                                            + metric);
                                            + metric);
                        }
                        }
                        // TODO(b/434910979): implement format support for Chronometer.
                    } else {
                    } else {
                        contentView.setViewVisibility(metricView.chronometerId(), View.GONE);
                        contentView.setViewVisibility(metricView.chronometerId(), View.GONE);
                        contentView.setViewVisibility(metricView.textValueId(), View.VISIBLE);
                        contentView.setViewVisibility(metricView.textValueId(), View.VISIBLE);
+61 −2
Original line number Original line Diff line number Diff line
@@ -93,6 +93,7 @@ public class Chronometer extends TextView {
    private boolean mRunning;
    private boolean mRunning;
    private boolean mLogged;
    private boolean mLogged;
    private String mFormat;
    private String mFormat;
    private boolean mUseAdaptiveFormat = false;
    private Formatter mFormatter;
    private Formatter mFormatter;
    private Locale mFormatterLocale;
    private Locale mFormatterLocale;
    private Object[] mFormatterArgs = new Object[1];
    private Object[] mFormatterArgs = new Object[1];
@@ -267,6 +268,21 @@ public class Chronometer extends TextView {
        }
        }
    }
    }


    /**
     * @hide
     */
    public boolean isUseAdaptiveFormat() {
        return mUseAdaptiveFormat;
    }

    /**
     * @hide
     */
    @android.view.RemotableViewMethod
    public void setUseAdaptiveFormat(boolean useAdaptiveFormat) {
        mUseAdaptiveFormat = useAdaptiveFormat;
    }

    /**
    /**
     * Returns the current format string as set through {@link #setFormat}.
     * Returns the current format string as set through {@link #setFormat}.
     */
     */
@@ -356,13 +372,20 @@ public class Chronometer extends TextView {
    private synchronized void updateText(long now) {
    private synchronized void updateText(long now) {
        updateBaseTimeIfSystemClockChanged();
        updateBaseTimeIfSystemClockChanged();
        mNow = now;
        mNow = now;

        long seconds = Math.round((mCountDown ? mBase - now - 499 : now - mBase) / 1000f);
        long seconds = Math.round((mCountDown ? mBase - now - 499 : now - mBase) / 1000f);
        boolean negative = false;
        boolean negative = false;
        if (seconds < 0) {
        if (seconds < 0) {
            seconds = -seconds;
            seconds = -seconds;
            negative = true;
            negative = true;
        }
        }
        String text = DateUtils.formatElapsedTime(mRecycle, seconds);
        String text;
        if (mUseAdaptiveFormat) {
            text = formatTextWithAdaptiveTimeFormat(Duration.ofSeconds(seconds));
        } else {
            text = DateUtils.formatElapsedTime(mRecycle, seconds);
        }

        if (negative) {
        if (negative) {
            text = getResources().getString(R.string.negative_duration, text);
            text = getResources().getString(R.string.negative_duration, text);
        }
        }
@@ -385,8 +408,44 @@ public class Chronometer extends TextView {
                }
                }
            }
            }
        }
        }

        if (!TextUtils.equals(getText(), text)) {
            setText(text);
            setText(text);
        }
        }
    }

    private String formatTextWithAdaptiveTimeFormat(Duration duration) {
        final Measure days = new Measure(duration.toDaysPart(), MeasureUnit.DAY);
        final Measure hours = new Measure(duration.toHoursPart(), MeasureUnit.HOUR);
        final Measure minutes = new Measure(duration.toMinutesPart(), MeasureUnit.MINUTE);
        final Measure seconds = new Measure(duration.toSecondsPart(), MeasureUnit.SECOND);
        final MeasureFormat formatter = MeasureFormat.getInstance(Locale.getDefault(),
                FormatWidth.NARROW);

        final ArrayList<Measure> partsList = new ArrayList<>();
        if (days.getNumber().intValue() != 0) {
            partsList.add(days);
            if (hours.getNumber().intValue() != 0) {
                partsList.add(hours);
            }
        } else if (hours.getNumber().intValue() != 0) {
            partsList.add(hours);
            if (minutes.getNumber().intValue() != 0) {
                partsList.add(minutes);
            }
        } else if (minutes.getNumber().intValue() != 0) {
            partsList.add(minutes);
            if (minutes.getNumber().intValue() < 3) {
              partsList.add(seconds);
            }
        }

        if (partsList.isEmpty()) {
            partsList.add(seconds);
        }

        return formatter.formatMeasures(partsList.toArray(new Measure[0]));
    }


    private static final long SIGNIFICANT_DRIFT_MILLIS = 500;
    private static final long SIGNIFICANT_DRIFT_MILLIS = 500;


+172 −0
Original line number Original line Diff line number Diff line
@@ -16,8 +16,11 @@


package android.widget;
package android.widget;



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


import static org.junit.Assert.assertArrayEquals;

import static java.time.temporal.ChronoUnit.MINUTES;
import static java.time.temporal.ChronoUnit.MINUTES;
import static java.time.temporal.ChronoUnit.SECONDS;
import static java.time.temporal.ChronoUnit.SECONDS;


@@ -32,9 +35,13 @@ import com.android.frameworks.coretests.R;


import java.time.Duration;
import java.time.Duration;
import java.time.Instant;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;


/**
/**
 * Test {@link Chronometer} counting up and down.
 * Test {@link Chronometer} counting up and down.
@@ -162,6 +169,171 @@ public class ChronometerTest extends ActivityInstrumentationTestCase2<Chronomete
        assertEquals("−00:09", ticks.get(11));
        assertEquals("−00:09", ticks.get(11));
    }
    }


    @UiThreadTest
    public void testChronometerDisplaysAdaptiveTimeFormat() throws Throwable {
        final List<String> expectedTicks = Arrays.asList(
                "9h 4m",
                "9h 4m",
                "9h 4m",
                "9h 4m",
                "9h 4m"
        );
        final Instant systemNow = Instant.now();
        testChronometerTicks(systemNow, expectedTicks, chronometer -> {
            chronometer.setBase(systemNow.plus(9, ChronoUnit.HOURS)
                    .plus(5, MINUTES));
            chronometer.setCountDown(true);
            chronometer.setUseAdaptiveFormat(true);
        });
    }

    @UiThreadTest
    public void testChronometerDisplaysCustomFormatting() throws Throwable {
        final List<String> expectedTicks = Arrays.asList(
                "Time elapsed: 00:01",
                "Time elapsed: 00:02",
                "Time elapsed: 00:03",
                "Time elapsed: 00:04",
                "Time elapsed: 00:05"
        );

        final Instant systemNow = Instant.now();
        testChronometerTicks(systemNow, expectedTicks, chronometer -> {
            chronometer.setFormat("Time elapsed: %s");
            chronometer.setCountDown(false);
            chronometer.setUseAdaptiveFormat(false); // Ensure adaptive format doesn't interfere.
        });
    }

    @UiThreadTest
    public void testChronometerAdaptiveTimeFormatSupportsCustomFormatting() throws Throwable {
        final List<String> expectedTicks = Arrays.asList(
                "Remaining time: 9h 4m",
                "Remaining time: 9h 4m",
                "Remaining time: 9h 4m",
                "Remaining time: 9h 4m",
                "Remaining time: 9h 4m"
        );

        final Instant systemNow = Instant.now();
        testChronometerTicks(systemNow, expectedTicks, chronometer -> {
            chronometer.setFormat("Remaining time: 9h 4m");
            chronometer.setBase(systemNow.plus(9, ChronoUnit.HOURS)
                    .plus(5, MINUTES));
            chronometer.setCountDown(true);
            chronometer.setUseAdaptiveFormat(true);
        });
    }

    @UiThreadTest
    public void testChronometerAdaptiveTimeFormatDisplaysNegativeTime() throws Throwable {
        final List<String> expectedTicks = Arrays.asList(
                "−1s",
                "−2s",
                "−3s",
                "−4s",
                "−5s"
        );

        final Instant systemNow = Instant.now();
        testChronometerTicks(systemNow, expectedTicks, chronometer -> {
            chronometer.setCountDown(true);
            chronometer.setUseAdaptiveFormat(true);
        });
    }

    @UiThreadTest
    public void testChronometerAdaptiveFormatSignificantParts() {
        Chronometer chronometer = new Chronometer(mActivity);
        chronometer.setUseAdaptiveFormat(true);
        chronometer.setCountDown(true);
        mActivity.setContentView(chronometer);

        // Days and Hours
        chronometer.setPausedDuration(Duration.ofDays(2).plusHours(3).plusMinutes(4));
        assertThat(chronometer.getText().toString()).isEqualTo("2d 3h");

        // Hours and Minutes
        chronometer.setPausedDuration(Duration.ofHours(3).plusMinutes(4).plusSeconds(5));
        assertThat(chronometer.getText().toString()).isEqualTo("3h 4m");

        // Minutes and Seconds
        chronometer.setPausedDuration(Duration.ofMinutes(4).plusSeconds(30));
        assertThat(chronometer.getText().toString()).isEqualTo("4m");

        chronometer.setPausedDuration(Duration.ofMinutes(4).plusSeconds(5));
        assertThat(chronometer.getText().toString()).isEqualTo("4m");

        chronometer.setPausedDuration(Duration.ofMinutes(3).plusSeconds(5));
        assertThat(chronometer.getText().toString()).isEqualTo("3m");

        chronometer.setPausedDuration(Duration.ofMinutes(2).plusSeconds(5));
        assertThat(chronometer.getText().toString()).isEqualTo("2m 5s");

        chronometer.setPausedDuration(Duration.ofMinutes(2));
        assertThat(chronometer.getText().toString()).isEqualTo("2m 0s");

        // Only Seconds
        chronometer.setPausedDuration(Duration.ofSeconds(5));
        assertThat(chronometer.getText().toString()).isEqualTo("5s");

        // Negative time
        chronometer.setPausedDuration(Duration.ofSeconds(-5));
        assertThat(chronometer.getText().toString()).isEqualTo("−5s");

        chronometer.setPausedDuration(Duration.ofMinutes(-1).plusSeconds(-5));
        assertThat(chronometer.getText().toString()).isEqualTo("−1m 5s");

        chronometer.setPausedDuration(Duration.ofHours(-1).plusMinutes(-5));
        assertThat(chronometer.getText().toString()).isEqualTo("−1h 5m");

        chronometer.setPausedDuration(Duration.ofDays(-1).plusHours(-5));
        assertThat(chronometer.getText().toString()).isEqualTo("−1d 5h");

        // Zero seconds
        chronometer.setPausedDuration(Duration.ZERO);
        assertThat(chronometer.getText().toString()).isEqualTo("0s");

        chronometer.setPausedDuration(Duration.ofMinutes(4));
        assertThat(chronometer.getText().toString()).isEqualTo("4m");

        chronometer.setPausedDuration(Duration.ofMinutes(3));
        assertThat(chronometer.getText().toString()).isEqualTo("3m");

        chronometer.setPausedDuration(Duration.ofMinutes(2));
        assertThat(chronometer.getText().toString()).isEqualTo("2m 0s");

        chronometer.setPausedDuration(Duration.ofMinutes(1));
        assertThat(chronometer.getText().toString()).isEqualTo("1m 0s");
    }

    private void testChronometerTicks(
            Instant clockSystemNow,
            List<String> expectedTicks,
            Consumer<Chronometer> chronometerConfigurator) throws Throwable {

        var clocks = new Object() {
            public Instant systemNow = clockSystemNow;
            public long elapsedRealtime = 1000L;
        };

        final int tickCount = expectedTicks.size();
        final ArrayList<String> actualTicks = new ArrayList<>();
        Chronometer chronometer = new Chronometer(mActivity, () -> clocks.elapsedRealtime,
                () -> clocks.systemNow, null, 0, 0);
        chronometerConfigurator.accept(chronometer);
        mActivity.setContentView(chronometer);

        for (int i = 0; i < tickCount; i++) {
            clocks.systemNow = clocks.systemNow.plus(1, ChronoUnit.SECONDS);
            clocks.elapsedRealtime += 1000L;
            chronometer.updateText();
            actualTicks.add(chronometer.getText().toString());
        }

        assertArrayEquals(expectedTicks.toArray(), actualTicks.toArray());
    }

    private void runOnUiThread(Runnable runnable) throws InterruptedException {
    private void runOnUiThread(Runnable runnable) throws InterruptedException {
        final CountDownLatch latch = new CountDownLatch(1);
        final CountDownLatch latch = new CountDownLatch(1);
        mActivity.runOnUiThread(() -> {
        mActivity.runOnUiThread(() -> {