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