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