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