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

Commit d6b10e74 authored by Xiaowen Lei's avatar Xiaowen Lei
Browse files

Add tracker to NotificationProgressBar.

The available length for the tracker to move is:
   available = trackWidth (minus padding) - trackerWidth.
The tracker moves evenly by scale, from scale = 0 to 1. See
`setTrackerPos` for the implementation.

Flag: android.app.api_rich_ongoing
Fix: 367805202
Fix: 372908099
Bug: 372909308
Test: post a ProgressStyle Notification
Test: patch ag/30006048 and run ProgressStyleNotificationScreenshotTest
Test: atest NotificationProgressBarTest
Change-Id: Iae744bea726db8f9f9bc029b17eef5933e909d44
parent 7af199f8
Loading
Loading
Loading
Loading
+277 −12
Original line number Diff line number Diff line
@@ -21,6 +21,9 @@ import android.annotation.Nullable;
import android.app.Notification.ProgressStyle;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.graphics.drawable.LayerDrawable;
@@ -52,7 +55,7 @@ import java.util.TreeSet;
 * represent Notification ProgressStyle progress, such as for ridesharing and navigation.
 */
@RemoteViews.RemoteView
public class NotificationProgressBar extends ProgressBar {
public final class NotificationProgressBar extends ProgressBar {
    private static final String TAG = "NotificationProgressBar";

