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 android.text.Layout.Alignment.ALIGN_NORMAL;
20 
21 import static com.android.launcher3.Flags.enableCursorHoverStates;
22 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
23 import static com.android.launcher3.icons.BitmapInfo.FLAG_NO_BADGE;
24 import static com.android.launcher3.icons.BitmapInfo.FLAG_SKIP_USER_BADGE;
25 import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
26 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
27 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE;
28 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
29 
30 import android.animation.Animator;
31 import android.animation.AnimatorListenerAdapter;
32 import android.animation.ObjectAnimator;
33 import android.content.Context;
34 import android.content.res.ColorStateList;
35 import android.content.res.TypedArray;
36 import android.graphics.Canvas;
37 import android.graphics.Color;
38 import android.graphics.Paint;
39 import android.graphics.Rect;
40 import android.graphics.drawable.ColorDrawable;
41 import android.graphics.drawable.Drawable;
42 import android.icu.text.MessageFormat;
43 import android.text.StaticLayout;
44 import android.text.TextPaint;
45 import android.text.TextUtils;
46 import android.text.TextUtils.TruncateAt;
47 import android.util.AttributeSet;
48 import android.util.Property;
49 import android.util.Size;
50 import android.util.TypedValue;
51 import android.view.KeyEvent;
52 import android.view.MotionEvent;
53 import android.view.View;
54 import android.view.ViewDebug;
55 import android.widget.TextView;
56 
57 import androidx.annotation.Nullable;
58 import androidx.annotation.UiThread;
59 import androidx.annotation.VisibleForTesting;
60 
61 import com.android.launcher3.accessibility.BaseAccessibilityDelegate;
62 import com.android.launcher3.dot.DotInfo;
63 import com.android.launcher3.dragndrop.DragOptions.PreDragCondition;
64 import com.android.launcher3.dragndrop.DraggableView;
65 import com.android.launcher3.folder.FolderIcon;
66 import com.android.launcher3.graphics.IconShape;
67 import com.android.launcher3.graphics.PreloadIconDrawable;
68 import com.android.launcher3.icons.DotRenderer;
69 import com.android.launcher3.icons.FastBitmapDrawable;
70 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
71 import com.android.launcher3.icons.PlaceHolderIconDrawable;
72 import com.android.launcher3.model.data.AppInfo;
73 import com.android.launcher3.model.data.ItemInfo;
74 import com.android.launcher3.model.data.ItemInfoWithIcon;
75 import com.android.launcher3.model.data.WorkspaceItemInfo;
76 import com.android.launcher3.popup.PopupContainerWithArrow;
77 import com.android.launcher3.search.StringMatcherUtility;
78 import com.android.launcher3.util.CancellableTask;
79 import com.android.launcher3.util.IntArray;
80 import com.android.launcher3.util.MultiTranslateDelegate;
81 import com.android.launcher3.util.SafeCloseable;
82 import com.android.launcher3.util.ShortcutUtil;
83 import com.android.launcher3.util.Themes;
84 import com.android.launcher3.views.ActivityContext;
85 import com.android.launcher3.views.IconLabelDotView;
86 
87 import java.text.NumberFormat;
88 import java.util.HashMap;
89 import java.util.Locale;
90 
91 /**
92  * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan
93  * because we want to make the bubble taller than the text and TextView's clip is
94  * too aggressive.
95  */
96 public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
97         IconLabelDotView, DraggableView, Reorderable {
98 
99     public static final int DISPLAY_WORKSPACE = 0;
100     public static final int DISPLAY_ALL_APPS = 1;
101     public static final int DISPLAY_FOLDER = 2;
102     public static final int DISPLAY_TASKBAR = 5;
103     public static final int DISPLAY_SEARCH_RESULT = 6;
104     public static final int DISPLAY_SEARCH_RESULT_SMALL = 7;
105     public static final int DISPLAY_PREDICTION_ROW = 8;
106     public static final int DISPLAY_SEARCH_RESULT_APP_ROW = 9;
107 
108     private static final float MIN_LETTER_SPACING = -0.05f;
109     private static final int MAX_SEARCH_LOOP_COUNT = 20;
110     private static final Character NEW_LINE = '\n';
111     private static final String EMPTY = "";
112     private static final StringMatcherUtility.StringMatcher MATCHER =
113             StringMatcherUtility.StringMatcher.getInstance();
114 
115     private static final int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed};
116 
117     private float mScaleForReorderBounce = 1f;
118 
119     private IntArray mBreakPointsIntArray;
120     private CharSequence mLastOriginalText;
121     private CharSequence mLastModifiedText;
122 
123     private static final Property<BubbleTextView, Float> DOT_SCALE_PROPERTY
124             = new Property<BubbleTextView, Float>(Float.TYPE, "dotScale") {
125         @Override
126         public Float get(BubbleTextView bubbleTextView) {
127             return bubbleTextView.mDotParams.scale;
128         }
129 
130         @Override
131         public void set(BubbleTextView bubbleTextView, Float value) {
132             bubbleTextView.mDotParams.scale = value;
133             bubbleTextView.invalidate();
134         }
135     };
136 
137     public static final Property<BubbleTextView, Float> TEXT_ALPHA_PROPERTY
138             = new Property<BubbleTextView, Float>(Float.class, "textAlpha") {
139         @Override
140         public Float get(BubbleTextView bubbleTextView) {
141             return bubbleTextView.mTextAlpha;
142         }
143 
144         @Override
145         public void set(BubbleTextView bubbleTextView, Float alpha) {
146             bubbleTextView.setTextAlpha(alpha);
147         }
148     };
149 
150     private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this);
151     private final ActivityContext mActivity;
152     private FastBitmapDrawable mIcon;
153     private DeviceProfile mDeviceProfile;
154     private boolean mCenterVertically;
155 
156     protected int mDisplay;
157 
158     private final CheckLongPressHelper mLongPressHelper;
159 
160     private boolean mLayoutHorizontal;
161     private final boolean mIsRtl;
162     private final int mIconSize;
163 
164     @ViewDebug.ExportedProperty(category = "launcher")
165     private boolean mHideBadge = false;
166     @ViewDebug.ExportedProperty(category = "launcher")
167     private boolean mSkipUserBadge = false;
168     @ViewDebug.ExportedProperty(category = "launcher")
169     private boolean mIsIconVisible = true;
170     @ViewDebug.ExportedProperty(category = "launcher")
171     private int mTextColor;
172     @ViewDebug.ExportedProperty(category = "launcher")
173     private ColorStateList mTextColorStateList;
174     @ViewDebug.ExportedProperty(category = "launcher")
175     private float mTextAlpha = 1;
176 
177     @ViewDebug.ExportedProperty(category = "launcher")
178     private DotInfo mDotInfo;
179     private DotRenderer mDotRenderer;
180     private Locale mCurrentLocale;
181     @ViewDebug.ExportedProperty(category = "launcher", deepExport = true)
182     protected DotRenderer.DrawParams mDotParams;
183     private Animator mDotScaleAnim;
184     private boolean mForceHideDot;
185 
186     // These fields, related to showing running apps, are only used for Taskbar.
187     private final Size mRunningAppIndicatorSize;
188     private final int mRunningAppIndicatorTopMargin;
189     private final Size mMinimizedAppIndicatorSize;
190     private final int mMinimizedAppIndicatorTopMargin;
191     private final Paint mRunningAppIndicatorPaint;
192     private final Rect mRunningAppIconBounds = new Rect();
193     private RunningAppState mRunningAppState;
194 
195     /**
196      * Various options for the running state of an app.
197      */
198     public enum RunningAppState {
199         NOT_RUNNING,
200         RUNNING,
201         MINIMIZED,
202     }
203 
204     @ViewDebug.ExportedProperty(category = "launcher")
205     private boolean mStayPressed;
206     @ViewDebug.ExportedProperty(category = "launcher")
207     private boolean mIgnorePressedStateChange;
208     @ViewDebug.ExportedProperty(category = "launcher")
209     private boolean mDisableRelayout = false;
210 
211     private CancellableTask mIconLoadRequest;
212 
213     private boolean mEnableIconUpdateAnimation = false;
214 
BubbleTextView(Context context)215     public BubbleTextView(Context context) {
216         this(context, null, 0);
217     }
218 
BubbleTextView(Context context, AttributeSet attrs)219     public BubbleTextView(Context context, AttributeSet attrs) {
220         this(context, attrs, 0);
221     }
222 
BubbleTextView(Context context, AttributeSet attrs, int defStyle)223     public BubbleTextView(Context context, AttributeSet attrs, int defStyle) {
224         super(context, attrs, defStyle);
225         mActivity = ActivityContext.lookupContext(context);
226         FastBitmapDrawable.setFlagHoverEnabled(enableCursorHoverStates());
227 
228         TypedArray a = context.obtainStyledAttributes(attrs,
229                 R.styleable.BubbleTextView, defStyle, 0);
230         mLayoutHorizontal = a.getBoolean(R.styleable.BubbleTextView_layoutHorizontal, false);
231         mIsRtl = (getResources().getConfiguration().getLayoutDirection()
232                 == View.LAYOUT_DIRECTION_RTL);
233         mDeviceProfile = mActivity.getDeviceProfile();
234         mCenterVertically = a.getBoolean(R.styleable.BubbleTextView_centerVertically, false);
235 
236         mDisplay = a.getInteger(R.styleable.BubbleTextView_iconDisplay, DISPLAY_WORKSPACE);
237         final int defaultIconSize;
238         if (mDisplay == DISPLAY_WORKSPACE) {
239             setTextSize(TypedValue.COMPLEX_UNIT_PX, mDeviceProfile.iconTextSizePx);
240             setCompoundDrawablePadding(mDeviceProfile.iconDrawablePaddingPx);
241             defaultIconSize = mDeviceProfile.iconSizePx;
242             setCenterVertically(mDeviceProfile.iconCenterVertically);
243         } else if (mDisplay == DISPLAY_ALL_APPS || mDisplay == DISPLAY_PREDICTION_ROW
244                 || mDisplay == DISPLAY_SEARCH_RESULT_APP_ROW) {
245             setTextSize(TypedValue.COMPLEX_UNIT_PX, mDeviceProfile.allAppsIconTextSizePx);
246             setCompoundDrawablePadding(mDeviceProfile.allAppsIconDrawablePaddingPx);
247             defaultIconSize = mDeviceProfile.allAppsIconSizePx;
248         } else if (mDisplay == DISPLAY_FOLDER) {
249             setTextSize(TypedValue.COMPLEX_UNIT_PX, mDeviceProfile.folderChildTextSizePx);
250             setCompoundDrawablePadding(mDeviceProfile.folderChildDrawablePaddingPx);
251             defaultIconSize = mDeviceProfile.folderChildIconSizePx;
252         } else if (mDisplay == DISPLAY_SEARCH_RESULT) {
253             setTextSize(TypedValue.COMPLEX_UNIT_PX, mDeviceProfile.allAppsIconTextSizePx);
254             defaultIconSize = getResources().getDimensionPixelSize(R.dimen.search_row_icon_size);
255         } else if (mDisplay == DISPLAY_SEARCH_RESULT_SMALL) {
256             defaultIconSize = getResources().getDimensionPixelSize(
257                     R.dimen.search_row_small_icon_size);
258         } else if (mDisplay == DISPLAY_TASKBAR) {
259             defaultIconSize = mDeviceProfile.taskbarIconSize;
260         } else {
261             // widget_selection or shortcut_popup
262             defaultIconSize = mDeviceProfile.iconSizePx;
263         }
264 
265 
266         mIconSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconSizeOverride,
267                 defaultIconSize);
268         a.recycle();
269 
270         mRunningAppIndicatorSize = new Size(
271                 getResources().getDimensionPixelSize(R.dimen.taskbar_running_app_indicator_width),
272                 getResources().getDimensionPixelSize(R.dimen.taskbar_running_app_indicator_height));
273         mMinimizedAppIndicatorSize = new Size(
274                 getResources().getDimensionPixelSize(R.dimen.taskbar_minimized_app_indicator_width),
275                 getResources().getDimensionPixelSize(
276                         R.dimen.taskbar_minimized_app_indicator_height));
277         mRunningAppIndicatorTopMargin =
278                 getResources().getDimensionPixelSize(
279                         R.dimen.taskbar_running_app_indicator_top_margin);
280         mMinimizedAppIndicatorTopMargin =
281                 getResources().getDimensionPixelSize(
282                         R.dimen.taskbar_minimized_app_indicator_top_margin);
283         mRunningAppIndicatorPaint = new Paint();
284         mRunningAppIndicatorPaint.setColor(getResources().getColor(
285                 R.color.taskbar_running_app_indicator_color, context.getTheme()));
286 
287         mLongPressHelper = new CheckLongPressHelper(this);
288 
289         mDotParams = new DotRenderer.DrawParams();
290 
291         mCurrentLocale = context.getResources().getConfiguration().locale;
292         setEllipsize(TruncateAt.END);
293         setAccessibilityDelegate(mActivity.getAccessibilityDelegate());
294         setTextAlpha(1f);
295     }
296 
297     @Override
onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)298     protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
299         // Disable marques when not focused to that, so that updating text does not cause relayout.
300         setEllipsize(focused ? TruncateAt.MARQUEE : TruncateAt.END);
301         super.onFocusChanged(focused, direction, previouslyFocusedRect);
302     }
303 
setHideBadge(boolean hideBadge)304     public void setHideBadge(boolean hideBadge) {
305         mHideBadge = hideBadge;
306     }
307 
setSkipUserBadge(boolean skipUserBadge)308     public void setSkipUserBadge(boolean skipUserBadge) {
309         mSkipUserBadge = skipUserBadge;
310     }
311 
312     /**
313      * Resets the view so it can be recycled.
314      */
reset()315     public void reset() {
316         mDotInfo = null;
317         mDotParams.dotColor = Color.TRANSPARENT;
318         mDotParams.appColor = Color.TRANSPARENT;
319         cancelDotScaleAnim();
320         mDotParams.scale = 0f;
321         mForceHideDot = false;
322         setBackground(null);
323 
324         setTag(null);
325         if (mIconLoadRequest != null) {
326             mIconLoadRequest.cancel();
327             mIconLoadRequest = null;
328         }
329         // Reset any shifty arrangements in case animation is disrupted.
330         setPivotY(0);
331         setAlpha(1);
332         setScaleY(1);
333         setTranslationY(0);
334         setMaxLines(1);
335         setVisibility(VISIBLE);
336     }
337 
cancelDotScaleAnim()338     private void cancelDotScaleAnim() {
339         if (mDotScaleAnim != null) {
340             mDotScaleAnim.cancel();
341         }
342     }
343 
animateDotScale(float... dotScales)344     public void animateDotScale(float... dotScales) {
345         cancelDotScaleAnim();
346         mDotScaleAnim = ObjectAnimator.ofFloat(this, DOT_SCALE_PROPERTY, dotScales);
347         mDotScaleAnim.addListener(new AnimatorListenerAdapter() {
348             @Override
349             public void onAnimationEnd(Animator animation) {
350                 mDotScaleAnim = null;
351             }
352         });
353         mDotScaleAnim.start();
354     }
355 
356     @UiThread
applyFromWorkspaceItem(WorkspaceItemInfo info)357     public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
358         applyFromWorkspaceItem(info, /* animate = */ false, /* staggerIndex = */ 0);
359     }
360 
361     @UiThread
applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex)362     public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex) {
363         applyFromWorkspaceItem(info, null);
364     }
365 
366     /**
367      * Returns whether the newInfo differs from the current getTag().
368      */
shouldAnimateIconChange(WorkspaceItemInfo newInfo)369     public boolean shouldAnimateIconChange(WorkspaceItemInfo newInfo) {
370         WorkspaceItemInfo oldInfo = getTag() instanceof WorkspaceItemInfo
371                 ? (WorkspaceItemInfo) getTag()
372                 : null;
373         boolean changedIcons = oldInfo != null && oldInfo.getTargetComponent() != null
374                 && newInfo.getTargetComponent() != null
375                 && !oldInfo.getTargetComponent().equals(newInfo.getTargetComponent());
376         return changedIcons && isShown();
377     }
378 
379     @Override
setAccessibilityDelegate(AccessibilityDelegate delegate)380     public void setAccessibilityDelegate(AccessibilityDelegate delegate) {
381         if (delegate instanceof BaseAccessibilityDelegate) {
382             super.setAccessibilityDelegate(delegate);
383         } else {
384             // NO-OP
385             // Workaround for b/129745295 where RecyclerView is setting our Accessibility
386             // delegate incorrectly. There are no cases when we shouldn't be using the
387             // LauncherAccessibilityDelegate for BubbleTextView.
388         }
389     }
390 
391     @UiThread
applyFromWorkspaceItem(WorkspaceItemInfo info, PreloadIconDrawable icon)392     public void applyFromWorkspaceItem(WorkspaceItemInfo info, PreloadIconDrawable icon) {
393         applyIconAndLabel(info);
394         setItemInfo(info);
395         applyLoadingState(icon);
396         applyDotState(info, false /* animate */);
397         setDownloadStateContentDescription(info, info.getProgressLevel());
398     }
399 
400     @UiThread
applyFromApplicationInfo(AppInfo info)401     public void applyFromApplicationInfo(AppInfo info) {
402         applyIconAndLabel(info);
403 
404         // We don't need to check the info since it's not a WorkspaceItemInfo
405         setItemInfo(info);
406 
407 
408         // Verify high res immediately
409         verifyHighRes();
410 
411         if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) {
412             applyProgressLevel();
413         }
414         applyDotState(info, false /* animate */);
415         setDownloadStateContentDescription(info, info.getProgressLevel());
416     }
417 
418     /**
419      * Apply label and tag using a generic {@link ItemInfoWithIcon}
420      */
421     @UiThread
applyFromItemInfoWithIcon(ItemInfoWithIcon info)422     public void applyFromItemInfoWithIcon(ItemInfoWithIcon info) {
423         applyIconAndLabel(info);
424         // We don't need to check the info since it's not a WorkspaceItemInfo
425         setItemInfo(info);
426 
427         // Verify high res immediately
428         verifyHighRes();
429 
430         setDownloadStateContentDescription(info, info.getProgressLevel());
431     }
432 
433     /** Updates whether the app this view represents is currently running. */
434     @UiThread
updateRunningState(RunningAppState runningAppState)435     public void updateRunningState(RunningAppState runningAppState) {
436         mRunningAppState = runningAppState;
437     }
438 
setItemInfo(ItemInfoWithIcon itemInfo)439     protected void setItemInfo(ItemInfoWithIcon itemInfo) {
440         setTag(itemInfo);
441     }
442 
443     @VisibleForTesting
444     @UiThread
applyIconAndLabel(ItemInfoWithIcon info)445     public void applyIconAndLabel(ItemInfoWithIcon info) {
446         int flags = shouldUseTheme() ? FLAG_THEMED : 0;
447         // Remove badge on icons smaller than 48dp.
448         if (mHideBadge || mDisplay == DISPLAY_SEARCH_RESULT_SMALL) {
449             flags |= FLAG_NO_BADGE;
450         }
451         if (mSkipUserBadge) {
452             flags |= FLAG_SKIP_USER_BADGE;
453         }
454         FastBitmapDrawable iconDrawable = info.newIcon(getContext(), flags);
455         mDotParams.appColor = iconDrawable.getIconColor();
456         mDotParams.dotColor = Themes.getAttrColor(getContext(), R.attr.notificationDotColor);
457         setIcon(iconDrawable);
458         applyLabel(info);
459     }
460 
shouldUseTheme()461     protected boolean shouldUseTheme() {
462         return (mDisplay == DISPLAY_WORKSPACE || mDisplay == DISPLAY_FOLDER
463                 || mDisplay == DISPLAY_TASKBAR) && Themes.isThemedIconEnabled(getContext());
464     }
465 
466     /**
467      * Only if actual text can be displayed in two line, the {@code true} value will be effective.
468      */
shouldUseTwoLine()469     protected boolean shouldUseTwoLine() {
470         return isCurrentLanguageEnglish() && (mDisplay == DISPLAY_ALL_APPS
471                 || mDisplay == DISPLAY_PREDICTION_ROW) && (Flags.enableTwolineToggle()
472                 && LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(getContext()));
473     }
474 
isCurrentLanguageEnglish()475     protected boolean isCurrentLanguageEnglish() {
476         return mCurrentLocale.equals(Locale.US);
477     }
478 
479     @UiThread
applyLabel(ItemInfo info)480     public void applyLabel(ItemInfo info) {
481         CharSequence label = info.title;
482         if (label != null) {
483             mLastOriginalText = label;
484             mLastModifiedText = mLastOriginalText;
485             mBreakPointsIntArray = StringMatcherUtility.getListOfBreakpoints(label, MATCHER);
486             setText(label);
487         }
488         if (info.contentDescription != null) {
489             setContentDescription(info.isDisabled()
490                     ? getContext().getString(R.string.disabled_app_label, info.contentDescription)
491                     : info.contentDescription);
492         }
493     }
494 
495     /** This is used for testing to forcefully set the display. */
496     @VisibleForTesting
setDisplay(int display)497     public void setDisplay(int display) {
498         mDisplay = display;
499     }
500 
501     /**
502      * Overrides the default long press timeout.
503      */
setLongPressTimeoutFactor(float longPressTimeoutFactor)504     public void setLongPressTimeoutFactor(float longPressTimeoutFactor) {
505         mLongPressHelper.setLongPressTimeoutFactor(longPressTimeoutFactor);
506     }
507 
508     @Override
refreshDrawableState()509     public void refreshDrawableState() {
510         if (!mIgnorePressedStateChange) {
511             super.refreshDrawableState();
512         }
513     }
514 
515     @Override
onCreateDrawableState(int extraSpace)516     protected int[] onCreateDrawableState(int extraSpace) {
517         final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
518         if (mStayPressed) {
519             mergeDrawableStates(drawableState, STATE_PRESSED);
520         }
521         return drawableState;
522     }
523 
524     /** Returns the icon for this view. */
getIcon()525     public FastBitmapDrawable getIcon() {
526         return mIcon;
527     }
528 
529     @Override
onTouchEvent(MotionEvent event)530     public boolean onTouchEvent(MotionEvent event) {
531         // ignore events if they happen in padding area
532         if (event.getAction() == MotionEvent.ACTION_DOWN
533                 && shouldIgnoreTouchDown(event.getX(), event.getY())) {
534             return false;
535         }
536         if (isLongClickable()) {
537             super.onTouchEvent(event);
538             mLongPressHelper.onTouchEvent(event);
539             // Keep receiving the rest of the events
540             return true;
541         } else {
542             return super.onTouchEvent(event);
543         }
544     }
545 
546     /**
547      * Returns true if the touch down at the provided position be ignored
548      */
shouldIgnoreTouchDown(float x, float y)549     protected boolean shouldIgnoreTouchDown(float x, float y) {
550         if (mDisplay == DISPLAY_TASKBAR) {
551             // Allow touching within padding on taskbar, given icon sizes are smaller.
552             return false;
553         }
554         return y < getPaddingTop()
555                 || x < getPaddingLeft()
556                 || y > getHeight() - getPaddingBottom()
557                 || x > getWidth() - getPaddingRight();
558     }
559 
setStayPressed(boolean stayPressed)560     void setStayPressed(boolean stayPressed) {
561         mStayPressed = stayPressed;
562         refreshDrawableState();
563     }
564 
565     @Override
onVisibilityAggregated(boolean isVisible)566     public void onVisibilityAggregated(boolean isVisible) {
567         super.onVisibilityAggregated(isVisible);
568         if (mIcon != null) {
569             mIcon.setVisible(isVisible, false);
570         }
571     }
572 
clearPressedBackground()573     public void clearPressedBackground() {
574         setPressed(false);
575         setStayPressed(false);
576     }
577 
578     @Override
onKeyUp(int keyCode, KeyEvent event)579     public boolean onKeyUp(int keyCode, KeyEvent event) {
580         // Unlike touch events, keypress event propagate pressed state change immediately,
581         // without waiting for onClickHandler to execute. Disable pressed state changes here
582         // to avoid flickering.
583         mIgnorePressedStateChange = true;
584         boolean result = super.onKeyUp(keyCode, event);
585         mIgnorePressedStateChange = false;
586         refreshDrawableState();
587         return result;
588     }
589 
590     @Override
onSizeChanged(int w, int h, int oldw, int oldh)591     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
592         super.onSizeChanged(w, h, oldw, oldh);
593         checkForEllipsis();
594     }
595 
596     @Override
onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter)597     protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
598         super.onTextChanged(text, start, lengthBefore, lengthAfter);
599         checkForEllipsis();
600     }
601 
checkForEllipsis()602     private void checkForEllipsis() {
603         float width = getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();
604         if (width <= 0) {
605             return;
606         }
607         setLetterSpacing(0);
608 
609         String text = getText().toString();
610         TextPaint paint = getPaint();
611         if (paint.measureText(text) < width) {
612             return;
613         }
614 
615         float spacing = findBestSpacingValue(paint, text, width, MIN_LETTER_SPACING);
616         // Reset the paint value so that the call to TextView does appropriate diff.
617         paint.setLetterSpacing(0);
618         setLetterSpacing(spacing);
619     }
620 
621     /**
622      * Find the appropriate text spacing to display the provided text
623      *
624      * @param paint          the paint used by the text view
625      * @param text           the text to display
626      * @param allowedWidthPx available space to render the text
627      * @param minSpacingEm   minimum spacing allowed between characters
628      * @return the final textSpacing value
629      * @see #setLetterSpacing(float)
630      */
findBestSpacingValue(TextPaint paint, String text, float allowedWidthPx, float minSpacingEm)631     private float findBestSpacingValue(TextPaint paint, String text, float allowedWidthPx,
632             float minSpacingEm) {
633         paint.setLetterSpacing(minSpacingEm);
634         if (paint.measureText(text) > allowedWidthPx) {
635             // If there is no result at high limit, we can do anything more
636             return minSpacingEm;
637         }
638 
639         float lowLimit = 0;
640         float highLimit = minSpacingEm;
641 
642         for (int i = 0; i < MAX_SEARCH_LOOP_COUNT; i++) {
643             float value = (lowLimit + highLimit) / 2;
644             paint.setLetterSpacing(value);
645             if (paint.measureText(text) < allowedWidthPx) {
646                 highLimit = value;
647             } else {
648                 lowLimit = value;
649             }
650         }
651 
652         // At the end error on the higher side
653         return highLimit;
654     }
655 
656     @SuppressWarnings("wrongcall")
drawWithoutDot(Canvas canvas)657     protected void drawWithoutDot(Canvas canvas) {
658         super.onDraw(canvas);
659     }
660 
661     @Override
onDraw(Canvas canvas)662     public void onDraw(Canvas canvas) {
663         super.onDraw(canvas);
664         drawDotIfNecessary(canvas);
665         drawRunningAppIndicatorIfNecessary(canvas);
666     }
667 
668     /**
669      * Draws the notification dot in the top right corner of the icon bounds.
670      *
671      * @param canvas The canvas to draw to.
672      */
drawDotIfNecessary(Canvas canvas)673     protected void drawDotIfNecessary(Canvas canvas) {
674         if (!mForceHideDot && (hasDot() || mDotParams.scale > 0)) {
675             getIconBounds(mDotParams.iconBounds);
676             Utilities.scaleRectAboutCenter(mDotParams.iconBounds,
677                     IconShape.INSTANCE.get(getContext()).getNormalizationScale());
678             final int scrollX = getScrollX();
679             final int scrollY = getScrollY();
680             canvas.translate(scrollX, scrollY);
681             mDotRenderer.draw(canvas, mDotParams);
682             canvas.translate(-scrollX, -scrollY);
683         }
684     }
685 
686     /** Draws a line under the app icon if this is representing a running app in Desktop Mode. */
drawRunningAppIndicatorIfNecessary(Canvas canvas)687     protected void drawRunningAppIndicatorIfNecessary(Canvas canvas) {
688         if (mRunningAppState == RunningAppState.NOT_RUNNING || mDisplay != DISPLAY_TASKBAR) {
689             return;
690         }
691         getIconBounds(mRunningAppIconBounds);
692         // TODO(b/333872717): update color, shape, and size of indicator
693         boolean isMinimized = mRunningAppState == RunningAppState.MINIMIZED;
694         int indicatorTop =
695                 mRunningAppIconBounds.bottom + (isMinimized ? mMinimizedAppIndicatorTopMargin
696                         : mRunningAppIndicatorTopMargin);
697         final Size indicatorSize =
698                 isMinimized ? mMinimizedAppIndicatorSize : mRunningAppIndicatorSize;
699         canvas.drawRect(mRunningAppIconBounds.centerX() - indicatorSize.getWidth() / 2,
700                 indicatorTop, mRunningAppIconBounds.centerX() + indicatorSize.getWidth() / 2,
701                 indicatorTop + indicatorSize.getHeight(), mRunningAppIndicatorPaint);
702     }
703 
704     @Override
setForceHideDot(boolean forceHideDot)705     public void setForceHideDot(boolean forceHideDot) {
706         if (mForceHideDot == forceHideDot) {
707             return;
708         }
709         mForceHideDot = forceHideDot;
710 
711         if (forceHideDot) {
712             invalidate();
713         } else if (hasDot()) {
714             animateDotScale(0, 1);
715         }
716     }
717 
718     @VisibleForTesting
getForceHideDot()719     public boolean getForceHideDot() {
720         return mForceHideDot;
721     }
722 
hasDot()723     public boolean hasDot() {
724         return mDotInfo != null;
725     }
726 
727     /**
728      * Get the icon bounds on the view depending on the layout type.
729      */
getIconBounds(Rect outBounds)730     public void getIconBounds(Rect outBounds) {
731         getIconBounds(mIconSize, outBounds);
732     }
733 
734     /**
735      * Get the icon bounds on the view depending on the layout type.
736      */
getIconBounds(int iconSize, Rect outBounds)737     public void getIconBounds(int iconSize, Rect outBounds) {
738         outBounds.set(0, 0, iconSize, iconSize);
739         if (mLayoutHorizontal) {
740             int top = (getHeight() - iconSize) / 2;
741             if (mIsRtl) {
742                 outBounds.offsetTo(getWidth() - iconSize - getPaddingRight(), top);
743             } else {
744                 outBounds.offsetTo(getPaddingLeft(), top);
745             }
746         } else {
747             outBounds.offset((getWidth() - iconSize) / 2, getPaddingTop());
748         }
749     }
750 
751     /**
752      * Sets whether the layout is horizontal.
753      */
setLayoutHorizontal(boolean layoutHorizontal)754     public void setLayoutHorizontal(boolean layoutHorizontal) {
755         if (mLayoutHorizontal == layoutHorizontal) {
756             return;
757         }
758 
759         mLayoutHorizontal = layoutHorizontal;
760         applyCompoundDrawables(getIconOrTransparentColor());
761     }
762 
763     /**
764      * Sets whether to vertically center the content.
765      */
setCenterVertically(boolean centerVertically)766     public void setCenterVertically(boolean centerVertically) {
767         mCenterVertically = centerVertically;
768     }
769 
770     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)771     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
772         int height = MeasureSpec.getSize(heightMeasureSpec);
773         if (mCenterVertically) {
774             Paint.FontMetrics fm = getPaint().getFontMetrics();
775             int cellHeightPx = mIconSize + getCompoundDrawablePadding() +
776                     (int) Math.ceil(fm.bottom - fm.top);
777             setPadding(getPaddingLeft(), (height - cellHeightPx) / 2, getPaddingRight(),
778                     getPaddingBottom());
779         }
780         // Only apply two line for all_apps and device search only if necessary.
781         if (shouldUseTwoLine() && (mLastOriginalText != null)) {
782             int allowedVerticalSpace = height - getPaddingTop() - getPaddingBottom()
783                     - mDeviceProfile.allAppsIconSizePx
784                     - mDeviceProfile.allAppsIconDrawablePaddingPx;
785             CharSequence modifiedString = modifyTitleToSupportMultiLine(
786                     MeasureSpec.getSize(widthMeasureSpec) - getCompoundPaddingLeft()
787                             - getCompoundPaddingRight(),
788                     allowedVerticalSpace,
789                     mLastOriginalText,
790                     getPaint(),
791                     mBreakPointsIntArray,
792                     getLineSpacingMultiplier(),
793                     getLineSpacingExtra());
794             if (!TextUtils.equals(modifiedString, mLastModifiedText)) {
795                 mLastModifiedText = modifiedString;
796                 setText(modifiedString);
797                 // if text contains NEW_LINE, set max lines to 2
798                 if (TextUtils.indexOf(modifiedString, NEW_LINE) != -1) {
799                     setSingleLine(false);
800                     setMaxLines(2);
801                 } else {
802                     setSingleLine(true);
803                     setMaxLines(1);
804                 }
805             }
806         }
807         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
808     }
809 
810     @Override
setTextColor(int color)811     public void setTextColor(int color) {
812         mTextColor = color;
813         mTextColorStateList = null;
814         super.setTextColor(getModifiedColor());
815     }
816 
817     @Override
setTextColor(ColorStateList colors)818     public void setTextColor(ColorStateList colors) {
819         mTextColor = colors.getDefaultColor();
820         mTextColorStateList = colors;
821         if (Float.compare(mTextAlpha, 1) == 0) {
822             super.setTextColor(colors);
823         } else {
824             super.setTextColor(getModifiedColor());
825         }
826     }
827 
shouldTextBeVisible()828     public boolean shouldTextBeVisible() {
829         // Text should be visible everywhere but the hotseat.
830         Object tag = getParent() instanceof FolderIcon ? ((View) getParent()).getTag() : getTag();
831         ItemInfo info = tag instanceof ItemInfo ? (ItemInfo) tag : null;
832         return info == null || (info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT
833                 && info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION);
834     }
835 
setTextVisibility(boolean visible)836     public void setTextVisibility(boolean visible) {
837         setTextAlpha(visible ? 1 : 0);
838     }
839 
setTextAlpha(float alpha)840     private void setTextAlpha(float alpha) {
841         mTextAlpha = alpha;
842         if (mTextColorStateList != null) {
843             setTextColor(mTextColorStateList);
844         } else {
845             super.setTextColor(getModifiedColor());
846         }
847     }
848 
getModifiedColor()849     private int getModifiedColor() {
850         if (mTextAlpha == 0) {
851             // Special case to prevent text shadows in high contrast mode
852             return Color.TRANSPARENT;
853         }
854         return setColorAlphaBound(mTextColor, Math.round(Color.alpha(mTextColor) * mTextAlpha));
855     }
856 
857     /**
858      * Creates an animator to fade the text in or out.
859      *
860      * @param fadeIn Whether the text should fade in or fade out.
861      */
createTextAlphaAnimator(boolean fadeIn)862     public ObjectAnimator createTextAlphaAnimator(boolean fadeIn) {
863         float toAlpha = shouldTextBeVisible() && fadeIn ? 1 : 0;
864         return ObjectAnimator.ofFloat(this, TEXT_ALPHA_PROPERTY, toAlpha);
865     }
866 
867     /**
868      * Generate a new string that will support two line text depending on the current string.
869      * This method calculates the limited width of a text view and creates a string to fit as
870      * many words as it can until the limit is reached. Once the limit is reached, we decide to
871      * either return the original title or continue on a new line. How to get the new string is by
872      * iterating through the list of break points and determining if the strings between the break
873      * points can fit within the line it is in. We will show the modified string if there is enough
874      * horizontal and vertical space, otherwise this method will just return the original string.
875      * Example assuming each character takes up one spot:
876      * title = "Battery Stats", breakpoint = [6], stringPtr = 0, limitedWidth = 7
877      * We get the current word -> from sublist(0, breakpoint[i]+1) so sublist (0,7) -> Battery,
878      * now stringPtr = 7 then from sublist(7) the current string is " Stats" and the runningWidth
879      * at this point exceeds limitedWidth and so we put " Stats" onto the next line (after checking
880      * if the first char is a SPACE, we trim to append "Stats". So resulting string would be
881      * "Battery\nStats"
882      */
modifyTitleToSupportMultiLine(int limitedWidth, int limitedHeight, CharSequence title, TextPaint paint, IntArray breakPoints, float spacingMultiplier, float spacingExtra)883     public static CharSequence modifyTitleToSupportMultiLine(int limitedWidth, int limitedHeight,
884             CharSequence title, TextPaint paint, IntArray breakPoints, float spacingMultiplier,
885             float spacingExtra) {
886         // current title is less than the width allowed so we can just skip
887         if (title == null || paint.measureText(title, 0, title.length()) <= limitedWidth) {
888             return title;
889         }
890         float currentWordWidth, runningWidth = 0;
891         CharSequence currentWord;
892         StringBuilder newString = new StringBuilder();
893         paint.setLetterSpacing(MIN_LETTER_SPACING);
894         int stringPtr = 0;
895         for (int i = 0; i < breakPoints.size() + 1; i++) {
896             if (i < breakPoints.size()) {
897                 currentWord = title.subSequence(stringPtr, breakPoints.get(i) + 1);
898             } else {
899                 // last word from recent breakpoint until the end of the string
900                 currentWord = title.subSequence(stringPtr, title.length());
901             }
902             currentWordWidth = paint.measureText(currentWord, 0, currentWord.length());
903             runningWidth += currentWordWidth;
904             if (runningWidth <= limitedWidth) {
905                 newString.append(currentWord);
906             } else {
907                 if (i != 0) {
908                     // If putting word onto a new line, make sure there is no space or new line
909                     // character in the beginning of the current word and just put in the rest of
910                     // the characters.
911                     CharSequence lastCharacters = title.subSequence(stringPtr, title.length());
912                     int beginningLetterType =
913                             Character.getType(Character.codePointAt(lastCharacters, 0));
914                     if (beginningLetterType == Character.SPACE_SEPARATOR
915                             || beginningLetterType == Character.LINE_SEPARATOR) {
916                         lastCharacters = lastCharacters.length() > 1
917                                 ? lastCharacters.subSequence(1, lastCharacters.length())
918                                 : EMPTY;
919                     }
920                     newString.append(NEW_LINE).append(lastCharacters);
921                     StaticLayout staticLayout = new StaticLayout(newString, paint, limitedWidth,
922                             ALIGN_NORMAL, spacingMultiplier, spacingExtra, false);
923                     if (staticLayout.getHeight() < limitedHeight) {
924                         return newString.toString();
925                     }
926                 }
927                 // if the first words exceeds width, just return as the first line will ellipse
928                 return title;
929             }
930             if (i >= breakPoints.size()) {
931                 // no need to look forward into the string if we've already finished processing
932                 break;
933             }
934             stringPtr = breakPoints.get(i) + 1;
935         }
936         return newString.toString();
937     }
938 
939     @Override
cancelLongPress()940     public void cancelLongPress() {
941         super.cancelLongPress();
942         mLongPressHelper.cancelLongPress();
943     }
944 
945     /**
946      * Applies the loading progress value to the progress bar.
947      *
948      * If this app is installing, the progress bar will be updated with the installation progress.
949      * If this app is installed and downloading incrementally, the progress bar will be updated
950      * with the total download progress.
951      */
applyLoadingState(PreloadIconDrawable icon)952     public void applyLoadingState(PreloadIconDrawable icon) {
953         if (getTag() instanceof ItemInfoWithIcon) {
954             WorkspaceItemInfo info = (WorkspaceItemInfo) getTag();
955             if ((info.runtimeStatusFlags & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0
956                     || info.hasPromiseIconUi()
957                     || (info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0
958                     || (icon != null)) {
959                 updateProgressBarUi(info.getProgressLevel() == 100 ? icon : null);
960             }
961         }
962     }
963 
updateProgressBarUi(PreloadIconDrawable oldIcon)964     private void updateProgressBarUi(PreloadIconDrawable oldIcon) {
965         FastBitmapDrawable originalIcon = mIcon;
966         PreloadIconDrawable preloadDrawable = applyProgressLevel();
967         if (preloadDrawable != null && oldIcon != null) {
968             preloadDrawable.maybePerformFinishedAnimation(oldIcon, () -> setIcon(originalIcon));
969         }
970     }
971 
972     /** Applies the given progress level to the this icon's progress bar. */
973     @Nullable
applyProgressLevel()974     public PreloadIconDrawable applyProgressLevel() {
975         if (!(getTag() instanceof ItemInfoWithIcon)
976                 || ((ItemInfoWithIcon) getTag()).isInactiveArchive()) {
977             return null;
978         }
979 
980         ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
981         int progressLevel = info.getProgressLevel();
982         if (progressLevel >= 100) {
983             setContentDescription(info.contentDescription != null
984                     ? info.contentDescription : "");
985         } else if (progressLevel > 0) {
986             setDownloadStateContentDescription(info, progressLevel);
987         } else {
988             setContentDescription(getContext()
989                     .getString(R.string.app_waiting_download_title, info.title));
990         }
991         if (mIcon != null) {
992             PreloadIconDrawable preloadIconDrawable;
993             if (mIcon instanceof PreloadIconDrawable) {
994                 preloadIconDrawable = (PreloadIconDrawable) mIcon;
995                 preloadIconDrawable.setLevel(progressLevel);
996                 preloadIconDrawable.setIsDisabled(isIconDisabled(info));
997             } else {
998                 preloadIconDrawable = makePreloadIcon();
999                 setIcon(preloadIconDrawable);
1000             }
1001             return preloadIconDrawable;
1002         }
1003         return null;
1004     }
1005 
1006     /**
1007      * Creates a PreloadIconDrawable with the appropriate progress level without mutating this
1008      * object.
1009      */
1010     @Nullable
makePreloadIcon()1011     public PreloadIconDrawable makePreloadIcon() {
1012         if (!(getTag() instanceof ItemInfoWithIcon)) {
1013             return null;
1014         }
1015 
1016         ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
1017         int progressLevel = info.getProgressLevel();
1018         final PreloadIconDrawable preloadDrawable = newPendingIcon(getContext(), info);
1019 
1020         preloadDrawable.setLevel(progressLevel);
1021         preloadDrawable.setIsDisabled(isIconDisabled(info));
1022         return preloadDrawable;
1023     }
1024 
1025     /**
1026      * Returns true to grey the icon if the icon is either suspended or if the icon is pending
1027      * download
1028      */
isIconDisabled(ItemInfoWithIcon info)1029     public boolean isIconDisabled(ItemInfoWithIcon info) {
1030         return info.isDisabled() || info.isPendingDownload();
1031     }
1032 
1033 
applyDotState(ItemInfo itemInfo, boolean animate)1034     public void applyDotState(ItemInfo itemInfo, boolean animate) {
1035         if (mIcon instanceof FastBitmapDrawable) {
1036             boolean wasDotted = mDotInfo != null;
1037             mDotInfo = mActivity.getDotInfoForItem(itemInfo);
1038             boolean isDotted = mDotInfo != null;
1039             float newDotScale = isDotted ? 1f : 0;
1040             if (mDisplay == DISPLAY_ALL_APPS) {
1041                 mDotRenderer = mActivity.getDeviceProfile().mDotRendererAllApps;
1042             } else {
1043                 mDotRenderer = mActivity.getDeviceProfile().mDotRendererWorkSpace;
1044             }
1045             if (wasDotted || isDotted) {
1046                 // Animate when a dot is first added or when it is removed.
1047                 if (animate && (wasDotted ^ isDotted) && isShown()) {
1048                     animateDotScale(newDotScale);
1049                 } else {
1050                     cancelDotScaleAnim();
1051                     mDotParams.scale = newDotScale;
1052                     invalidate();
1053                 }
1054             }
1055             if (!TextUtils.isEmpty(itemInfo.contentDescription)) {
1056                 if (itemInfo.isDisabled()) {
1057                     setContentDescription(getContext().getString(R.string.disabled_app_label,
1058                             itemInfo.contentDescription));
1059                 } else if (hasDot()) {
1060                     int count = mDotInfo.getNotificationCount();
1061                     setContentDescription(
1062                             getAppLabelPluralString(itemInfo.contentDescription.toString(), count));
1063                 } else {
1064                     setContentDescription(itemInfo.contentDescription);
1065                 }
1066             }
1067         }
1068     }
1069 
setDownloadStateContentDescription(ItemInfoWithIcon info, int progressLevel)1070     private void setDownloadStateContentDescription(ItemInfoWithIcon info, int progressLevel) {
1071         if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_ARCHIVED) != 0 && progressLevel == 0) {
1072             setContentDescription(getContext().getString(R.string.app_archived_title, info.title));
1073         } else if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK)
1074                 != 0) {
1075             String percentageString = NumberFormat.getPercentInstance()
1076                     .format(progressLevel * 0.01);
1077             if ((info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0) {
1078                 setContentDescription(getContext()
1079                         .getString(
1080                                 R.string.app_installing_title, info.title, percentageString));
1081             } else if ((info.runtimeStatusFlags
1082                     & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) {
1083                 setContentDescription(getContext()
1084                         .getString(
1085                                 R.string.app_downloading_title, info.title, percentageString));
1086             }
1087         }
1088     }
1089 
1090     /**
1091      * Sets the icon for this view based on the layout direction.
1092      */
setIcon(FastBitmapDrawable icon)1093     protected void setIcon(FastBitmapDrawable icon) {
1094         if (mIsIconVisible) {
1095             applyCompoundDrawables(icon);
1096         }
1097         mIcon = icon;
1098         if (mIcon != null) {
1099             mIcon.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
1100         }
1101     }
1102 
1103     @Override
setIconVisible(boolean visible)1104     public void setIconVisible(boolean visible) {
1105         mIsIconVisible = visible;
1106         if (!mIsIconVisible) {
1107             resetIconScale();
1108         }
1109         Drawable icon = getIconOrTransparentColor();
1110         applyCompoundDrawables(icon);
1111     }
1112 
getIconOrTransparentColor()1113     private Drawable getIconOrTransparentColor() {
1114         return mIsIconVisible ? mIcon : new ColorDrawable(Color.TRANSPARENT);
1115     }
1116 
1117     /** Sets the icon visual state to disabled or not. */
setIconDisabled(boolean isDisabled)1118     public void setIconDisabled(boolean isDisabled) {
1119         if (mIcon != null) {
1120             mIcon.setIsDisabled(isDisabled);
1121         }
1122     }
1123 
iconUpdateAnimationEnabled()1124     protected boolean iconUpdateAnimationEnabled() {
1125         return mEnableIconUpdateAnimation;
1126     }
1127 
applyCompoundDrawables(Drawable icon)1128     protected void applyCompoundDrawables(Drawable icon) {
1129         if (icon == null) {
1130             // Icon can be null when we use the BubbleTextView for text only.
1131             return;
1132         }
1133 
1134         // If we had already set an icon before, disable relayout as the icon size is the
1135         // same as before.
1136         mDisableRelayout = mIcon != null;
1137 
1138         icon.setBounds(0, 0, mIconSize, mIconSize);
1139 
1140         updateIcon(icon);
1141 
1142         // If the current icon is a placeholder color, animate its update.
1143         if (mIcon != null
1144                 && mIcon instanceof PlaceHolderIconDrawable
1145                 && iconUpdateAnimationEnabled()) {
1146             ((PlaceHolderIconDrawable) mIcon).animateIconUpdate(icon);
1147         }
1148 
1149         mDisableRelayout = false;
1150     }
1151 
1152     @Override
requestLayout()1153     public void requestLayout() {
1154         if (!mDisableRelayout) {
1155             super.requestLayout();
1156         }
1157     }
1158 
1159     /**
1160      * Applies the item info if it is same as what the view is pointing to currently.
1161      */
1162     @Override
reapplyItemInfo(ItemInfoWithIcon info)1163     public void reapplyItemInfo(ItemInfoWithIcon info) {
1164         if (getTag() == info) {
1165             mIconLoadRequest = null;
1166             mDisableRelayout = true;
1167             mEnableIconUpdateAnimation = true;
1168 
1169             // Optimization: Starting in N, pre-uploads the bitmap to RenderThread.
1170             info.bitmap.icon.prepareToDraw();
1171 
1172             if (info instanceof AppInfo) {
1173                 applyFromApplicationInfo((AppInfo) info);
1174             } else if (info instanceof WorkspaceItemInfo) {
1175                 applyFromWorkspaceItem((WorkspaceItemInfo) info);
1176                 mActivity.invalidateParent(info);
1177             } else if (info != null) {
1178                 applyFromItemInfoWithIcon(info);
1179             }
1180 
1181             mDisableRelayout = false;
1182             mEnableIconUpdateAnimation = false;
1183         }
1184     }
1185 
1186     /**
1187      * Verifies that the current icon is high-res otherwise posts a request to load the icon.
1188      */
verifyHighRes()1189     public void verifyHighRes() {
1190         if (mIconLoadRequest != null) {
1191             mIconLoadRequest.cancel();
1192             mIconLoadRequest = null;
1193         }
1194         if (getTag() instanceof ItemInfoWithIcon) {
1195             ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
1196             if (info.usingLowResIcon()) {
1197                 mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache()
1198                         .updateIconInBackground(BubbleTextView.this, info);
1199             }
1200         }
1201     }
1202 
getIconSize()1203     public int getIconSize() {
1204         return mIconSize;
1205     }
1206 
isDisplaySearchResult()1207     public boolean isDisplaySearchResult() {
1208         return mDisplay == DISPLAY_SEARCH_RESULT
1209                 || mDisplay == DISPLAY_SEARCH_RESULT_SMALL
1210                 || mDisplay == DISPLAY_SEARCH_RESULT_APP_ROW;
1211     }
1212 
getIconDisplay()1213     public int getIconDisplay() {
1214         return mDisplay;
1215     }
1216 
1217     @Override
getTranslateDelegate()1218     public MultiTranslateDelegate getTranslateDelegate() {
1219         return mTranslateDelegate;
1220     }
1221 
1222     @Override
setReorderBounceScale(float scale)1223     public void setReorderBounceScale(float scale) {
1224         mScaleForReorderBounce = scale;
1225         super.setScaleX(scale);
1226         super.setScaleY(scale);
1227     }
1228 
1229     @Override
getReorderBounceScale()1230     public float getReorderBounceScale() {
1231         return mScaleForReorderBounce;
1232     }
1233 
1234     @Override
getViewType()1235     public int getViewType() {
1236         return DRAGGABLE_ICON;
1237     }
1238 
1239     @Override
getWorkspaceVisualDragBounds(Rect bounds)1240     public void getWorkspaceVisualDragBounds(Rect bounds) {
1241         getIconBounds(mIconSize, bounds);
1242     }
1243 
getSourceVisualDragBounds(Rect bounds)1244     public void getSourceVisualDragBounds(Rect bounds) {
1245         getIconBounds(mIconSize, bounds);
1246     }
1247 
1248     @Override
prepareDrawDragView()1249     public SafeCloseable prepareDrawDragView() {
1250         resetIconScale();
1251         setForceHideDot(true);
1252         return () -> {
1253         };
1254     }
1255 
resetIconScale()1256     private void resetIconScale() {
1257         if (mIcon != null) {
1258             mIcon.resetScale();
1259         }
1260     }
1261 
updateIcon(Drawable newIcon)1262     private void updateIcon(Drawable newIcon) {
1263         if (mLayoutHorizontal) {
1264             setCompoundDrawablesRelative(newIcon, null, null, null);
1265         } else {
1266             setCompoundDrawables(null, newIcon, null, null);
1267         }
1268     }
1269 
getAppLabelPluralString(String appName, int notificationCount)1270     private String getAppLabelPluralString(String appName, int notificationCount) {
1271         MessageFormat icuCountFormat = new MessageFormat(
1272                 getResources().getString(R.string.dotted_app_label),
1273                 Locale.getDefault());
1274         HashMap<String, Object> args = new HashMap();
1275         args.put("app_name", appName);
1276         args.put("count", notificationCount);
1277         return icuCountFormat.format(args);
1278     }
1279 
1280     /**
1281      * Starts a long press action and returns the corresponding pre-drag condition
1282      */
startLongPressAction()1283     public PreDragCondition startLongPressAction() {
1284         PopupContainerWithArrow popup = PopupContainerWithArrow.showForIcon(this);
1285         return popup != null ? popup.createPreDragCondition(true) : null;
1286     }
1287 
1288     /**
1289      * Returns true if the view can show long-press popup
1290      */
canShowLongPressPopup()1291     public boolean canShowLongPressPopup() {
1292         return getTag() instanceof ItemInfo && ShortcutUtil.supportsShortcuts((ItemInfo) getTag());
1293     }
1294 
1295     /** Returns the package name of the app this icon represents. */
getTargetPackageName()1296     public String getTargetPackageName() {
1297         Object tag = getTag();
1298         if (tag instanceof ItemInfo itemInfo) {
1299             return itemInfo.getTargetPackage();
1300         }
1301         return null;
1302     }
1303 }
1304