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

Commit 5bb5a9cc authored by Xiaowen Lei's avatar Xiaowen Lei
Browse files

Implement fallbacks for when the drawable doesn't have enough width.

Also:
  - For better clarity, renamed `processAndConvertToViewParts` to
    `processModelAndConvertToViewParts`, and
    `processAndConvertToDrawableParts` to
    `processPartsAndConvertToDrawableParts`.
  - Fixed pre-existing failures in NotificationProgressModelTest. And a
    few tests were failing the verifs on process or indeterminateColor.
  - Cleaned up / reassigned a couple of TODOs.

Test notification config, on Felix:
  - 10 segments, each of length 10
    - Case 1: segments of multiple colors
    - Case 2: segments of same color
  - 4 points, at 0, 1, 55, 100

Flag: android.app.api_rich_ongoing
Fix: 372908709
Bug: 390196782
Test: Post ProgressStyle notifs via test app
Test: atest NotificationProgressBarTest
Test: atest NotificationProgressModelTest
Change-Id: I57333e2c14058298b5caf707442176883162b345
parent 21ea1530
Loading
Loading
Loading
Loading
+25 −2
Original line number Diff line number Diff line
@@ -11854,7 +11854,7 @@ public class Notification implements Parcelable
                    // If segment limit is exceeded. All segments will be replaced
                    // with a single segment
                    boolean allSameColor = true;
                    int firstSegmentColor = segments.get(0).getColor();
                    int firstSegmentColor = segments.getFirst().getColor();
                    for (int i = 1; i < segments.size(); i++) {
                        if (segments.get(i).getColor() != firstSegmentColor) {
@@ -11887,8 +11887,31 @@ public class Notification implements Parcelable
                    }
                }
                // If the segments and points can't all fit inside the progress drawable, the
                // view will replace all segments with a single segment.
                final int segmentsFallbackColor;
                if (segments.size() <= 1) {
                    segmentsFallbackColor = NotificationProgressModel.INVALID_COLOR;
                } else {
                    boolean allSameColor = true;
                    int firstSegmentColor = segments.getFirst().getColor();
                    for (int i = 1; i < segments.size(); i++) {
                        if (segments.get(i).getColor() != firstSegmentColor) {
                            allSameColor = false;
                            break;
                        }
                    }
                    // If the segments are of the same color, the view can just use that color.
                    // In that case there is no need to send the fallback color.
                    segmentsFallbackColor = allSameColor ? NotificationProgressModel.INVALID_COLOR
                            : sanitizeProgressColor(Notification.COLOR_DEFAULT, backgroundColor,
                                    defaultProgressColor);
                }
                model = new NotificationProgressModel(segments, points,
                        Math.clamp(mProgress, 0, totalLength), mIsStyledByProgress);
                        Math.clamp(mProgress, 0, totalLength), mIsStyledByProgress,
                        segmentsFallbackColor);
            }
            return model;
        }
