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