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