• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.widget;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.Notification.ProgressStyle;
22 import android.content.Context;
23 import android.content.res.ColorStateList;
24 import android.content.res.TypedArray;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.Matrix;
28 import android.graphics.Rect;
29 import android.graphics.drawable.Animatable2;
30 import android.graphics.drawable.AnimatedVectorDrawable;
31 import android.graphics.drawable.Drawable;
32 import android.graphics.drawable.Icon;
33 import android.graphics.drawable.LayerDrawable;
34 import android.os.Bundle;
35 import android.util.AttributeSet;
36 import android.util.Log;
37 import android.util.Pair;
38 import android.view.RemotableViewMethod;
39 import android.widget.ProgressBar;
40 import android.widget.RemoteViews;
41 
42 import androidx.annotation.ColorInt;
43 
44 import com.android.internal.R;
45 import com.android.internal.annotations.VisibleForTesting;
46 import com.android.internal.util.Preconditions;
47 import com.android.internal.widget.NotificationProgressDrawable.DrawablePart;
48 import com.android.internal.widget.NotificationProgressDrawable.DrawablePoint;
49 import com.android.internal.widget.NotificationProgressDrawable.DrawableSegment;
50 
51 import java.util.ArrayList;
52 import java.util.Collections;
53 import java.util.HashMap;
54 import java.util.List;
55 import java.util.Map;
56 import java.util.Objects;
57 import java.util.SortedSet;
58 import java.util.TreeSet;
59 
60 /**
61  * NotificationProgressBar extends the capabilities of ProgressBar by adding functionalities to
62  * represent Notification ProgressStyle progress, such as for ridesharing and navigation.
63  */
64 @RemoteViews.RemoteView
65 public final class NotificationProgressBar extends ProgressBar implements
66         NotificationProgressDrawable.BoundsChangeListener {
67     private static final String TAG = "NotificationProgressBar";
68     private static final boolean DEBUG = false;
69     private static final float FADED_OPACITY = 0.5f;
70 
71     private Animatable2.AnimationCallback mIndeterminateAnimationCallback = null;
72 
73     private NotificationProgressDrawable mNotificationProgressDrawable;
74     private final Rect mProgressDrawableBounds = new Rect();
75 
76     private NotificationProgressModel mProgressModel;
77 
78     @Nullable
79     private List<Part> mParts = null;
80 
81     // List of drawable parts before segment splitting by process.
82     @Nullable
83     private List<DrawablePart> mProgressDrawableParts = null;
84 
85     /** @see R.styleable#NotificationProgressBar_segMinWidth */
86     private final float mSegMinWidth;
87     /** @see R.styleable#NotificationProgressBar_segSegGap */
88     private final float mSegSegGap;
89     /** @see R.styleable#NotificationProgressBar_segPointGap */
90     private final float mSegPointGap;
91 
92     @Nullable
93     private Drawable mTracker = null;
94     private boolean mHasTrackerIcon = false;
95 
96     /** @see R.styleable#NotificationProgressBar_trackerHeight */
97     private final int mTrackerHeight;
98     private int mTrackerDrawWidth = 0;
99     private int mTrackerPos;
100     private final Matrix mMatrix = new Matrix();
101     private Matrix mTrackerDrawMatrix = null;
102 
103     private float mProgressFraction = 0;
104     /**
105      * The location of progress on the stretched and rescaled progress bar, in fraction. Used for
106      * calculating the tracker position. If stretching and rescaling is not needed, ==
107      * mProgressFraction.
108      */
109     private float mAdjustedProgressFraction = 0;
110     /** Indicates whether mTrackerPos needs to be recalculated before the tracker is drawn. */
111     private boolean mTrackerPosIsDirty = false;
112 
NotificationProgressBar(Context context)113     public NotificationProgressBar(Context context) {
114         this(context, null);
115     }
116 
NotificationProgressBar(Context context, AttributeSet attrs)117     public NotificationProgressBar(Context context, AttributeSet attrs) {
118         this(context, attrs, R.attr.progressBarStyle);
119     }
120 
NotificationProgressBar(Context context, AttributeSet attrs, int defStyleAttr)121     public NotificationProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
122         this(context, attrs, defStyleAttr, 0);
123     }
124 
NotificationProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)125     public NotificationProgressBar(Context context, AttributeSet attrs, int defStyleAttr,
126             int defStyleRes) {
127         super(context, attrs, defStyleAttr, defStyleRes);
128 
129         final TypedArray a = context.obtainStyledAttributes(attrs,
130                 R.styleable.NotificationProgressBar, defStyleAttr, defStyleRes);
131         saveAttributeDataForStyleable(context, R.styleable.NotificationProgressBar, attrs, a,
132                 defStyleAttr,
133                 defStyleRes);
134 
135         try {
136             mNotificationProgressDrawable = getNotificationProgressDrawable();
137             mNotificationProgressDrawable.setBoundsChangeListener(this);
138         } catch (IllegalStateException ex) {
139             Log.e(TAG, "Can't get NotificationProgressDrawable", ex);
140         }
141 
142         mSegMinWidth = a.getDimension(R.styleable.NotificationProgressBar_segMinWidth, 0f);
143         mSegSegGap = a.getDimension(R.styleable.NotificationProgressBar_segSegGap, 0f);
144         mSegPointGap = a.getDimension(R.styleable.NotificationProgressBar_segPointGap, 0f);
145 
146         // Supports setting the tracker in xml, but ProgressStyle notifications set/override it
147         // via {@code #setProgressTrackerIcon}.
148         final Drawable tracker = a.getDrawable(R.styleable.NotificationProgressBar_tracker);
149         setTracker(tracker);
150 
151         // If this is configured to be a non-zero size, will scale and crop the tracker drawable to
152         // ensure its aspect ratio is between 2:1 to 1:2.
153         mTrackerHeight = a.getDimensionPixelSize(R.styleable.NotificationProgressBar_trackerHeight,
154                 0);
155     }
156 
157     @Override
setIndeterminateDrawable(Drawable d)158     public void setIndeterminateDrawable(Drawable d) {
159         final Drawable oldDrawable = getIndeterminateDrawable();
160         if (oldDrawable != d) {
161             if (mIndeterminateAnimationCallback != null) {
162                 ((AnimatedVectorDrawable) oldDrawable).unregisterAnimationCallback(
163                         mIndeterminateAnimationCallback);
164                 mIndeterminateAnimationCallback = null;
165             }
166             if (d instanceof AnimatedVectorDrawable) {
167                 mIndeterminateAnimationCallback = new Animatable2.AnimationCallback() {
168                     @Override
169                     public void onAnimationEnd(Drawable drawable) {
170                         super.onAnimationEnd(drawable);
171 
172                         if (shouldLoopIndeterminateAnimation()) {
173                             ((AnimatedVectorDrawable) drawable).start();
174                         }
175                     }
176                 };
177                 ((AnimatedVectorDrawable) d).registerAnimationCallback(
178                         mIndeterminateAnimationCallback);
179             }
180         }
181 
182         super.setIndeterminateDrawable(d);
183     }
184 
shouldLoopIndeterminateAnimation()185     private boolean shouldLoopIndeterminateAnimation() {
186         return isIndeterminate() && isAttachedToWindow() && isAggregatedVisible();
187     }
188 
189     /**
190      * Setter for the notification progress model.
191      *
192      * @see NotificationProgressModel#fromBundle
193      */
194     @RemotableViewMethod
setProgressModel(@ullable Bundle bundle)195     public void setProgressModel(@Nullable Bundle bundle) {
196         Preconditions.checkArgument(bundle != null, "Bundle shouldn't be null");
197 
198         mProgressModel = NotificationProgressModel.fromBundle(bundle);
199         final boolean isIndeterminate = mProgressModel.isIndeterminate();
200         setIndeterminate(isIndeterminate);
201 
202         if (isIndeterminate) {
203             final int indeterminateColor = mProgressModel.getIndeterminateColor();
204             setIndeterminateTintList(ColorStateList.valueOf(indeterminateColor));
205         } else {
206             // TODO: b/372908709 - maybe don't rerun the entire calculation every time the
207             //  progress model is updated? For example, if the segments and parts aren't changed,
208             //  there is no need to call `processModelAndConvertToViewParts` again.
209 
210             final int progress = mProgressModel.getProgress();
211             final int progressMax = mProgressModel.getProgressMax();
212 
213             mParts = processModelAndConvertToViewParts(mProgressModel.getSegments(),
214                     mProgressModel.getPoints(),
215                     progress,
216                     progressMax);
217 
218             setMax(progressMax);
219             setProgress(progress);
220 
221             if (mNotificationProgressDrawable != null
222                     && mNotificationProgressDrawable.getBounds().width() != 0) {
223                 updateDrawableParts();
224             }
225         }
226     }
227 
228     @NonNull
getNotificationProgressDrawable()229     private NotificationProgressDrawable getNotificationProgressDrawable() {
230         final Drawable d = getProgressDrawable();
231         if (d == null) {
232             throw new IllegalStateException("getProgressDrawable() returns null");
233         }
234         if (!(d instanceof LayerDrawable)) {
235             throw new IllegalStateException("getProgressDrawable() doesn't return a LayerDrawable");
236         }
237 
238         final Drawable layer = ((LayerDrawable) d).findDrawableByLayerId(R.id.background);
239         if (!(layer instanceof NotificationProgressDrawable)) {
240             throw new IllegalStateException(
241                     "Couldn't get NotificationProgressDrawable, retrieved drawable is: " + (
242                             layer != null ? layer.toString() : null));
243         }
244 
245         return (NotificationProgressDrawable) layer;
246     }
247 
248     /**
249      * Setter for the progress tracker icon.
250      *
251      * @see #setProgressTrackerIconAsync
252      */
253     @RemotableViewMethod(asyncImpl = "setProgressTrackerIconAsync")
setProgressTrackerIcon(@ullable Icon icon)254     public void setProgressTrackerIcon(@Nullable Icon icon) {
255         final Drawable progressTrackerDrawable;
256         if (icon != null) {
257             progressTrackerDrawable = icon.loadDrawable(getContext());
258         } else {
259             progressTrackerDrawable = null;
260         }
261         setTracker(progressTrackerDrawable);
262     }
263 
264     /**
265      * Async version of {@link #setProgressTrackerIcon}
266      */
setProgressTrackerIconAsync(@ullable Icon icon)267     public Runnable setProgressTrackerIconAsync(@Nullable Icon icon) {
268         final Drawable progressTrackerDrawable;
269         if (icon != null) {
270             progressTrackerDrawable = icon.loadDrawable(getContext());
271         } else {
272             progressTrackerDrawable = null;
273         }
274         return () -> setTracker(progressTrackerDrawable);
275     }
276 
setTracker(@ullable Drawable tracker)277     private void setTracker(@Nullable Drawable tracker) {
278         if (tracker == mTracker) return;
279 
280         if (mTracker != null) {
281             mTracker.setCallback(null);
282         }
283 
284         if (tracker != null) {
285             tracker.setCallback(this);
286             if (getMirrorForRtl()) {
287                 tracker.setAutoMirrored(true);
288             }
289 
290             if (canResolveLayoutDirection()) {
291                 tracker.setLayoutDirection(getLayoutDirection());
292             }
293         }
294 
295         final boolean trackerSizeChanged = trackerSizeChanged(tracker, mTracker);
296 
297         mTracker = tracker;
298         final boolean hasTrackerIcon = (mTracker != null);
299         if (mHasTrackerIcon != hasTrackerIcon) {
300             mHasTrackerIcon = hasTrackerIcon;
301             if (mNotificationProgressDrawable != null
302                     && mNotificationProgressDrawable.getBounds().width() != 0
303                     && mProgressModel.isStyledByProgress()) {
304                 updateDrawableParts();
305             }
306         }
307 
308         configureTrackerBounds();
309         updateTrackerAndBarPos(getWidth(), getHeight());
310 
311         // Change in tracker size may lead to change in measured view size.
312         // @see #onMeasure.
313         if (trackerSizeChanged) requestLayout();
314 
315         invalidate();
316 
317         if (tracker != null && tracker.isStateful()) {
318             // Note that if the states are different this won't work.
319             // For now, let's consider that an app bug.
320             tracker.setState(getDrawableState());
321         }
322     }
323 
trackerSizeChanged(@ullable Drawable newTracker, @Nullable Drawable oldTracker)324     private static boolean trackerSizeChanged(@Nullable Drawable newTracker,
325             @Nullable Drawable oldTracker) {
326         if (newTracker == null && oldTracker == null) return false;
327         if (newTracker == null && oldTracker != null) return true;
328         if (newTracker != null && oldTracker == null) return true;
329 
330         return newTracker.getIntrinsicWidth() != oldTracker.getIntrinsicWidth()
331                 || newTracker.getIntrinsicHeight() != oldTracker.getIntrinsicHeight();
332     }
333 
configureTrackerBounds()334     private void configureTrackerBounds() {
335         // Reset the tracker draw matrix to null
336         mTrackerDrawMatrix = null;
337         mTrackerDrawWidth = 0;
338 
339         if (mTracker == null) return;
340         if (mTrackerHeight <= 0) {
341             mTrackerDrawWidth = mTracker.getIntrinsicWidth();
342             return;
343         }
344 
345         final int dWidth = mTracker.getIntrinsicWidth();
346         final int dHeight = mTracker.getIntrinsicHeight();
347         if (dWidth <= 0 || dHeight <= 0) {
348             return;
349         }
350         final int maxDWidth = dHeight * 2;
351         final int maxDHeight = dWidth * 2;
352 
353         mTrackerDrawMatrix = mMatrix;
354         float scale;
355         float dx = 0, dy = 0;
356 
357         if (dWidth > maxDWidth) {
358             scale = (float) mTrackerHeight / (float) dHeight;
359             dx = (maxDWidth * scale - dWidth * scale) * 0.5f;
360             mTrackerDrawWidth = (int) (maxDWidth * scale);
361         } else if (dHeight > maxDHeight) {
362             scale = (float) mTrackerHeight * 0.5f / (float) dWidth;
363             dy = (maxDHeight * scale - dHeight * scale) * 0.5f;
364             mTrackerDrawWidth = mTrackerHeight / 2;
365         } else {
366             scale = (float) mTrackerHeight / (float) dHeight;
367             mTrackerDrawWidth = (int) (dWidth * scale);
368         }
369 
370         mTrackerDrawMatrix.setScale(scale, scale);
371         mTrackerDrawMatrix.postTranslate(Math.round(dx), Math.round(dy));
372     }
373 
374     // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't
375     // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}.
376     @Override
setProgress(int progress)377     public synchronized void setProgress(int progress) {
378         super.setProgress(progress);
379 
380         onMaybeVisualProgressChanged();
381     }
382 
383     // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't
384     // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}.
385     @Override
setProgress(int progress, boolean animate)386     public void setProgress(int progress, boolean animate) {
387         // Animation isn't supported by NotificationProgressBar.
388         super.setProgress(progress, false);
389 
390         onMaybeVisualProgressChanged();
391     }
392 
393     // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't
394     // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}.
395     @Override
setMin(int min)396     public synchronized void setMin(int min) {
397         super.setMin(min);
398 
399         onMaybeVisualProgressChanged();
400     }
401 
402     // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't
403     // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}.
404     @Override
setMax(int max)405     public synchronized void setMax(int max) {
406         super.setMax(max);
407 
408         onMaybeVisualProgressChanged();
409     }
410 
onMaybeVisualProgressChanged()411     private void onMaybeVisualProgressChanged() {
412         float progressFraction = getProgressFraction();
413         if (mProgressFraction == progressFraction) return;
414 
415         mProgressFraction = progressFraction;
416         mTrackerPosIsDirty = true;
417         invalidate();
418     }
419 
420     @Override
verifyDrawable(@onNull Drawable who)421     protected boolean verifyDrawable(@NonNull Drawable who) {
422         return who == mTracker || super.verifyDrawable(who);
423     }
424 
425     @Override
jumpDrawablesToCurrentState()426     public void jumpDrawablesToCurrentState() {
427         super.jumpDrawablesToCurrentState();
428 
429         if (mTracker != null) {
430             mTracker.jumpToCurrentState();
431         }
432     }
433 
434     @Override
drawableStateChanged()435     protected void drawableStateChanged() {
436         super.drawableStateChanged();
437 
438         final Drawable tracker = mTracker;
439         if (tracker != null && tracker.isStateful() && tracker.setState(getDrawableState())) {
440             invalidateDrawable(tracker);
441         }
442     }
443 
444     @Override
drawableHotspotChanged(float x, float y)445     public void drawableHotspotChanged(float x, float y) {
446         super.drawableHotspotChanged(x, y);
447 
448         if (mTracker != null) {
449             mTracker.setHotspot(x, y);
450         }
451     }
452 
453     @Override
onSizeChanged(int w, int h, int oldw, int oldh)454     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
455         super.onSizeChanged(w, h, oldw, oldh);
456 
457         updateTrackerAndBarPos(w, h);
458     }
459 
460     @Override
onDrawableBoundsChanged()461     public void onDrawableBoundsChanged() {
462         final Rect progressDrawableBounds = mNotificationProgressDrawable.getBounds();
463 
464         if (mProgressDrawableBounds.equals(progressDrawableBounds)) return;
465 
466         if (mProgressDrawableBounds.width() != progressDrawableBounds.width()) {
467             updateDrawableParts();
468         }
469 
470         mProgressDrawableBounds.set(progressDrawableBounds);
471     }
472 
updateDrawableParts()473     private void updateDrawableParts() {
474         if (DEBUG) {
475             Log.d(TAG, "updateDrawableParts() called. mNotificationProgressDrawable = "
476                     + mNotificationProgressDrawable + ", mParts = " + mParts);
477         }
478 
479         if (mNotificationProgressDrawable == null) return;
480         if (mParts == null) return;
481 
482         final float width = mNotificationProgressDrawable.getBounds().width();
483         if (width == 0) {
484             if (mProgressDrawableParts != null) {
485                 if (DEBUG) {
486                     Log.d(TAG, "Clearing mProgressDrawableParts");
487                 }
488                 mProgressDrawableParts.clear();
489                 mNotificationProgressDrawable.setParts(mProgressDrawableParts);
490             }
491             return;
492         }
493 
494         final float pointRadius = mNotificationProgressDrawable.getPointRadius();
495         mProgressDrawableParts = processPartsAndConvertToDrawableParts(
496                 mParts,
497                 width,
498                 mSegSegGap,
499                 mSegPointGap,
500                 pointRadius,
501                 mHasTrackerIcon,
502                 mTrackerDrawWidth
503         );
504 
505         final float progressFraction = getProgressFraction();
506         final boolean isStyledByProgress = mProgressModel.isStyledByProgress();
507         final float progressGap = mHasTrackerIcon ? 0F : mSegSegGap;
508         Pair<List<DrawablePart>, Float> p = null;
509         try {
510             p = maybeStretchAndRescaleSegments(
511                     mParts,
512                     mProgressDrawableParts,
513                     mSegMinWidth,
514                     pointRadius,
515                     progressFraction,
516                     isStyledByProgress,
517                     progressGap
518             );
519         } catch (NotEnoughWidthToFitAllPartsException ex) {
520             Log.w(TAG, "Failed to stretch and rescale segments", ex);
521         }
522 
523         List<ProgressStyle.Segment> fallbackSegments = null;
524         if (p == null && mProgressModel.getSegments().size() > 1) {
525             Log.w(TAG, "Falling back to single segment");
526             try {
527                 fallbackSegments = List.of(new ProgressStyle.Segment(getMax()).setColor(
528                         mProgressModel.getSegmentsFallbackColor()
529                                 == NotificationProgressModel.INVALID_COLOR
530                                 ? mProgressModel.getSegments().getFirst().getColor()
531                                 : mProgressModel.getSegmentsFallbackColor()));
532                 p = processModelAndConvertToFinalDrawableParts(
533                         fallbackSegments,
534                         mProgressModel.getPoints(),
535                         mProgressModel.getProgress(),
536                         getMax(),
537                         width,
538                         mSegSegGap,
539                         mSegPointGap,
540                         pointRadius,
541                         mHasTrackerIcon,
542                         mSegMinWidth,
543                         isStyledByProgress,
544                         mTrackerDrawWidth);
545             } catch (NotEnoughWidthToFitAllPartsException ex) {
546                 Log.w(TAG, "Failed to stretch and rescale segments with single segment fallback",
547                         ex);
548             }
549         }
550 
551         if (p == null && !mProgressModel.getPoints().isEmpty()) {
552             Log.w(TAG, "Falling back to single segment and no points");
553             if (fallbackSegments == null) {
554                 fallbackSegments = List.of(new ProgressStyle.Segment(getMax()).setColor(
555                         mProgressModel.getSegmentsFallbackColor()
556                                 == NotificationProgressModel.INVALID_COLOR
557                                 ? mProgressModel.getSegments().getFirst().getColor()
558                                 : mProgressModel.getSegmentsFallbackColor()));
559             }
560             try {
561                 p = processModelAndConvertToFinalDrawableParts(
562                         fallbackSegments,
563                         Collections.emptyList(),
564                         mProgressModel.getProgress(),
565                         getMax(),
566                         width,
567                         mSegSegGap,
568                         mSegPointGap,
569                         pointRadius,
570                         mHasTrackerIcon,
571                         mSegMinWidth,
572                         isStyledByProgress,
573                         mTrackerDrawWidth);
574             } catch (NotEnoughWidthToFitAllPartsException ex) {
575                 Log.w(TAG,
576                         "Failed to stretch and rescale segments with single segments and no points",
577                         ex);
578             }
579         }
580 
581         if (p == null) {
582             Log.w(TAG, "Falling back to no stretching and rescaling");
583             p = maybeSplitDrawableSegmentsByProgress(
584                     mParts,
585                     mProgressDrawableParts,
586                     progressFraction,
587                     isStyledByProgress,
588                     progressGap);
589         }
590 
591         // Extend the first and last segments to fill the entire width.
592         p.first.getFirst().setStart(0);
593         p.first.getLast().setEnd(width);
594 
595         if (DEBUG) {
596             Log.d(TAG, "Updating NotificationProgressDrawable parts");
597         }
598         mNotificationProgressDrawable.setParts(p.first);
599         mAdjustedProgressFraction =
600                 (p.second - mTrackerDrawWidth / 2F) / (width - mTrackerDrawWidth);
601 
602         mNotificationProgressDrawable.updateEndDotColor(getEndDotColor(fallbackSegments));
603     }
604 
getEndDotColor(List<ProgressStyle.Segment> fallbackSegments)605     private int getEndDotColor(List<ProgressStyle.Segment> fallbackSegments) {
606         if (!mProgressModel.isStyledByProgress()) return Color.TRANSPARENT;
607         if (mProgressModel.getProgress() == mProgressModel.getProgressMax()) {
608             return Color.TRANSPARENT;
609         }
610 
611         return fallbackSegments == null ? mProgressModel.getSegments().getLast().getColor()
612                 : fallbackSegments.getLast().getColor();
613     }
614 
updateTrackerAndBarPos(int w, int h)615     private void updateTrackerAndBarPos(int w, int h) {
616         final int paddedHeight = h - mPaddingTop - mPaddingBottom;
617         final Drawable bar = getCurrentDrawable();
618         final Drawable tracker = mTracker;
619 
620         // The max height does not incorporate padding, whereas the height
621         // parameter does.
622         final int barHeight = Math.min(getMaxHeight(), paddedHeight);
623         final int trackerHeight = tracker == null ? 0
624                 : ((mTrackerHeight <= 0) ? tracker.getIntrinsicHeight() : mTrackerHeight);
625 
626         // Apply offset to whichever item is taller.
627         final int barOffsetY;
628         final int trackerOffsetY;
629         if (trackerHeight > barHeight) {
630             final int offsetHeight = (paddedHeight - trackerHeight) / 2;
631             barOffsetY = offsetHeight + (trackerHeight - barHeight) / 2;
632             trackerOffsetY = offsetHeight;
633         } else {
634             final int offsetHeight = (paddedHeight - barHeight) / 2;
635             barOffsetY = offsetHeight;
636             trackerOffsetY = offsetHeight + (barHeight - trackerHeight) / 2;
637         }
638 
639         if (bar != null) {
640             final int barWidth = w - mPaddingRight - mPaddingLeft;
641             bar.setBounds(0, barOffsetY, barWidth, barOffsetY + barHeight);
642         }
643 
644         if (tracker != null) {
645             setTrackerPos(w, tracker, mAdjustedProgressFraction, trackerOffsetY);
646         }
647     }
648 
getProgressFraction()649     private float getProgressFraction() {
650         int min = getMin();
651         int max = getMax();
652         int range = max - min;
653         return getProgressFraction(range, (getProgress() - min));
654     }
655 
getProgressFraction(int progressMax, int progress)656     private static float getProgressFraction(int progressMax, int progress) {
657         return progressMax > 0 ? progress / (float) progressMax : 0;
658     }
659 
660     /**
661      * Updates the tracker drawable bounds.
662      *
663      * @param w                Width of the view, including padding
664      * @param tracker          Drawable used for the tracker
665      * @param progressFraction Current progress between 0 and 1
666      * @param offsetY          Vertical offset for centering. If set to
667      *                         {@link Integer#MIN_VALUE}, the current offset will be used.
668      */
setTrackerPos(int w, Drawable tracker, float progressFraction, int offsetY)669     private void setTrackerPos(int w, Drawable tracker, float progressFraction, int offsetY) {
670         int available = w - mPaddingLeft - mPaddingRight;
671         final int trackerWidth = tracker.getIntrinsicWidth();
672         final int trackerHeight = tracker.getIntrinsicHeight();
673         available -= mTrackerDrawWidth;
674 
675         final int trackerPos = (int) (progressFraction * available + 0.5f);
676 
677         final int top, bottom;
678         if (offsetY == Integer.MIN_VALUE) {
679             final Rect oldBounds = tracker.getBounds();
680             top = oldBounds.top;
681             bottom = oldBounds.bottom;
682         } else {
683             top = offsetY;
684             bottom = offsetY + trackerHeight;
685         }
686 
687         mTrackerPos = (isLayoutRtl() && getMirrorForRtl()) ? available - trackerPos : trackerPos;
688         final int left = 0;
689         final int right = left + trackerWidth;
690 
691         final Drawable background = getBackground();
692         if (background != null) {
693             final int bkgOffsetX = mPaddingLeft;
694             final int bkgOffsetY = mPaddingTop;
695             background.setHotspotBounds(left + bkgOffsetX, top + bkgOffsetY, right + bkgOffsetX,
696                     bottom + bkgOffsetY);
697         }
698 
699         // Canvas will be translated, so 0,0 is where we start drawing
700         tracker.setBounds(left, top, right, bottom);
701 
702         mTrackerPosIsDirty = false;
703     }
704 
705     @Override
onResolveDrawables(int layoutDirection)706     public void onResolveDrawables(int layoutDirection) {
707         super.onResolveDrawables(layoutDirection);
708 
709         if (mTracker != null) {
710             mTracker.setLayoutDirection(layoutDirection);
711         }
712     }
713 
714     @Override
onDraw(Canvas canvas)715     protected synchronized void onDraw(Canvas canvas) {
716         super.onDraw(canvas);
717 
718         if (isIndeterminate()) return;
719         drawTracker(canvas);
720     }
721 
722     /**
723      * Draw the tracker.
724      */
drawTracker(Canvas canvas)725     private void drawTracker(Canvas canvas) {
726         if (mTracker == null) return;
727 
728         if (mTrackerPosIsDirty) {
729             setTrackerPos(getWidth(), mTracker, mAdjustedProgressFraction, Integer.MIN_VALUE);
730         }
731 
732         final int saveCount = canvas.save();
733         // Translate the canvas origin to tracker position to make the draw matrix and the RtL
734         // transformations work.
735         canvas.translate(mPaddingLeft + mTrackerPos, mPaddingTop);
736 
737         if (mTrackerHeight > 0) {
738             canvas.clipRect(0, 0, mTrackerDrawWidth, mTrackerHeight);
739         }
740 
741         if (mTrackerDrawMatrix != null) {
742             canvas.concat(mTrackerDrawMatrix);
743         }
744         mTracker.draw(canvas);
745         canvas.restoreToCount(saveCount);
746     }
747 
748     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)749     protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
750         Drawable d = getCurrentDrawable();
751 
752         int trackerHeight = mTracker == null ? 0 : mTracker.getIntrinsicHeight();
753         int dw = 0;
754         int dh = 0;
755         if (d != null) {
756             dw = Math.max(getMinWidth(), Math.min(getMaxWidth(), d.getIntrinsicWidth()));
757             dh = Math.max(getMinHeight(), Math.min(getMaxHeight(), d.getIntrinsicHeight()));
758             dh = Math.max(trackerHeight, dh);
759         }
760         dw += mPaddingLeft + mPaddingRight;
761         dh += mPaddingTop + mPaddingBottom;
762 
763         setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0),
764                 resolveSizeAndState(dh, heightMeasureSpec, 0));
765     }
766 
767     @Override
getAccessibilityClassName()768     public CharSequence getAccessibilityClassName() {
769         return NotificationProgressBar.class.getName();
770     }
771 
772     @Override
onRtlPropertiesChanged(int layoutDirection)773     public void onRtlPropertiesChanged(int layoutDirection) {
774         super.onRtlPropertiesChanged(layoutDirection);
775 
776         final Drawable tracker = mTracker;
777         if (tracker != null) {
778             setTrackerPos(getWidth(), tracker, mAdjustedProgressFraction, Integer.MIN_VALUE);
779 
780             // Since we draw translated, the drawable's bounds that it signals
781             // for invalidation won't be the actual bounds we want invalidated,
782             // so just invalidate this whole view.
783             invalidate();
784         }
785     }
786 
787     /**
788      * Processes the ProgressStyle data and convert to a list of {@code Part}.
789      */
790     @VisibleForTesting
processModelAndConvertToViewParts( List<ProgressStyle.Segment> segments, List<ProgressStyle.Point> points, int progress, int progressMax )791     public static List<Part> processModelAndConvertToViewParts(
792             List<ProgressStyle.Segment> segments,
793             List<ProgressStyle.Point> points,
794             int progress,
795             int progressMax
796     ) {
797         if (segments.isEmpty()) {
798             throw new IllegalArgumentException("List of segments shouldn't be empty");
799         }
800 
801         final int totalLength = segments.stream().mapToInt(ProgressStyle.Segment::getLength).sum();
802         if (progressMax != totalLength) {
803             throw new IllegalArgumentException("Invalid progressMax : " + progressMax);
804         }
805 
806         for (ProgressStyle.Segment segment : segments) {
807             final int length = segment.getLength();
808             if (length <= 0) {
809                 throw new IllegalArgumentException("Invalid segment length : " + length);
810             }
811         }
812 
813         if (progress < 0 || progress > progressMax) {
814             throw new IllegalArgumentException("Invalid progress : " + progress);
815         }
816 
817 
818         for (ProgressStyle.Point point : points) {
819             final int pos = point.getPosition();
820             if (pos < 0 || pos > progressMax) {
821                 throw new IllegalArgumentException("Invalid Point position : " + pos);
822             }
823         }
824 
825         // There should be no points at start or end. If there are, drop them with a warning.
826         points.removeIf(point -> {
827             final int pos = point.getPosition();
828             if (pos == 0) {
829                 Log.w(TAG, "Dropping point at start");
830                 return true;
831             } else if (pos == progressMax) {
832                 Log.w(TAG, "Dropping point at end");
833                 return true;
834             }
835             return false;
836         });
837 
838         final Map<Integer, ProgressStyle.Segment> startToSegmentMap = generateStartToSegmentMap(
839                 segments);
840         final Map<Integer, ProgressStyle.Point> positionToPointMap = generatePositionToPointMap(
841                 points);
842         final SortedSet<Integer> sortedPos = generateSortedPositionSet(startToSegmentMap,
843                 positionToPointMap);
844 
845         final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap = splitSegmentsByPoints(
846                 startToSegmentMap, sortedPos, progressMax);
847 
848         return convertToViewParts(startToSplitSegmentMap, positionToPointMap, sortedPos,
849                 progressMax);
850     }
851 
852     // Any segment with a point on it gets split by the point.
splitSegmentsByPoints( Map<Integer, ProgressStyle.Segment> startToSegmentMap, SortedSet<Integer> sortedPos, int progressMax )853     private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPoints(
854             Map<Integer, ProgressStyle.Segment> startToSegmentMap,
855             SortedSet<Integer> sortedPos,
856             int progressMax
857     ) {
858         int prevSegStart = 0;
859         for (Integer pos : sortedPos) {
860             if (pos == 0 || pos == progressMax) continue;
861             if (startToSegmentMap.containsKey(pos)) {
862                 prevSegStart = pos;
863                 continue;
864             }
865 
866             final ProgressStyle.Segment prevSeg = startToSegmentMap.get(prevSegStart);
867             final ProgressStyle.Segment leftSeg = new ProgressStyle.Segment(
868                     pos - prevSegStart).setColor(prevSeg.getColor());
869             final ProgressStyle.Segment rightSeg = new ProgressStyle.Segment(
870                     prevSegStart + prevSeg.getLength() - pos).setColor(prevSeg.getColor());
871 
872             startToSegmentMap.put(prevSegStart, leftSeg);
873             startToSegmentMap.put(pos, rightSeg);
874 
875             prevSegStart = pos;
876         }
877 
878         return startToSegmentMap;
879     }
880 
convertToViewParts( Map<Integer, ProgressStyle.Segment> startToSegmentMap, Map<Integer, ProgressStyle.Point> positionToPointMap, SortedSet<Integer> sortedPos, int progressMax )881     private static List<Part> convertToViewParts(
882             Map<Integer, ProgressStyle.Segment> startToSegmentMap,
883             Map<Integer, ProgressStyle.Point> positionToPointMap,
884             SortedSet<Integer> sortedPos,
885             int progressMax
886     ) {
887         List<Part> parts = new ArrayList<>();
888         for (Integer pos : sortedPos) {
889             if (positionToPointMap.containsKey(pos)) {
890                 final ProgressStyle.Point point = positionToPointMap.get(pos);
891                 parts.add(new Point(point.getColor()));
892             }
893             if (startToSegmentMap.containsKey(pos)) {
894                 final ProgressStyle.Segment seg = startToSegmentMap.get(pos);
895                 parts.add(new Segment((float) seg.getLength() / progressMax, seg.getColor()));
896             }
897         }
898 
899         return parts;
900     }
901 
902     @ColorInt
maybeGetFadedColor(@olorInt int color, boolean fade)903     private static int maybeGetFadedColor(@ColorInt int color, boolean fade) {
904         if (!fade) return color;
905 
906         return getFadedColor(color);
907     }
908 
909     /**
910      * Get a color that's the input color with opacity updated to FADED_OPACITY.
911      */
912     @ColorInt
getFadedColor(@olorInt int color)913     static int getFadedColor(@ColorInt int color) {
914         return Color.argb(
915                 (int) (Color.alpha(color) * FADED_OPACITY + 0.5f),
916                 Color.red(color),
917                 Color.green(color),
918                 Color.blue(color));
919     }
920 
generateStartToSegmentMap( List<ProgressStyle.Segment> segments )921     private static Map<Integer, ProgressStyle.Segment> generateStartToSegmentMap(
922             List<ProgressStyle.Segment> segments
923     ) {
924         final Map<Integer, ProgressStyle.Segment> startToSegmentMap = new HashMap<>();
925 
926         int currentStart = 0;  // Initial start position is 0
927 
928         for (ProgressStyle.Segment segment : segments) {
929             // Use the current start position as the key, and the segment as the value
930             startToSegmentMap.put(currentStart, segment);
931 
932             // Update the start position for the next segment
933             currentStart += segment.getLength();
934         }
935 
936         return startToSegmentMap;
937     }
938 
generatePositionToPointMap( List<ProgressStyle.Point> points )939     private static Map<Integer, ProgressStyle.Point> generatePositionToPointMap(
940             List<ProgressStyle.Point> points
941     ) {
942         final Map<Integer, ProgressStyle.Point> positionToPointMap = new HashMap<>();
943 
944         for (ProgressStyle.Point point : points) {
945             positionToPointMap.put(point.getPosition(), point);
946         }
947 
948         return positionToPointMap;
949     }
950 
generateSortedPositionSet( Map<Integer, ProgressStyle.Segment> startToSegmentMap, Map<Integer, ProgressStyle.Point> positionToPointMap )951     private static SortedSet<Integer> generateSortedPositionSet(
952             Map<Integer, ProgressStyle.Segment> startToSegmentMap,
953             Map<Integer, ProgressStyle.Point> positionToPointMap
954     ) {
955         final SortedSet<Integer> sortedPos = new TreeSet<>(startToSegmentMap.keySet());
956         sortedPos.addAll(positionToPointMap.keySet());
957 
958         return sortedPos;
959     }
960 
961     /**
962      * Processes the list of {@code Part} and convert to a list of {@code DrawablePart}.
963      */
964     @VisibleForTesting
processPartsAndConvertToDrawableParts( List<Part> parts, float totalWidth, float segSegGap, float segPointGap, float pointRadius, boolean hasTrackerIcon, int trackerDrawWidth)965     public static List<DrawablePart> processPartsAndConvertToDrawableParts(
966             List<Part> parts,
967             float totalWidth,
968             float segSegGap,
969             float segPointGap,
970             float pointRadius,
971             boolean hasTrackerIcon,
972             int trackerDrawWidth) {
973         List<DrawablePart> drawableParts = new ArrayList<>();
974 
975         float available = totalWidth - trackerDrawWidth;
976         // Generally, we will start the first segment at (x+trackerDrawWidth/2, y) and end the last
977         // segment at (x+w-trackerDrawWidth/2, y)
978         float x = trackerDrawWidth / 2F;
979 
980         final int nParts = parts.size();
981         for (int iPart = 0; iPart < nParts; iPart++) {
982             final Part part = parts.get(iPart);
983             final Part prevPart = iPart == 0 ? null : parts.get(iPart - 1);
984             final Part nextPart = iPart + 1 == nParts ? null : parts.get(iPart + 1);
985             if (part instanceof Segment segment) {
986                 final float segWidth = segment.mFraction * available;
987                 // Advance the start position to account for a point immediately prior.
988                 final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap);
989                 final float start = x + startOffset;
990                 // Retract the end position to account for the padding and a point immediately
991                 // after.
992                 final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap,
993                         segSegGap, hasTrackerIcon);
994                 final float end = x + segWidth - endOffset;
995 
996                 drawableParts.add(new DrawableSegment(start, end, segment.mColor, segment.mFaded));
997 
998                 segment.mStart = x;
999                 segment.mEnd = x + segWidth;
1000 
1001                 // Advance the current position to account for the segment's fraction of the total
1002                 // width (ignoring offset and padding)
1003                 x += segWidth;
1004             } else if (part instanceof Point point) {
1005                 final float pointWidth = 2 * pointRadius;
1006                 float start = x - pointRadius;
1007                 float end = x + pointRadius;
1008 
1009                 drawableParts.add(new DrawablePoint(start, end, point.mColor));
1010             }
1011         }
1012 
1013         return drawableParts;
1014     }
1015 
getSegStartOffset(Part prevPart, float pointRadius, float segPointGap)1016     private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap) {
1017         if (!(prevPart instanceof Point)) return 0F;
1018         return pointRadius + segPointGap;
1019     }
1020 
getSegEndOffset(Segment seg, Part nextPart, float pointRadius, float segPointGap, float segSegGap, boolean hasTrackerIcon)1021     private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius,
1022             float segPointGap, float segSegGap, boolean hasTrackerIcon) {
1023         if (nextPart == null) return 0F;
1024         if (nextPart instanceof Segment nextSeg) {
1025             if (!seg.mFaded && nextSeg.mFaded) {
1026                 // @see Segment#mFaded
1027                 return hasTrackerIcon ? 0F : segSegGap;
1028             }
1029             return segSegGap;
1030         }
1031 
1032         return segPointGap + pointRadius;
1033     }
1034 
1035     /**
1036      * Processes the list of {@code DrawablePart} data and convert to a pair of:
1037      * - list of processed {@code DrawablePart}.
1038      * - location of progress on the stretched and rescaled progress bar.
1039      */
1040     @VisibleForTesting
maybeStretchAndRescaleSegments( List<Part> parts, List<DrawablePart> drawableParts, float segmentMinWidth, float pointRadius, float progressFraction, boolean isStyledByProgress, float progressGap )1041     public static Pair<List<DrawablePart>, Float> maybeStretchAndRescaleSegments(
1042             List<Part> parts,
1043             List<DrawablePart> drawableParts,
1044             float segmentMinWidth,
1045             float pointRadius,
1046             float progressFraction,
1047             boolean isStyledByProgress,
1048             float progressGap
1049     ) throws NotEnoughWidthToFitAllPartsException {
1050         final List<DrawableSegment> drawableSegments = drawableParts
1051                 .stream()
1052                 .filter(DrawableSegment.class::isInstance)
1053                 .map(DrawableSegment.class::cast)
1054                 .toList();
1055         float totalExcessWidth = 0;
1056         float totalPositiveExcessWidth = 0;
1057         for (DrawableSegment drawableSegment : drawableSegments) {
1058             final float excessWidth = drawableSegment.getWidth() - segmentMinWidth;
1059             totalExcessWidth += excessWidth;
1060             if (excessWidth > 0) totalPositiveExcessWidth += excessWidth;
1061         }
1062 
1063         // All drawable segments are above minimum width. No need to stretch and rescale.
1064         if (totalExcessWidth == totalPositiveExcessWidth) {
1065             return maybeSplitDrawableSegmentsByProgress(
1066                     parts,
1067                     drawableParts,
1068                     progressFraction,
1069                     isStyledByProgress,
1070                     progressGap);
1071         }
1072 
1073         if (totalExcessWidth < 0) {
1074             throw new NotEnoughWidthToFitAllPartsException(
1075                     "Not enough width to satisfy the minimum width for segments.");
1076         }
1077 
1078         final int nParts = drawableParts.size();
1079         float startOffset = 0;
1080         for (int iPart = 0; iPart < nParts; iPart++) {
1081             final DrawablePart drawablePart = drawableParts.get(iPart);
1082             if (drawablePart instanceof DrawableSegment drawableSegment) {
1083                 final float origDrawableSegmentWidth = drawableSegment.getWidth();
1084 
1085                 float drawableSegmentWidth = segmentMinWidth;
1086                 // Allocate the totalExcessWidth to the segments above minimum, proportionally to
1087                 // their initial excessWidth.
1088                 if (origDrawableSegmentWidth > segmentMinWidth) {
1089                     drawableSegmentWidth +=
1090                             totalExcessWidth * (origDrawableSegmentWidth - segmentMinWidth)
1091                                     / totalPositiveExcessWidth;
1092                 }
1093 
1094                 final float widthDiff = drawableSegmentWidth - drawableSegment.getWidth();
1095 
1096                 // Adjust drawable segments to new widths
1097                 drawableSegment.setStart(drawableSegment.getStart() + startOffset);
1098                 drawableSegment.setEnd(
1099                         drawableSegment.getStart() + origDrawableSegmentWidth + widthDiff);
1100 
1101                 // Also adjust view segments to new width. (For view segments, only start is
1102                 // needed?)
1103                 // Check that segments and drawableSegments are of the same size?
1104                 final Segment segment = (Segment) parts.get(iPart);
1105                 final float origSegmentWidth = segment.getWidth();
1106                 segment.mStart = segment.mStart + startOffset;
1107                 segment.mEnd = segment.mStart + origSegmentWidth + widthDiff;
1108 
1109                 // Increase startOffset for the subsequent segments.
1110                 startOffset += widthDiff;
1111             } else if (drawablePart instanceof DrawablePoint drawablePoint) {
1112                 drawablePoint.setStart(drawablePoint.getStart() + startOffset);
1113                 drawablePoint.setEnd(drawablePoint.getStart() + 2 * pointRadius);
1114             }
1115         }
1116 
1117         return maybeSplitDrawableSegmentsByProgress(
1118                 parts,
1119                 drawableParts,
1120                 progressFraction,
1121                 isStyledByProgress,
1122                 progressGap);
1123     }
1124 
1125     /**
1126      * Find the location of progress on the stretched and rescaled progress bar.
1127      * If isStyledByProgress is true, also split the drawable segment with the progress value in its
1128      * range. Style the drawable parts after process with reduced opacity and segment height.
1129      */
maybeSplitDrawableSegmentsByProgress( List<Part> parts, List<DrawablePart> drawableParts, float progressFraction, boolean isStyledByProgress, float progressGap )1130     private static Pair<List<DrawablePart>, Float> maybeSplitDrawableSegmentsByProgress(
1131             // Needed to get the original segment start and end positions in pixels.
1132             List<Part> parts,
1133             List<DrawablePart> drawableParts,
1134             float progressFraction,
1135             boolean isStyledByProgress,
1136             float progressGap
1137     ) {
1138         if (progressFraction == 1) {
1139             return new Pair<>(drawableParts, drawableParts.getLast().getEnd());
1140         }
1141 
1142         int iPartFirstSegmentToStyle = -1;
1143         int iPartSegmentToSplit = -1;
1144         float rescaledProgressX = 0;
1145         float startFraction = 0;
1146         final int nParts = parts.size();
1147         for (int iPart = 0; iPart < nParts; iPart++) {
1148             final Part part = parts.get(iPart);
1149             if (!(part instanceof Segment segment)) continue;
1150             if (startFraction == progressFraction) {
1151                 iPartFirstSegmentToStyle = iPart;
1152                 rescaledProgressX = segment.mStart;
1153                 break;
1154             } else if (startFraction < progressFraction
1155                     && progressFraction < startFraction + segment.mFraction) {
1156                 iPartSegmentToSplit = iPart;
1157                 rescaledProgressX = segment.mStart
1158                         + (progressFraction - startFraction) / segment.mFraction
1159                         * segment.getWidth();
1160                 break;
1161             }
1162             startFraction += segment.mFraction;
1163         }
1164 
1165         if (!isStyledByProgress) return new Pair<>(drawableParts, rescaledProgressX);
1166 
1167         List<DrawablePart> splitDrawableParts = new ArrayList<>();
1168         boolean styleRemainingParts = false;
1169         for (int iPart = 0; iPart < nParts; iPart++) {
1170             final DrawablePart drawablePart = drawableParts.get(iPart);
1171             if (drawablePart instanceof DrawablePoint drawablePoint) {
1172                 final int color = maybeGetFadedColor(drawablePoint.getColor(), styleRemainingParts);
1173                 splitDrawableParts.add(
1174                         new DrawablePoint(drawablePoint.getStart(), drawablePoint.getEnd(), color));
1175             }
1176             if (iPart == iPartFirstSegmentToStyle) styleRemainingParts = true;
1177             if (drawablePart instanceof DrawableSegment drawableSegment) {
1178                 if (iPart == iPartSegmentToSplit) {
1179                     if (rescaledProgressX <= drawableSegment.getStart()) {
1180                         styleRemainingParts = true;
1181                         final int color = maybeGetFadedColor(drawableSegment.getColor(), true);
1182                         splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(),
1183                                 drawableSegment.getEnd(), color, true));
1184                     } else if (drawableSegment.getStart() < rescaledProgressX
1185                             && rescaledProgressX < drawableSegment.getEnd()) {
1186                         splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(),
1187                                 rescaledProgressX - progressGap, drawableSegment.getColor()));
1188                         final int color = maybeGetFadedColor(drawableSegment.getColor(), true);
1189                         splitDrawableParts.add(
1190                                 new DrawableSegment(rescaledProgressX, drawableSegment.getEnd(),
1191                                         color, true));
1192                         styleRemainingParts = true;
1193                     } else {
1194                         splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(),
1195                                 drawableSegment.getEnd(), drawableSegment.getColor()));
1196                         styleRemainingParts = true;
1197                     }
1198                 } else {
1199                     final int color = maybeGetFadedColor(drawableSegment.getColor(),
1200                             styleRemainingParts);
1201                     splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(),
1202                             drawableSegment.getEnd(), color, styleRemainingParts));
1203                 }
1204             }
1205         }
1206 
1207         return new Pair<>(splitDrawableParts, rescaledProgressX);
1208     }
1209 
1210     /**
1211      * Processes the ProgressStyle data and convert to a pair of:
1212      * - list of processed {@code DrawablePart}.
1213      * - location of progress on the stretched and rescaled progress bar.
1214      */
1215     @VisibleForTesting
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, int trackerDrawWidth )1216     public static Pair<List<DrawablePart>, Float> processModelAndConvertToFinalDrawableParts(
1217             List<ProgressStyle.Segment> segments,
1218             List<ProgressStyle.Point> points,
1219             int progress,
1220             int progressMax,
1221             float totalWidth,
1222             float segSegGap,
1223             float segPointGap,
1224             float pointRadius,
1225             boolean hasTrackerIcon,
1226             float segmentMinWidth,
1227             boolean isStyledByProgress,
1228             int trackerDrawWidth
1229     ) throws NotEnoughWidthToFitAllPartsException {
1230         List<Part> parts = processModelAndConvertToViewParts(segments, points, progress,
1231                 progressMax);
1232         List<DrawablePart> drawableParts = processPartsAndConvertToDrawableParts(parts, totalWidth,
1233                 segSegGap, segPointGap, pointRadius, hasTrackerIcon, trackerDrawWidth);
1234         return maybeStretchAndRescaleSegments(parts, drawableParts, segmentMinWidth, pointRadius,
1235                 getProgressFraction(progressMax, progress), isStyledByProgress,
1236                 hasTrackerIcon ? 0F : segSegGap);
1237     }
1238 
1239     /**
1240      * A part of the progress bar, which is either a {@link Segment} with non-zero length, or a
1241      * {@link Point} with zero length.
1242      */
1243     public interface Part {
1244     }
1245 
1246     /**
1247      * A segment is a part of the progress bar with non-zero length. For example, it can
1248      * represent a portion in a navigation journey with certain traffic condition.
1249      */
1250     public static final class Segment implements Part {
1251         private final float mFraction;
1252         @ColorInt
1253         private final int mColor;
1254         /**
1255          * Whether the segment is faded or not.
1256          * <p>
1257          * <pre>
1258          *     When mFaded is set to true, a combination of the following is done to the segment:
1259          *       1. The drawing color is mColor with opacity updated to FADED_OPACITY.
1260          *       2. The gap between faded and non-faded segments is:
1261          *          - the segment-segment gap, when there is no tracker icon
1262          *          - 0, when there is tracker icon
1263          *     </pre>
1264          * </p>
1265          */
1266         private final boolean mFaded;
1267 
1268         /** Start position (in pixels) */
1269         private float mStart;
1270         /** End position (in pixels */
1271         private float mEnd;
1272 
Segment(float fraction, @ColorInt int color)1273         public Segment(float fraction, @ColorInt int color) {
1274             this(fraction, color, false);
1275         }
1276 
Segment(float fraction, @ColorInt int color, boolean faded)1277         public Segment(float fraction, @ColorInt int color, boolean faded) {
1278             mFraction = fraction;
1279             mColor = color;
1280             mFaded = faded;
1281         }
1282 
1283         /** Returns the calculated drawing width of the part */
getWidth()1284         public float getWidth() {
1285             return mEnd - mStart;
1286         }
1287 
1288         @Override
toString()1289         public String toString() {
1290             return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", faded="
1291                     + this.mFaded + "), mStart = " + this.mStart + ", mEnd = " + this.mEnd;
1292         }
1293 
1294         // Needed for unit tests
1295         @Override
equals(@ndroidx.annotation.Nullable Object other)1296         public boolean equals(@androidx.annotation.Nullable Object other) {
1297             if (this == other) return true;
1298 
1299             if (other == null || getClass() != other.getClass()) return false;
1300 
1301             Segment that = (Segment) other;
1302             if (Float.compare(this.mFraction, that.mFraction) != 0) return false;
1303             if (this.mColor != that.mColor) return false;
1304             return this.mFaded == that.mFaded;
1305         }
1306 
1307         @Override
hashCode()1308         public int hashCode() {
1309             return Objects.hash(mFraction, mColor, mFaded);
1310         }
1311     }
1312 
1313     /**
1314      * A point is a part of the progress bar with zero length. Points are designated points within a
1315      * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop
1316      * ride-share journey.
1317      */
1318     public static final class Point implements Part {
1319         @ColorInt
1320         private final int mColor;
1321 
Point(@olorInt int color)1322         public Point(@ColorInt int color) {
1323             mColor = color;
1324         }
1325 
1326         @Override
toString()1327         public String toString() {
1328             return "Point(color=" + this.mColor + ")";
1329         }
1330 
1331         // Needed for unit tests.
1332         @Override
equals(@ndroidx.annotation.Nullable Object other)1333         public boolean equals(@androidx.annotation.Nullable Object other) {
1334             if (this == other) return true;
1335 
1336             if (other == null || getClass() != other.getClass()) return false;
1337 
1338             Point that = (Point) other;
1339 
1340             return this.mColor == that.mColor;
1341         }
1342 
1343         @Override
hashCode()1344         public int hashCode() {
1345             return Objects.hash(mColor);
1346         }
1347     }
1348 
1349     public static class NotEnoughWidthToFitAllPartsException extends Exception {
NotEnoughWidthToFitAllPartsException(String message)1350         public NotEnoughWidthToFitAllPartsException(String message) {
1351             super(message);
1352         }
1353     }
1354 }
1355