[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);
+ }
+}