Loading core/java/android/app/Notification.java +112 −8 Original line number Diff line number Diff line Loading @@ -115,6 +115,7 @@ import com.android.internal.graphics.ColorUtils; 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; Loading Loading @@ -7318,12 +7319,16 @@ public class Notification implements Parcelable */ @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. Loading Loading @@ -11657,6 +11662,7 @@ public class Notification implements Parcelable 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. Loading @@ -11678,10 +11684,28 @@ public class Notification implements Parcelable 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()) { Loading @@ -11703,7 +11727,10 @@ public class Notification implements Parcelable 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()) { Loading @@ -11726,8 +11753,10 @@ public class Notification implements Parcelable 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()) { Loading @@ -11749,7 +11778,10 @@ public class Notification implements Parcelable 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<>(); Loading @@ -11771,6 +11803,78 @@ public class Notification implements Parcelable 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 Loading core/java/com/android/internal/widget/NotificationProgressBar.java +58 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -42,6 +49,10 @@ import java.util.TreeSet; */ @RemoteViews.RemoteView public class NotificationProgressBar extends ProgressBar { private NotificationProgressModel mProgressModel; @Nullable private Drawable mProgressTrackerDrawable = null; public NotificationProgressBar(Context context) { this(context, null); } Loading @@ -59,6 +70,53 @@ public class NotificationProgressBar extends ProgressBar { super(context, attrs, defStyleAttr, defStyleRes); } /** * 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}. Loading core/java/com/android/internal/widget/NotificationProgressModel.java 0 → 100644 +183 −0 Original line number Diff line number Diff line /* * 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); } } core/res/res/layout/notification_template_material_progress.xml +3 −2 Original line number Diff line number Diff line Loading @@ -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" /> Loading core/tests/coretests/src/com/android/internal/widget/NotificationProgressModelTest.java 0 → 100644 +111 −0 Original line number Diff line number Diff line /* * 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); } } Loading
core/java/android/app/Notification.java +112 −8 Original line number Diff line number Diff line Loading @@ -115,6 +115,7 @@ import com.android.internal.graphics.ColorUtils; 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; Loading Loading @@ -7318,12 +7319,16 @@ public class Notification implements Parcelable */ @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. Loading Loading @@ -11657,6 +11662,7 @@ public class Notification implements Parcelable 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. Loading @@ -11678,10 +11684,28 @@ public class Notification implements Parcelable 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()) { Loading @@ -11703,7 +11727,10 @@ public class Notification implements Parcelable 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()) { Loading @@ -11726,8 +11753,10 @@ public class Notification implements Parcelable 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()) { Loading @@ -11749,7 +11778,10 @@ public class Notification implements Parcelable 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<>(); Loading @@ -11771,6 +11803,78 @@ public class Notification implements Parcelable 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 Loading
core/java/com/android/internal/widget/NotificationProgressBar.java +58 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -42,6 +49,10 @@ import java.util.TreeSet; */ @RemoteViews.RemoteView public class NotificationProgressBar extends ProgressBar { private NotificationProgressModel mProgressModel; @Nullable private Drawable mProgressTrackerDrawable = null; public NotificationProgressBar(Context context) { this(context, null); } Loading @@ -59,6 +70,53 @@ public class NotificationProgressBar extends ProgressBar { super(context, attrs, defStyleAttr, defStyleRes); } /** * 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}. Loading
core/java/com/android/internal/widget/NotificationProgressModel.java 0 → 100644 +183 −0 Original line number Diff line number Diff line /* * 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); } }
core/res/res/layout/notification_template_material_progress.xml +3 −2 Original line number Diff line number Diff line Loading @@ -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" /> Loading
core/tests/coretests/src/com/android/internal/widget/NotificationProgressModelTest.java 0 → 100644 +111 −0 Original line number Diff line number Diff line /* * 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); } }