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.drawable.AdaptiveIconDrawable.getExtraInsetFraction; 20 21 import static com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN; 22 import static com.android.launcher3.Flags.enableSmartspaceAsAWidget; 23 import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED; 24 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; 25 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; 26 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN; 27 28 import android.annotation.SuppressLint; 29 import android.app.ActivityManager; 30 import android.app.ActivityOptions; 31 import android.app.Person; 32 import android.app.WallpaperManager; 33 import android.content.Context; 34 import android.content.pm.LauncherActivityInfo; 35 import android.content.pm.LauncherApps; 36 import android.content.pm.ShortcutInfo; 37 import android.content.res.Configuration; 38 import android.content.res.Resources; 39 import android.graphics.Bitmap; 40 import android.graphics.BlendMode; 41 import android.graphics.BlendModeColorFilter; 42 import android.graphics.Color; 43 import android.graphics.ColorFilter; 44 import android.graphics.LightingColorFilter; 45 import android.graphics.Matrix; 46 import android.graphics.Paint; 47 import android.graphics.Point; 48 import android.graphics.PointF; 49 import android.graphics.Rect; 50 import android.graphics.RectF; 51 import android.graphics.drawable.AdaptiveIconDrawable; 52 import android.graphics.drawable.BitmapDrawable; 53 import android.graphics.drawable.ColorDrawable; 54 import android.graphics.drawable.Drawable; 55 import android.graphics.drawable.InsetDrawable; 56 import android.os.Build; 57 import android.os.Build.VERSION_CODES; 58 import android.os.DeadObjectException; 59 import android.os.Handler; 60 import android.os.Message; 61 import android.os.TransactionTooLargeException; 62 import android.text.Spannable; 63 import android.text.SpannableString; 64 import android.text.TextUtils; 65 import android.text.style.TtsSpan; 66 import android.util.DisplayMetrics; 67 import android.util.Log; 68 import android.util.Pair; 69 import android.util.TypedValue; 70 import android.view.MotionEvent; 71 import android.view.View; 72 import android.view.ViewConfiguration; 73 import android.view.ViewGroup; 74 import android.view.animation.Interpolator; 75 76 import androidx.annotation.ChecksSdkIntAtLeast; 77 import androidx.annotation.IntDef; 78 import androidx.annotation.NonNull; 79 import androidx.annotation.Nullable; 80 import androidx.annotation.WorkerThread; 81 import androidx.core.graphics.ColorUtils; 82 83 import com.android.launcher3.dragndrop.FolderAdaptiveIcon; 84 import com.android.launcher3.graphics.TintedDrawableSpan; 85 import com.android.launcher3.icons.BitmapInfo; 86 import com.android.launcher3.icons.LauncherIcons; 87 import com.android.launcher3.icons.ShortcutCachingLogic; 88 import com.android.launcher3.icons.ThemedIconDrawable; 89 import com.android.launcher3.model.data.ItemInfo; 90 import com.android.launcher3.model.data.ItemInfoWithIcon; 91 import com.android.launcher3.pm.ShortcutConfigActivityInfo; 92 import com.android.launcher3.pm.UserCache; 93 import com.android.launcher3.shortcuts.ShortcutKey; 94 import com.android.launcher3.shortcuts.ShortcutRequest; 95 import com.android.launcher3.testing.shared.ResourceUtils; 96 import com.android.launcher3.util.FlagOp; 97 import com.android.launcher3.util.IntArray; 98 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; 99 import com.android.launcher3.util.Themes; 100 import com.android.launcher3.views.ActivityContext; 101 import com.android.launcher3.views.BaseDragLayer; 102 import com.android.launcher3.widget.PendingAddShortcutInfo; 103 104 import java.lang.reflect.Method; 105 import java.util.Collections; 106 import java.util.List; 107 import java.util.Locale; 108 import java.util.function.Predicate; 109 import java.util.regex.Matcher; 110 import java.util.regex.Pattern; 111 112 /** 113 * Various utilities shared amongst the Launcher's classes. 114 */ 115 public final class Utilities { 116 117 private static final String TAG = "Launcher.Utilities"; 118 119 private static final Pattern sTrimPattern = 120 Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$"); 121 122 private static final Matrix sMatrix = new Matrix(); 123 private static final Matrix sInverseMatrix = new Matrix(); 124 125 public static final String[] EMPTY_STRING_ARRAY = new String[0]; 126 public static final Person[] EMPTY_PERSON_ARRAY = new Person[0]; 127 128 @ChecksSdkIntAtLeast(api = VERSION_CODES.S) 129 public static final boolean ATLEAST_S = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S; 130 131 @ChecksSdkIntAtLeast(api = VERSION_CODES.TIRAMISU, codename = "T") 132 public static final boolean ATLEAST_T = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU; 133 134 @ChecksSdkIntAtLeast(api = VERSION_CODES.UPSIDE_DOWN_CAKE, codename = "U") 135 public static final boolean ATLEAST_U = Build.VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE; 136 137 @ChecksSdkIntAtLeast(api = VERSION_CODES.VANILLA_ICE_CREAM, codename = "V") 138 public static final boolean ATLEAST_V = Build.VERSION.SDK_INT 139 >= VERSION_CODES.VANILLA_ICE_CREAM; 140 141 /** 142 * Set on a motion event dispatched from the nav bar. See {@link MotionEvent#setEdgeFlags(int)}. 143 */ 144 public static final int EDGE_NAV_BAR = 1 << 8; 145 146 /** 147 * Indicates if the device has a debug build. Should only be used to store additional info or 148 * add extra logging and not for changing the app behavior. 149 * @deprecated Use {@link BuildConfig#IS_DEBUG_DEVICE} directly 150 */ 151 @Deprecated 152 public static final boolean IS_DEBUG_DEVICE = BuildConfig.IS_DEBUG_DEVICE; 153 154 public static final int TRANSLATE_UP = 0; 155 public static final int TRANSLATE_DOWN = 1; 156 public static final int TRANSLATE_LEFT = 2; 157 public static final int TRANSLATE_RIGHT = 3; 158 159 public static final boolean SHOULD_SHOW_FIRST_PAGE_WIDGET = 160 enableSmartspaceAsAWidget() && WIDGET_ON_FIRST_SCREEN; 161 162 @IntDef({TRANSLATE_UP, TRANSLATE_DOWN, TRANSLATE_LEFT, TRANSLATE_RIGHT}) 163 public @interface AdjustmentDirection{} 164 165 /** 166 * Returns true if theme is dark. 167 */ isDarkTheme(Context context)168 public static boolean isDarkTheme(Context context) { 169 Configuration configuration = context.getResources().getConfiguration(); 170 int nightMode = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK; 171 return nightMode == Configuration.UI_MODE_NIGHT_YES; 172 } 173 174 private static boolean sIsRunningInTestHarness = ActivityManager.isRunningInTestHarness(); 175 isRunningInTestHarness()176 public static boolean isRunningInTestHarness() { 177 return sIsRunningInTestHarness; 178 } 179 enableRunningInTestHarnessForTests()180 public static void enableRunningInTestHarnessForTests() { 181 sIsRunningInTestHarness = true; 182 } 183 isPropertyEnabled(String propertyName)184 public static boolean isPropertyEnabled(String propertyName) { 185 return Log.isLoggable(propertyName, Log.VERBOSE); 186 } 187 188 /** 189 * Given a coordinate relative to the descendant, find the coordinate in a parent view's 190 * coordinates. 191 * 192 * @param descendant The descendant to which the passed coordinate is relative. 193 * @param ancestor The root view to make the coordinates relative to. 194 * @param coord The coordinate that we want mapped. 195 * @param includeRootScroll Whether or not to account for the scroll of the descendant: 196 * sometimes this is relevant as in a child's coordinates within the descendant. 197 * @return The factor by which this descendant is scaled relative to this DragLayer. Caution 198 * this scale factor is assumed to be equal in X and Y, and so if at any point this 199 * assumption fails, we will need to return a pair of scale factors. 200 */ getDescendantCoordRelativeToAncestor( View descendant, View ancestor, float[] coord, boolean includeRootScroll)201 public static float getDescendantCoordRelativeToAncestor( 202 View descendant, View ancestor, float[] coord, boolean includeRootScroll) { 203 return getDescendantCoordRelativeToAncestor(descendant, ancestor, coord, includeRootScroll, 204 false); 205 } 206 207 /** 208 * Given a coordinate relative to the descendant, find the coordinate in a parent view's 209 * coordinates. 210 * 211 * @param descendant The descendant to which the passed coordinate is relative. 212 * @param ancestor The root view to make the coordinates relative to. 213 * @param coord The coordinate that we want mapped. 214 * @param includeRootScroll Whether or not to account for the scroll of the descendant: 215 * sometimes this is relevant as in a child's coordinates within the descendant. 216 * @param ignoreTransform If true, view transform is ignored 217 * @return The factor by which this descendant is scaled relative to this DragLayer. Caution 218 * this scale factor is assumed to be equal in X and Y, and so if at any point this 219 * assumption fails, we will need to return a pair of scale factors. 220 */ getDescendantCoordRelativeToAncestor(View descendant, View ancestor, float[] coord, boolean includeRootScroll, boolean ignoreTransform)221 public static float getDescendantCoordRelativeToAncestor(View descendant, View ancestor, 222 float[] coord, boolean includeRootScroll, boolean ignoreTransform) { 223 float scale = 1.0f; 224 View v = descendant; 225 while(v != ancestor && v != null) { 226 // For TextViews, scroll has a meaning which relates to the text position 227 // which is very strange... ignore the scroll. 228 if (v != descendant || includeRootScroll) { 229 offsetPoints(coord, -v.getScrollX(), -v.getScrollY()); 230 } 231 232 if (!ignoreTransform) { 233 v.getMatrix().mapPoints(coord); 234 } 235 offsetPoints(coord, v.getLeft(), v.getTop()); 236 scale *= v.getScaleX(); 237 238 v = v.getParent() instanceof View ? (View) v.getParent() : null; 239 } 240 return scale; 241 } 242 243 /** 244 * Returns bounds for a child view of DragLayer, in drag layer coordinates. 245 * 246 * see {@link com.android.launcher3.dragndrop.DragLayer}. 247 * 248 * @param viewBounds Bounds of the view wanted in drag layer coordinates, relative to the view 249 * itself. eg. (0, 0, view.getWidth, view.getHeight) 250 * @param ignoreTransform If true, view transform is ignored 251 * @param outRect The out rect where we return the bounds of {@param view} in drag layer coords. 252 */ getBoundsForViewInDragLayer(BaseDragLayer dragLayer, View view, Rect viewBounds, boolean ignoreTransform, float[] recycle, RectF outRect)253 public static void getBoundsForViewInDragLayer(BaseDragLayer dragLayer, View view, 254 Rect viewBounds, boolean ignoreTransform, float[] recycle, RectF outRect) { 255 float[] points = recycle == null ? new float[4] : recycle; 256 points[0] = viewBounds.left; 257 points[1] = viewBounds.top; 258 points[2] = viewBounds.right; 259 points[3] = viewBounds.bottom; 260 261 Utilities.getDescendantCoordRelativeToAncestor(view, dragLayer, points, 262 false, ignoreTransform); 263 outRect.set( 264 Math.min(points[0], points[2]), 265 Math.min(points[1], points[3]), 266 Math.max(points[0], points[2]), 267 Math.max(points[1], points[3])); 268 } 269 270 /** 271 * Similar to {@link #mapCoordInSelfToDescendant(View descendant, View root, float[] coord)} 272 * but accepts a Rect instead of float[]. 273 */ mapRectInSelfToDescendant(View descendant, View root, Rect rect)274 public static void mapRectInSelfToDescendant(View descendant, View root, Rect rect) { 275 float[] coords = new float[]{rect.left, rect.top, rect.right, rect.bottom}; 276 mapCoordInSelfToDescendant(descendant, root, coords); 277 rect.set((int) coords[0], (int) coords[1], (int) coords[2], (int) coords[3]); 278 } 279 280 /** 281 * Inverse of {@link #getDescendantCoordRelativeToAncestor(View, View, float[], boolean)}. 282 */ mapCoordInSelfToDescendant(View descendant, View root, float[] coord)283 public static void mapCoordInSelfToDescendant(View descendant, View root, float[] coord) { 284 sMatrix.reset(); 285 View v = descendant; 286 while(v != root) { 287 sMatrix.postTranslate(-v.getScrollX(), -v.getScrollY()); 288 sMatrix.postConcat(v.getMatrix()); 289 sMatrix.postTranslate(v.getLeft(), v.getTop()); 290 v = (View) v.getParent(); 291 } 292 sMatrix.postTranslate(-v.getScrollX(), -v.getScrollY()); 293 sMatrix.invert(sInverseMatrix); 294 sInverseMatrix.mapPoints(coord); 295 } 296 297 /** 298 * Sets {@param out} to be same as {@param in} by rounding individual values 299 */ roundArray(float[] in, int[] out)300 public static void roundArray(float[] in, int[] out) { 301 for (int i = 0; i < in.length; i++) { 302 out[i] = Math.round(in[i]); 303 } 304 } 305 offsetPoints(float[] points, float offsetX, float offsetY)306 public static void offsetPoints(float[] points, float offsetX, float offsetY) { 307 for (int i = 0; i < points.length; i += 2) { 308 points[i] += offsetX; 309 points[i + 1] += offsetY; 310 } 311 } 312 313 /** 314 * Utility method to determine whether the given point, in local coordinates, 315 * is inside the view, where the area of the view is expanded by the slop factor. 316 * This method is called while processing touch-move events to determine if the event 317 * is still within the view. 318 */ pointInView(View v, float localX, float localY, float slop)319 public static boolean pointInView(View v, float localX, float localY, float slop) { 320 return localX >= -slop && localY >= -slop && localX < (v.getWidth() + slop) && 321 localY < (v.getHeight() + slop); 322 } 323 scaleRectFAboutCenter(RectF r, float scale)324 public static void scaleRectFAboutCenter(RectF r, float scale) { 325 scaleRectFAboutCenter(r, scale, scale); 326 } 327 328 /** 329 * Similar to {@link #scaleRectAboutCenter(Rect, float)} except this allows different scales 330 * for X and Y 331 */ scaleRectFAboutCenter(RectF r, float scaleX, float scaleY)332 public static void scaleRectFAboutCenter(RectF r, float scaleX, float scaleY) { 333 float px = r.centerX(); 334 float py = r.centerY(); 335 r.offset(-px, -py); 336 r.left = r.left * scaleX; 337 r.top = r.top * scaleY; 338 r.right = r.right * scaleX; 339 r.bottom = r.bottom * scaleY; 340 r.offset(px, py); 341 } 342 scaleRectAboutCenter(Rect r, float scale)343 public static void scaleRectAboutCenter(Rect r, float scale) { 344 if (scale != 1.0f) { 345 float cx = r.exactCenterX(); 346 float cy = r.exactCenterY(); 347 r.left = Math.round(cx + (r.left - cx) * scale); 348 r.top = Math.round(cy + (r.top - cy) * scale); 349 r.right = Math.round(cx + (r.right - cx) * scale); 350 r.bottom = Math.round(cy + (r.bottom - cy) * scale); 351 } 352 } 353 shrinkRect(Rect r, float scaleX, float scaleY)354 public static float shrinkRect(Rect r, float scaleX, float scaleY) { 355 float scale = Math.min(Math.min(scaleX, scaleY), 1.0f); 356 if (scale < 1.0f) { 357 int deltaX = (int) (r.width() * (scaleX - scale) * 0.5f); 358 r.left += deltaX; 359 r.right -= deltaX; 360 361 int deltaY = (int) (r.height() * (scaleY - scale) * 0.5f); 362 r.top += deltaY; 363 r.bottom -= deltaY; 364 } 365 return scale; 366 } 367 368 /** 369 * Sets the x and y pivots for scaling from one Rect to another. 370 * 371 * @param src the source rectangle to scale from. 372 * @param dst the destination rectangle to scale to. 373 * @param outPivot the pivots set for scaling from src to dst. 374 */ getPivotsForScalingRectToRect(Rect src, Rect dst, PointF outPivot)375 public static void getPivotsForScalingRectToRect(Rect src, Rect dst, PointF outPivot) { 376 float pivotXPct = ((float) src.left - dst.left) / ((float) dst.width() - src.width()); 377 outPivot.x = dst.left + dst.width() * pivotXPct; 378 379 float pivotYPct = ((float) src.top - dst.top) / ((float) dst.height() - src.height()); 380 outPivot.y = dst.top + dst.height() * pivotYPct; 381 } 382 383 /** 384 * Maps t from one range to another range. 385 * @param t The value to map. 386 * @param fromMin The lower bound of the range that t is being mapped from. 387 * @param fromMax The upper bound of the range that t is being mapped from. 388 * @param toMin The lower bound of the range that t is being mapped to. 389 * @param toMax The upper bound of the range that t is being mapped to. 390 * @return The mapped value of t. 391 */ mapToRange(float t, float fromMin, float fromMax, float toMin, float toMax, Interpolator interpolator)392 public static float mapToRange(float t, float fromMin, float fromMax, float toMin, float toMax, 393 Interpolator interpolator) { 394 if (fromMin == fromMax || toMin == toMax) { 395 Log.e(TAG, "mapToRange: range has 0 length"); 396 return toMin; 397 } 398 float progress = getProgress(t, fromMin, fromMax); 399 return mapRange(interpolator.getInterpolation(progress), toMin, toMax); 400 } 401 402 /** Bounds t between a lower and upper bound and maps the result to a range. */ mapBoundToRange(float t, float lowerBound, float upperBound, float toMin, float toMax, Interpolator interpolator)403 public static float mapBoundToRange(float t, float lowerBound, float upperBound, 404 float toMin, float toMax, Interpolator interpolator) { 405 return mapToRange(boundToRange(t, lowerBound, upperBound), lowerBound, upperBound, 406 toMin, toMax, interpolator); 407 } 408 getProgress(float current, float min, float max)409 public static float getProgress(float current, float min, float max) { 410 return Math.abs(current - min) / Math.abs(max - min); 411 } 412 mapRange(float value, float min, float max)413 public static float mapRange(float value, float min, float max) { 414 return min + (value * (max - min)); 415 } 416 417 /** 418 * Trims the string, removing all whitespace at the beginning and end of the string. 419 * Non-breaking whitespaces are also removed. 420 */ 421 @NonNull trim(CharSequence s)422 public static String trim(CharSequence s) { 423 if (s == null) { 424 return ""; 425 } 426 427 // Just strip any sequence of whitespace or java space characters from the beginning and end 428 Matcher m = sTrimPattern.matcher(s); 429 return m.replaceAll("$1"); 430 } 431 432 /** 433 * Calculates the height of a given string at a specific text size. 434 */ calculateTextHeight(float textSizePx)435 public static int calculateTextHeight(float textSizePx) { 436 Paint p = new Paint(); 437 p.setTextSize(textSizePx); 438 Paint.FontMetrics fm = p.getFontMetrics(); 439 return (int) Math.ceil(fm.bottom - fm.top); 440 } 441 isRtl(Resources res)442 public static boolean isRtl(Resources res) { 443 return res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 444 } 445 446 /** Converts a pixel value (px) to scale pixel value (SP) for the current device. */ pxToSp(float size)447 public static float pxToSp(float size) { 448 return size / Resources.getSystem().getDisplayMetrics().scaledDensity; 449 } 450 dpiFromPx(float size, int densityDpi)451 public static float dpiFromPx(float size, int densityDpi) { 452 float densityRatio = (float) densityDpi / DisplayMetrics.DENSITY_DEFAULT; 453 return (size / densityRatio); 454 } 455 456 /** Converts a dp value to pixels for the current device. */ dpToPx(float dp)457 public static int dpToPx(float dp) { 458 return (int) (dp * Resources.getSystem().getDisplayMetrics().density); 459 } 460 461 /** Converts a dp value to pixels for a certain density. */ dpToPx(float dp, int densityDpi)462 public static int dpToPx(float dp, int densityDpi) { 463 float densityRatio = (float) densityDpi / DisplayMetrics.DENSITY_DEFAULT; 464 return (int) (dp * densityRatio); 465 } 466 pxFromSp(float size, DisplayMetrics metrics)467 public static int pxFromSp(float size, DisplayMetrics metrics) { 468 return pxFromSp(size, metrics, 1f); 469 } 470 pxFromSp(float size, DisplayMetrics metrics, float scale)471 public static int pxFromSp(float size, DisplayMetrics metrics, float scale) { 472 float value = scale * TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, size, metrics); 473 return ResourceUtils.roundPxValueFromFloat(value); 474 } 475 createDbSelectionQuery(String columnName, IntArray values)476 public static String createDbSelectionQuery(String columnName, IntArray values) { 477 return String.format(Locale.ENGLISH, "%s IN (%s)", columnName, values.toConcatString()); 478 } 479 isBootCompleted()480 public static boolean isBootCompleted() { 481 return "1".equals(getSystemProperty("sys.boot_completed", "1")); 482 } 483 getSystemProperty(String property, String defaultValue)484 public static String getSystemProperty(String property, String defaultValue) { 485 try { 486 Class clazz = Class.forName("android.os.SystemProperties"); 487 Method getter = clazz.getDeclaredMethod("get", String.class); 488 String value = (String) getter.invoke(null, property); 489 if (!TextUtils.isEmpty(value)) { 490 return value; 491 } 492 } catch (Exception e) { 493 Log.d(TAG, "Unable to read system properties"); 494 } 495 return defaultValue; 496 } 497 498 /** 499 * Ensures that a value is within given bounds. Specifically: 500 * If value is less than lowerBound, return lowerBound; else if value is greater than upperBound, 501 * return upperBound; else return value unchanged. 502 */ boundToRange(int value, int lowerBound, int upperBound)503 public static int boundToRange(int value, int lowerBound, int upperBound) { 504 return Math.max(lowerBound, Math.min(value, upperBound)); 505 } 506 507 /** 508 * @see #boundToRange(int, int, int). 509 */ boundToRange(float value, float lowerBound, float upperBound)510 public static float boundToRange(float value, float lowerBound, float upperBound) { 511 return Math.max(lowerBound, Math.min(value, upperBound)); 512 } 513 514 /** 515 * @see #boundToRange(int, int, int). 516 */ boundToRange(long value, long lowerBound, long upperBound)517 public static long boundToRange(long value, long lowerBound, long upperBound) { 518 return Math.max(lowerBound, Math.min(value, upperBound)); 519 } 520 521 /** 522 * Wraps a message with a TTS span, so that a different message is spoken than 523 * what is getting displayed. 524 * @param msg original message 525 * @param ttsMsg message to be spoken 526 */ wrapForTts(CharSequence msg, String ttsMsg)527 public static CharSequence wrapForTts(CharSequence msg, String ttsMsg) { 528 SpannableString spanned = new SpannableString(msg); 529 spanned.setSpan(new TtsSpan.TextBuilder(ttsMsg).build(), 530 0, spanned.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); 531 return spanned; 532 } 533 534 /** 535 * Prefixes a text with the provided icon 536 */ prefixTextWithIcon(Context context, int iconRes, CharSequence msg)537 public static CharSequence prefixTextWithIcon(Context context, int iconRes, CharSequence msg) { 538 // Update the hint to contain the icon. 539 // Prefix the original hint with two spaces. The first space gets replaced by the icon 540 // using span. The second space is used for a singe space character between the hint 541 // and the icon. 542 SpannableString spanned = new SpannableString(" " + msg); 543 spanned.setSpan(new TintedDrawableSpan(context, iconRes), 544 0, 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); 545 return spanned; 546 } 547 isWallpaperSupported(Context context)548 public static boolean isWallpaperSupported(Context context) { 549 return context.getSystemService(WallpaperManager.class).isWallpaperSupported(); 550 } 551 isWallpaperAllowed(Context context)552 public static boolean isWallpaperAllowed(Context context) { 553 return context.getSystemService(WallpaperManager.class).isSetWallpaperAllowed(); 554 } 555 isBinderSizeError(Exception e)556 public static boolean isBinderSizeError(Exception e) { 557 return e.getCause() instanceof TransactionTooLargeException 558 || e.getCause() instanceof DeadObjectException; 559 } 560 561 /** 562 * Utility method to post a runnable on the handler, skipping the synchronization barriers. 563 */ postAsyncCallback(Handler handler, Runnable callback)564 public static void postAsyncCallback(Handler handler, Runnable callback) { 565 Message msg = Message.obtain(handler, callback); 566 msg.setAsynchronous(true); 567 handler.sendMessage(msg); 568 } 569 570 /** 571 * Utility method to allow background activity launch for the provided activity options 572 */ allowBGLaunch(ActivityOptions options)573 public static ActivityOptions allowBGLaunch(ActivityOptions options) { 574 if (ATLEAST_U) { 575 options.setPendingIntentBackgroundActivityStartMode( 576 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); 577 } 578 return options; 579 } 580 581 /** 582 * Returns the full drawable for info as multiple layers of AdaptiveIconDrawable. The second 583 * drawable in the Pair is the badge used with the icon. 584 * 585 * @param useTheme If true, will theme icons when applicable 586 */ 587 @SuppressLint("UseCompatLoadingForDrawables") 588 @Nullable 589 @WorkerThread 590 public static <T extends Context & ActivityContext> Pair<AdaptiveIconDrawable, Drawable> getFullDrawable(T context, ItemInfo info, int width, int height, boolean useTheme)591 getFullDrawable(T context, ItemInfo info, int width, int height, boolean useTheme) { 592 useTheme &= Themes.isThemedIconEnabled(context); 593 LauncherAppState appState = LauncherAppState.getInstance(context); 594 Drawable mainIcon = null; 595 596 Drawable badge = null; 597 if ((info instanceof ItemInfoWithIcon iiwi) && !iiwi.usingLowResIcon()) { 598 badge = iiwi.bitmap.getBadgeDrawable(context, useTheme); 599 } 600 601 if (info instanceof PendingAddShortcutInfo) { 602 ShortcutConfigActivityInfo activityInfo = 603 ((PendingAddShortcutInfo) info).getActivityInfo(context); 604 mainIcon = activityInfo.getFullResIcon(appState.getIconCache()); 605 } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { 606 LauncherActivityInfo activityInfo = context.getSystemService(LauncherApps.class) 607 .resolveActivity(info.getIntent(), info.user); 608 if (activityInfo == null) { 609 return null; 610 } 611 mainIcon = appState.getIconProvider().getIcon( 612 activityInfo, appState.getInvariantDeviceProfile().fillResIconDpi); 613 } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { 614 List<ShortcutInfo> siList = ShortcutKey.fromItemInfo(info) 615 .buildRequest(context) 616 .query(ShortcutRequest.ALL); 617 if (siList.isEmpty()) { 618 return null; 619 } else { 620 ShortcutInfo si = siList.get(0); 621 mainIcon = ShortcutCachingLogic.getIcon(context, si, 622 appState.getInvariantDeviceProfile().fillResIconDpi); 623 // Only fetch badge if the icon is on workspace 624 if (info.id != ItemInfo.NO_ID && badge == null) { 625 badge = appState.getIconCache().getShortcutInfoBadge(si) 626 .newIcon(context, FLAG_THEMED); 627 } 628 } 629 } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { 630 FolderAdaptiveIcon icon = FolderAdaptiveIcon.createFolderAdaptiveIcon( 631 context, info.id, new Point(width, height)); 632 if (icon == null) { 633 return null; 634 } 635 mainIcon = icon; 636 badge = icon.getBadge(); 637 } 638 639 if (mainIcon == null) { 640 return null; 641 } 642 AdaptiveIconDrawable result; 643 if (mainIcon instanceof AdaptiveIconDrawable aid) { 644 result = aid; 645 } else { 646 // Wrap the main icon in AID 647 try (LauncherIcons li = LauncherIcons.obtain(context)) { 648 result = li.wrapToAdaptiveIcon(mainIcon, null); 649 } 650 } 651 if (result == null) { 652 return null; 653 } 654 655 // Inject monochrome icon drawable 656 if (ATLEAST_T && useTheme) { 657 result.mutate(); 658 int[] colors = ThemedIconDrawable.getColors(context); 659 Drawable mono = result.getMonochrome(); 660 661 if (mono != null) { 662 mono.setTint(colors[1]); 663 } else if (info instanceof ItemInfoWithIcon iiwi) { 664 // Inject a previously generated monochrome icon 665 Bitmap monoBitmap = iiwi.bitmap.getMono(); 666 if (monoBitmap != null) { 667 // Use BitmapDrawable instead of FastBitmapDrawable so that the colorState is 668 // preserved in constantState 669 mono = new BitmapDrawable(monoBitmap); 670 mono.setColorFilter(new BlendModeColorFilter(colors[1], BlendMode.SRC_IN)); 671 // Inset the drawable according to the AdaptiveIconDrawable layers 672 mono = new InsetDrawable(mono, getExtraInsetFraction() / 2); 673 } 674 } 675 if (mono != null) { 676 result = new AdaptiveIconDrawable(new ColorDrawable(colors[0]), mono); 677 } 678 } 679 680 if (badge == null) { 681 badge = BitmapInfo.LOW_RES_INFO.withFlags( 682 UserCache.INSTANCE.get(context) 683 .getUserInfo(info.user) 684 .applyBitmapInfoFlags(FlagOp.NO_OP)) 685 .getBadgeDrawable(context, useTheme); 686 if (badge == null) { 687 badge = new ColorDrawable(Color.TRANSPARENT); 688 } 689 } 690 return Pair.create(result, badge); 691 } 692 squaredHypot(float x, float y)693 public static float squaredHypot(float x, float y) { 694 return x * x + y * y; 695 } 696 squaredTouchSlop(Context context)697 public static float squaredTouchSlop(Context context) { 698 float slop = ViewConfiguration.get(context).getScaledTouchSlop(); 699 return slop * slop; 700 } 701 702 /** 703 * Rotates `inOutBounds` by `delta` 90-degree increments. Rotation is visually CCW. Parent 704 * sizes represent the "space" that will rotate carrying inOutBounds along with it to determine 705 * the final bounds. 706 */ rotateBounds(Rect inOutBounds, int parentWidth, int parentHeight, int delta)707 public static void rotateBounds(Rect inOutBounds, int parentWidth, int parentHeight, 708 int delta) { 709 int rdelta = ((delta % 4) + 4) % 4; 710 int origLeft = inOutBounds.left; 711 switch (rdelta) { 712 case 0: 713 return; 714 case 1: 715 inOutBounds.left = inOutBounds.top; 716 inOutBounds.top = parentWidth - inOutBounds.right; 717 inOutBounds.right = inOutBounds.bottom; 718 inOutBounds.bottom = parentWidth - origLeft; 719 return; 720 case 2: 721 inOutBounds.left = parentWidth - inOutBounds.right; 722 inOutBounds.right = parentWidth - origLeft; 723 return; 724 case 3: 725 inOutBounds.left = parentHeight - inOutBounds.bottom; 726 inOutBounds.bottom = inOutBounds.right; 727 inOutBounds.right = parentHeight - inOutBounds.top; 728 inOutBounds.top = origLeft; 729 return; 730 } 731 } 732 733 /** 734 * Make a color filter that blends a color into the destination based on a scalable amout. 735 * 736 * @param color to blend in. 737 * @param tintAmount [0-1] 0 no tinting, 1 full color. 738 * @return ColorFilter for tinting, or {@code null} if no filter is needed. 739 */ makeColorTintingColorFilter(int color, float tintAmount)740 public static ColorFilter makeColorTintingColorFilter(int color, float tintAmount) { 741 if (tintAmount == 0f) { 742 return null; 743 } 744 return new LightingColorFilter( 745 // This isn't blending in white, its making a multiplication mask for the base color 746 ColorUtils.blendARGB(Color.WHITE, 0, tintAmount), 747 ColorUtils.blendARGB(0, color, tintAmount)); 748 } 749 getViewBounds(@onNull View v)750 public static Rect getViewBounds(@NonNull View v) { 751 int[] pos = new int[2]; 752 v.getLocationOnScreen(pos); 753 return new Rect(pos[0], pos[1], pos[0] + v.getWidth(), pos[1] + v.getHeight()); 754 } 755 756 /** 757 * Returns a list of screen-splitting options depending on the device orientation (split top for 758 * portrait, split right for landscape) 759 */ getSplitPositionOptions( DeviceProfile dp)760 public static List<SplitPositionOption> getSplitPositionOptions( 761 DeviceProfile dp) { 762 int splitIconRes = dp.isLeftRightSplit 763 ? R.drawable.ic_split_horizontal 764 : R.drawable.ic_split_vertical; 765 int stagePosition = dp.isLeftRightSplit 766 ? STAGE_POSITION_BOTTOM_OR_RIGHT 767 : STAGE_POSITION_TOP_OR_LEFT; 768 return Collections.singletonList(new SplitPositionOption( 769 splitIconRes, 770 R.string.recent_task_option_split_screen, 771 stagePosition, 772 STAGE_TYPE_MAIN 773 )); 774 } 775 776 /** Logs the Scale and Translate properties of a matrix. Ignores skew and perspective. */ logMatrix(String label, Matrix matrix)777 public static void logMatrix(String label, Matrix matrix) { 778 float[] matrixValues = new float[9]; 779 matrix.getValues(matrixValues); 780 Log.d(label, String.format("%s: %s\nscale (x,y) = (%f, %f)\ntranslate (x,y) = (%f, %f)", 781 label, matrix, matrixValues[Matrix.MSCALE_X], matrixValues[Matrix.MSCALE_Y], 782 matrixValues[Matrix.MTRANS_X], matrixValues[Matrix.MTRANS_Y] 783 )); 784 } 785 786 /** 787 * Translates the {@code targetView} so that it overlaps with {@code exclusionBounds} as little 788 * as possible, while remaining within {@code inclusionBounds}. 789 * <p> 790 * {@code inclusionBounds} will always take precedence over {@code exclusionBounds}, so if 791 * {@code targetView} needs to be translated outside of {@code inclusionBounds} to fully fix an 792 * overlap with {@code exclusionBounds}, then {@code targetView} will only be translated up to 793 * the border of {@code inclusionBounds}. 794 * <p> 795 * Note: {@code targetViewBounds}, {@code inclusionBounds} and {@code exclusionBounds} must all 796 * be in relation to the same reference point on screen. 797 * <p> 798 * @param targetView the view being translated 799 * @param targetViewBounds the bounds of the {@code targetView} 800 * @param inclusionBounds the bounds the {@code targetView} absolutely must stay within 801 * @param exclusionBounds the bounds to try to move the {@code targetView} away from 802 * @param adjustmentDirection the translation direction that should be attempted to fix an 803 * overlap 804 */ translateOverlappingView( @onNull View targetView, @NonNull Rect targetViewBounds, @NonNull Rect inclusionBounds, @NonNull Rect exclusionBounds, @AdjustmentDirection int adjustmentDirection)805 public static void translateOverlappingView( 806 @NonNull View targetView, 807 @NonNull Rect targetViewBounds, 808 @NonNull Rect inclusionBounds, 809 @NonNull Rect exclusionBounds, 810 @AdjustmentDirection int adjustmentDirection) { 811 switch (adjustmentDirection) { 812 case TRANSLATE_RIGHT: 813 targetView.setTranslationX(Math.min( 814 // Translate to the right if the view is overlapping on the left. 815 Math.max(0, exclusionBounds.right - targetViewBounds.left), 816 // Do not translate beyond the inclusion bounds. 817 inclusionBounds.right - targetViewBounds.right)); 818 break; 819 case TRANSLATE_LEFT: 820 targetView.setTranslationX(Math.max( 821 // Translate to the left if the view is overlapping on the right. 822 Math.min(0, exclusionBounds.left - targetViewBounds.right), 823 // Do not translate beyond the inclusion bounds. 824 inclusionBounds.left - targetViewBounds.left)); 825 break; 826 case TRANSLATE_DOWN: 827 targetView.setTranslationY(Math.min( 828 // Translate downwards if the view is overlapping on the top. 829 Math.max(0, exclusionBounds.bottom - targetViewBounds.top), 830 // Do not translate beyond the inclusion bounds. 831 inclusionBounds.bottom - targetViewBounds.bottom)); 832 break; 833 case TRANSLATE_UP: 834 targetView.setTranslationY(Math.max( 835 // Translate upwards if the view is overlapping on the bottom. 836 Math.min(0, exclusionBounds.top - targetViewBounds.bottom), 837 // Do not translate beyond the inclusion bounds. 838 inclusionBounds.top - targetViewBounds.top)); 839 break; 840 default: 841 // No-Op 842 } 843 } 844 845 /** 846 * Does a depth-first search through the View hierarchy starting at root, to find a view that 847 * matches the predicate. Returns null if no View was found. View has a findViewByPredicate 848 * member function but it is currently a @hide API. 849 */ 850 @Nullable findViewByPredicate(@onNull View root, @NonNull Predicate<View> predicate)851 public static <T extends View> T findViewByPredicate(@NonNull View root, 852 @NonNull Predicate<View> predicate) { 853 if (predicate.test(root)) { 854 return (T) root; 855 } 856 if (root instanceof ViewGroup parent) { 857 int count = parent.getChildCount(); 858 for (int i = 0; i < count; i++) { 859 View view = findViewByPredicate(parent.getChildAt(i), predicate); 860 if (view != null) { 861 return (T) view; 862 } 863 } 864 } 865 return null; 866 } 867 } 868