Loading core/api/current.txt +3 −0 Original line number Diff line number Diff line Loading @@ -7091,9 +7091,12 @@ package android.app { method @NonNull public static android.app.Notification.Metric.TimeDifference forPausedStopwatch(@NonNull java.time.Duration, int); method @NonNull public static android.app.Notification.Metric.TimeDifference forPausedTimer(@NonNull java.time.Duration, int); method @NonNull public static android.app.Notification.Metric.TimeDifference forStopwatch(@NonNull java.time.Instant, int); method @NonNull public static android.app.Notification.Metric.TimeDifference forStopwatch(long, int); method @NonNull public static android.app.Notification.Metric.TimeDifference forTimer(@NonNull java.time.Instant, int); method @NonNull public static android.app.Notification.Metric.TimeDifference forTimer(long, int); method public int getFormat(); method @Nullable public java.time.Duration getPausedDuration(); method @Nullable public Long getZeroElapsedRealtime(); method @Nullable public java.time.Instant getZeroTime(); method public boolean isStopwatch(); method public boolean isTimer(); core/java/android/app/Notification.java +156 −48 Original line number Diff line number Diff line Loading @@ -41,6 +41,7 @@ import android.annotation.ColorRes; import android.annotation.DimenRes; import android.annotation.Dimension; import android.annotation.DrawableRes; import android.annotation.ElapsedRealtimeLong; import android.annotation.FlaggedApi; import android.annotation.IdRes; import android.annotation.IntDef; Loading Loading @@ -154,6 +155,7 @@ import java.util.Locale; import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.function.LongSupplier; /** * A class that represents how a persistent notification is to be presented to Loading Loading @@ -1824,6 +1826,16 @@ public class Notification implements Parcelable @VisibleForTesting public static InstantSource sSystemClock = InstantSource.system(); /** * Provider of "elapsedRealTime" (milliseconds since boot, not affected by moving the system * clock). Normally {@link SystemClock#elapsedRealtime()}, but overridable for testing. * * @hide */ @Nullable @VisibleForTesting public static LongSupplier sElapsedRealtimeClock = () -> SystemClock.elapsedRealtime(); @UnsupportedAppUsage private Icon mSmallIcon; @UnsupportedAppUsage Loading Loading @@ -3111,6 +3123,13 @@ public class Notification implements Parcelable return sSystemClock != null ? sSystemClock : InstantSource.system(); } @NonNull private static LongSupplier getElapsedRealtimeClock() { return sElapsedRealtimeClock != null ? sElapsedRealtimeClock : () -> SystemClock.elapsedRealtime(); } private static LocalDate getToday() { return getSystemClock().instant() .atZone(ZoneId.systemDefault()) Loading Loading @@ -12229,10 +12248,16 @@ public class Notification implements Parcelable * This represents a timer, a stopwatch, or a countdown to an event. * * <p>When representing a <em>running</em> timer (or stopwatch, etc), this value specifies * a reference instant for when that timer will hit zero, called the "zero time". * In this case the time displayed is defined as the difference between the * "zero time" instant, and {@link Instant#now()}, meaning it will show a live-updated * timer. * a reference instant for when that timer will hit zero (or the stopwatch was at zero, * respectively), called the "zero time". In this case the time displayed is defined as the * difference between the "zero time" and the current time, meaning it will show a * live-updated timer. * * <p>The zero time can be specified as an {@link Instant} (in which case it corresponds * to a "real-world" point in time, from {@link InstantSource#system}), or as milliseconds * since boot (from {@link SystemClock#elapsedRealtime()}). The latter might be suitable * when the timer is tied to {@link AlarmManager#ELAPSED_REALTIME} alarm in * {@link AlarmManager}. * * <p>When representing a <em>paused</em> timer (or stopwatch, etc), this value specifies * the duration as a fixed value. Loading Loading @@ -12261,12 +12286,14 @@ public class Notification implements Parcelable public @interface Format {} private static final String KEY_ZERO_TIME = "zeroTime"; private static final String KEY_ZERO_ELAPSED_REALTIME = "zeroElapsedRealtime"; private static final String KEY_PAUSED_DURATION = "pausedDuration"; private static final String KEY_COUNT_DOWN = "countDown"; private static final String KEY_FORMAT = "format"; // One of these two will be present. @Nullable private final Instant mZeroTime; @Nullable @ElapsedRealtimeLong private final Long mZeroElapsedRealtime; @Nullable private final Duration mPausedDuration; private final boolean mCountDown; private final @Format int mFormat; Loading @@ -12278,10 +12305,24 @@ public class Notification implements Parcelable */ @NonNull public static TimeDifference forTimer(@NonNull Instant endTime, @Format int format) { return new TimeDifference(requireNonNull(endTime), /* pausedDuration= */ null, return new TimeDifference(requireNonNull(endTime), /* zeroElapsedRealtime= */ null, /* pausedDuration= */ null, /* countDown= */ true, format); } /** * Creates a "running timer" metric, which will show a countdown to {@code endTime}, * specified in the {@link SystemClock#elapsedRealtime()} frame of reference. * * @param endTime elapsed realtime at which the timer reaches zero */ @NonNull public static TimeDifference forTimer(@ElapsedRealtimeLong long endTime, @Format int format) { return new TimeDifference(/* zeroTime= */ null, endTime, /* pausedDuration= */ null, /* countDown= */ true, format); } /** * Creates a "running stopwatch" metric, which will show the time elapsed since * {@code startTime}. Loading @@ -12291,39 +12332,57 @@ public class Notification implements Parcelable @NonNull public static TimeDifference forStopwatch(@NonNull Instant startTime, @Format int format) { return new TimeDifference(requireNonNull(startTime), /* pausedDuration= */ null, return new TimeDifference(requireNonNull(startTime), /* zeroElapsedRealtime= */ null, /* pausedDuration= */ null, /* countDown= */ false, format); } /** * Creates a "running stopwatch" metric, which will show the time elapsed since * {@code startTime}, specified in the {@link SystemClock#elapsedRealtime()} frame of * reference. * * @param startTime elapsed realtime at which the stopwatch started */ @NonNull public static TimeDifference forStopwatch(@ElapsedRealtimeLong long startTime, @Format int format) { return new TimeDifference(/* zeroTime= */ null, startTime, /* pausedDuration= */ null, /* countDown= */ false, format); } /** * Creates a "paused timer" metric, showing the {@code remainingTime}. */ @NonNull public static TimeDifference forPausedTimer(@NonNull Duration remainingTime, @Format int format) { return new TimeDifference(/* zeroTime= */ null, requireNonNull(remainingTime), /* countDown= */ true, format); return new TimeDifference(/* zeroTime= */ null, /* zeroElapsedRealtime= */ null, requireNonNull(remainingTime), /* countDown= */ true, format); } /** * Creates a "paused timer" metric, showing the {@code elapsedTime}. * Creates a "paused stopwatch" metric, showing the {@code elapsedTime}. */ @NonNull public static TimeDifference forPausedStopwatch(@NonNull Duration elapsedTime, @Format int format) { return new TimeDifference(/* zeroTime= */ null, requireNonNull(elapsedTime), /* countDown= */ false, format); } private TimeDifference(@Nullable Instant zeroTime, @Nullable Duration pausedDuration, boolean countDown, @Format int format) { checkArgument((zeroTime != null) ^ (pausedDuration != null), "Either zeroTime or pausedDuration must be present, and not both. " + "Received %s,%s", zeroTime, pausedDuration); return new TimeDifference(/* zeroTime= */ null, /* zeroElapsedRealtime= */ null, requireNonNull(elapsedTime), /* countDown= */ false, format); } private TimeDifference(@Nullable Instant zeroTime, @Nullable @ElapsedRealtimeLong Long zeroElapsedRealtime, @Nullable Duration pausedDuration, boolean countDown, @Format int format) { checkArgument((zeroTime != null ? 1 : 0) + (zeroElapsedRealtime != null ? 1 : 0) + (pausedDuration != null ? 1 : 0) == 1, "Exactly one of zeroTime, zeroElapsedRealtime, or pausedDuration must be " + "present; received %s,%s,%s", zeroTime, zeroElapsedRealtime, pausedDuration); checkArgument(format >= FORMAT_AUTOMATIC && format <= FORMAT_CHRONOMETER, "Invalid format: %s", format); mZeroTime = zeroTime; mZeroElapsedRealtime = zeroElapsedRealtime; mPausedDuration = pausedDuration; mCountDown = countDown; mFormat = format; Loading @@ -12333,10 +12392,12 @@ public class Notification implements Parcelable private static TimeDifference fromBundle(Bundle bundle) { Instant zeroTime = bundle.containsKey(KEY_ZERO_TIME) ? Instant.ofEpochMilli(bundle.getLong(KEY_ZERO_TIME)) : null; Long zeroElapsedRealtime = bundle.containsKey(KEY_ZERO_ELAPSED_REALTIME) ? bundle.getLong(KEY_ZERO_ELAPSED_REALTIME) : null; Duration pausedDuration = bundle.containsKey(KEY_PAUSED_DURATION) ? Duration.ofMillis(bundle.getLong(KEY_PAUSED_DURATION)) : null; if (zeroTime != null || pausedDuration != null) { return new TimeDifference(zeroTime, pausedDuration, return new TimeDifference(zeroTime, zeroElapsedRealtime, pausedDuration, bundle.getBoolean(KEY_COUNT_DOWN), bundle.getInt(KEY_FORMAT, FORMAT_AUTOMATIC)); } else { Loading @@ -12349,6 +12410,8 @@ public class Notification implements Parcelable protected void toBundle(Bundle bundle) { if (mZeroTime != null) { bundle.putLong(KEY_ZERO_TIME, mZeroTime.toEpochMilli()); } else if (mZeroElapsedRealtime != null) { bundle.putLong(KEY_ZERO_ELAPSED_REALTIME, mZeroElapsedRealtime); } else if (mPausedDuration != null) { bundle.putLong(KEY_PAUSED_DURATION, mPausedDuration.toMillis()); } Loading @@ -12361,6 +12424,7 @@ public class Notification implements Parcelable if (!(obj instanceof TimeDifference that)) return false; if (this == that) return true; return Objects.equals(this.mZeroTime, that.mZeroTime) && Objects.equals(this.mZeroElapsedRealtime, that.mZeroElapsedRealtime) && Objects.equals(this.mPausedDuration, that.mPausedDuration) && this.mCountDown == that.mCountDown && this.mFormat == that.mFormat; Loading @@ -12368,41 +12432,70 @@ public class Notification implements Parcelable @Override public int hashCode() { return Objects.hash(mZeroTime, mPausedDuration, mCountDown, mFormat); return Objects.hash(mZeroTime, mZeroElapsedRealtime, mPausedDuration, mCountDown, mFormat); } @Override public String toString() { return "TimeDifference{" + "mZeroTime=" + mZeroTime + ", mPausedDuration=" + mPausedDuration + ", mCountDown=" + mCountDown + ", mFormat=" + mFormat + "}"; StringBuilder sb = new StringBuilder("TimeDifference{"); if (mZeroTime != null) { sb.append("mZeroTime=").append(mZeroTime); } else if (mZeroElapsedRealtime != null) { sb.append("mZeroElapsedRealtime=").append(mZeroElapsedRealtime); } else if (mPausedDuration != null) { sb.append("mPausedDuration=").append(mPausedDuration); } sb.append(", mCountDown=").append(mCountDown) .append(", mFormat=").append(mFormat) .append("}"); return sb.toString(); } /** * The instant at which the time difference is zero. * The {@link Instant} at which the time difference is zero. Only valid for an * {@link Instant}-based {@link TimeDifference}. * * <ul> * <li>For a running timer this is the {@code endTime} supplied to * {@link #forTimer}. * {@link #forTimer(Instant, int)}. * <li>For a running stopwatch this is the {@code startTime} supplied to * {@link #forStopwatch}. * <li>This is {@code null} for paused timers or stopwatches. * {@link #forStopwatch(Instant, int)}. * <li>For running timers or stopwatches based on elapsed realtime (as well as * paused timers and stopwatches), this is {@code null}. * </ul> */ @Nullable public Instant getZeroTime() { return mZeroTime; } /** * The elapsed realtime at which the time difference is zero. Only valid for an * {@link SystemClock#elapsedRealtime()}-based {@link TimeDifference}. * * <ul> * <li>For a running timer this is the {@code endTime} supplied to * {@link #forTimer(long, int)}. * <li>For a running stopwatch this is the {@code startTime} supplied to * {@link #forStopwatch(long, int)}. * <li>For running timers or stopwatches based on {@link Instant} (as well as * paused timers and stopwatches), this is {@code null}. * </ul> */ @SuppressLint("AutoBoxing") @Nullable @ElapsedRealtimeLong public Long getZeroElapsedRealtime() { return mZeroElapsedRealtime; } /** * The fixed time difference, for a paused timer or stopwatch. * * <ul> * <li>For a paused timer this is the {@code remainingTime} supplied to * {@link #forPausedTimer}. * <li>For a paused stopwatch this is the {@code elapsedTime} supplied to * {@link #forPausedStopwatch}. * <li>This is {@code null} for running timers or stopwatches. * <li>For running timers or stopwatches this is {@code null}. * </ul> */ @Nullable public Duration getPausedDuration() { Loading @@ -12411,7 +12504,7 @@ public class Notification implements Parcelable /** * Whether this {@link TimeDifference} value represents a stopwatch -- when running, * it counts up from {@link #getZeroTime()}. * it counts up from {@link #getZeroTime()} (or {@link #getZeroElapsedRealtime()}). */ public boolean isStopwatch() { return !mCountDown; Loading @@ -12419,7 +12512,7 @@ public class Notification implements Parcelable /** * Whether this {@link TimeDifference} value represents a timer -- when running, * it counts down to {@link #getZeroTime()}. * it counts down to {@link #getZeroTime()} (or {@link #getZeroElapsedRealtime()}). */ public boolean isTimer() { return mCountDown; Loading @@ -12436,21 +12529,9 @@ public class Notification implements Parcelable @NonNull @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public ValueString toValueString(Context context) { Duration duration; if (mPausedDuration != null) { duration = mPausedDuration; } else { // If the timer/stopwatch is running we likely want a Chronometer view, so this // path is mostly for debugging/completeness. Instant now = getSystemClock().instant(); if (isStopwatch()) { duration = Duration.between(mZeroTime, now); } else { duration = Duration.between(now, mZeroTime); } } Duration duration = getCurrentDuration(); duration = duration.truncatedTo(SECONDS); // ms are ignored and we don't want -0:00 Duration absDuration = duration.abs(); Measure hours = new Measure(absDuration.toHours(), MeasureUnit.HOUR); Measure minutes = new Measure(absDuration.toMinutesPart(), MeasureUnit.MINUTE); Loading @@ -12464,6 +12545,33 @@ public class Notification implements Parcelable return new ValueString(text, null); } private Duration getCurrentDuration() { if (mPausedDuration != null) { return mPausedDuration; } else if (mZeroTime != null) { // If the timer/stopwatch is running we likely want a Chronometer view, so this // path is mostly for debugging/completeness. Instant now = getSystemClock().instant(); if (isStopwatch()) { return Duration.between(mZeroTime, now); } else { return Duration.between(now, mZeroTime); } } else if (mZeroElapsedRealtime != null) { // If the timer/stopwatch is running we likely want a Chronometer view, so this // path is mostly for debugging/completeness. long elapsedRealtimeNow = getElapsedRealtimeClock().getAsLong(); if (isStopwatch()) { return Duration.ofMillis(elapsedRealtimeNow - mZeroElapsedRealtime); } else { return Duration.ofMillis(mZeroElapsedRealtime - elapsedRealtimeNow); } } else { throw new IllegalStateException( "None of mPausedDuration, mZeroTime, mZeroElapsedRealtime set!"); } } private static String formatAbsoluteDuration(@Format int format, Measure hours, Measure minutes, Measure seconds) { if (format == FORMAT_ADAPTIVE) { Loading core/tests/coretests/src/android/app/NotificationMetricStyleTest.java +39 −0 Original line number Diff line number Diff line Loading @@ -51,6 +51,7 @@ import android.app.Notification.MetricStyle; import android.content.ContentResolver; import android.content.Context; import android.os.Bundle; import android.os.SystemClock; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; Loading Loading @@ -100,6 +101,8 @@ public class NotificationMetricStyleTest { // December 18, 2025 -> more than 4 months away private static final LocalDate FAR_AWAY = LocalDate.of(2025, 12, 18); private static final long ELAPSED_REALTIME = 300_000; private static final String NNBSP = "\u202f"; private Context mContext; Loading @@ -121,6 +124,7 @@ public class NotificationMetricStyleTest { Settings.System.putString(mContext.getContentResolver(), Settings.System.TIME_12_24, "12"); Notification.sSystemClock = () -> NOW; Notification.sElapsedRealtimeClock = () -> ELAPSED_REALTIME; } @After Loading @@ -134,6 +138,7 @@ public class NotificationMetricStyleTest { Settings.System.putString(mContext.getContentResolver(), Settings.System.TIME_12_24, mPrevious24HourSetting); Notification.sSystemClock = InstantSource.system(); Notification.sElapsedRealtimeClock = () -> SystemClock.elapsedRealtime(); } @Test Loading Loading @@ -314,6 +319,40 @@ public class NotificationMetricStyleTest { new ValueString("500:40:00")); } @Test public void valueToString_timeDifferenceInstant_updatesWithSystemClock() { TimeDifference runningTimer = TimeDifference.forTimer( NOW.plusSeconds(60), // Rings in 60 seconds TimeDifference.FORMAT_CHRONOMETER); expect.that(runningTimer.toValueString(mContext)).isEqualTo(new ValueString("1:00")); Notification.sElapsedRealtimeClock = () -> ELAPSED_REALTIME + Duration.ofSeconds(10).toMillis(); expect.that(runningTimer.toValueString(mContext)).isEqualTo(new ValueString("1:00")); Notification.sSystemClock = () -> NOW.plusSeconds(3); expect.that(runningTimer.toValueString(mContext)).isEqualTo(new ValueString("0:57")); } @Test public void valueToString_timeDifferenceElapsedRealtime_updatesWithElapsedRealtime() { TimeDifference runningTimer = TimeDifference.forTimer( ELAPSED_REALTIME + Duration.ofSeconds(60).toMillis(), // Rings in 60 seconds TimeDifference.FORMAT_CHRONOMETER); expect.that(runningTimer.toValueString(mContext)).isEqualTo(new ValueString("1:00")); Notification.sSystemClock = () -> NOW.plusSeconds(3); expect.that(runningTimer.toValueString(mContext)).isEqualTo(new ValueString("1:00")); Notification.sElapsedRealtimeClock = () -> ELAPSED_REALTIME + Duration.ofSeconds(10).toMillis(); expect.that(runningTimer.toValueString(mContext)).isEqualTo(new ValueString("0:50")); } @Test public void valueToString_timeDifferencePaused() { TimeDifference pausedTimer = TimeDifference.forPausedTimer( Loading Loading
core/api/current.txt +3 −0 Original line number Diff line number Diff line Loading @@ -7091,9 +7091,12 @@ package android.app { method @NonNull public static android.app.Notification.Metric.TimeDifference forPausedStopwatch(@NonNull java.time.Duration, int); method @NonNull public static android.app.Notification.Metric.TimeDifference forPausedTimer(@NonNull java.time.Duration, int); method @NonNull public static android.app.Notification.Metric.TimeDifference forStopwatch(@NonNull java.time.Instant, int); method @NonNull public static android.app.Notification.Metric.TimeDifference forStopwatch(long, int); method @NonNull public static android.app.Notification.Metric.TimeDifference forTimer(@NonNull java.time.Instant, int); method @NonNull public static android.app.Notification.Metric.TimeDifference forTimer(long, int); method public int getFormat(); method @Nullable public java.time.Duration getPausedDuration(); method @Nullable public Long getZeroElapsedRealtime(); method @Nullable public java.time.Instant getZeroTime(); method public boolean isStopwatch(); method public boolean isTimer();
core/java/android/app/Notification.java +156 −48 Original line number Diff line number Diff line Loading @@ -41,6 +41,7 @@ import android.annotation.ColorRes; import android.annotation.DimenRes; import android.annotation.Dimension; import android.annotation.DrawableRes; import android.annotation.ElapsedRealtimeLong; import android.annotation.FlaggedApi; import android.annotation.IdRes; import android.annotation.IntDef; Loading Loading @@ -154,6 +155,7 @@ import java.util.Locale; import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.function.LongSupplier; /** * A class that represents how a persistent notification is to be presented to Loading Loading @@ -1824,6 +1826,16 @@ public class Notification implements Parcelable @VisibleForTesting public static InstantSource sSystemClock = InstantSource.system(); /** * Provider of "elapsedRealTime" (milliseconds since boot, not affected by moving the system * clock). Normally {@link SystemClock#elapsedRealtime()}, but overridable for testing. * * @hide */ @Nullable @VisibleForTesting public static LongSupplier sElapsedRealtimeClock = () -> SystemClock.elapsedRealtime(); @UnsupportedAppUsage private Icon mSmallIcon; @UnsupportedAppUsage Loading Loading @@ -3111,6 +3123,13 @@ public class Notification implements Parcelable return sSystemClock != null ? sSystemClock : InstantSource.system(); } @NonNull private static LongSupplier getElapsedRealtimeClock() { return sElapsedRealtimeClock != null ? sElapsedRealtimeClock : () -> SystemClock.elapsedRealtime(); } private static LocalDate getToday() { return getSystemClock().instant() .atZone(ZoneId.systemDefault()) Loading Loading @@ -12229,10 +12248,16 @@ public class Notification implements Parcelable * This represents a timer, a stopwatch, or a countdown to an event. * * <p>When representing a <em>running</em> timer (or stopwatch, etc), this value specifies * a reference instant for when that timer will hit zero, called the "zero time". * In this case the time displayed is defined as the difference between the * "zero time" instant, and {@link Instant#now()}, meaning it will show a live-updated * timer. * a reference instant for when that timer will hit zero (or the stopwatch was at zero, * respectively), called the "zero time". In this case the time displayed is defined as the * difference between the "zero time" and the current time, meaning it will show a * live-updated timer. * * <p>The zero time can be specified as an {@link Instant} (in which case it corresponds * to a "real-world" point in time, from {@link InstantSource#system}), or as milliseconds * since boot (from {@link SystemClock#elapsedRealtime()}). The latter might be suitable * when the timer is tied to {@link AlarmManager#ELAPSED_REALTIME} alarm in * {@link AlarmManager}. * * <p>When representing a <em>paused</em> timer (or stopwatch, etc), this value specifies * the duration as a fixed value. Loading Loading @@ -12261,12 +12286,14 @@ public class Notification implements Parcelable public @interface Format {} private static final String KEY_ZERO_TIME = "zeroTime"; private static final String KEY_ZERO_ELAPSED_REALTIME = "zeroElapsedRealtime"; private static final String KEY_PAUSED_DURATION = "pausedDuration"; private static final String KEY_COUNT_DOWN = "countDown"; private static final String KEY_FORMAT = "format"; // One of these two will be present. @Nullable private final Instant mZeroTime; @Nullable @ElapsedRealtimeLong private final Long mZeroElapsedRealtime; @Nullable private final Duration mPausedDuration; private final boolean mCountDown; private final @Format int mFormat; Loading @@ -12278,10 +12305,24 @@ public class Notification implements Parcelable */ @NonNull public static TimeDifference forTimer(@NonNull Instant endTime, @Format int format) { return new TimeDifference(requireNonNull(endTime), /* pausedDuration= */ null, return new TimeDifference(requireNonNull(endTime), /* zeroElapsedRealtime= */ null, /* pausedDuration= */ null, /* countDown= */ true, format); } /** * Creates a "running timer" metric, which will show a countdown to {@code endTime}, * specified in the {@link SystemClock#elapsedRealtime()} frame of reference. * * @param endTime elapsed realtime at which the timer reaches zero */ @NonNull public static TimeDifference forTimer(@ElapsedRealtimeLong long endTime, @Format int format) { return new TimeDifference(/* zeroTime= */ null, endTime, /* pausedDuration= */ null, /* countDown= */ true, format); } /** * Creates a "running stopwatch" metric, which will show the time elapsed since * {@code startTime}. Loading @@ -12291,39 +12332,57 @@ public class Notification implements Parcelable @NonNull public static TimeDifference forStopwatch(@NonNull Instant startTime, @Format int format) { return new TimeDifference(requireNonNull(startTime), /* pausedDuration= */ null, return new TimeDifference(requireNonNull(startTime), /* zeroElapsedRealtime= */ null, /* pausedDuration= */ null, /* countDown= */ false, format); } /** * Creates a "running stopwatch" metric, which will show the time elapsed since * {@code startTime}, specified in the {@link SystemClock#elapsedRealtime()} frame of * reference. * * @param startTime elapsed realtime at which the stopwatch started */ @NonNull public static TimeDifference forStopwatch(@ElapsedRealtimeLong long startTime, @Format int format) { return new TimeDifference(/* zeroTime= */ null, startTime, /* pausedDuration= */ null, /* countDown= */ false, format); } /** * Creates a "paused timer" metric, showing the {@code remainingTime}. */ @NonNull public static TimeDifference forPausedTimer(@NonNull Duration remainingTime, @Format int format) { return new TimeDifference(/* zeroTime= */ null, requireNonNull(remainingTime), /* countDown= */ true, format); return new TimeDifference(/* zeroTime= */ null, /* zeroElapsedRealtime= */ null, requireNonNull(remainingTime), /* countDown= */ true, format); } /** * Creates a "paused timer" metric, showing the {@code elapsedTime}. * Creates a "paused stopwatch" metric, showing the {@code elapsedTime}. */ @NonNull public static TimeDifference forPausedStopwatch(@NonNull Duration elapsedTime, @Format int format) { return new TimeDifference(/* zeroTime= */ null, requireNonNull(elapsedTime), /* countDown= */ false, format); } private TimeDifference(@Nullable Instant zeroTime, @Nullable Duration pausedDuration, boolean countDown, @Format int format) { checkArgument((zeroTime != null) ^ (pausedDuration != null), "Either zeroTime or pausedDuration must be present, and not both. " + "Received %s,%s", zeroTime, pausedDuration); return new TimeDifference(/* zeroTime= */ null, /* zeroElapsedRealtime= */ null, requireNonNull(elapsedTime), /* countDown= */ false, format); } private TimeDifference(@Nullable Instant zeroTime, @Nullable @ElapsedRealtimeLong Long zeroElapsedRealtime, @Nullable Duration pausedDuration, boolean countDown, @Format int format) { checkArgument((zeroTime != null ? 1 : 0) + (zeroElapsedRealtime != null ? 1 : 0) + (pausedDuration != null ? 1 : 0) == 1, "Exactly one of zeroTime, zeroElapsedRealtime, or pausedDuration must be " + "present; received %s,%s,%s", zeroTime, zeroElapsedRealtime, pausedDuration); checkArgument(format >= FORMAT_AUTOMATIC && format <= FORMAT_CHRONOMETER, "Invalid format: %s", format); mZeroTime = zeroTime; mZeroElapsedRealtime = zeroElapsedRealtime; mPausedDuration = pausedDuration; mCountDown = countDown; mFormat = format; Loading @@ -12333,10 +12392,12 @@ public class Notification implements Parcelable private static TimeDifference fromBundle(Bundle bundle) { Instant zeroTime = bundle.containsKey(KEY_ZERO_TIME) ? Instant.ofEpochMilli(bundle.getLong(KEY_ZERO_TIME)) : null; Long zeroElapsedRealtime = bundle.containsKey(KEY_ZERO_ELAPSED_REALTIME) ? bundle.getLong(KEY_ZERO_ELAPSED_REALTIME) : null; Duration pausedDuration = bundle.containsKey(KEY_PAUSED_DURATION) ? Duration.ofMillis(bundle.getLong(KEY_PAUSED_DURATION)) : null; if (zeroTime != null || pausedDuration != null) { return new TimeDifference(zeroTime, pausedDuration, return new TimeDifference(zeroTime, zeroElapsedRealtime, pausedDuration, bundle.getBoolean(KEY_COUNT_DOWN), bundle.getInt(KEY_FORMAT, FORMAT_AUTOMATIC)); } else { Loading @@ -12349,6 +12410,8 @@ public class Notification implements Parcelable protected void toBundle(Bundle bundle) { if (mZeroTime != null) { bundle.putLong(KEY_ZERO_TIME, mZeroTime.toEpochMilli()); } else if (mZeroElapsedRealtime != null) { bundle.putLong(KEY_ZERO_ELAPSED_REALTIME, mZeroElapsedRealtime); } else if (mPausedDuration != null) { bundle.putLong(KEY_PAUSED_DURATION, mPausedDuration.toMillis()); } Loading @@ -12361,6 +12424,7 @@ public class Notification implements Parcelable if (!(obj instanceof TimeDifference that)) return false; if (this == that) return true; return Objects.equals(this.mZeroTime, that.mZeroTime) && Objects.equals(this.mZeroElapsedRealtime, that.mZeroElapsedRealtime) && Objects.equals(this.mPausedDuration, that.mPausedDuration) && this.mCountDown == that.mCountDown && this.mFormat == that.mFormat; Loading @@ -12368,41 +12432,70 @@ public class Notification implements Parcelable @Override public int hashCode() { return Objects.hash(mZeroTime, mPausedDuration, mCountDown, mFormat); return Objects.hash(mZeroTime, mZeroElapsedRealtime, mPausedDuration, mCountDown, mFormat); } @Override public String toString() { return "TimeDifference{" + "mZeroTime=" + mZeroTime + ", mPausedDuration=" + mPausedDuration + ", mCountDown=" + mCountDown + ", mFormat=" + mFormat + "}"; StringBuilder sb = new StringBuilder("TimeDifference{"); if (mZeroTime != null) { sb.append("mZeroTime=").append(mZeroTime); } else if (mZeroElapsedRealtime != null) { sb.append("mZeroElapsedRealtime=").append(mZeroElapsedRealtime); } else if (mPausedDuration != null) { sb.append("mPausedDuration=").append(mPausedDuration); } sb.append(", mCountDown=").append(mCountDown) .append(", mFormat=").append(mFormat) .append("}"); return sb.toString(); } /** * The instant at which the time difference is zero. * The {@link Instant} at which the time difference is zero. Only valid for an * {@link Instant}-based {@link TimeDifference}. * * <ul> * <li>For a running timer this is the {@code endTime} supplied to * {@link #forTimer}. * {@link #forTimer(Instant, int)}. * <li>For a running stopwatch this is the {@code startTime} supplied to * {@link #forStopwatch}. * <li>This is {@code null} for paused timers or stopwatches. * {@link #forStopwatch(Instant, int)}. * <li>For running timers or stopwatches based on elapsed realtime (as well as * paused timers and stopwatches), this is {@code null}. * </ul> */ @Nullable public Instant getZeroTime() { return mZeroTime; } /** * The elapsed realtime at which the time difference is zero. Only valid for an * {@link SystemClock#elapsedRealtime()}-based {@link TimeDifference}. * * <ul> * <li>For a running timer this is the {@code endTime} supplied to * {@link #forTimer(long, int)}. * <li>For a running stopwatch this is the {@code startTime} supplied to * {@link #forStopwatch(long, int)}. * <li>For running timers or stopwatches based on {@link Instant} (as well as * paused timers and stopwatches), this is {@code null}. * </ul> */ @SuppressLint("AutoBoxing") @Nullable @ElapsedRealtimeLong public Long getZeroElapsedRealtime() { return mZeroElapsedRealtime; } /** * The fixed time difference, for a paused timer or stopwatch. * * <ul> * <li>For a paused timer this is the {@code remainingTime} supplied to * {@link #forPausedTimer}. * <li>For a paused stopwatch this is the {@code elapsedTime} supplied to * {@link #forPausedStopwatch}. * <li>This is {@code null} for running timers or stopwatches. * <li>For running timers or stopwatches this is {@code null}. * </ul> */ @Nullable public Duration getPausedDuration() { Loading @@ -12411,7 +12504,7 @@ public class Notification implements Parcelable /** * Whether this {@link TimeDifference} value represents a stopwatch -- when running, * it counts up from {@link #getZeroTime()}. * it counts up from {@link #getZeroTime()} (or {@link #getZeroElapsedRealtime()}). */ public boolean isStopwatch() { return !mCountDown; Loading @@ -12419,7 +12512,7 @@ public class Notification implements Parcelable /** * Whether this {@link TimeDifference} value represents a timer -- when running, * it counts down to {@link #getZeroTime()}. * it counts down to {@link #getZeroTime()} (or {@link #getZeroElapsedRealtime()}). */ public boolean isTimer() { return mCountDown; Loading @@ -12436,21 +12529,9 @@ public class Notification implements Parcelable @NonNull @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public ValueString toValueString(Context context) { Duration duration; if (mPausedDuration != null) { duration = mPausedDuration; } else { // If the timer/stopwatch is running we likely want a Chronometer view, so this // path is mostly for debugging/completeness. Instant now = getSystemClock().instant(); if (isStopwatch()) { duration = Duration.between(mZeroTime, now); } else { duration = Duration.between(now, mZeroTime); } } Duration duration = getCurrentDuration(); duration = duration.truncatedTo(SECONDS); // ms are ignored and we don't want -0:00 Duration absDuration = duration.abs(); Measure hours = new Measure(absDuration.toHours(), MeasureUnit.HOUR); Measure minutes = new Measure(absDuration.toMinutesPart(), MeasureUnit.MINUTE); Loading @@ -12464,6 +12545,33 @@ public class Notification implements Parcelable return new ValueString(text, null); } private Duration getCurrentDuration() { if (mPausedDuration != null) { return mPausedDuration; } else if (mZeroTime != null) { // If the timer/stopwatch is running we likely want a Chronometer view, so this // path is mostly for debugging/completeness. Instant now = getSystemClock().instant(); if (isStopwatch()) { return Duration.between(mZeroTime, now); } else { return Duration.between(now, mZeroTime); } } else if (mZeroElapsedRealtime != null) { // If the timer/stopwatch is running we likely want a Chronometer view, so this // path is mostly for debugging/completeness. long elapsedRealtimeNow = getElapsedRealtimeClock().getAsLong(); if (isStopwatch()) { return Duration.ofMillis(elapsedRealtimeNow - mZeroElapsedRealtime); } else { return Duration.ofMillis(mZeroElapsedRealtime - elapsedRealtimeNow); } } else { throw new IllegalStateException( "None of mPausedDuration, mZeroTime, mZeroElapsedRealtime set!"); } } private static String formatAbsoluteDuration(@Format int format, Measure hours, Measure minutes, Measure seconds) { if (format == FORMAT_ADAPTIVE) { Loading
core/tests/coretests/src/android/app/NotificationMetricStyleTest.java +39 −0 Original line number Diff line number Diff line Loading @@ -51,6 +51,7 @@ import android.app.Notification.MetricStyle; import android.content.ContentResolver; import android.content.Context; import android.os.Bundle; import android.os.SystemClock; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; Loading Loading @@ -100,6 +101,8 @@ public class NotificationMetricStyleTest { // December 18, 2025 -> more than 4 months away private static final LocalDate FAR_AWAY = LocalDate.of(2025, 12, 18); private static final long ELAPSED_REALTIME = 300_000; private static final String NNBSP = "\u202f"; private Context mContext; Loading @@ -121,6 +124,7 @@ public class NotificationMetricStyleTest { Settings.System.putString(mContext.getContentResolver(), Settings.System.TIME_12_24, "12"); Notification.sSystemClock = () -> NOW; Notification.sElapsedRealtimeClock = () -> ELAPSED_REALTIME; } @After Loading @@ -134,6 +138,7 @@ public class NotificationMetricStyleTest { Settings.System.putString(mContext.getContentResolver(), Settings.System.TIME_12_24, mPrevious24HourSetting); Notification.sSystemClock = InstantSource.system(); Notification.sElapsedRealtimeClock = () -> SystemClock.elapsedRealtime(); } @Test Loading Loading @@ -314,6 +319,40 @@ public class NotificationMetricStyleTest { new ValueString("500:40:00")); } @Test public void valueToString_timeDifferenceInstant_updatesWithSystemClock() { TimeDifference runningTimer = TimeDifference.forTimer( NOW.plusSeconds(60), // Rings in 60 seconds TimeDifference.FORMAT_CHRONOMETER); expect.that(runningTimer.toValueString(mContext)).isEqualTo(new ValueString("1:00")); Notification.sElapsedRealtimeClock = () -> ELAPSED_REALTIME + Duration.ofSeconds(10).toMillis(); expect.that(runningTimer.toValueString(mContext)).isEqualTo(new ValueString("1:00")); Notification.sSystemClock = () -> NOW.plusSeconds(3); expect.that(runningTimer.toValueString(mContext)).isEqualTo(new ValueString("0:57")); } @Test public void valueToString_timeDifferenceElapsedRealtime_updatesWithElapsedRealtime() { TimeDifference runningTimer = TimeDifference.forTimer( ELAPSED_REALTIME + Duration.ofSeconds(60).toMillis(), // Rings in 60 seconds TimeDifference.FORMAT_CHRONOMETER); expect.that(runningTimer.toValueString(mContext)).isEqualTo(new ValueString("1:00")); Notification.sSystemClock = () -> NOW.plusSeconds(3); expect.that(runningTimer.toValueString(mContext)).isEqualTo(new ValueString("1:00")); Notification.sElapsedRealtimeClock = () -> ELAPSED_REALTIME + Duration.ofSeconds(10).toMillis(); expect.that(runningTimer.toValueString(mContext)).isEqualTo(new ValueString("0:50")); } @Test public void valueToString_timeDifferencePaused() { TimeDifference pausedTimer = TimeDifference.forPausedTimer( Loading