    private NotificationProgressModel mProgressModel;
@@ -61,7 +64,7 @@ public class NotificationProgressBar extends ProgressBar {
    private List<Part> mProgressDrawableParts = null;

    @Nullable
    private Drawable mProgressTrackerDrawable = null;
    private Drawable mTracker = null;

    public NotificationProgressBar(Context context) {
        this(context, null);
@@ -78,28 +81,44 @@ public class NotificationProgressBar extends ProgressBar {
    public NotificationProgressBar(Context context, AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        final TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.NotificationProgressBar, defStyleAttr, defStyleRes);
        saveAttributeDataForStyleable(context, R.styleable.NotificationProgressBar, attrs, a,
                defStyleAttr,
                defStyleRes);

        // Supports setting the tracker in xml, but ProgressStyle notifications set/override it
        // via {@code setProgressTrackerIcon}.
        final Drawable tracker = a.getDrawable(R.styleable.NotificationProgressBar_tracker);
        setTracker(tracker);
    }

    /**
     * Setter for the notification progress model.
     *
     * @see NotificationProgressModel#fromBundle
     * @see #setProgressModelAsync
     */
    @RemotableViewMethod(asyncImpl = "setProgressModelAsync")
    @RemotableViewMethod
    public void setProgressModel(@Nullable Bundle bundle) {
        Preconditions.checkArgument(bundle != null,
                "Bundle shouldn't be null");

        mProgressModel = NotificationProgressModel.fromBundle(bundle);
        final boolean isIndeterminate = mProgressModel.isIndeterminate();
        setIndeterminate(isIndeterminate);

        if (mProgressModel.isIndeterminate()) {
        if (isIndeterminate) {
            final int indeterminateColor = mProgressModel.getIndeterminateColor();
            setIndeterminateTintList(ColorStateList.valueOf(indeterminateColor));
        } else {
            final int progress = mProgressModel.getProgress();
            final int progressMax = mProgressModel.getProgressMax();
            mProgressDrawableParts = processAndConvertToDrawableParts(mProgressModel.getSegments(),
                    mProgressModel.getPoints(),
                    mProgressModel.getProgress(), mProgressModel.isStyledByProgress());
                    progress,
                    progressMax,
                    mProgressModel.isStyledByProgress());

            try {
                final NotificationProgressDrawable drawable = getNotificationProgressDrawable();
@@ -107,6 +126,9 @@ public class NotificationProgressBar extends ProgressBar {
            } catch (IllegalStateException ex) {
                Log.e(TAG, "Can't set parts because can't get NotificationProgressDrawable", ex);
            }

            setMax(progressMax);
            setProgress(progress);
        }
    }

@@ -137,6 +159,13 @@ public class NotificationProgressBar extends ProgressBar {
     */
    @RemotableViewMethod(asyncImpl = "setProgressTrackerIconAsync")
    public void setProgressTrackerIcon(@Nullable Icon icon) {
        final Drawable progressTrackerDrawable;
        if (icon != null) {
            progressTrackerDrawable = icon.loadDrawable(getContext());
        } else {
            progressTrackerDrawable = null;
        }
        setTracker(progressTrackerDrawable);
    }

    /**
@@ -150,12 +179,245 @@ public class NotificationProgressBar extends ProgressBar {
            progressTrackerDrawable = null;
        }
        return () -> {
            setProgressTrackerDrawable(progressTrackerDrawable);
            setTracker(progressTrackerDrawable);
        };
    }

    private void setProgressTrackerDrawable(@Nullable  Drawable drawable) {
        mProgressTrackerDrawable = drawable;
    private void setTracker(@Nullable Drawable tracker) {
        if (isIndeterminate() && tracker != null) {
            return;
        }

        final boolean needUpdate = mTracker != null && tracker != mTracker;
        if (needUpdate) {
            mTracker.setCallback(null);
        }

        if (tracker != null) {
            tracker.setCallback(this);
            if (canResolveLayoutDirection()) {
                tracker.setLayoutDirection(getLayoutDirection());
            }

            // If we're updating get the new states
            if (needUpdate && (tracker.getIntrinsicWidth() != mTracker.getIntrinsicWidth()
                    || tracker.getIntrinsicHeight() != mTracker.getIntrinsicHeight())) {
                requestLayout();
            }
        }

        mTracker = tracker;
        invalidate();

        if (needUpdate) {
            updateTrackerAndBarPos(getWidth(), getHeight());
            if (tracker != null && tracker.isStateful()) {
                // Note that if the states are different this won't work.
                // For now, let's consider that an app bug.
                tracker.setState(getDrawableState());
            }
        }
    }

    @Override
    @RemotableViewMethod
    public synchronized void setIndeterminate(boolean indeterminate) {
        super.setIndeterminate(indeterminate);

        if (isIndeterminate()) {
            setTracker(null);
        }
    }

    @Override
    protected boolean verifyDrawable(@NonNull Drawable who) {
        return who == mTracker || super.verifyDrawable(who);
    }

    @Override
    public void jumpDrawablesToCurrentState() {
        super.jumpDrawablesToCurrentState();

        if (mTracker != null) {
            mTracker.jumpToCurrentState();
        }
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();

        final Drawable tracker = mTracker;
        if (tracker != null && tracker.isStateful()
                && tracker.setState(getDrawableState())) {
            invalidateDrawable(tracker);
        }
    }

    @Override
    public void drawableHotspotChanged(float x, float y) {
        super.drawableHotspotChanged(x, y);

        if (mTracker != null) {
            mTracker.setHotspot(x, y);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        updateTrackerAndBarPos(w, h);
    }

    private void updateTrackerAndBarPos(int w, int h) {
        final int paddedHeight = h - mPaddingTop - mPaddingBottom;
        final Drawable bar = getCurrentDrawable();
        final Drawable tracker = mTracker;

        // The max height does not incorporate padding, whereas the height
        // parameter does.
        final int barHeight = Math.min(getMaxHeight(), paddedHeight);
        final int trackerHeight = tracker == null ? 0 : tracker.getIntrinsicHeight();

        // Apply offset to whichever item is taller.
        final int barOffsetY;
        final int trackerOffsetY;
        if (trackerHeight > barHeight) {
            final int offsetHeight = (paddedHeight - trackerHeight) / 2;
            barOffsetY = offsetHeight + (trackerHeight - barHeight) / 2;
            trackerOffsetY = offsetHeight;
        } else {
            final int offsetHeight = (paddedHeight - barHeight) / 2;
            barOffsetY = offsetHeight;
            trackerOffsetY = offsetHeight + (barHeight - trackerHeight) / 2;
        }

        if (bar != null) {
            final int barWidth = w - mPaddingRight - mPaddingLeft;
            bar.setBounds(0, barOffsetY, barWidth, barOffsetY + barHeight);
        }

        if (tracker != null) {
            setTrackerPos(w, tracker, getScale(), trackerOffsetY);
        }
    }

    private float getScale() {
        int min = getMin();
        int max = getMax();
        int range = max - min;
        return range > 0 ? (getProgress() - min) / (float) range : 0;
    }

    /**
     * Updates the tracker drawable bounds.
     *
     * @param w Width of the view, including padding
     * @param tracker Drawable used for the tracker
     * @param scale Current progress between 0 and 1
     * @param offsetY Vertical offset for centering. If set to
     *            {@link Integer#MIN_VALUE}, the current offset will be used.
     */
    private void setTrackerPos(int w, Drawable tracker, float scale, int offsetY) {
        int available = w - mPaddingLeft - mPaddingRight;
        final int trackerWidth = tracker.getIntrinsicWidth();
        final int trackerHeight = tracker.getIntrinsicHeight();
        available -= trackerWidth;

        final int trackerPos = (int) (scale * available + 0.5f);

        final int top, bottom;
        if (offsetY == Integer.MIN_VALUE) {
            final Rect oldBounds = tracker.getBounds();
            top = oldBounds.top;
            bottom = oldBounds.bottom;
        } else {
            top = offsetY;
            bottom = offsetY + trackerHeight;
        }

        final int left = (isLayoutRtl() && getMirrorForRtl()) ? available - trackerPos : trackerPos;
        final int right = left + trackerWidth;

        final Drawable background = getBackground();
        if (background != null) {
            final int bkgOffsetX = mPaddingLeft;
            final int bkgOffsetY = mPaddingTop;
            background.setHotspotBounds(left + bkgOffsetX, top + bkgOffsetY,
                    right + bkgOffsetX, bottom + bkgOffsetY);
        }

        // Canvas will be translated, so 0,0 is where we start drawing
        tracker.setBounds(left, top, right, bottom);
    }

    @Override
    public void onResolveDrawables(int layoutDirection) {
        super.onResolveDrawables(layoutDirection);

        if (mTracker != null) {
            mTracker.setLayoutDirection(layoutDirection);
        }
    }

    @Override
    protected synchronized void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawTracker(canvas);
    }

    /**
     * Draw the tracker.
     */
    private void drawTracker(Canvas canvas) {
        if (mTracker != null) {
            final int saveCount = canvas.save();
            // Translate the padding. For the x, we need to allow the tracker to
            // draw in its extra space
            canvas.translate(mPaddingLeft, mPaddingTop);
            mTracker.draw(canvas);
            canvas.restoreToCount(saveCount);
        }
    }

    @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Drawable d = getCurrentDrawable();

        int trackerHeight = mTracker == null ? 0 : mTracker.getIntrinsicHeight();
        int dw = 0;
        int dh = 0;
        if (d != null) {
            dw = Math.max(getMinWidth(), Math.min(getMaxWidth(), d.getIntrinsicWidth()));
            dh = Math.max(getMinHeight(), Math.min(getMaxHeight(), d.getIntrinsicHeight()));
            dh = Math.max(trackerHeight, dh);
        }
        dw += mPaddingLeft + mPaddingRight;
        dh += mPaddingTop + mPaddingBottom;

        setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0),
                resolveSizeAndState(dh, heightMeasureSpec, 0));
    }

