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

Commit b14e1595 authored by Xiaowen Lei's avatar Xiaowen Lei Committed by Android (Google) Code Review
Browse files

Merge "Add tracker to NotificationProgressBar." into main

parents 6777ef7e d6b10e74
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),