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

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

Initial pieces of MetricStyle layouts

Add conversion of different MetricValue subclasses to displayable strings, and (temporarily) show the metrics in an InboxStyle layout.

Bug: 415827681
Test: atest NotificationMetricStyleTest
Flag: android.app.api_metric_style
Change-Id: I50cf2064a326abae315be25c7902d0382e08afc9
parent 43844aaa
Loading
Loading
Loading
Loading
+265 −11
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static com.android.internal.util.Preconditions.checkArgument;
import static java.time.temporal.ChronoUnit.SECONDS;
import static java.util.Objects.requireNonNull;
import android.annotation.ColorInt;
@@ -74,6 +75,11 @@ import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.icu.number.NumberFormatter;
import android.icu.number.Precision;
import android.icu.text.MeasureFormat;
import android.icu.util.Measure;
import android.icu.util.MeasureUnit;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.PlayerBase;
@@ -96,6 +102,7 @@ import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
@@ -131,12 +138,17 @@ import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.time.Duration;
import java.time.Instant;
import java.time.InstantSource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
@@ -1818,6 +1830,16 @@ public class Notification implements Parcelable
    @FlaggedApi(Flags.FLAG_API_METRIC_STYLE)
    static final String EXTRA_METRICS = "android.metrics";
    /**
     * {@link InstantSource} used for obtaining "now". Normally {@link InstantSource#system()},
     * but overridable for testing.
     *
     * @hide
     */
    @Nullable
    @VisibleForTesting
    public static InstantSource sSystemClock = InstantSource.system();
    @UnsupportedAppUsage
    private Icon mSmallIcon;
    @UnsupportedAppUsage
@@ -3097,6 +3119,17 @@ public class Notification implements Parcelable
        }
    }
    @NonNull
    private static InstantSource getSystemClock() {
        return sSystemClock != null ? sSystemClock : InstantSource.system();
    }
    private static LocalDate getToday() {
        return getSystemClock().instant()
                .atZone(ZoneId.systemDefault())
                .toLocalDate();
    }
    private static void visitIconUri(@NonNull Consumer<Uri> visitor, @Nullable Icon icon) {
        if (icon == null) return;
        final int iconType = icon.getType();
@@ -11591,6 +11624,7 @@ public class Notification implements Parcelable
                    Bundle.class);
            if (bundles != null) {
                for (Bundle bundle : bundles) {
                    if (bundle != null) {
                        Metric metric = Metric.fromBundle(bundle);
                        if (metric != null) {
                            addMetric(metric);
@@ -11598,6 +11632,7 @@ public class Notification implements Parcelable
                    }
                }
            }
        }
        /** @hide */
        @Override
@@ -11627,9 +11662,54 @@ public class Notification implements Parcelable
        /** @hide */
        @Override
        public RemoteViews makeExpandedContentView() {
            return null;
            // TODO(b/415828647): Implement for MetricStyle
            // Remember: Add new layout resources to isStandardLayout()
            // TODO: b/415828647 - Implement properly; this is a temporary version using
            //  InboxStyle, for prototyping.
            // And remember: Add new layout resources to isStandardLayout()
            StandardTemplateParams p = mBuilder.mParams.reset()
                    .viewType(StandardTemplateParams.VIEW_TYPE_EXPANDED)
                    .fillTextsFrom(mBuilder).text(null);
            TemplateBindResult result = new TemplateBindResult();
            RemoteViews contentView = getStandardView(mBuilder.getInboxLayoutResource(), p, result);
            int[] rowIds = {R.id.inbox_text0, R.id.inbox_text1, R.id.inbox_text2, R.id.inbox_text3,
                    R.id.inbox_text4, R.id.inbox_text5, R.id.inbox_text6};
            // Make sure all rows are gone in case we reuse a view.
            for (int rowId : rowIds) {
                contentView.setViewVisibility(rowId, View.GONE);
            }
            int i = 0;
            int topPadding = mBuilder.mContext.getResources().getDimensionPixelSize(
                    R.dimen.notification_inbox_item_top_padding);
            boolean first = true;
            int onlyViewId = 0;
            while (i < mMetrics.size() && i < MAX_METRICS) {
                Metric metric = mMetrics.get(i);
                contentView.setViewVisibility(rowIds[i], View.VISIBLE);
                Metric.MetricValue.ValueString valueString = metric.getValue().toValueString(
                        mBuilder.mContext);
                contentView.setTextViewText(rowIds[i],
                        metric.getLabel() + ": " + valueString.text
                                + (valueString.subtext != null ? " " + valueString.subtext : ""));
                mBuilder.setTextViewColorSecondary(contentView, rowIds[i], p);
                contentView.setViewPadding(rowIds[i], 0, topPadding, 0, 0);
                if (first) {
                    onlyViewId = rowIds[i];
                } else {
                    onlyViewId = 0;
                }
                first = false;
                i++;
            }
            if (onlyViewId != 0) {
                // We only have 1 entry, lets make it look like the normal Text of a Bigtext
                topPadding = mBuilder.mContext.getResources().getDimensionPixelSize(
                        R.dimen.notification_text_margin_top);
                contentView.setViewPadding(onlyViewId, 0, topPadding, 0, 0);
            }
            return contentView;
        }
    }
