diff options
| -rw-r--r-- | core/api/current.txt | 41 | ||||
| -rw-r--r-- | core/java/android/app/Notification.java | 820 | ||||
| -rw-r--r-- | core/res/res/values/dimens.xml | 6 | ||||
| -rw-r--r-- | core/res/res/values/symbols.xml | 3 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/app/NotificationTest.java | 303 |
5 files changed, 1166 insertions, 7 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 8eb881139b34..59da04ce55a2 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -6854,6 +6854,47 @@ package android.app { method public android.app.Notification.MessagingStyle.Message setData(String, android.net.Uri); } + @FlaggedApi("android.app.api_rich_ongoing") public static class Notification.ProgressStyle extends android.app.Notification.Style { + ctor public Notification.ProgressStyle(); + method @NonNull public android.app.Notification.ProgressStyle addProgressSegment(@NonNull android.app.Notification.ProgressStyle.Segment); + method @NonNull public android.app.Notification.ProgressStyle addProgressStep(@NonNull android.app.Notification.ProgressStyle.Step); + method public int getProgress(); + method @Nullable public android.graphics.drawable.Icon getProgressEndIcon(); + method public int getProgressMax(); + method @NonNull public java.util.List<android.app.Notification.ProgressStyle.Segment> getProgressSegments(); + method @Nullable public android.graphics.drawable.Icon getProgressStartIcon(); + method @NonNull public java.util.List<android.app.Notification.ProgressStyle.Step> getProgressSteps(); + method @Nullable public android.graphics.drawable.Icon getProgressTrackerIcon(); + method public boolean isProgressIndeterminate(); + method public boolean isStyledByProgress(); + method @NonNull public android.app.Notification.ProgressStyle setProgress(int); + method @NonNull public android.app.Notification.ProgressStyle setProgressEndIcon(@Nullable android.graphics.drawable.Icon); + method @NonNull public android.app.Notification.ProgressStyle setProgressIndeterminate(boolean); + method @NonNull public android.app.Notification.ProgressStyle setProgressSegments(@NonNull java.util.List<android.app.Notification.ProgressStyle.Segment>); + method @NonNull public android.app.Notification.ProgressStyle setProgressStartIcon(@Nullable android.graphics.drawable.Icon); + method @NonNull public android.app.Notification.ProgressStyle setProgressSteps(@NonNull java.util.List<android.app.Notification.ProgressStyle.Step>); + method @NonNull public android.app.Notification.ProgressStyle setProgressTrackerIcon(@Nullable android.graphics.drawable.Icon); + method @NonNull public android.app.Notification.ProgressStyle setStyledByProgress(boolean); + } + + public static final class Notification.ProgressStyle.Segment { + ctor public Notification.ProgressStyle.Segment(int); + method @ColorInt public int getColor(); + method public int getLength(); + method public int getStableId(); + method @NonNull public android.app.Notification.ProgressStyle.Segment setColor(@ColorInt int); + method @NonNull public android.app.Notification.ProgressStyle.Segment setStableId(int); + } + + public static final class Notification.ProgressStyle.Step { + ctor public Notification.ProgressStyle.Step(int); + method @ColorInt public int getColor(); + method public int getPosition(); + method public int getStableId(); + method @NonNull public android.app.Notification.ProgressStyle.Step setColor(@ColorInt int); + method @NonNull public android.app.Notification.ProgressStyle.Step setStableId(int); + } + public abstract static class Notification.Style { ctor @Deprecated public Notification.Style(); method public android.app.Notification build(); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 392a1f113c23..a39f216d033e 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -121,7 +121,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -783,10 +782,32 @@ public class Notification implements Parcelable @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) public static final int FLAG_PROMOTED_ONGOING = 0x00040000; - private static final List<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Arrays.asList( - BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class, - DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class, - MessagingStyle.class, CallStyle.class); + private static final Set<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Set.of( + BigTextStyle.class, + BigPictureStyle.class, + InboxStyle.class, + MediaStyle.class, + DecoratedCustomViewStyle.class, + DecoratedMediaCustomViewStyle.class, + MessagingStyle.class, + CallStyle.class + ); + + private static boolean isPlatformStyle(Style style) { + if (style == null) { + return false; + } + + if (PLATFORM_STYLE_CLASSES.contains(style.getClass())) { + return true; + } + + if (Flags.apiRichOngoing()) { + return style.getClass() == ProgressStyle.class; + } + + return false; + } /** @hide */ @IntDef(flag = true, prefix = {"FLAG_"}, value = { @@ -1620,6 +1641,66 @@ public class Notification implements Parcelable public static final String EXTRA_COLORIZED = "android.colorized"; /** + * {@link #extras} key: an arraylist of {@link android.app.Notification.ProgressStyle.Segment} + * bundles provided by a + * {@link android.app.Notification.ProgressStyle} notification as supplied to + * {@link ProgressStyle#setProgressSegments} + * or {@link ProgressStyle#addProgressSegment(ProgressStyle.Segment)}. + * This extra is a parcelable array list of bundles. + * @hide + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static final String EXTRA_PROGRESS_SEGMENTS = "android.progressSegments"; + + /** + * {@link #extras} key: an arraylist of {@link android.app.Notification.ProgressStyle.Step} + * bundles provided by a + * {@link android.app.Notification.ProgressStyle} notification as supplied to + * {@link ProgressStyle#setProgressSteps} + * or {@link ProgressStyle#addProgressStep(ProgressStyle.Step)}. + * This extra is a parcelable array list of bundles. + * @hide + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static final String EXTRA_PROGRESS_STEPS = "android.progressSteps"; + + /** + * {@link #extras} key: whether the progress bar should be styled by its progress as + * supplied to {@link ProgressStyle#setStyledByProgress}. + * This extra is a boolean. + * @hide + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static final String EXTRA_STYLED_BY_PROGRESS = "android.styledByProgress"; + + /** + * {@link #extras} key: this is an {@link Icon} of an image to be + * shown as progress bar progress tracker icon in {@link ProgressStyle}, supplied to + *{@link ProgressStyle#setProgressTrackerIcon(Icon)}. + * @hide + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static final String EXTRA_PROGRESS_TRACKER_ICON = "android.progressTrackerIcon"; + + /** + * {@link #extras} key: this is an {@link Icon} of an image to be + * shown at the beginning of the progress bar in {@link ProgressStyle}, supplied to + *{@link ProgressStyle#setProgressStartIcon(Icon)}. + * @hide + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static final String EXTRA_PROGRESS_START_ICON = "android.progressStartIcon"; + + /** + * {@link #extras} key: this is an {@link Icon} of an image to be + * shown at the end of the progress bar in {@link ProgressStyle}, supplied to + *{@link ProgressStyle#setProgressEndIcon(Icon)}. + * @hide + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static final String EXTRA_PROGRESS_END_ICON = "android.progressEndIcon"; + + /** * @hide */ public static final String EXTRA_BUILDER_APPLICATION_INFO = "android.appInfo"; @@ -3072,6 +3153,9 @@ public class Notification implements Parcelable if (Flags.apiRichOngoing()) { visitIconUri(visitor, extras.getParcelable(EXTRA_ENROUTE_OVERLAY_ICON, Icon.class)); + visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_TRACKER_ICON, Icon.class)); + visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_START_ICON, Icon.class)); + visitIconUri(visitor, extras.getParcelable(EXTRA_PROGRESS_END_ICON, Icon.class)); } if (mBubbleMetadata != null) { @@ -6630,7 +6714,7 @@ public class Notification implements Parcelable // Custom views which come from a platform style class are safe, and thus do not need to // be wrapped. Any subclass of those styles has the opportunity to make arbitrary // changes to the RemoteViews, and thus can't be trusted as a fully vetted view. - if (fromStyle && PLATFORM_STYLE_CLASSES.contains(mStyle.getClass())) { + if (fromStyle && isPlatformStyle(mStyle)) { return false; } return mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S; @@ -7971,6 +8055,12 @@ public class Notification implements Parcelable return innerClass; } } + + if (Flags.apiRichOngoing()) { + if (templateClass.equals(ProgressStyle.class.getName())) { + return ProgressStyle.class; + } + } return null; } @@ -11220,6 +11310,724 @@ public class Notification implements Parcelable } } + + /** + * A Notification Style used to to define a notification whose expanded state includes + * a highly customizable progress bar with segments, steps, a custom tracker icon, + * and custom icons at the start and end of the progress bar. + * + * This style is suggested for use cases where the app is showing a tracker to the + * user of a thing they are interested in: the location of a car on its way + * to pick them up, food being delivered, or their own progress in a navigation + * journey. + * + * To use this style with your Notification, feed it to + * {@link Notification.Builder#setStyle(android.app.Notification.Style)} like so: + * <pre class="prettyprint"> + * new Notification.Builder(context) + * .setSmallIcon(R.drawable.ic_notification) + * .setColor(Color.GREEN) + * .setColorized(true) + * .setContentTitle("Arrive 10:08 AM"). + * .setContentText("Dominique Ansel Bakery Soho") + * .addAction(new Notification.Action("Exit navigation",...)) + * .setStyle(new Notification.ProgressStyle() + * .setStyledByProgress(false) + * .setProgress(456) + * .setProgressTrackerIcon(Icon.createWithResource(R.drawable.ic_driving_tracker)) + * .addProgressSegment(new Segment(41).setColor(Color.BLACK)) + * .addProgressSegment(new Segment(552).setColor(Color.YELLOW)) + * .addProgressSegment(new Segment(253).setColor(Color.YELLOW)) + * .addProgressSegment(new Segment(94).setColor(Color.BLUE)) + * .addProgressStep(new Step(60).setColor(Color.RED)) + * .addProgressStep(new Step(560).setColor(Color.YELLOW)) + * ) + * </pre> + * + * + * + * NOTE: The progress bar layout will be mirrored for RTL layout. + * NOTE: The extras set by {@link Notification.Builder#setProgress} will be overridden by + * the values set on this style object when the notification is built. + * + */ + @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) + public static class ProgressStyle extends Notification.Style { + private static final String KEY_ELEMENT_STABLE_ID = "stableId"; + private static final String KEY_ELEMENT_COLOR = "colorInt"; + private static final String KEY_SEGMENT_LENGTH = "length"; + private static final String KEY_STEP_POSITION = "position"; + + private static final int MAX_PROGRESS_SEGMENT_LIMIT = 15; + private static final int MAX_PROGRESS_STEP_LIMIT = 5; + private static final int DEFAULT_PROGRESS_MAX = 100; + + private List<Segment> mProgressSegments = new ArrayList<>(); + private List<Step> mProgressSteps = new ArrayList<>(); + + private int mProgress = 0; + + private boolean mIndeterminate; + + private boolean mIsStyledByProgress = true; + + @Nullable + private Icon mTrackerIcon; + @Nullable + private Icon mStartIcon; + @Nullable + private Icon mEndIcon; + + /** + * @hide + */ + @Override + public boolean areNotificationsVisiblyDifferent(Style other) { + if (other == null || getClass() != other.getClass()) { + return true; + } + + final ProgressStyle progressStyle = (ProgressStyle) other; + + /** + * @see #setProgressIndeterminate + */ + if (!Objects.equals(mIndeterminate, progressStyle.mIndeterminate)) { + return true; + } + boolean nonIndeterminateCheckResult = false; + if (!mIndeterminate) { + nonIndeterminateCheckResult = !Objects.equals(mProgress, progressStyle.mProgress) + || !Objects.equals(mIsStyledByProgress, progressStyle.mIsStyledByProgress) + || !Objects.equals(mProgressSegments, progressStyle.mProgressSegments) + || !Objects.equals(mProgressSteps, progressStyle.mProgressSteps) + || !Objects.equals(mTrackerIcon, progressStyle.mTrackerIcon); + } + + return !Objects.equals(mStartIcon, progressStyle.mStartIcon) + || !Objects.equals(mEndIcon, progressStyle.mEndIcon) + || nonIndeterminateCheckResult; + } + + /** + * Gets the segments that define the background layer of the progress bar. + * + * If no segments are provided, the progress bar will be rendered with a single segment + * with length 100 and default color. + * + * @see #setProgressSegments + * @see #addProgressSegment + * @see Segment + */ + public @NonNull List<Segment> getProgressSegments() { + return mProgressSegments; + } + + /** + * Sets or replaces the segments of the progress bar. + * + * Segments allow for creating progress bars with multiple colors or sections + * to represent different stages or categories of progress. + * For example, Traffic conditions along a navigation journey. + * @see Segment + */ + public @NonNull ProgressStyle setProgressSegments(@NonNull List<Segment> progressSegments) { + mProgressSegments = new ArrayList<>(progressSegments.size()); + return this; + } + + /** + * Appends a segment to the end of the progress bar. + * + * Segments allow for creating progress bars with multiple colors or sections + * to represent different stages or categories of progress. + * For example, Traffic conditions along a navigation journey. + * @see Segment + */ + public @NonNull ProgressStyle addProgressSegment(@NonNull Segment segment) { + if (mProgressSegments == null) { + mProgressSegments = new ArrayList<>(); + } + mProgressSegments.add(segment); + + return this; + } + + /** + * Gets the steps that are displayed on the progress bar. + *. + * @see #setProgressSteps + * @see #addProgressStep + * @see Step + */ + public @NonNull List<Step> getProgressSteps() { + return mProgressSteps; + } + + /** + * Replaces all the progress steps. + * + * Steps are designated points within a progressbar to visualize + * distinct stages or milestones. + * For example, you might use steps to mark stops in a multi-stop + * navigation journey, where each step represents a destination. + * @see Step + */ + public @NonNull ProgressStyle setProgressSteps(@NonNull List<Step> steps) { + mProgressSteps = new ArrayList<>(steps); + return this; + } + + /** + * Adds another step. + * + * Steps are designated points within a progressbar to visualize + * distinct stages or milestones. + * For example, you might use steps to mark stops in a multi-stop + * navigation journey, where each step represents a destination. + * + * Steps can be added in any order, as their + * position within the progress bar is determined by their individual + * {@link Step#getPosition()}. + * @see Step + */ + public @NonNull ProgressStyle addProgressStep(@NonNull Step step) { + if (mProgressSteps == null) { + mProgressSteps = new ArrayList<>(); + } + mProgressSteps.add(step); + + return this; + } + + /** + * Gets the progress value of the progress bar. + * @see #setProgress + */ + public int getProgress() { + return mProgress; + } + + /** + * Specifies the progress (in the same units as {@link Segment#getLength()}) + * of the tracker along the length of the bar. + * + * The max progress value is the sum of all Segment lengths. + * The default value is 0. + */ + public @NonNull ProgressStyle setProgress(int progress) { + mProgress = progress; + return this; + } + + /** + * Gets the sum of the lengths of all Segments in the style, which + * defines the maximum progress. Defaults to 100 when segments are omitted. + */ + public int getProgressMax() { + final List<Segment> progressSegment = mProgressSegments; + if (progressSegment == null || progressSegment.isEmpty()) { + return DEFAULT_PROGRESS_MAX; + } else { + int progressMax = 0; + int validSegmentCount = 0; + for (int i = 0; i < progressSegment.size() + && validSegmentCount < MAX_PROGRESS_SEGMENT_LIMIT; i++) { + int segmentLength = progressSegment.get(i).getLength(); + if (segmentLength > 0) { + try { + progressMax = Math.addExact(progressMax, segmentLength); + validSegmentCount++; + } catch (ArithmeticException e) { + Log.e(TAG, + "Notification.ProgressStyle segment total overflowed.", e); + return DEFAULT_PROGRESS_MAX; + } + } + } + + if (validSegmentCount == 0) { + return DEFAULT_PROGRESS_MAX; + } + + return progressMax; + } + + } + + /** + * Get indeterminate value of the progress bar. + * @see #setProgressIndeterminate + */ + public boolean isProgressIndeterminate() { + return mIndeterminate; + } + + /** + * Used to indicate an initialization state without a known progress amount. + * When specified, the following fields are ignored: + * @see #setProgress + * @see #setProgressSegments + * @see #setProgressSteps + * @see #setProgressTrackerIcon + * @see #setStyledByProgress + * + * If the app provides exactly one Segment, that segment's color will be + * used to style the indeterminate bar. + */ + public @NonNull ProgressStyle setProgressIndeterminate(boolean indeterminate) { + mIndeterminate = indeterminate; + return this; + } + + /** + * Gets whether the progress bar's style is based on its progress. + * @see #setStyledByProgress + */ + public boolean isStyledByProgress() { + return mIsStyledByProgress; + } + + /** + * Indicates whether the segments and steps will be styled differently + * based on whether they are behind or ahead of the current progress. + * When true, segments appearing ahead of the current progress will be given a + * slightly different appearance to indicate that it is part of the progress bar + * that is not "filled". + * When false, all segments will be given the filled appearance, and it will be + * the app's responsibility to use #setProgressTrackerIcon or segment colors + * to make the current progress clear to the user. + * the default value is true. + */ + public @NonNull ProgressStyle setStyledByProgress(boolean enabled) { + mIsStyledByProgress = enabled; + return this; + } + + + /** + * Gets the progress tracker icon for the progress bar. + * @see #setProgressTrackerIcon + */ + public @Nullable Icon getProgressTrackerIcon() { + return mTrackerIcon; + } + + /** + * An optional icon that can appear as an overlay on the bar at the point of + * current progress. + * Aspect ratio may be anywhere from 2:1 to 1:2; content outside that + * aspect ratio range will be cropped. + * This icon will be mirrored in RTL. + */ + public @NonNull ProgressStyle setProgressTrackerIcon(@Nullable Icon trackerIcon) { + mTrackerIcon = trackerIcon; + return this; + } + + /** + * Gets the progress bar start icon. + * @see #setProgressStartIcon + */ + public @Nullable Icon getProgressStartIcon() { + return mStartIcon; + } + + /** + * An optional square icon that appears at the start of the progress bar. + * This icon will be cropped to its central square. + * This icon will NOT be mirrored in RTL layouts. + */ + public @NonNull ProgressStyle setProgressStartIcon(@Nullable Icon startIcon) { + mStartIcon = startIcon; + return this; + } + + /** + * Gets the progress bar end icon. + * @see #setProgressEndIcon(Icon) + */ + public @Nullable Icon getProgressEndIcon() { + return mEndIcon; + } + + /** + * An optional square icon that appears at the end of the progress bar. + * This icon will be cropped to its central square. + * This icon will NOT be mirrored in RTL layouts. + */ + public @NonNull ProgressStyle setProgressEndIcon(@Nullable Icon endIcon) { + mEndIcon = endIcon; + return this; + } + + /** + * @hide + */ + @Override + public void purgeResources() { + super.purgeResources(); + if (mTrackerIcon != null) { + mTrackerIcon.convertToAshmem(); + } + if (mStartIcon != null) { + mStartIcon.convertToAshmem(); + } + if (mEndIcon != null) { + mEndIcon.convertToAshmem(); + } + } + + /** + * @hide + */ + @Override + public void reduceImageSizes(Context context) { + super.reduceImageSizes(context); + + final Resources resources = context.getResources(); + + int progressIconSize = + resources.getDimensionPixelSize(R.dimen.notification_progress_icon_size); + if (mStartIcon != null) { + mStartIcon.scaleDownIfNecessary(progressIconSize, progressIconSize); + } + if (mEndIcon != null) { + mEndIcon.scaleDownIfNecessary(progressIconSize, progressIconSize); + } + if (mTrackerIcon != null) { + int progressTrackerWidth = resources.getDimensionPixelSize( + R.dimen.notification_progress_tracker_width); + int progressTrackerHeight = resources.getDimensionPixelSize( + R.dimen.notification_progress_tracker_height); + mTrackerIcon.scaleDownIfNecessary(progressTrackerWidth, progressTrackerHeight); + } + } + + /** + * @hide + */ + @Override + public void addExtras(Bundle extras) { + super.addExtras(extras); + extras.putParcelableArrayList(EXTRA_PROGRESS_SEGMENTS, + getProgressSegmentsAsBundleList(mProgressSegments)); + extras.putParcelableArrayList(EXTRA_PROGRESS_STEPS, + getProgressStepsAsBundleList(mProgressSteps)); + + extras.putInt(EXTRA_PROGRESS, mProgress); + extras.putBoolean(EXTRA_PROGRESS_INDETERMINATE, mIndeterminate); + extras.putInt(EXTRA_PROGRESS_MAX, getProgressMax()); + extras.putBoolean(EXTRA_STYLED_BY_PROGRESS, mIsStyledByProgress); + + if (mTrackerIcon != null) { + extras.putParcelable(EXTRA_PROGRESS_TRACKER_ICON, mTrackerIcon); + } else { + extras.remove(EXTRA_PROGRESS_TRACKER_ICON); + } + + if (mStartIcon != null) { + extras.putParcelable(EXTRA_PROGRESS_START_ICON, mStartIcon); + } else { + extras.remove(EXTRA_PROGRESS_START_ICON); + } + + if (mEndIcon != null) { + extras.putParcelable(EXTRA_PROGRESS_END_ICON, mEndIcon); + } else { + extras.remove(EXTRA_PROGRESS_END_ICON); + } + } + + /** + * @hide + */ + @Override + protected void restoreFromExtras(Bundle extras) { + super.restoreFromExtras(extras); + mProgressSegments = getProgressSegmentsFromBundleList( + extras.getParcelableArrayList(EXTRA_PROGRESS_SEGMENTS, Bundle.class)); + mProgress = extras.getInt(EXTRA_PROGRESS, 0); + mIndeterminate = extras.getBoolean(EXTRA_PROGRESS_INDETERMINATE, false); + mIsStyledByProgress = extras.getBoolean(EXTRA_STYLED_BY_PROGRESS, true); + mTrackerIcon = extras.getParcelable(EXTRA_PROGRESS_TRACKER_ICON, Icon.class); + mStartIcon = extras.getParcelable(EXTRA_PROGRESS_START_ICON, Icon.class); + mEndIcon = extras.getParcelable(EXTRA_PROGRESS_END_ICON, Icon.class); + mProgressSteps = getProgressStepsFromBundleList( + extras.getParcelableArrayList(EXTRA_PROGRESS_STEPS, Bundle.class)); + } + + /** + * @hide + */ + @Override + public boolean displayCustomViewInline() { + // This is a lie; True is returned for progress notifications to make sure + // that the custom view is not used instead of the template, but it will not + // actually be included. + return true; + } + + private static @NonNull ArrayList<Bundle> getProgressSegmentsAsBundleList( + @Nullable List<Segment> progressSegments) { + final ArrayList<Bundle> segments = new ArrayList<>(); + if (progressSegments != null && !progressSegments.isEmpty()) { + for (int i = 0; i < progressSegments.size(); i++) { + final Segment segment = progressSegments.get(i); + if (segment.getLength() <= 0) { + continue; + } + + final Bundle bundle = new Bundle(); + bundle.putInt(KEY_SEGMENT_LENGTH, segment.getLength()); + bundle.putInt(KEY_ELEMENT_STABLE_ID, segment.getStableId()); + bundle.putInt(KEY_ELEMENT_COLOR, segment.getColor()); + + segments.add(bundle); + } + } + + return segments; + } + + private static @NonNull List<Segment> getProgressSegmentsFromBundleList( + @Nullable List<Bundle> segmentBundleList) { + final ArrayList<Segment> segments = new ArrayList<>(); + if (segmentBundleList != null && !segmentBundleList.isEmpty()) { + for (int i = 0; i < segmentBundleList.size(); i++) { + final Bundle segmentBundle = segmentBundleList.get(i); + final int length = segmentBundle.getInt(KEY_SEGMENT_LENGTH); + if (length <= 0) { + continue; + } + + final int stableId = segmentBundle.getInt(KEY_ELEMENT_STABLE_ID); + final int color = segmentBundle.getInt(KEY_ELEMENT_COLOR, + Notification.COLOR_DEFAULT); + final Segment segment = new Segment(length) + .setStableId(stableId).setColor(color); + + segments.add(segment); + } + } + + return segments; + } + + private static @NonNull ArrayList<Bundle> getProgressStepsAsBundleList( + @Nullable List<Step> progressSteps) { + final ArrayList<Bundle> steps = new ArrayList<>(); + if (progressSteps != null && !progressSteps.isEmpty()) { + for (int i = 0; i < progressSteps.size(); i++) { + final Step step = progressSteps.get(i); + if (step.getPosition() < 0) { + continue; + } + + final Bundle bundle = new Bundle(); + bundle.putInt(KEY_STEP_POSITION, step.getPosition()); + bundle.putInt(KEY_ELEMENT_STABLE_ID, step.getStableId()); + bundle.putInt(KEY_ELEMENT_COLOR, step.getColor()); + + steps.add(bundle); + } + } + + return steps; + } + + private static @NonNull List<Step> getProgressStepsFromBundleList( + @Nullable List<Bundle> stepBundleList) { + final ArrayList<Step> steps = new ArrayList<>(); + + if (stepBundleList != null && !stepBundleList.isEmpty()) { + for (int i = 0; i < stepBundleList.size(); i++) { + final Bundle segmentBundle = stepBundleList.get(i); + final int position = segmentBundle.getInt(KEY_STEP_POSITION); + if (position < 0) { + continue; + } + final int stableId = segmentBundle.getInt(KEY_ELEMENT_STABLE_ID); + final int color = segmentBundle.getInt(KEY_ELEMENT_COLOR, + Notification.COLOR_DEFAULT); + final Step step = new Step(position).setStableId(stableId).setColor(color); + steps.add(step); + } + } + + return steps; + } + + /** + * A segment of the progress bar, which defines its length and color. + * Segments allow for creating progress bars with multiple colors or sections + * to represent different stages or categories of progress. + * For example, Traffic conditions along a navigation journey. + */ + public static final class Segment { + private int mLength; + private int mStableId = 0; + @ColorInt + private int mColor = Notification.COLOR_DEFAULT; + + /** + * Create a segment with a non-zero length. + * @param length + * See {@link #getLength} + */ + public Segment(int length) { + mLength = length; + } + + /** + * The length of this Segment within the progress bar. + * This value has no units, it is just relative to the length of other segments, + * and the value provided to {@link ProgressStyle#setProgress}. + */ + public int getLength() { + return mLength; + } + + /** + * Gets the stable id of this Segment. + * + * @see #setStableId + */ + public int getStableId() { + return mStableId; + } + + /** + * Optional ID used to uniquely identify the element across updates. + */ + public @NonNull Segment setStableId(int stableId) { + mStableId = stableId; + return this; + } + + /** + * Returns the color of this Segment. + * + * @see #setColor + */ + @ColorInt + public int getColor() { + return mColor; + } + + /** + * Optional color of this Segment + */ + public @NonNull Segment setColor(@ColorInt int color) { + mColor = color; + return this; + } + + /** + * Needed for {@link Notification.Style#areNotificationsVisiblyDifferent} + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Segment segment = (Segment) o; + return mLength == segment.mLength && mStableId == segment.mStableId + && mColor == segment.mColor; + } + + @Override + public int hashCode() { + return Objects.hash(mLength, mStableId, mColor); + } + } + + /** + * A step within the progress bar, defining its position and color. + * Steps are designated points within a progressbar to visualize + * distinct stages or milestones. + * For example, you might use steps to mark stops in a multi-stop + * navigation journey, where each step represents a destination. + */ + public static final class Step { + + private int mPosition; + private int mStableId; + @ColorInt + private int mColor = Notification.COLOR_DEFAULT; + + /** + * Create a step element. + * The position of this step on the progress bar + * relative to {@link ProgressStyle#getProgressMax} + * @param position + * See {@link #getPosition} + */ + public Step(int position) { + mPosition = position; + } + + /** + * Gets the position of this Step. + * The position of this step on the progress bar + * relative to {@link ProgressStyle#getProgressMax}. + */ + public int getPosition() { + return mPosition; + } + + + /** + * Optional ID used to uniqurely identify the element across updates. + */ + public int getStableId() { + return mStableId; + } + + /** + * Optional ID used to uniqurely identify the element across updates. + */ + public @NonNull Step setStableId(int stableId) { + mStableId = stableId; + return this; + } + + /** + * Returns the color of this Segment. + * + * @see #setColor + */ + @ColorInt + public int getColor() { + return mColor; + } + + /** + * Optional color of this Segment + */ + public @NonNull Step setColor(@ColorInt int color) { + mColor = color; + return this; + } + + /** + * Needed for {@link Notification.Style#areNotificationsVisiblyDifferent} + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Step step = (Step) o; + return mPosition == step.mPosition && mStableId == step.mStableId + && mColor == step.mColor; + } + + @Override + public int hashCode() { + return Objects.hash(mPosition, mStableId, mColor); + } + } + } + /** * Notification style for custom views that are decorated by the system * diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index f397ef2b151c..6683dc044c9a 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -808,6 +808,12 @@ This is bigger than displayed because listeners can use it for other displays e.g. wearables. --> <dimen name="notification_person_icon_max_size">144dp</dimen> + <!-- The size of the progress bar icon --> + <dimen name="notification_progress_icon_size">20dp</dimen> + <!-- The size of the progress tracker width --> + <dimen name="notification_progress_tracker_width">40dp</dimen> + <!-- The size of the progress tracker height --> + <dimen name="notification_progress_tracker_height">20dp</dimen> <!-- The maximum size of the small notification icon on low memory devices. --> <dimen name="notification_small_icon_size_low_ram">@dimen/notification_small_icon_size</dimen> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 76a8e9254e2f..5f40a6c7eba4 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3855,6 +3855,9 @@ <java-symbol type="dimen" name="notification_custom_view_max_image_height"/> <java-symbol type="dimen" name="notification_custom_view_max_image_width"/> <java-symbol type="dimen" name="notification_person_icon_max_size" /> + <java-symbol type="dimen" name="notification_progress_icon_size" /> + <java-symbol type="dimen" name="notification_progress_tracker_width" /> + <java-symbol type="dimen" name="notification_progress_tracker_height" /> <java-symbol type="dimen" name="notification_small_icon_size_low_ram"/> <java-symbol type="dimen" name="notification_big_picture_max_height_low_ram"/> diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index 0f73df92ca93..edcea241e620 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -97,7 +97,6 @@ import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.TextAppearanceSpan; import android.util.Pair; -import android.util.Slog; import android.widget.RemoteViews; import androidx.test.InstrumentationRegistry; @@ -118,6 +117,7 @@ import org.junit.Test; import org.junit.rules.TestRule; import org.junit.runner.RunWith; +import java.util.Collections; import java.util.List; import java.util.function.Consumer; @@ -2114,6 +2114,300 @@ public class NotificationTest { assertThat(n.getWhen()).isEqualTo(9); } + @Test + public void getNotificationStyleClass_forPlatformClassName_returnsPlatformClass() { + final List<Class<? extends Notification.Style>> platformStyleClasses = List.of( + Notification.BigTextStyle.class, Notification.BigPictureStyle.class, + Notification.MessagingStyle.class, Notification.CallStyle.class, + Notification.InboxStyle.class, Notification.MediaStyle.class, + Notification.DecoratedCustomViewStyle.class, + Notification.DecoratedMediaCustomViewStyle.class + ); + + for (Class<? extends Notification.Style> platformStyleClass : platformStyleClasses) { + assertThat(Notification.getNotificationStyleClass(platformStyleClass.getName())) + .isEqualTo(platformStyleClass); + } + } + + @Test + public void getNotificationStyleClass_forNotPlatformClassName_returnsNull() { + assertThat(Notification.getNotificationStyleClass(NotAPlatformStyle.class.getName())) + .isNull(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_richOngoingEnabled_platformClass() { + assertThat( + Notification.getNotificationStyleClass(Notification.ProgressStyle.class.getName())) + .isEqualTo(Notification.ProgressStyle.class); + } + + @Test + @DisableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_richOngoingDisabled_notPlatformClass() { + assertThat( + Notification.getNotificationStyleClass(Notification.ProgressStyle.class.getName())) + .isNull(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onSegmentChange_visiblyDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .addProgressSegment(new Notification.ProgressStyle.Segment(100)) + .addProgressSegment(new Notification.ProgressStyle.Segment(50) + .setColor(Color.RED))); + + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .addProgressSegment(new Notification.ProgressStyle.Segment(100)) + .addProgressSegment(new Notification.ProgressStyle.Segment(50) + .setColor(Color.BLUE))); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void indeterminateProgressStyle_onSegmentChange_visiblyNotDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true) + .addProgressSegment(new Notification.ProgressStyle.Segment(100)) + .addProgressSegment(new Notification.ProgressStyle.Segment(50) + .setColor(Color.RED))); + + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true) + .addProgressSegment(new Notification.ProgressStyle.Segment(100)) + .addProgressSegment(new Notification.ProgressStyle.Segment(50) + .setColor(Color.BLUE))); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onStartIconChange_visiblyDifferent() { + final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource( + mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96)); + + final Icon icon2 = Icon.createWithBitmap( + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressStartIcon(icon1)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressStartIcon(icon2)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onEndIconChange_visiblyDifferent() { + final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource( + mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96)); + + final Icon icon2 = Icon.createWithBitmap( + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressEndIcon(icon1)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressEndIcon(icon2)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onProgressChange_visiblyDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgress(20)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgress(21)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void indeterminateProgressStyle_onProgressChange_visiblyNotDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .setProgressIndeterminate(true).setProgress(20)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .setProgressIndeterminate(true).setProgress(21)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onIsStyledByProgressChange_visiblyDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setStyledByProgress(true)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setStyledByProgress(false)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void indeterminateProgressStyle_onIsStyledByProgressChange_visiblyNotDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .setProgressIndeterminate(true).setStyledByProgress(true)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .setProgressIndeterminate(true).setStyledByProgress(false)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onProgressStepChange_visiblyDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .addProgressStep(new Notification.ProgressStyle.Step(10))); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .addProgressStep(new Notification.ProgressStyle.Step(12))); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void indeterminateProgressStyle_onProgressStepChange_visiblyNotDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true) + .addProgressStep(new Notification.ProgressStyle.Step(10))); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true) + .addProgressStep(new Notification.ProgressStyle.Step(12))); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onTrackerIconChange_visiblyDifferent() { + final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource( + mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96)); + + final Icon icon2 = Icon.createWithBitmap( + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressTrackerIcon(icon1)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressTrackerIcon(icon2)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void indeterminateProgressStyle_onTrackerIconChange_visiblyNotDifferent() { + final Icon icon1 = Icon.createWithBitmap(BitmapFactory.decodeResource( + mContext.getResources(), com.android.frameworks.coretests.R.drawable.test128x96)); + + final Icon icon2 = Icon.createWithBitmap( + Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)); + + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true) + .setProgressTrackerIcon(icon1)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle() + .setProgressIndeterminate(true).setProgressTrackerIcon(icon2)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_onIndeterminateChange_visiblyDifferent() { + final Notification.Builder nProgress1 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(true)); + final Notification.Builder nProgress2 = new Notification.Builder(mContext, "test") + .setStyle(new Notification.ProgressStyle().setProgressIndeterminate(false)); + + assertThat(Notification.areStyledNotificationsVisiblyDifferent(nProgress1, nProgress2)) + .isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_getProgressMax_default100() { + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + assertThat(progressStyle.getProgressMax()).isEqualTo(100); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_getProgressMax_nooSegments_returnsDefault() { + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + progressStyle.setProgressSegments(Collections.emptyList()); + assertThat(progressStyle.getProgressMax()).isEqualTo(100); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_getProgressMax_returnsSumOfSegmentLength() { + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + progressStyle + .addProgressSegment(new Notification.ProgressStyle.Segment(10)) + .addProgressSegment(new Notification.ProgressStyle.Segment(20)); + + assertThat(progressStyle.getProgressMax()).isEqualTo(30); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_getProgressMax_onSegmentOverflow_returnsDefault() { + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + progressStyle + .addProgressSegment(new Notification.ProgressStyle.Segment(Integer.MAX_VALUE)) + .addProgressSegment(new Notification.ProgressStyle.Segment(10)); + + assertThat(progressStyle.getProgressMax()).isEqualTo(100); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_indeterminate_defaultValueFalse() { + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + + assertThat(progressStyle1.isProgressIndeterminate()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_styledByProgress_defaultValueTrue() { + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + + assertThat(progressStyle1.isStyledByProgress()).isTrue(); + } private void assertValid(Notification.Colors c) { // Assert that all colors are populated assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID); @@ -2214,4 +2508,11 @@ public class NotificationTest { new Intent(action).setPackage(mContext.getPackageName()), PendingIntent.FLAG_MUTABLE); } + + private static class NotAPlatformStyle extends Notification.Style { + @Override + public boolean areNotificationsVisiblyDifferent(Notification.Style other) { + return false; + } + } } |