Loading core/java/android/app/Notification.java +4 −2 Original line number Original line Diff line number Diff line Loading @@ -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, Loading @@ -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); Loading core/java/android/widget/Chronometer.java +61 −2 Original line number Original line Diff line number Diff line Loading @@ -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]; Loading Loading @@ -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}. */ */ Loading Loading @@ -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); } } Loading @@ -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; Loading core/tests/coretests/src/android/widget/ChronometerTest.java +172 −0 Original line number Original line Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading Loading @@ -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(() -> { Loading Loading
core/java/android/app/Notification.java +4 −2 Original line number Original line Diff line number Diff line Loading @@ -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, Loading @@ -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); Loading
core/java/android/widget/Chronometer.java +61 −2 Original line number Original line Diff line number Diff line Loading @@ -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]; Loading Loading @@ -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}. */ */ Loading Loading @@ -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); } } Loading @@ -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; Loading
core/tests/coretests/src/android/widget/ChronometerTest.java +172 −0 Original line number Original line Diff line number Diff line Loading @@ -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; Loading @@ -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. Loading Loading @@ -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(() -> { Loading