+146 −34
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ import com.android.internal.widget.NotificationProgressDrawable.DrawablePoint;
import com.android.internal.widget.NotificationProgressDrawable.DrawableSegment;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -161,7 +162,7 @@ public final class NotificationProgressBar extends ProgressBar implements
            final int progress = mProgressModel.getProgress();
            final int progressMax = mProgressModel.getProgressMax();

            mParts = processAndConvertToViewParts(mProgressModel.getSegments(),
            mParts = processModelAndConvertToViewParts(mProgressModel.getSegments(),
                    mProgressModel.getPoints(),
                    progress,
                    progressMax);
@@ -439,23 +440,107 @@ public final class NotificationProgressBar extends ProgressBar implements
            return;
        }

        mProgressDrawableParts = processAndConvertToDrawableParts(
        final float segSegGap = mNotificationProgressDrawable.getSegSegGap();
        final float segPointGap = mNotificationProgressDrawable.getSegPointGap();
        final float pointRadius = mNotificationProgressDrawable.getPointRadius();
        mProgressDrawableParts = processPartsAndConvertToDrawableParts(
                mParts,
                width,
                mNotificationProgressDrawable.getSegSegGap(),
                mNotificationProgressDrawable.getSegPointGap(),
                mNotificationProgressDrawable.getPointRadius(),
                segSegGap,
                segPointGap,
                pointRadius,
                mHasTrackerIcon
        );
        Pair<List<DrawablePart>, Float> p = maybeStretchAndRescaleSegments(

        final float segmentMinWidth = mNotificationProgressDrawable.getSegmentMinWidth();
        final float progressFraction = getProgressFraction();
        final boolean isStyledByProgress = mProgressModel.isStyledByProgress();
        final float progressGap =
                mHasTrackerIcon ? 0F : mNotificationProgressDrawable.getSegSegGap();
        Pair<List<DrawablePart>, Float> p = null;
        try {
            p = maybeStretchAndRescaleSegments(
                    mParts,
                    mProgressDrawableParts,
                mNotificationProgressDrawable.getSegmentMinWidth(),
                mNotificationProgressDrawable.getPointRadius(),
                getProgressFraction(),
                    segmentMinWidth,
                    pointRadius,
                    progressFraction,
                    width,
                mProgressModel.isStyledByProgress(),
                mHasTrackerIcon ? 0F : mNotificationProgressDrawable.getSegSegGap());
                    isStyledByProgress,
                    progressGap
            );
        } catch (NotEnoughWidthToFitAllPartsException ex) {
            Log.w(TAG, "Failed to stretch and rescale segments", ex);
        }

        List<ProgressStyle.Segment> fallbackSegments = null;
        if (p == null && mProgressModel.getSegments().size() > 1) {
            Log.w(TAG, "Falling back to single segment");
            try {
                fallbackSegments = List.of(new ProgressStyle.Segment(getMax()).setColor(
                        mProgressModel.getSegmentsFallbackColor()
                                == NotificationProgressModel.INVALID_COLOR
                                ? mProgressModel.getSegments().getFirst().getColor()
                                : mProgressModel.getSegmentsFallbackColor()));
                p = processModelAndConvertToFinalDrawableParts(
                        fallbackSegments,
                        mProgressModel.getPoints(),
                        mProgressModel.getProgress(),
                        getMax(),
                        width,
                        segSegGap,
                        segPointGap,
                        pointRadius,
                        mHasTrackerIcon,
                        segmentMinWidth,
                        isStyledByProgress
                );
            } catch (NotEnoughWidthToFitAllPartsException ex) {
                Log.w(TAG, "Failed to stretch and rescale segments with single segment fallback",
                        ex);
            }
        }

        if (p == null && !mProgressModel.getPoints().isEmpty()) {
            Log.w(TAG, "Falling back to single segment and no points");
            if (fallbackSegments == null) {
                fallbackSegments = List.of(new ProgressStyle.Segment(getMax()).setColor(
                        mProgressModel.getSegmentsFallbackColor()
                                == NotificationProgressModel.INVALID_COLOR
                                ? mProgressModel.getSegments().getFirst().getColor()
                                : mProgressModel.getSegmentsFallbackColor()));
            }
            try {
                p = processModelAndConvertToFinalDrawableParts(
                        fallbackSegments,
                        Collections.emptyList(),
                        mProgressModel.getProgress(),
                        getMax(),
                        width,
                        segSegGap,
                        segPointGap,
                        pointRadius,
                        mHasTrackerIcon,
                        segmentMinWidth,
                        isStyledByProgress
                );
            } catch (NotEnoughWidthToFitAllPartsException ex) {
                Log.w(TAG,
                        "Failed to stretch and rescale segments with single segments and no points",
                        ex);
            }
        }

        if (p == null) {
            Log.w(TAG, "Falling back to no stretching and rescaling");
            p = maybeSplitDrawableSegmentsByProgress(
                    mParts,
                    mProgressDrawableParts,
                    progressFraction,
                    width,
                    isStyledByProgress,
                    progressGap);
        }

        if (DEBUG) {
            Log.d(TAG, "Updating NotificationProgressDrawable parts");
@@ -502,7 +587,11 @@ public final class NotificationProgressBar extends ProgressBar implements
        int min = getMin();
        int max = getMax();
        int range = max - min;
        return range > 0 ? (getProgress() - min) / (float) range : 0;
        return getProgressFraction(range, (getProgress() - min));
    }

    private static float getProgressFraction(int progressMax, int progress) {
        return progressMax > 0 ? progress / (float) progressMax : 0;
    }

    /**
@@ -636,7 +725,7 @@ public final class NotificationProgressBar extends ProgressBar implements
     * Processes the ProgressStyle data and convert to a list of {@code Part}.
     */
    @VisibleForTesting
    public static List<Part> processAndConvertToViewParts(
    public static List<Part> processModelAndConvertToViewParts(
            List<ProgressStyle.Segment> segments,
            List<ProgressStyle.Point> points,
            int progress,
@@ -796,7 +885,7 @@ public final class NotificationProgressBar extends ProgressBar implements
     * Processes the list of {@code Part} and convert to a list of {@code DrawablePart}.
     */
    @VisibleForTesting
    public static List<DrawablePart> processAndConvertToDrawableParts(
    public static List<DrawablePart> processPartsAndConvertToDrawableParts(
            List<Part> parts,
            float totalWidth,
            float segSegGap,
@@ -823,7 +912,7 @@ public final class NotificationProgressBar extends ProgressBar implements
                // Retract the end position to account for the padding and a point immediately
                // after.
                final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap,
                        segSegGap, iPart == nParts - 2, totalWidth, hasTrackerIcon);
                        segSegGap, iPart == nParts - 2, hasTrackerIcon);
                final float end = x + segWidth - endOffset;

                drawableParts.add(new DrawableSegment(start, end, segment.mColor, segment.mFaded));
@@ -864,7 +953,7 @@ public final class NotificationProgressBar extends ProgressBar implements
    }

    private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius,
            float segPointGap, float segSegGap, boolean isSecondToLastPart, float totalWidth,
            float segPointGap, float segSegGap, boolean isSecondToLastPart,
            boolean hasTrackerIcon) {
        if (nextPart == null) return 0F;
        if (nextPart instanceof Segment nextSeg) {
@@ -894,7 +983,7 @@ public final class NotificationProgressBar extends ProgressBar implements
            float totalWidth,
            boolean isStyledByProgress,
            float progressGap
    ) {
    ) throws NotEnoughWidthToFitAllPartsException {
        final List<DrawableSegment> drawableSegments = drawableParts
                .stream()
                .filter(DrawableSegment.class::isInstance)
@@ -920,16 +1009,8 @@ public final class NotificationProgressBar extends ProgressBar implements
        }

        if (totalExcessWidth < 0) {
            // TODO: b/372908709 - throw an error so that the caller can catch and go to fallback
            //  option. (instead of return.)
            Log.w(TAG, "Not enough width to satisfy the minimum width for segments.");
            return maybeSplitDrawableSegmentsByProgress(
                    parts,
                    drawableParts,
                    progressFraction,
                    totalWidth,
                    isStyledByProgress,
                    progressGap);
            throw new NotEnoughWidthToFitAllPartsException(
                    "Not enough width to satisfy the minimum width for segments.");
        }

        final int nParts = drawableParts.size();
@@ -1003,8 +1084,7 @@ public final class NotificationProgressBar extends ProgressBar implements
        final int nParts = parts.size();
        for (int iPart = 0; iPart < nParts; iPart++) {
            final Part part = parts.get(iPart);
            if (!(part instanceof Segment)) continue;
            final Segment segment = (Segment) part;
            if (!(part instanceof Segment segment)) continue;
            if (startFraction == progressFraction) {
                iPartFirstSegmentToStyle = iPart;
                rescaledProgressX = segment.mStart;
@@ -1065,12 +1145,38 @@ public final class NotificationProgressBar extends ProgressBar implements
        return new Pair<>(splitDrawableParts, rescaledProgressX);
    }

    /**
     * Processes the ProgressStyle data and convert to a pair of:
     * - list of processed {@code DrawablePart}.
     * - location of progress on the stretched and rescaled progress bar.
     */
    @VisibleForTesting
    public static Pair<List<DrawablePart>, Float> processModelAndConvertToFinalDrawableParts(
            List<ProgressStyle.Segment> segments,
            List<ProgressStyle.Point> points,
            int progress,
            int progressMax,
            float totalWidth,
            float segSegGap,
            float segPointGap,
            float pointRadius,
            boolean hasTrackerIcon,
            float segmentMinWidth,
            boolean isStyledByProgress
    ) throws NotEnoughWidthToFitAllPartsException {
        List<Part> parts = processModelAndConvertToViewParts(segments, points, progress,
                progressMax);
        List<DrawablePart> drawableParts = processPartsAndConvertToDrawableParts(parts, totalWidth,
                segSegGap, segPointGap, pointRadius, hasTrackerIcon);
        return maybeStretchAndRescaleSegments(parts, drawableParts, segmentMinWidth, pointRadius,
                getProgressFraction(progressMax, progress), totalWidth, isStyledByProgress,
                hasTrackerIcon ? 0F : segSegGap);
    }

    /**
     * A part of the progress bar, which is either a {@link Segment} with non-zero length, or a
     * {@link Point} with zero length.
     */
    // TODO: b/372908709 - maybe this should be made private? Only test the final
    //  NotificationDrawable.Parts.
    public interface Part {
    }

@@ -1176,4 +1282,10 @@ public final class NotificationProgressBar extends ProgressBar implements
            return Objects.hash(mColor);
        }
    }

    public static class NotEnoughWidthToFitAllPartsException extends Exception {
        public NotEnoughWidthToFitAllPartsException(String message) {
            super(message);
        }
    }
}
+30 −10
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@