@@ -12086,6 +12166,26 @@ public class Notification implements Parcelable
            /** @hide */
            protected abstract void toBundle(Bundle bundle);
            /** @hide */
            @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
            public record ValueString(String text, @Nullable String subtext) {
                public ValueString(String text) {
                    this(text, null);
                }
            }
            /**
             * Returns a string representation of the {@link MetricValue}, in the format of a pair
             * of text / nullable subtext. Note that for some kinds of values, notably
             * {@link TimeDifference}, this string representation may change over subsequent calls,
             * even though the object itself is immutable.
             *
             * @hide
             */
            @NonNull
            @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
            public abstract ValueString toValueString(Context context);
        }
        /**
@@ -12293,6 +12393,65 @@ public class Notification implements Parcelable
            public int getFormat() {
                return mFormat;
            }
            /** @hide */
            @Override
            @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.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);
                Measure seconds = new Measure(absDuration.toSecondsPart(), MeasureUnit.SECOND);
                String absText = formatAbsoluteDuration(mFormat, hours, minutes, seconds);
                String text = duration.isNegative()
                        ? context.getString(R.string.negative_duration, absText)
                        : absText;
                return new ValueString(text, null);
            }
            private static String formatAbsoluteDuration(@Format int format, Measure hours,
                    Measure minutes, Measure seconds) {
                if (format == FORMAT_ADAPTIVE) {
                    MeasureFormat formatter = MeasureFormat.getInstance(Locale.getDefault(),
                            MeasureFormat.FormatWidth.NARROW);
                    ArrayList<Measure> partsList = new ArrayList<>();
                    if (hours.getNumber().intValue() != 0) {
                        partsList.add(hours);
                    }
                    if (minutes.getNumber().intValue() != 0) {
                        partsList.add(minutes);
                    }
                    if (seconds.getNumber().intValue() != 0 || partsList.isEmpty()) {
                        partsList.add(seconds);
                    }
                    return formatter.formatMeasures(partsList.toArray(new Measure[0]));
                } else {
                    // FORMAT_AUTOMATIC / FORMAT_CHRONOMETER
                    MeasureFormat formatter = MeasureFormat.getInstance(Locale.getDefault(),
                            MeasureFormat.FormatWidth.NUMERIC);
                    return hours.getNumber().intValue() != 0
                            ? formatter.formatMeasures(hours, minutes, seconds)
                            : formatter.formatMeasures(minutes, seconds);
                }
            }
        }
        /** A metric value for showing a date. */
