• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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.launcher3;
18 
19 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
20 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
21 
22 import android.animation.Animator;
23 import android.animation.AnimatorListenerAdapter;
24 import android.animation.ObjectAnimator;
25 import android.content.Context;
26 import android.content.res.ColorStateList;
27 import android.content.res.TypedArray;
28 import android.graphics.Canvas;
29 import android.graphics.Color;
30 import android.graphics.Paint;
31 import android.graphics.PointF;
32 import android.graphics.Rect;
33 import android.graphics.drawable.ColorDrawable;
34 import android.graphics.drawable.Drawable;
35 import android.text.TextUtils.TruncateAt;
36 import android.util.AttributeSet;
37 import android.util.Property;
38 import android.util.TypedValue;
39 import android.view.KeyEvent;
40 import android.view.MotionEvent;
41 import android.view.View;
42 import android.view.ViewDebug;
43 import android.widget.TextView;
44 
45 import androidx.annotation.Nullable;
46 import androidx.annotation.UiThread;
47 
48 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
49 import com.android.launcher3.dot.DotInfo;
50 import com.android.launcher3.dragndrop.DraggableView;
51 import com.android.launcher3.folder.FolderIcon;
52 import com.android.launcher3.graphics.IconPalette;
53 import com.android.launcher3.graphics.IconShape;
54 import com.android.launcher3.graphics.PreloadIconDrawable;
55 import com.android.launcher3.icons.DotRenderer;
56 import com.android.launcher3.icons.FastBitmapDrawable;
57 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
58 import com.android.launcher3.icons.PlaceHolderIconDrawable;
59 import com.android.launcher3.icons.cache.HandlerRunnable;
60 import com.android.launcher3.model.data.AppInfo;
61 import com.android.launcher3.model.data.ItemInfo;
62 import com.android.launcher3.model.data.ItemInfoWithIcon;
63 import com.android.launcher3.model.data.PackageItemInfo;
64 import com.android.launcher3.model.data.SearchActionItemInfo;
65 import com.android.launcher3.model.data.WorkspaceItemInfo;
66 import com.android.launcher3.util.SafeCloseable;
67 import com.android.launcher3.views.ActivityContext;
68 import com.android.launcher3.views.IconLabelDotView;
69 
70 import java.text.NumberFormat;
71 
72 /**
73  * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan
74  * because we want to make the bubble taller than the text and TextView's clip is
75  * too aggressive.
76  */
77 public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
78         IconLabelDotView, DraggableView, Reorderable {
79 
80     private static final int DISPLAY_WORKSPACE = 0;
81     private static final int DISPLAY_ALL_APPS = 1;
82     private static final int DISPLAY_FOLDER = 2;
83     protected static final int DISPLAY_TASKBAR = 5;
84     private static final int DISPLAY_SEARCH_RESULT = 6;
85     private static final int DISPLAY_SEARCH_RESULT_SMALL = 7;
86 
87     private static final int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed};
88     private static final float HIGHLIGHT_SCALE = 1.16f;
89 
90     private final PointF mTranslationForReorderBounce = new PointF(0, 0);
91     private final PointF mTranslationForReorderPreview = new PointF(0, 0);
92 
93     private float mScaleForReorderBounce = 1f;
94 
95     private static final Property<BubbleTextView, Float> DOT_SCALE_PROPERTY
96             = new Property<BubbleTextView, Float>(Float.TYPE, "dotScale") {
97         @Override
98         public Float get(BubbleTextView bubbleTextView) {
99             return bubbleTextView.mDotParams.scale;
100         }
101 
102         @Override
103         public void set(BubbleTextView bubbleTextView, Float value) {
104             bubbleTextView.mDotParams.scale = value;
105             bubbleTextView.invalidate();
106         }
107     };
108 
109     public static final Property<BubbleTextView, Float> TEXT_ALPHA_PROPERTY
110             = new Property<BubbleTextView, Float>(Float.class, "textAlpha") {
111         @Override
112         public Float get(BubbleTextView bubbleTextView) {
113             return bubbleTextView.mTextAlpha;
114         }
115 
116         @Override
117         public void set(BubbleTextView bubbleTextView, Float alpha) {
118             bubbleTextView.setTextAlpha(alpha);
119         }
120     };
121 
122     private final ActivityContext mActivity;
123     private FastBitmapDrawable mIcon;
124     private boolean mCenterVertically;
125 
126     protected final int mDisplay;
127 
128     private final CheckLongPressHelper mLongPressHelper;
129 
130     private final boolean mLayoutHorizontal;
131     private final int mIconSize;
132 
133     @ViewDebug.ExportedProperty(category = "launcher")
134     private boolean mIsIconVisible = true;
135     @ViewDebug.ExportedProperty(category = "launcher")
136     private int mTextColor;
137     @ViewDebug.ExportedProperty(category = "launcher")
138     private float mTextAlpha = 1;
139 
140     @ViewDebug.ExportedProperty(category = "launcher")
141     private DotInfo mDotInfo;
142     private DotRenderer mDotRenderer;
143     @ViewDebug.ExportedProperty(category = "launcher", deepExport = true)
144     protected DotRenderer.DrawParams mDotParams;
145     private Animator mDotScaleAnim;
146     private boolean mForceHideDot;
147 
148     @ViewDebug.ExportedProperty(category = "launcher")
149     private boolean mStayPressed;
150     @ViewDebug.ExportedProperty(category = "launcher")
151     private boolean mIgnorePressedStateChange;
152     @ViewDebug.ExportedProperty(category = "launcher")
153     private boolean mDisableRelayout = false;
154 
155     private HandlerRunnable mIconLoadRequest;
156 
157     private boolean mEnableIconUpdateAnimation = false;
158 
BubbleTextView(Context context)159     public BubbleTextView(Context context) {
160         this(context, null, 0);
161     }
162 
BubbleTextView(Context context, AttributeSet attrs)163     public BubbleTextView(Context context, AttributeSet attrs) {
164         this(context, attrs, 0);
165     }
166 
BubbleTextView(Context context, AttributeSet attrs, int defStyle)167     public BubbleTextView(Context context, AttributeSet attrs, int defStyle) {
168         super(context, attrs, defStyle);
169         mActivity = ActivityContext.lookupContext(context);
170 
171         TypedArray a = context.obtainStyledAttributes(attrs,
172                 R.styleable.BubbleTextView, defStyle, 0);
173         mLayoutHorizontal = a.getBoolean(R.styleable.BubbleTextView_layoutHorizontal, false);
174         DeviceProfile grid = mActivity.getDeviceProfile();
175 
176         mDisplay = a.getInteger(R.styleable.BubbleTextView_iconDisplay, DISPLAY_WORKSPACE);
177         final int defaultIconSize;
178         if (mDisplay == DISPLAY_WORKSPACE) {
179             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx);
180             setCompoundDrawablePadding(grid.iconDrawablePaddingPx);
181             defaultIconSize = grid.iconSizePx;
182             setCenterVertically(grid.isScalableGrid);
183         } else if (mDisplay == DISPLAY_ALL_APPS) {
184             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx);
185             setCompoundDrawablePadding(grid.allAppsIconDrawablePaddingPx);
186             defaultIconSize = grid.allAppsIconSizePx;
187         } else if (mDisplay == DISPLAY_FOLDER) {
188             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.folderChildTextSizePx);
189             setCompoundDrawablePadding(grid.folderChildDrawablePaddingPx);
190             defaultIconSize = grid.folderChildIconSizePx;
191         } else if (mDisplay == DISPLAY_SEARCH_RESULT) {
192             defaultIconSize = getResources().getDimensionPixelSize(R.dimen.search_row_icon_size);
193         } else if (mDisplay == DISPLAY_SEARCH_RESULT_SMALL) {
194             defaultIconSize = getResources().getDimensionPixelSize(
195                     R.dimen.search_row_small_icon_size);
196         } else if (mDisplay == DISPLAY_TASKBAR) {
197             defaultIconSize = grid.iconSizePx;
198         } else {
199             // widget_selection or shortcut_popup
200             defaultIconSize = grid.iconSizePx;
201         }
202 
203         mCenterVertically = a.getBoolean(R.styleable.BubbleTextView_centerVertically, false);
204 
205         mIconSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconSizeOverride,
206                 defaultIconSize);
207         a.recycle();
208 
209         mLongPressHelper = new CheckLongPressHelper(this);
210 
211         mDotParams = new DotRenderer.DrawParams();
212 
213         setEllipsize(TruncateAt.END);
214         setAccessibilityDelegate(mActivity.getAccessibilityDelegate());
215         setTextAlpha(1f);
216     }
217 
218     @Override
onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)219     protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
220         // Disable marques when not focused to that, so that updating text does not cause relayout.
221         setEllipsize(focused ? TruncateAt.MARQUEE : TruncateAt.END);
222         super.onFocusChanged(focused, direction, previouslyFocusedRect);
223     }
224 
225     /**
226      * Resets the view so it can be recycled.
227      */
reset()228     public void reset() {
229         mDotInfo = null;
230         mDotParams.color = Color.TRANSPARENT;
231         cancelDotScaleAnim();
232         mDotParams.scale = 0f;
233         mForceHideDot = false;
234         setBackground(null);
235     }
236 
cancelDotScaleAnim()237     private void cancelDotScaleAnim() {
238         if (mDotScaleAnim != null) {
239             mDotScaleAnim.cancel();
240         }
241     }
242 
animateDotScale(float... dotScales)243     private void animateDotScale(float... dotScales) {
244         cancelDotScaleAnim();
245         mDotScaleAnim = ObjectAnimator.ofFloat(this, DOT_SCALE_PROPERTY, dotScales);
246         mDotScaleAnim.addListener(new AnimatorListenerAdapter() {
247             @Override
248             public void onAnimationEnd(Animator animation) {
249                 mDotScaleAnim = null;
250             }
251         });
252         mDotScaleAnim.start();
253     }
254 
255     @UiThread
applyFromWorkspaceItem(WorkspaceItemInfo info)256     public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
257         applyFromWorkspaceItem(info, false);
258     }
259 
260     @Override
setAccessibilityDelegate(AccessibilityDelegate delegate)261     public void setAccessibilityDelegate(AccessibilityDelegate delegate) {
262         if (delegate instanceof LauncherAccessibilityDelegate) {
263             super.setAccessibilityDelegate(delegate);
264         } else {
265             // NO-OP
266             // Workaround for b/129745295 where RecyclerView is setting our Accessibility
267             // delegate incorrectly. There are no cases when we shouldn't be using the
268             // LauncherAccessibilityDelegate for BubbleTextView.
269         }
270     }
271 
272     @UiThread
applyFromWorkspaceItem(WorkspaceItemInfo info, boolean promiseStateChanged)273     public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean promiseStateChanged) {
274         applyIconAndLabel(info);
275         setTag(info);
276         applyLoadingState(promiseStateChanged);
277         applyDotState(info, false /* animate */);
278         setDownloadStateContentDescription(info, info.getProgressLevel());
279     }
280 
281     @UiThread
applyFromApplicationInfo(AppInfo info)282     public void applyFromApplicationInfo(AppInfo info) {
283         applyIconAndLabel(info);
284 
285         // We don't need to check the info since it's not a WorkspaceItemInfo
286         super.setTag(info);
287 
288         // Verify high res immediately
289         verifyHighRes();
290 
291         if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) {
292             applyProgressLevel();
293         }
294         applyDotState(info, false /* animate */);
295         setDownloadStateContentDescription(info, info.getProgressLevel());
296     }
297 
298     /**
299      * Apply label and tag using a generic {@link ItemInfoWithIcon}
300      */
301     @UiThread
applyFromItemInfoWithIcon(ItemInfoWithIcon info)302     public void applyFromItemInfoWithIcon(ItemInfoWithIcon info) {
303         applyIconAndLabel(info);
304         // We don't need to check the info since it's not a WorkspaceItemInfo
305         super.setTag(info);
306 
307         // Verify high res immediately
308         verifyHighRes();
309 
310         setDownloadStateContentDescription(info, info.getProgressLevel());
311     }
312 
313     /**
314      * Apply label and tag using a {@link SearchActionItemInfo}
315      */
316     @UiThread
applyFromSearchActionItemInfo(SearchActionItemInfo searchActionItemInfo)317     public void applyFromSearchActionItemInfo(SearchActionItemInfo searchActionItemInfo) {
318         applyIconAndLabel(searchActionItemInfo);
319         setTag(searchActionItemInfo);
320     }
321 
322     @UiThread
applyIconAndLabel(ItemInfoWithIcon info)323     protected void applyIconAndLabel(ItemInfoWithIcon info) {
324         boolean useTheme = mDisplay == DISPLAY_WORKSPACE || mDisplay == DISPLAY_FOLDER;
325         FastBitmapDrawable iconDrawable = info.newIcon(getContext(), useTheme);
326         mDotParams.color = IconPalette.getMutedColor(iconDrawable.getIconColor(), 0.54f);
327 
328         setIcon(iconDrawable);
329         applyLabel(info);
330     }
331 
332     @UiThread
applyLabel(ItemInfoWithIcon info)333     private void applyLabel(ItemInfoWithIcon info) {
334         setText(info.title);
335         if (info.contentDescription != null) {
336             setContentDescription(info.isDisabled()
337                     ? getContext().getString(R.string.disabled_app_label, info.contentDescription)
338                     : info.contentDescription);
339         }
340     }
341 
342     /**
343      * Overrides the default long press timeout.
344      */
setLongPressTimeoutFactor(float longPressTimeoutFactor)345     public void setLongPressTimeoutFactor(float longPressTimeoutFactor) {
346         mLongPressHelper.setLongPressTimeoutFactor(longPressTimeoutFactor);
347     }
348 
349     @Override
refreshDrawableState()350     public void refreshDrawableState() {
351         if (!mIgnorePressedStateChange) {
352             super.refreshDrawableState();
353         }
354     }
355 
356     @Override
onCreateDrawableState(int extraSpace)357     protected int[] onCreateDrawableState(int extraSpace) {
358         final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
359         if (mStayPressed) {
360             mergeDrawableStates(drawableState, STATE_PRESSED);
361         }
362         return drawableState;
363     }
364 
365     /** Returns the icon for this view. */
getIcon()366     public FastBitmapDrawable getIcon() {
367         return mIcon;
368     }
369 
370     @Override
onTouchEvent(MotionEvent event)371     public boolean onTouchEvent(MotionEvent event) {
372         // ignore events if they happen in padding area
373         if (event.getAction() == MotionEvent.ACTION_DOWN
374                 && shouldIgnoreTouchDown(event.getX(), event.getY())) {
375             return false;
376         }
377         if (isLongClickable()) {
378             super.onTouchEvent(event);
379             mLongPressHelper.onTouchEvent(event);
380             // Keep receiving the rest of the events
381             return true;
382         } else {
383             return super.onTouchEvent(event);
384         }
385     }
386 
387     /**
388      * Returns true if the touch down at the provided position be ignored
389      */
shouldIgnoreTouchDown(float x, float y)390     protected boolean shouldIgnoreTouchDown(float x, float y) {
391         return y < getPaddingTop()
392                 || x < getPaddingLeft()
393                 || y > getHeight() - getPaddingBottom()
394                 || x > getWidth() - getPaddingRight();
395     }
396 
setStayPressed(boolean stayPressed)397     void setStayPressed(boolean stayPressed) {
398         mStayPressed = stayPressed;
399         refreshDrawableState();
400     }
401 
402     @Override
onVisibilityAggregated(boolean isVisible)403     public void onVisibilityAggregated(boolean isVisible) {
404         super.onVisibilityAggregated(isVisible);
405         if (mIcon != null) {
406             mIcon.setVisible(isVisible, false);
407         }
408     }
409 
clearPressedBackground()410     void clearPressedBackground() {
411         setPressed(false);
412         setStayPressed(false);
413     }
414 
415     @Override
onKeyUp(int keyCode, KeyEvent event)416     public boolean onKeyUp(int keyCode, KeyEvent event) {
417         // Unlike touch events, keypress event propagate pressed state change immediately,
418         // without waiting for onClickHandler to execute. Disable pressed state changes here
419         // to avoid flickering.
420         mIgnorePressedStateChange = true;
421         boolean result = super.onKeyUp(keyCode, event);
422         mIgnorePressedStateChange = false;
423         refreshDrawableState();
424         return result;
425     }
426 
427     @SuppressWarnings("wrongcall")
drawWithoutDot(Canvas canvas)428     protected void drawWithoutDot(Canvas canvas) {
429         super.onDraw(canvas);
430     }
431 
432     @Override
onDraw(Canvas canvas)433     public void onDraw(Canvas canvas) {
434         super.onDraw(canvas);
435         drawDotIfNecessary(canvas);
436     }
437 
438     /**
439      * Draws the notification dot in the top right corner of the icon bounds.
440      *
441      * @param canvas The canvas to draw to.
442      */
drawDotIfNecessary(Canvas canvas)443     protected void drawDotIfNecessary(Canvas canvas) {
444         if (!mForceHideDot && (hasDot() || mDotParams.scale > 0)) {
445             getIconBounds(mDotParams.iconBounds);
446             Utilities.scaleRectAboutCenter(mDotParams.iconBounds,
447                     IconShape.getNormalizationScale());
448             final int scrollX = getScrollX();
449             final int scrollY = getScrollY();
450             canvas.translate(scrollX, scrollY);
451             mDotRenderer.draw(canvas, mDotParams);
452             canvas.translate(-scrollX, -scrollY);
453         }
454     }
455 
456     @Override
setForceHideDot(boolean forceHideDot)457     public void setForceHideDot(boolean forceHideDot) {
458         if (mForceHideDot == forceHideDot) {
459             return;
460         }
461         mForceHideDot = forceHideDot;
462 
463         if (forceHideDot) {
464             invalidate();
465         } else if (hasDot()) {
466             animateDotScale(0, 1);
467         }
468     }
469 
hasDot()470     private boolean hasDot() {
471         return mDotInfo != null;
472     }
473 
getIconBounds(Rect outBounds)474     public void getIconBounds(Rect outBounds) {
475         getIconBounds(this, outBounds, mIconSize);
476     }
477 
getIconBounds(View iconView, Rect outBounds, int iconSize)478     public static void getIconBounds(View iconView, Rect outBounds, int iconSize) {
479         int top = iconView.getPaddingTop();
480         int left = (iconView.getWidth() - iconSize) / 2;
481         int right = left + iconSize;
482         int bottom = top + iconSize;
483         outBounds.set(left, top, right, bottom);
484     }
485 
486 
487     /**
488      * Sets whether to vertically center the content.
489      */
setCenterVertically(boolean centerVertically)490     public void setCenterVertically(boolean centerVertically) {
491         mCenterVertically = centerVertically;
492     }
493 
494     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)495     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
496         if (mCenterVertically) {
497             Paint.FontMetrics fm = getPaint().getFontMetrics();
498             int cellHeightPx = mIconSize + getCompoundDrawablePadding() +
499                     (int) Math.ceil(fm.bottom - fm.top);
500             int height = MeasureSpec.getSize(heightMeasureSpec);
501             setPadding(getPaddingLeft(), (height - cellHeightPx) / 2, getPaddingRight(),
502                     getPaddingBottom());
503         }
504         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
505     }
506 
507     @Override
setTextColor(int color)508     public void setTextColor(int color) {
509         mTextColor = color;
510         super.setTextColor(getModifiedColor());
511     }
512 
513     @Override
setTextColor(ColorStateList colors)514     public void setTextColor(ColorStateList colors) {
515         mTextColor = colors.getDefaultColor();
516         if (Float.compare(mTextAlpha, 1) == 0) {
517             super.setTextColor(colors);
518         } else {
519             super.setTextColor(getModifiedColor());
520         }
521     }
522 
shouldTextBeVisible()523     public boolean shouldTextBeVisible() {
524         // Text should be visible everywhere but the hotseat.
525         Object tag = getParent() instanceof FolderIcon ? ((View) getParent()).getTag() : getTag();
526         ItemInfo info = tag instanceof ItemInfo ? (ItemInfo) tag : null;
527         return info == null || (info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT
528                 && info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION);
529     }
530 
setTextVisibility(boolean visible)531     public void setTextVisibility(boolean visible) {
532         setTextAlpha(visible ? 1 : 0);
533     }
534 
setTextAlpha(float alpha)535     private void setTextAlpha(float alpha) {
536         mTextAlpha = alpha;
537         super.setTextColor(getModifiedColor());
538     }
539 
getModifiedColor()540     private int getModifiedColor() {
541         if (mTextAlpha == 0) {
542             // Special case to prevent text shadows in high contrast mode
543             return Color.TRANSPARENT;
544         }
545         return setColorAlphaBound(mTextColor, Math.round(Color.alpha(mTextColor) * mTextAlpha));
546     }
547 
548     /**
549      * Creates an animator to fade the text in or out.
550      *
551      * @param fadeIn Whether the text should fade in or fade out.
552      */
createTextAlphaAnimator(boolean fadeIn)553     public ObjectAnimator createTextAlphaAnimator(boolean fadeIn) {
554         float toAlpha = shouldTextBeVisible() && fadeIn ? 1 : 0;
555         return ObjectAnimator.ofFloat(this, TEXT_ALPHA_PROPERTY, toAlpha);
556     }
557 
558     @Override
cancelLongPress()559     public void cancelLongPress() {
560         super.cancelLongPress();
561         mLongPressHelper.cancelLongPress();
562     }
563 
564     /**
565      * Applies the loading progress value to the progress bar.
566      *
567      * If this app is installing, the progress bar will be updated with the installation progress.
568      * If this app is installed and downloading incrementally, the progress bar will be updated
569      * with the total download progress.
570      */
applyLoadingState(boolean promiseStateChanged)571     public void applyLoadingState(boolean promiseStateChanged) {
572         if (getTag() instanceof ItemInfoWithIcon) {
573             WorkspaceItemInfo info = (WorkspaceItemInfo) getTag();
574             if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE)
575                     != 0) {
576                 updateProgressBarUi(info.getProgressLevel() == 100);
577             } else if (info.hasPromiseIconUi() || (info.runtimeStatusFlags
578                         & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
579                 updateProgressBarUi(promiseStateChanged);
580             }
581         }
582     }
583 
updateProgressBarUi(boolean maybePerformFinishedAnimation)584     private void updateProgressBarUi(boolean maybePerformFinishedAnimation) {
585         PreloadIconDrawable preloadDrawable = applyProgressLevel();
586         if (preloadDrawable != null && maybePerformFinishedAnimation) {
587             preloadDrawable.maybePerformFinishedAnimation();
588         }
589     }
590 
591     /** Applies the given progress level to the this icon's progress bar. */
592     @Nullable
applyProgressLevel()593     public PreloadIconDrawable applyProgressLevel() {
594         if (!(getTag() instanceof ItemInfoWithIcon)) {
595             return null;
596         }
597 
598         ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
599         int progressLevel = info.getProgressLevel();
600         if (progressLevel >= 100) {
601             setContentDescription(info.contentDescription != null
602                     ? info.contentDescription : "");
603         } else if (progressLevel > 0) {
604             setDownloadStateContentDescription(info, progressLevel);
605         } else {
606             setContentDescription(getContext()
607                     .getString(R.string.app_waiting_download_title, info.title));
608         }
609         if (mIcon != null) {
610             PreloadIconDrawable preloadIconDrawable;
611             if (mIcon instanceof PreloadIconDrawable) {
612                 preloadIconDrawable = (PreloadIconDrawable) mIcon;
613                 preloadIconDrawable.setLevel(progressLevel);
614                 preloadIconDrawable.setIsDisabled(!info.isAppStartable());
615             } else {
616                 preloadIconDrawable = makePreloadIcon();
617                 setIcon(preloadIconDrawable);
618             }
619             return preloadIconDrawable;
620         }
621         return null;
622     }
623 
624     /**
625      * Creates a PreloadIconDrawable with the appropriate progress level without mutating this
626      * object.
627      */
628     @Nullable
makePreloadIcon()629     public PreloadIconDrawable makePreloadIcon() {
630         if (!(getTag() instanceof ItemInfoWithIcon)) {
631             return null;
632         }
633 
634         ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
635         int progressLevel = info.getProgressLevel();
636         final PreloadIconDrawable preloadDrawable = newPendingIcon(getContext(), info);
637 
638         preloadDrawable.setLevel(progressLevel);
639         preloadDrawable.setIsDisabled(!info.isAppStartable());
640 
641         return preloadDrawable;
642     }
643 
applyDotState(ItemInfo itemInfo, boolean animate)644     public void applyDotState(ItemInfo itemInfo, boolean animate) {
645         if (mIcon instanceof FastBitmapDrawable) {
646             boolean wasDotted = mDotInfo != null;
647             mDotInfo = mActivity.getDotInfoForItem(itemInfo);
648             boolean isDotted = mDotInfo != null;
649             float newDotScale = isDotted ? 1f : 0;
650             if (mDisplay == DISPLAY_ALL_APPS) {
651                 mDotRenderer = mActivity.getDeviceProfile().mDotRendererAllApps;
652             } else {
653                 mDotRenderer = mActivity.getDeviceProfile().mDotRendererWorkSpace;
654             }
655             if (wasDotted || isDotted) {
656                 // Animate when a dot is first added or when it is removed.
657                 if (animate && (wasDotted ^ isDotted) && isShown()) {
658                     animateDotScale(newDotScale);
659                 } else {
660                     cancelDotScaleAnim();
661                     mDotParams.scale = newDotScale;
662                     invalidate();
663                 }
664             }
665             if (itemInfo.contentDescription != null) {
666                 if (itemInfo.isDisabled()) {
667                     setContentDescription(getContext().getString(R.string.disabled_app_label,
668                             itemInfo.contentDescription));
669                 } else if (hasDot()) {
670                     int count = mDotInfo.getNotificationCount();
671                     setContentDescription(getContext().getResources().getQuantityString(
672                             R.plurals.dotted_app_label, count, itemInfo.contentDescription, count));
673                 } else {
674                     setContentDescription(itemInfo.contentDescription);
675                 }
676             }
677         }
678     }
679 
setDownloadStateContentDescription(ItemInfoWithIcon info, int progressLevel)680     private void setDownloadStateContentDescription(ItemInfoWithIcon info, int progressLevel) {
681         if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK)
682                 != 0) {
683             String percentageString = NumberFormat.getPercentInstance()
684                     .format(progressLevel * 0.01);
685             if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
686                 setContentDescription(getContext()
687                         .getString(
688                             R.string.app_installing_title, info.title, percentageString));
689             } else if ((info.runtimeStatusFlags
690                     & ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) {
691                 setContentDescription(getContext()
692                         .getString(
693                             R.string.app_downloading_title, info.title, percentageString));
694             }
695         }
696     }
697 
698     /**
699      * Sets the icon for this view based on the layout direction.
700      */
setIcon(FastBitmapDrawable icon)701     protected void setIcon(FastBitmapDrawable icon) {
702         if (mIsIconVisible) {
703             applyCompoundDrawables(icon);
704         }
705         mIcon = icon;
706         if (mIcon != null) {
707             mIcon.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
708         }
709     }
710 
711     @Override
setIconVisible(boolean visible)712     public void setIconVisible(boolean visible) {
713         mIsIconVisible = visible;
714         if (!mIsIconVisible) {
715             resetIconScale();
716         }
717         Drawable icon = visible ? mIcon : new ColorDrawable(Color.TRANSPARENT);
718         applyCompoundDrawables(icon);
719     }
720 
iconUpdateAnimationEnabled()721     protected boolean iconUpdateAnimationEnabled() {
722         return mEnableIconUpdateAnimation;
723     }
724 
applyCompoundDrawables(Drawable icon)725     protected void applyCompoundDrawables(Drawable icon) {
726         // If we had already set an icon before, disable relayout as the icon size is the
727         // same as before.
728         mDisableRelayout = mIcon != null;
729 
730         icon.setBounds(0, 0, mIconSize, mIconSize);
731 
732         updateIcon(icon);
733 
734         // If the current icon is a placeholder color, animate its update.
735         if (mIcon != null
736                 && mIcon instanceof PlaceHolderIconDrawable
737                 && iconUpdateAnimationEnabled()) {
738             ((PlaceHolderIconDrawable) mIcon).animateIconUpdate(icon);
739         }
740 
741         mDisableRelayout = false;
742     }
743 
744     @Override
requestLayout()745     public void requestLayout() {
746         if (!mDisableRelayout) {
747             super.requestLayout();
748         }
749     }
750 
751     /**
752      * Applies the item info if it is same as what the view is pointing to currently.
753      */
754     @Override
reapplyItemInfo(ItemInfoWithIcon info)755     public void reapplyItemInfo(ItemInfoWithIcon info) {
756         if (getTag() == info) {
757             mIconLoadRequest = null;
758             mDisableRelayout = true;
759             mEnableIconUpdateAnimation = true;
760 
761             // Optimization: Starting in N, pre-uploads the bitmap to RenderThread.
762             info.bitmap.icon.prepareToDraw();
763 
764             if (info instanceof AppInfo) {
765                 applyFromApplicationInfo((AppInfo) info);
766             } else if (info instanceof WorkspaceItemInfo) {
767                 applyFromWorkspaceItem((WorkspaceItemInfo) info);
768                 mActivity.invalidateParent(info);
769             } else if (info instanceof PackageItemInfo) {
770                 applyFromItemInfoWithIcon((PackageItemInfo) info);
771             } else if (info instanceof SearchActionItemInfo) {
772                 applyFromSearchActionItemInfo((SearchActionItemInfo) info);
773             }
774 
775             mDisableRelayout = false;
776             mEnableIconUpdateAnimation = false;
777         }
778     }
779 
780     /**
781      * Verifies that the current icon is high-res otherwise posts a request to load the icon.
782      */
verifyHighRes()783     public void verifyHighRes() {
784         if (mIconLoadRequest != null) {
785             mIconLoadRequest.cancel();
786             mIconLoadRequest = null;
787         }
788         if (getTag() instanceof ItemInfoWithIcon) {
789             ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
790             if (info.usingLowResIcon()) {
791                 mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache()
792                         .updateIconInBackground(BubbleTextView.this, info);
793             }
794         }
795     }
796 
getIconSize()797     public int getIconSize() {
798         return mIconSize;
799     }
800 
updateTranslation()801     private void updateTranslation() {
802         super.setTranslationX(mTranslationForReorderBounce.x + mTranslationForReorderPreview.x);
803         super.setTranslationY(mTranslationForReorderBounce.y + mTranslationForReorderPreview.y);
804     }
805 
setReorderBounceOffset(float x, float y)806     public void setReorderBounceOffset(float x, float y) {
807         mTranslationForReorderBounce.set(x, y);
808         updateTranslation();
809     }
810 
getReorderBounceOffset(PointF offset)811     public void getReorderBounceOffset(PointF offset) {
812         offset.set(mTranslationForReorderBounce);
813     }
814 
815     @Override
setReorderPreviewOffset(float x, float y)816     public void setReorderPreviewOffset(float x, float y) {
817         mTranslationForReorderPreview.set(x, y);
818         updateTranslation();
819     }
820 
821     @Override
getReorderPreviewOffset(PointF offset)822     public void getReorderPreviewOffset(PointF offset) {
823         offset.set(mTranslationForReorderPreview);
824     }
825 
setReorderBounceScale(float scale)826     public void setReorderBounceScale(float scale) {
827         mScaleForReorderBounce = scale;
828         super.setScaleX(scale);
829         super.setScaleY(scale);
830     }
831 
getReorderBounceScale()832     public float getReorderBounceScale() {
833         return mScaleForReorderBounce;
834     }
835 
getView()836     public View getView() {
837         return this;
838     }
839 
840     @Override
getViewType()841     public int getViewType() {
842         return DRAGGABLE_ICON;
843     }
844 
845     @Override
getWorkspaceVisualDragBounds(Rect bounds)846     public void getWorkspaceVisualDragBounds(Rect bounds) {
847         DeviceProfile grid = mActivity.getDeviceProfile();
848         BubbleTextView.getIconBounds(this, bounds, grid.iconSizePx);
849     }
850 
getIconSizeForDisplay(int display)851     private int getIconSizeForDisplay(int display) {
852         DeviceProfile grid = mActivity.getDeviceProfile();
853         switch (display) {
854             case DISPLAY_ALL_APPS:
855                 return grid.allAppsIconSizePx;
856             case DISPLAY_WORKSPACE:
857             case DISPLAY_FOLDER:
858             default:
859                 return grid.iconSizePx;
860         }
861     }
862 
getSourceVisualDragBounds(Rect bounds)863     public void getSourceVisualDragBounds(Rect bounds) {
864         BubbleTextView.getIconBounds(this, bounds, getIconSizeForDisplay(mDisplay));
865     }
866 
867     @Override
prepareDrawDragView()868     public SafeCloseable prepareDrawDragView() {
869         resetIconScale();
870         setForceHideDot(true);
871         return () -> { };
872     }
873 
resetIconScale()874     private void resetIconScale() {
875         if (mIcon instanceof FastBitmapDrawable) {
876             ((FastBitmapDrawable) mIcon).resetScale();
877         }
878     }
879 
updateIcon(Drawable newIcon)880     private void updateIcon(Drawable newIcon) {
881         if (mLayoutHorizontal) {
882             setCompoundDrawablesRelative(newIcon, null, null, null);
883         } else {
884             setCompoundDrawables(null, newIcon, null, null);
885         }
886     }
887 }
888