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