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