[RONs] Bind ProgressStyle values to NotificationProgressBar

Bug: 370497239
Test: NotificationProgressModelTest and post a ProgressStyle Notification.
Flag: android.app.api_rich_ongoing
Change-Id: Idc51c8ee1f35b4d385d92827983535d60aceef96
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index bc7ebce..8b33417 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -115,6 +115,7 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.ContrastColorUtil;
 import com.android.internal.util.NotificationBigTextNormalizer;
+import com.android.internal.widget.NotificationProgressModel;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -7318,12 +7319,16 @@
          */
         @VisibleForTesting
         public static int ensureButtonFillContrast(int color, int bg) {
-            return isColorDark(bg)
-                    ? ContrastColorUtil.findContrastColorAgainstDark(color, bg, true, 1.3)
-                    : ContrastColorUtil.findContrastColor(color, bg, true, 1.3);
+            return ensureColorContrast(color, bg, 1.3);
         }
 
 
+        private static int ensureColorContrast(int color, int bg, double contrastRatio) {
+            return isColorDark(bg)
+                    ? ContrastColorUtil.findContrastColorAgainstDark(color, bg, true, contrastRatio)
+                    : ContrastColorUtil.findContrastColor(color, bg, true, contrastRatio);
+        }
+
         /**
          * @return Whether we are currently building a notification from a legacy (an app that
          *         doesn't create material notifications by itself) app.
@@ -11657,6 +11662,7 @@
             StandardTemplateParams p = mBuilder.mParams.reset()
                     .viewType(StandardTemplateParams.VIEW_TYPE_BIG)
                     .allowTextWithProgress(true)
+                    .hideProgress(true)
                     .fillTextsFrom(mBuilder);
 
             // Replace the text with the big text, but only if the big text is not empty.
@@ -11678,10 +11684,28 @@
                 contentView.setViewVisibility(R.id.notification_progress_end_icon, View.GONE);
             }
 
+            contentView.setViewVisibility(R.id.progress, View.VISIBLE);
+
+            final int backgroundColor = mBuilder.getColors(p).getBackgroundColor();
+            final int defaultProgressColor = mBuilder.getPrimaryAccentColor(p);
+            final NotificationProgressModel model = createProgressModel(
+                    defaultProgressColor, backgroundColor);
+            contentView.setBundle(R.id.progress,
+                    "setProgressModel", model.toBundle());
+
+            if (mTrackerIcon != null) {
+                contentView.setIcon(R.id.progress,
+                        "setProgressTrackerIcon",
+                        mTrackerIcon);
+            }
+
             return contentView;
         }
 
-        private static @NonNull ArrayList<Bundle> getProgressSegmentsAsBundleList(
+        /**
+         * @hide
+         */
+        public static @NonNull ArrayList<Bundle> getProgressSegmentsAsBundleList(
                 @Nullable List<Segment> progressSegments) {
             final ArrayList<Bundle> segments = new ArrayList<>();
             if (progressSegments != null && !progressSegments.isEmpty()) {
@@ -11703,7 +11727,10 @@
             return segments;
         }
 
-        private static @NonNull List<Segment> getProgressSegmentsFromBundleList(
+        /**
+         * @hide
+         */
+        public  static @NonNull List<Segment> getProgressSegmentsFromBundleList(
                 @Nullable List<Bundle> segmentBundleList) {
             final ArrayList<Segment> segments = new ArrayList<>();
             if (segmentBundleList != null && !segmentBundleList.isEmpty()) {
@@ -11726,8 +11753,10 @@
 
             return segments;
         }
-
-        private static @NonNull ArrayList<Bundle> getProgressPointsAsBundleList(
+        /**
+         * @hide
+         */
+        public static @NonNull ArrayList<Bundle> getProgressPointsAsBundleList(
                 @Nullable List<Point> progressPoints) {
             final ArrayList<Bundle> points = new ArrayList<>();
             if (progressPoints != null && !progressPoints.isEmpty()) {
@@ -11749,7 +11778,10 @@
             return points;
         }
 
-        private static @NonNull List<Point> getProgressPointsFromBundleList(
+        /**
+         * @hide
+         */
+        public static @NonNull List<Point> getProgressPointsFromBundleList(
                 @Nullable List<Bundle> pointBundleList) {
             final ArrayList<Point> points = new ArrayList<>();
 
@@ -11771,6 +11803,78 @@
             return points;
         }
 
+        @NonNull
+        private NotificationProgressModel createProgressModel(int defaultProgressColor,
+                int backgroundColor) {
+            final NotificationProgressModel model;
+            if (mIndeterminate) {
+                final int indeterminateColor;
+                if (!mProgressSegments.isEmpty()) {
+                    indeterminateColor = mProgressSegments.get(0).mColor;
+                } else {
+                    indeterminateColor = defaultProgressColor;
+                }
+
+                model = new NotificationProgressModel(
+                        sanitizeProgressColor(indeterminateColor,
+                                backgroundColor, defaultProgressColor));
+            } else {
+
+                // Ensure segment color contrasts.
+                final List<Segment> segments = new ArrayList<>();
+                for (Segment segment : mProgressSegments) {
+                    segments.add(sanitizeSegment(segment, backgroundColor,
+                            defaultProgressColor));
+                }
+
+                // Create default segment when no segments are provided.
+                if (segments.isEmpty()) {
+                    segments.add(sanitizeSegment(new Segment(100), backgroundColor,
+                            defaultProgressColor));
+                }
+
+                // Ensure point color contrasts.
+                final List<Point> points = new ArrayList<>();
+                for (Point point : mProgressPoints) {
+                    points.add(sanitizePoint(point, backgroundColor, defaultProgressColor));
+                }
+
+                model = new NotificationProgressModel(segments, points,
+                        mProgress, mIsStyledByProgress);
+            }
+            return model;
+        }
+
+        private Segment sanitizeSegment(@NonNull Segment segment,
+                @ColorInt int bg,
+                @ColorInt int defaultColor) {
+            return new Segment(segment.getLength())
+                    .setId(segment.getId())
+                    .setColor(sanitizeProgressColor(segment.getColor(), bg, defaultColor));
+        }
+
+        private Point sanitizePoint(@NonNull Point point,
+                @ColorInt int bg,
+                @ColorInt int defaultColor) {
+            return new Point(point.getPosition()).setId(point.getId())
+                    .setColor(sanitizeProgressColor(point.getColor(), bg, defaultColor));
+        }
+
+        /**
+         * Finds steps and points fill color with sufficient contrast over bg (1.3:1) that
+         * has the same hue as the original color, but is lightened or darkened depending on
+         * whether the background is dark or light.
+         *
+         */
+        private int sanitizeProgressColor(@ColorInt int color,
+                @ColorInt int bg,
+                @ColorInt int defaultColor) {
+            return Builder.ensureColorContrast(
+                    Color.alpha(color) == 0 ? defaultColor : color,
+                    bg,
+                    1.3);
+        }
+
         /**
          * A segment of the progress bar, which defines its length and color.
          * Segments allow for creating progress bars with multiple colors or sections
diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java
index 12e1dd9..3e597d7 100644
--- a/core/java/com/android/internal/widget/NotificationProgressBar.java
+++ b/core/java/com/android/internal/widget/NotificationProgressBar.java
@@ -16,15 +16,22 @@
 
 package com.android.internal.widget;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.Notification.ProgressStyle;
 import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
 import android.util.AttributeSet;
+import android.view.RemotableViewMethod;
 import android.widget.ProgressBar;
 import android.widget.RemoteViews;
 
 import androidx.annotation.ColorInt;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
 import com.android.internal.widget.NotificationProgressDrawable.Part;
 import com.android.internal.widget.NotificationProgressDrawable.Point;
 import com.android.internal.widget.NotificationProgressDrawable.Segment;
@@ -42,6 +49,10 @@
  */
 @RemoteViews.RemoteView
 public class NotificationProgressBar extends ProgressBar {
+    private NotificationProgressModel mProgressModel;
+    @Nullable
+    private Drawable mProgressTrackerDrawable = null;
+
     public NotificationProgressBar(Context context) {
         this(context, null);
     }
@@ -60,6 +71,53 @@
     }
 
     /**
+     * Setter for the notification progress model.
+     *
+     * @see NotificationProgressModel#fromBundle
+     * @see #setProgressModelAsync
+     */
+    @RemotableViewMethod(asyncImpl = "setProgressModelAsync")
+    public void setProgressModel(@Nullable Bundle bundle) {
+        Preconditions.checkArgument(bundle != null,
+                "Bundle shouldn't be null");
+
+        mProgressModel = NotificationProgressModel.fromBundle(bundle);
+    }
+
+    private void setProgressModel(@NonNull NotificationProgressModel model) {
+        mProgressModel = model;
+    }
+
+    /**
+     * Setter for the progress tracker icon.
+     *
+     * @see #setProgressTrackerIconAsync
+     */
+    @RemotableViewMethod(asyncImpl = "setProgressTrackerIconAsync")
+    public void setProgressTrackerIcon(@Nullable Icon icon) {
+    }
+
+
+    /**
+     * Async version of {@link #setProgressTrackerIcon}
+     */
+    public Runnable setProgressTrackerIconAsync(@Nullable Icon icon) {
+        final Drawable progressTrackerDrawable;
+        if (icon != null) {
+            progressTrackerDrawable = icon.loadDrawable(getContext());
+        } else {
+            progressTrackerDrawable = null;
+        }
+        return () -> {
+            setProgressTrackerDrawable(progressTrackerDrawable);
+        };
+    }
+
+    private void setProgressTrackerDrawable(@Nullable  Drawable drawable) {
+        mProgressTrackerDrawable = drawable;
+    }
+
+    /**
      * Processes the ProgressStyle data and convert to list of {@code
      * NotificationProgressDrawable.Part}.
      */
diff --git a/core/java/com/android/internal/widget/NotificationProgressModel.java b/core/java/com/android/internal/widget/NotificationProgressModel.java
new file mode 100644
index 0000000..e51ea99
--- /dev/null
+++ b/core/java/com/android/internal/widget/NotificationProgressModel.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+
+import android.annotation.ColorInt;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.app.Flags;
+import android.app.Notification;
+import android.app.Notification.ProgressStyle.Point;
+import android.app.Notification.ProgressStyle.Segment;
+import android.graphics.Color;
+import android.os.Bundle;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Data model for {@link NotificationProgressBar}.
+ *
+ * This class holds the necessary data to render the notification progressbar.
+ * It is used to bind the progress style progress data to {@link NotificationProgressBar}.
+ *
+ * @hide
+ * @see NotificationProgressModel#toBundle
+ * @see NotificationProgressModel#fromBundle
+ */
+@FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
+public final class NotificationProgressModel {
+    private static final int INVALID_INDETERMINATE_COLOR = Color.TRANSPARENT;
+    private static final String KEY_SEGMENTS = "segments";
+    private static final String KEY_POINTS = "points";
+    private static final String KEY_PROGRESS = "progress";
+    private static final String KEY_IS_STYLED_BY_PROGRESS = "isStyledByProgress";
+    private static final String KEY_INDETERMINATE_COLOR = "indeterminateColor";
+    private final List<Segment> mSegments;
+    private final List<Point> mPoints;
+    private final int mProgress;
+    private final boolean mIsStyledByProgress;
+    @ColorInt
+    private final int mIndeterminateColor;
+
+    public NotificationProgressModel(
+            @NonNull List<Segment> segments,
+            @NonNull List<Point> points,
+            int progress,
+            boolean isStyledByProgress
+    ) {
+        Preconditions.checkArgument(progress >= 0);
+        Preconditions.checkArgument(!segments.isEmpty());
+        mSegments = segments;
+        mPoints = points;
+        mProgress = progress;
+        mIsStyledByProgress = isStyledByProgress;
+        mIndeterminateColor = INVALID_INDETERMINATE_COLOR;
+    }
+
+    public NotificationProgressModel(
+            @ColorInt int indeterminateColor
+    ) {
+        Preconditions.checkArgument(indeterminateColor != INVALID_INDETERMINATE_COLOR);
+        mSegments = Collections.emptyList();
+        mPoints = Collections.emptyList();
+        mProgress = 0;
+        mIsStyledByProgress = false;
+        mIndeterminateColor = indeterminateColor;
+    }
+
+    public List<Segment> getSegments() {
+        return mSegments;
+    }
+
+    public List<Point> getPoints() {
+        return mPoints;
+    }
+
+    public int getProgress() {
+        return mProgress;
+    }
+
+    public boolean isStyledByProgress() {
+        return mIsStyledByProgress;
+    }
+
+    @ColorInt
+    public int getIndeterminateColor() {
+        return mIndeterminateColor;
+    }
+
+    public boolean isIndeterminate() {
+        return mIndeterminateColor != INVALID_INDETERMINATE_COLOR;
+    }
+
+    /**
+     * Returns a {@link Bundle} representation of this {@link NotificationProgressModel}.
+     */
+    @NonNull
+    public Bundle toBundle() {
+        final Bundle bundle = new Bundle();
+        if (mIndeterminateColor != INVALID_INDETERMINATE_COLOR) {
+            bundle.putInt(KEY_INDETERMINATE_COLOR, mIndeterminateColor);
+        } else {
+            bundle.putParcelableList(KEY_SEGMENTS,
+                    Notification.ProgressStyle.getProgressSegmentsAsBundleList(mSegments));
+            bundle.putParcelableList(KEY_POINTS,
+                    Notification.ProgressStyle.getProgressPointsAsBundleList(mPoints));
+            bundle.putInt(KEY_PROGRESS, mProgress);
+            bundle.putBoolean(KEY_IS_STYLED_BY_PROGRESS, mIsStyledByProgress);
+        }
+        return bundle;
+    }
+
+    /**
+     * Creates a {@link NotificationProgressModel} from a {@link Bundle}.
+     */
+    @NonNull
+    public static NotificationProgressModel fromBundle(@NonNull Bundle bundle) {
+        final int indeterminateColor = bundle.getInt(KEY_INDETERMINATE_COLOR,
+                INVALID_INDETERMINATE_COLOR);
+        if (indeterminateColor != INVALID_INDETERMINATE_COLOR) {
+            return new NotificationProgressModel(indeterminateColor);
+        } else {
+            final List<Segment> segments =
+                    Notification.ProgressStyle.getProgressSegmentsFromBundleList(
+                            bundle.getParcelableArrayList(KEY_SEGMENTS, Bundle.class));
+            final List<Point> points =
+                    Notification.ProgressStyle.getProgressPointsFromBundleList(
+                            bundle.getParcelableArrayList(KEY_POINTS, Bundle.class));
+            final int progress = bundle.getInt(KEY_PROGRESS);
+            final boolean isStyledByProgress = bundle.getBoolean(KEY_IS_STYLED_BY_PROGRESS);
+            return new NotificationProgressModel(segments, points, progress, isStyledByProgress);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "NotificationProgressModel{"
+                + "mSegments=" + mSegments
+                + ", mPoints=" + mPoints
+                + ", mProgress=" + mProgress
+                + ", mIsStyledByProgress=" + mIsStyledByProgress
+                + ", mIndeterminateColor=" + mIndeterminateColor + "}";
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        final NotificationProgressModel that = (NotificationProgressModel) o;
+        return mProgress == that.mProgress
+                && mIsStyledByProgress == that.mIsStyledByProgress
+                && mIndeterminateColor == that.mIndeterminateColor
+                && Objects.equals(mSegments, that.mSegments)
+                && Objects.equals(mPoints, that.mPoints);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mSegments,
+                mPoints,
+                mProgress,
+                mIsStyledByProgress,
+                mIndeterminateColor);
+    }
+}
diff --git a/core/res/res/layout/notification_template_material_progress.xml b/core/res/res/layout/notification_template_material_progress.xml
index b413c70..fdcefcc 100644
--- a/core/res/res/layout/notification_template_material_progress.xml
+++ b/core/res/res/layout/notification_template_material_progress.xml
@@ -75,10 +75,11 @@
                         />
 
 
-                    <include
+                    <com.android.internal.widget.NotificationProgressBar
+                        android:id="@+id/progress"
                         android:layout_width="0dp"
                         android:layout_height="@dimen/notification_progress_bar_height"
-                        layout="@layout/notification_template_progress"
+                        style="@style/Widget.Material.Light.ProgressBar.Horizontal"
                         android:layout_weight="1"
                         />
 
diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressModelTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressModelTest.java
new file mode 100644
index 0000000..962399e
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressModelTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Flags;
+import android.app.Notification;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.List;
+
+@SmallTest
+@EnableFlags(Flags.FLAG_API_RICH_ONGOING)
+public class NotificationProgressModelTest {
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Test(expected = IllegalArgumentException.class)
+    public void throw_exception_on_transparent_indeterminate_color() {
+        new NotificationProgressModel(Color.TRANSPARENT);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void throw_exception_on_empty_segments() {
+        new NotificationProgressModel(List.of(),
+                List.of(),
+                10,
+                false);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void throw_exception_on_negative_progress() {
+        new NotificationProgressModel(
+                List.of(new Notification.ProgressStyle.Segment(50).setColor(Color.YELLOW)),
+                List.of(),
+                -1,
+                false);
+    }
+
+    @Test
+    public void save_and_restore_indeterminate_progress_model() {
+        // GIVEN
+        final NotificationProgressModel savedModel = new NotificationProgressModel(Color.RED);
+        final Bundle bundle = savedModel.toBundle();
+
+        // WHEN
+        final NotificationProgressModel restoredModel =
+                NotificationProgressModel.fromBundle(bundle);
+
+        // THEN
+        assertThat(restoredModel.getIndeterminateColor()).isEqualTo(Color.RED);
+        assertThat(restoredModel.isIndeterminate()).isTrue();
+        assertThat(restoredModel.getProgress()).isEqualTo(-1);
+        assertThat(restoredModel.getSegments()).isEmpty();
+        assertThat(restoredModel.getPoints()).isEmpty();
+        assertThat(restoredModel.isStyledByProgress()).isFalse();
+    }
+
+    @Test
+    public void save_and_restore_non_indeterminate_progress_model() {
+        // GIVEN
+        final List<Notification.ProgressStyle.Segment> segments = List.of(
+                new Notification.ProgressStyle.Segment(50).setColor(Color.YELLOW),
+                new Notification.ProgressStyle.Segment(50).setColor(Color.LTGRAY));
+        final List<Notification.ProgressStyle.Point> points = List.of(
+                new Notification.ProgressStyle.Point(0).setColor(Color.RED),
+                new Notification.ProgressStyle.Point(20).setColor(Color.BLUE));
+        final NotificationProgressModel savedModel = new NotificationProgressModel(segments,
+                points,
+                100,
+                true);
+
+        final Bundle bundle = savedModel.toBundle();
+
+        // WHEN
+        final NotificationProgressModel restoredModel =
+                NotificationProgressModel.fromBundle(bundle);
+
+        // THEN
+        assertThat(restoredModel.isIndeterminate()).isFalse();
+        assertThat(restoredModel.getSegments()).isEqualTo(segments);
+        assertThat(restoredModel.getPoints()).isEqualTo(points);
+        assertThat(restoredModel.getProgress()).isEqualTo(100);
+        assertThat(restoredModel.isStyledByProgress()).isTrue();
+        assertThat(restoredModel.getIndeterminateColor()).isEqualTo(-1);
+    }
+}