@@ -12400,14 +12559,67 @@ public class Notification implements Parcelable
            public @Format int getFormat() {
                return mFormat;
            }
            /** @hide */
            @Override
            @NonNull
            @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
            public ValueString toValueString(Context context) {
                // DateUtils.formatDateTime expects epoch millis, so make up a time.
                LocalDateTime localDateTime = mValue.atStartOfDay();
                String formatted = DateUtils.formatDateTime(context,
                        localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(),
                        getFormatFlags(mFormat, mValue));
                return new ValueString(formatted, null);
            }
            private static int getFormatFlags(@Format int format, LocalDate date) {
                switch (format) {
                    case FORMAT_LONG_DATE:
                        return DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH
                                | DateUtils.FORMAT_SHOW_YEAR;
                    case FORMAT_SHORT_DATE:
                        return DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE
                                | DateUtils.FORMAT_SHOW_YEAR;
                    case FORMAT_AUTOMATIC:
                    default:
                        return getAutomaticFormatFlags(date);
                }
            }
            // Whole-month interval in either direction of the current month in which a date is
            // considered "close to today" (e.g. if today is Feb 10 2025 then any date in
            // Nov 1 2024 .. May 31 2025 is considered "close").
            private static final int CLOSE_DATE_MONTH_SPAN = 3;
            private static int getAutomaticFormatFlags(LocalDate date) {
                YearMonth currentMonth = YearMonth.from(getToday());
                YearMonth dateMonth = YearMonth.from(date);
                long monthsBetween = Math.abs(ChronoUnit.MONTHS.between(currentMonth, dateMonth));
                if (monthsBetween <= CLOSE_DATE_MONTH_SPAN) {
                    // Date is "close" to today -> FORMAT_SHORT_DATE but without year
                    return DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE
                            | DateUtils.FORMAT_NO_YEAR;
                } else {
                    // Otherwise -> same as FORMAT_SHORT_DATE
                    return DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE
                            | DateUtils.FORMAT_SHOW_YEAR;
                }
            }
        }
        /**
         * A metric value for showing a clock time.
         *
         * The time will be shown as-is, so should be in a user-understandable timezone (most
         * likely the device's own, unless it's clear from context that it would be different, such
         * as a flight's arrival time).
         * <p>Only hour and minutes will be displayed (according to the user's preference for 12-
         * or 24- hour time, e.g. 14:30 or 2:30 PM); seconds and lower are truncated.
         *
         * <p>The time should be in a user-understandable timezone (most likely the device's own,
         * unless it's clear from context that it would be different, such as a flight's arrival
         * time on a different city).
         */
        public static final class FixedTime extends MetricValue {
@@ -12417,8 +12629,6 @@ public class Notification implements Parcelable
            /**
             * Creates a {@link FixedTime} with the specified {@link LocalTime}.
             *
             * <p>Maximum precision is seconds; milliseconds will be ignored.
             */
            public FixedTime(@NonNull LocalTime value) {
                mValue = requireNonNull(value).truncatedTo(ChronoUnit.SECONDS);
@@ -12464,6 +12674,22 @@ public class Notification implements Parcelable
            public @NonNull LocalTime getValue() {
                return mValue;
            }
            /** @hide */
            @Override
            @NonNull
            @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
            public ValueString toValueString(Context context) {
                // DateUtils.formatDateTime expects epoch millis, so make up a date.
                LocalDateTime localDateTime = mValue.atDate(getToday());
                String formatted = DateUtils.formatDateTime(context,
                        localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(),
                        DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_NO_NOON
                                | DateUtils.FORMAT_NO_MIDNIGHT);
                return new ValueString(formatted, null);
            }
        }
        /** Metric corresponding to an integer value. */
@@ -12543,6 +12769,14 @@ public class Notification implements Parcelable
            public String getUnit() {
                return mUnit;
            }
            /** @hide */
            @Override
            @NonNull
            @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
            public ValueString toValueString(Context context) {
                return new ValueString(String.valueOf(mValue), mUnit);
            }
        }
        /** Metric corresponding to a floating point value. */
@@ -12681,6 +12915,18 @@ public class Notification implements Parcelable
            public int getMaxFractionDigits() {
                return mMaxFractionDigits;
            }
            /** @hide */
            @Override
            @NonNull
            @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
            public ValueString toValueString(Context context) {
                String formatted = NumberFormatter.withLocale(Locale.getDefault())
                        .precision(Precision.minMaxFraction(mMinFractionDigits, mMaxFractionDigits))
                        .format(mValue)
                        .toString();
                return new ValueString(formatted, mUnit);
            }
        }
        /** Metric corresponding to a string value. */
@@ -12729,6 +12975,14 @@ public class Notification implements Parcelable
            @NonNull public String getValue() {
                return mValue;
            }
            /** @hide */
            @Override
            @NonNull
            @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
            public ValueString toValueString(Context context) {
                return new ValueString(mValue, null);
            }
        }
    }
+377 −0

File changed.

Preview size limit exceeded, changes collapsed.