package com.android.internal.widget;


import android.annotation.ColorInt;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
@@ -45,16 +44,20 @@ import java.util.Objects;
 */
@FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
public final class NotificationProgressModel {
    private static final int INVALID_INDETERMINATE_COLOR = Color.TRANSPARENT;
    public static final int INVALID_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_SEGMENTS_FALLBACK_COLOR = "segmentsFallColor";
    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 mSegmentsFallbackColor;

    @ColorInt
    private final int mIndeterminateColor;

@@ -62,7 +65,8 @@ public final class NotificationProgressModel {
            @NonNull List<Segment> segments,
            @NonNull List<Point> points,
            int progress,
            boolean isStyledByProgress
            boolean isStyledByProgress,
            @ColorInt int segmentsFallbackColor
    ) {
        Preconditions.checkArgument(progress >= 0);
        Preconditions.checkArgument(!segments.isEmpty());
@@ -70,17 +74,19 @@ public final class NotificationProgressModel {
        mPoints = points;
        mProgress = progress;
        mIsStyledByProgress = isStyledByProgress;
        mIndeterminateColor = INVALID_INDETERMINATE_COLOR;
        mSegmentsFallbackColor = segmentsFallbackColor;
        mIndeterminateColor = INVALID_COLOR;
    }

    public NotificationProgressModel(
            @ColorInt int indeterminateColor
    ) {
        Preconditions.checkArgument(indeterminateColor != INVALID_INDETERMINATE_COLOR);
        Preconditions.checkArgument(indeterminateColor != INVALID_COLOR);
        mSegments = Collections.emptyList();
        mPoints = Collections.emptyList();
        mProgress = 0;
        mIsStyledByProgress = false;
        mSegmentsFallbackColor = INVALID_COLOR;
        mIndeterminateColor = indeterminateColor;
    }

@@ -104,13 +110,18 @@ public final class NotificationProgressModel {
        return mIsStyledByProgress;
    }

    @ColorInt
    public int getSegmentsFallbackColor() {
        return mSegmentsFallbackColor;
    }

    @ColorInt
    public int getIndeterminateColor() {
        return mIndeterminateColor;
    }

    public boolean isIndeterminate() {
        return mIndeterminateColor != INVALID_INDETERMINATE_COLOR;
        return mIndeterminateColor != INVALID_COLOR;
    }

    /**
@@ -119,7 +130,7 @@ public final class NotificationProgressModel {
    @NonNull
    public Bundle toBundle() {
        final Bundle bundle = new Bundle();
        if (mIndeterminateColor != INVALID_INDETERMINATE_COLOR) {
        if (mIndeterminateColor != INVALID_COLOR) {
            bundle.putInt(KEY_INDETERMINATE_COLOR, mIndeterminateColor);
        } else {
            bundle.putParcelableList(KEY_SEGMENTS,
@@ -128,6 +139,9 @@ public final class NotificationProgressModel {
                    Notification.ProgressStyle.getProgressPointsAsBundleList(mPoints));
            bundle.putInt(KEY_PROGRESS, mProgress);
            bundle.putBoolean(KEY_IS_STYLED_BY_PROGRESS, mIsStyledByProgress);
            if (mSegmentsFallbackColor != INVALID_COLOR) {
                bundle.putInt(KEY_SEGMENTS_FALLBACK_COLOR, mSegmentsFallbackColor);
            }
        }
        return bundle;
    }
@@ -138,8 +152,8 @@ public final class NotificationProgressModel {
    @NonNull
    public static NotificationProgressModel fromBundle(@NonNull Bundle bundle) {
        final int indeterminateColor = bundle.getInt(KEY_INDETERMINATE_COLOR,
                INVALID_INDETERMINATE_COLOR);
        if (indeterminateColor != INVALID_INDETERMINATE_COLOR) {
                INVALID_COLOR);
        if (indeterminateColor != INVALID_COLOR) {
            return new NotificationProgressModel(indeterminateColor);
        } else {
            final List<Segment> segments =
@@ -150,7 +164,10 @@ public final class NotificationProgressModel {
                            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);
            final int segmentsFallbackColor = bundle.getInt(KEY_SEGMENTS_FALLBACK_COLOR,
                    INVALID_COLOR);
            return new NotificationProgressModel(segments, points, progress, isStyledByProgress,
                    segmentsFallbackColor);
        }
    }

@@ -161,6 +178,7 @@ public final class NotificationProgressModel {
                + ", mPoints=" + mPoints
                + ", mProgress=" + mProgress
                + ", mIsStyledByProgress=" + mIsStyledByProgress
                + ", mSegmentsFallbackColor=" + mSegmentsFallbackColor
                + ", mIndeterminateColor=" + mIndeterminateColor + "}";
    }

@@ -171,6 +189,7 @@ public final class NotificationProgressModel {
        final NotificationProgressModel that = (NotificationProgressModel) o;
        return mProgress == that.mProgress
                && mIsStyledByProgress == that.mIsStyledByProgress
                && mSegmentsFallbackColor == that.mSegmentsFallbackColor
                && mIndeterminateColor == that.mIndeterminateColor
                && Objects.equals(mSegments, that.mSegments)
                && Objects.equals(mPoints, that.mPoints);
@@ -182,6 +201,7 @@ public final class NotificationProgressModel {
                mPoints,
                mProgress,
                mIsStyledByProgress,
                mSegmentsFallbackColor,
                mIndeterminateColor);
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -7585,7 +7585,7 @@
    <!-- Used to config the segments of a NotificationProgressDrawable. -->
    <!-- @hide internal use only -->
    <declare-styleable name="NotificationProgressDrawableSegments">
        <!-- TODO: b/372908709 - maybe move this to NotificationProgressBar, because that's the only
        <!-- TODO: b/390196782 - maybe move this to NotificationProgressBar, because that's the only
         place this is used actually. Same for NotificationProgressDrawable.segSegGap/segPointGap
         above. -->
        <!-- Minimum required drawing width. The drawing width refers to the width after
+312 −68

File changed.

Preview size limit exceeded, changes collapsed.

Loading