    @Override
    public CharSequence getAccessibilityClassName() {
        return NotificationProgressBar.class.getName();
    }

    @Override
    public void onRtlPropertiesChanged(int layoutDirection) {
        super.onRtlPropertiesChanged(layoutDirection);

        final Drawable tracker = mTracker;
        if (tracker != null) {
            setTrackerPos(getWidth(), tracker, getScale(), Integer.MIN_VALUE);

            // Since we draw translated, the drawable's bounds that it signals
            // for invalidation won't be the actual bounds we want invalidated,
            // so just invalidate this whole view.
            invalidate();
        }
    }

    /**
@@ -167,12 +429,18 @@ public class NotificationProgressBar extends ProgressBar {
            List<ProgressStyle.Segment> segments,
            List<ProgressStyle.Point> points,
            int progress,
            int progressMax,
            boolean isStyledByProgress
    ) {
        if (segments.isEmpty()) {
            throw new IllegalArgumentException("List of segments shouldn't be empty");
        }

        final int totalLength = segments.stream().mapToInt(ProgressStyle.Segment::getLength).sum();
        if (progressMax != totalLength) {
            throw new IllegalArgumentException("Invalid progressMax : " + progressMax);
        }

        for (ProgressStyle.Segment segment : segments) {
            final int length = segment.getLength();
            if (length <= 0) {
@@ -180,8 +448,6 @@ public class NotificationProgressBar extends ProgressBar {
            }
        }

        final int progressMax = segments.stream().mapToInt(ProgressStyle.Segment::getLength).sum();

        if (progress < 0 || progress > progressMax) {
            throw new IllegalArgumentException("Invalid progress : " + progress);
        }
@@ -208,7 +474,6 @@ public class NotificationProgressBar extends ProgressBar {
                isStyledByProgress);
    }


    // Any segment with a point on it gets split by the point.
    // If isStyledByProgress is true, also split the segment with the progress value in its range.
    private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPointsAndProgress(
+4 −0
Original line number Diff line number Diff line
@@ -96,6 +96,10 @@ public final class NotificationProgressModel {
        return mProgress;
    }

    public int getProgressMax() {
        return mSegments.stream().mapToInt(Notification.ProgressStyle.Segment::getLength).sum();
    }

    public boolean isStyledByProgress() {
        return mIsStyledByProgress;
    }
+6 −0
Original line number Diff line number Diff line
@@ -5731,6 +5731,12 @@
        </attr>
    </declare-styleable>
    <!-- @hide internal use only -->
    <declare-styleable name="NotificationProgressBar">
        <!-- Draws the tracker on a NotificationProgressBar. -->
        <attr name="tracker" format="reference" />
    </declare-styleable>
    <declare-styleable name="StackView">
        <!-- Color of the res-out outline. -->
        <attr name="resOutColor" format="color" />
+47 −7
Original line number Diff line number Diff line
@@ -41,9 +41,26 @@ public class NotificationProgressBarTest {
        List<ProgressStyle.Segment> segments = new ArrayList<>();
        List<ProgressStyle.Point> points = new ArrayList<>();
        int progress = 50;
        int progressMax = 100;
        boolean isStyledByProgress = true;

        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
                progressMax,
                isStyledByProgress);
    }

    @Test(expected = IllegalArgumentException.class)
    public void processAndConvertToDrawableParts_segmentsLengthNotMatchingProgressMax() {
        List<ProgressStyle.Segment> segments = new ArrayList<>();
        segments.add(new ProgressStyle.Segment(50));
        segments.add(new ProgressStyle.Segment(100));
        List<ProgressStyle.Point> points = new ArrayList<>();
        int progress = 50;
        int progressMax = 100;
        boolean isStyledByProgress = true;

        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
                progressMax,
                isStyledByProgress);
    }

@@ -51,11 +68,14 @@ public class NotificationProgressBarTest {
    public void processAndConvertToDrawableParts_segmentLengthIsNegative() {
        List<ProgressStyle.Segment> segments = new ArrayList<>();
        segments.add(new ProgressStyle.Segment(-50));
        segments.add(new ProgressStyle.Segment(150));
        List<ProgressStyle.Point> points = new ArrayList<>();
        int progress = 50;
        int progressMax = 100;
        boolean isStyledByProgress = true;

        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
                progressMax,
                isStyledByProgress);
    }

@@ -63,11 +83,14 @@ public class NotificationProgressBarTest {
    public void processAndConvertToDrawableParts_segmentLengthIsZero() {
        List<ProgressStyle.Segment> segments = new ArrayList<>();
        segments.add(new ProgressStyle.Segment(0));
        segments.add(new ProgressStyle.Segment(100));
        List<ProgressStyle.Point> points = new ArrayList<>();
        int progress = 50;
        int progressMax = 100;
        boolean isStyledByProgress = true;

        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
                progressMax,
                isStyledByProgress);
    }

@@ -77,9 +100,11 @@ public class NotificationProgressBarTest {
        segments.add(new ProgressStyle.Segment(100));
        List<ProgressStyle.Point> points = new ArrayList<>();
        int progress = -50;
        int progressMax = 100;
        boolean isStyledByProgress = true;

        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
                progressMax,
                isStyledByProgress);
    }

@@ -89,10 +114,11 @@ public class NotificationProgressBarTest {
        segments.add(new ProgressStyle.Segment(100).setColor(Color.RED));
        List<ProgressStyle.Point> points = new ArrayList<>();
        int progress = 0;
        int progressMax = 100;
        boolean isStyledByProgress = true;

        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
                segments, points, progress, isStyledByProgress);
                segments, points, progress, progressMax, isStyledByProgress);

        int fadedRed = 0x7FFF0000;
        List<Part> expected = new ArrayList<>(List.of(new Segment(1f, fadedRed, true)));
@@ -106,10 +132,11 @@ public class NotificationProgressBarTest {
        segments.add(new ProgressStyle.Segment(100).setColor(Color.RED));
        List<ProgressStyle.Point> points = new ArrayList<>();
        int progress = 100;
        int progressMax = 100;
        boolean isStyledByProgress = true;

        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
                segments, points, progress, isStyledByProgress);
                segments, points, progress, progressMax, isStyledByProgress);

        List<Part> expected = new ArrayList<>(List.of(new Segment(1f, Color.RED)));

@@ -122,10 +149,11 @@ public class NotificationProgressBarTest {
        segments.add(new ProgressStyle.Segment(100));
        List<ProgressStyle.Point> points = new ArrayList<>();
        int progress = 150;
        int progressMax = 100;
        boolean isStyledByProgress = true;

        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
                isStyledByProgress);
                progressMax, isStyledByProgress);
    }

    @Test(expected = IllegalArgumentException.class)
@@ -135,9 +163,11 @@ public class NotificationProgressBarTest {
        List<ProgressStyle.Point> points = new ArrayList<>();
        points.add(new ProgressStyle.Point(-50).setColor(Color.RED));
        int progress = 50;
        int progressMax = 100;
        boolean isStyledByProgress = true;

        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
                progressMax,
                isStyledByProgress);
    }

@@ -148,9 +178,11 @@ public class NotificationProgressBarTest {
        List<ProgressStyle.Point> points = new ArrayList<>();
        points.add(new ProgressStyle.Point(0).setColor(Color.RED));
        int progress = 50;
        int progressMax = 100;
        boolean isStyledByProgress = true;

        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
                progressMax,
                isStyledByProgress);
    }

@@ -161,9 +193,11 @@ public class NotificationProgressBarTest {
        List<ProgressStyle.Point> points = new ArrayList<>();
        points.add(new ProgressStyle.Point(100).setColor(Color.RED));
        int progress = 50;
        int progressMax = 100;
        boolean isStyledByProgress = true;

        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
                progressMax,
                isStyledByProgress);
    }

@@ -174,9 +208,11 @@ public class NotificationProgressBarTest {
        List<ProgressStyle.Point> points = new ArrayList<>();
        points.add(new ProgressStyle.Point(150).setColor(Color.RED));
        int progress = 50;
        int progressMax = 100;
        boolean isStyledByProgress = true;

        NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
                progressMax,
                isStyledByProgress);
    }

@@ -187,10 +223,11 @@ public class NotificationProgressBarTest {
        segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
        List<ProgressStyle.Point> points = new ArrayList<>();
        int progress = 60;
        int progressMax = 100;
        boolean isStyledByProgress = true;

        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
                segments, points, progress, isStyledByProgress);
                segments, points, progress, progressMax, isStyledByProgress);

        // Colors with 50% opacity
        int fadedGreen = 0x7F00FF00;
@@ -213,6 +250,7 @@ public class NotificationProgressBarTest {
        points.add(new ProgressStyle.Point(60).setColor(Color.BLUE));
        points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
        int progress = 60;
        int progressMax = 100;
        boolean isStyledByProgress = true;

        // Colors with 50% opacity
@@ -231,7 +269,7 @@ public class NotificationProgressBarTest {
                new Segment(0.25f, fadedBlue, true)));

        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
                segments, points, progress, isStyledByProgress);
                segments, points, progress, progressMax, isStyledByProgress);

        assertThat(parts).isEqualTo(expected);
    }
@@ -247,10 +285,11 @@ public class NotificationProgressBarTest {
        points.add(new ProgressStyle.Point(60).setColor(Color.BLUE));
        points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
        int progress = 60;
        int progressMax = 100;
        boolean isStyledByProgress = true;

        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
                segments, points, progress, isStyledByProgress);
                segments, points, progress, progressMax, isStyledByProgress);

        // Colors with 50% opacity
        int fadedGreen = 0x7F00FF00;
@@ -281,10 +320,11 @@ public class NotificationProgressBarTest {
        points.add(new ProgressStyle.Point(25).setColor(Color.BLUE));
        points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
        int progress = 60;
        int progressMax = 100;
        boolean isStyledByProgress = false;

        List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
                segments, points, progress, isStyledByProgress);
                segments, points, progress, progressMax, isStyledByProgress);

        List<Part> expected = new ArrayList<>(List.of(
                new Segment(0.15f, Color.RED),