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.model.data.ItemInfoWithIcon.FLAG_ICON_BADGED; 20 21 import android.annotation.TargetApi; 22 import android.app.ActivityManager; 23 import android.app.Person; 24 import android.app.WallpaperManager; 25 import android.content.BroadcastReceiver; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.SharedPreferences; 30 import android.content.pm.LauncherActivityInfo; 31 import android.content.pm.LauncherApps; 32 import android.content.pm.PackageInfo; 33 import android.content.pm.PackageManager; 34 import android.content.pm.ResolveInfo; 35 import android.content.pm.ShortcutInfo; 36 import android.content.res.Configuration; 37 import android.content.res.Resources; 38 import android.database.ContentObserver; 39 import android.graphics.Bitmap; 40 import android.graphics.Color; 41 import android.graphics.ColorFilter; 42 import android.graphics.LightingColorFilter; 43 import android.graphics.Matrix; 44 import android.graphics.Paint; 45 import android.graphics.Point; 46 import android.graphics.Rect; 47 import android.graphics.RectF; 48 import android.graphics.drawable.ColorDrawable; 49 import android.graphics.drawable.Drawable; 50 import android.graphics.drawable.InsetDrawable; 51 import android.net.Uri; 52 import android.os.Build; 53 import android.os.DeadObjectException; 54 import android.os.Handler; 55 import android.os.Message; 56 import android.os.TransactionTooLargeException; 57 import android.provider.Settings; 58 import android.text.Spannable; 59 import android.text.SpannableString; 60 import android.text.TextUtils; 61 import android.text.style.TtsSpan; 62 import android.util.DisplayMetrics; 63 import android.util.Log; 64 import android.util.TypedValue; 65 import android.view.MotionEvent; 66 import android.view.View; 67 import android.view.ViewConfiguration; 68 import android.view.animation.Interpolator; 69 import android.widget.LinearLayout; 70 71 import androidx.core.graphics.ColorUtils; 72 import androidx.core.os.BuildCompat; 73 74 import com.android.launcher3.dragndrop.FolderAdaptiveIcon; 75 import com.android.launcher3.graphics.GridCustomizationsProvider; 76 import com.android.launcher3.graphics.TintedDrawableSpan; 77 import com.android.launcher3.icons.BitmapInfo; 78 import com.android.launcher3.icons.FastBitmapDrawable; 79 import com.android.launcher3.icons.LauncherIcons; 80 import com.android.launcher3.icons.ShortcutCachingLogic; 81 import com.android.launcher3.model.data.ItemInfo; 82 import com.android.launcher3.model.data.ItemInfoWithIcon; 83 import com.android.launcher3.pm.ShortcutConfigActivityInfo; 84 import com.android.launcher3.shortcuts.ShortcutKey; 85 import com.android.launcher3.shortcuts.ShortcutRequest; 86 import com.android.launcher3.util.IntArray; 87 import com.android.launcher3.util.PackageManagerHelper; 88 import com.android.launcher3.views.BaseDragLayer; 89 import com.android.launcher3.widget.PendingAddShortcutInfo; 90 91 import java.lang.reflect.Method; 92 import java.util.Arrays; 93 import java.util.List; 94 import java.util.Locale; 95 import java.util.function.Consumer; 96 import java.util.regex.Matcher; 97 import java.util.regex.Pattern; 98 99 /** 100 * Various utilities shared amongst the Launcher's classes. 101 */ 102 public final class Utilities { 103 104 private static final String TAG = "Launcher.Utilities"; 105 106 private static final Pattern sTrimPattern = 107 Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$"); 108 109 private static final float[] sTmpFloatArray = new float[4]; 110 111 private static final int[] sLoc0 = new int[2]; 112 private static final int[] sLoc1 = new int[2]; 113 private static final Matrix sMatrix = new Matrix(); 114 private static final Matrix sInverseMatrix = new Matrix(); 115 116 public static final String[] EMPTY_STRING_ARRAY = new String[0]; 117 public static final Person[] EMPTY_PERSON_ARRAY = new Person[0]; 118 119 public static final boolean ATLEAST_P = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P; 120 121 public static final boolean ATLEAST_Q = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; 122 123 public static final boolean ATLEAST_R = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R; 124 125 public static final boolean ATLEAST_S = BuildCompat.isAtLeastS() 126 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S; 127 128 /** 129 * Set on a motion event dispatched from the nav bar. See {@link MotionEvent#setEdgeFlags(int)}. 130 */ 131 public static final int EDGE_NAV_BAR = 1 << 8; 132 133 /** 134 * Indicates if the device has a debug build. Should only be used to store additional info or 135 * add extra logging and not for changing the app behavior. 136 */ 137 public static final boolean IS_DEBUG_DEVICE = 138 Build.TYPE.toLowerCase(Locale.ROOT).contains("debug") || 139 Build.TYPE.toLowerCase(Locale.ROOT).equals("eng"); 140 141 /** 142 * Returns true if theme is dark. 143 */ isDarkTheme(Context context)144 public static boolean isDarkTheme(Context context) { 145 Configuration configuration = context.getResources().getConfiguration(); 146 int nightMode = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK; 147 return nightMode == Configuration.UI_MODE_NIGHT_YES; 148 } 149 isDevelopersOptionsEnabled(Context context)150 public static boolean isDevelopersOptionsEnabled(Context context) { 151 return Settings.Global.getInt(context.getApplicationContext().getContentResolver(), 152 Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0; 153 } 154 155 // An intent extra to indicate the horizontal scroll of the wallpaper. 156 public static final String EXTRA_WALLPAPER_OFFSET = "com.android.launcher3.WALLPAPER_OFFSET"; 157 public static final String EXTRA_WALLPAPER_FLAVOR = "com.android.launcher3.WALLPAPER_FLAVOR"; 158 159 // An intent extra to indicate the launch source by launcher. 160 public static final String EXTRA_WALLPAPER_LAUNCH_SOURCE = 161 "com.android.wallpaper.LAUNCH_SOURCE"; 162 163 public static boolean IS_RUNNING_IN_TEST_HARNESS = 164 ActivityManager.isRunningInTestHarness(); 165 enableRunningInTestHarnessForTests()166 public static void enableRunningInTestHarnessForTests() { 167 IS_RUNNING_IN_TEST_HARNESS = true; 168 } 169 isPropertyEnabled(String propertyName)170 public static boolean isPropertyEnabled(String propertyName) { 171 return Log.isLoggable(propertyName, Log.VERBOSE); 172 } 173 existsStyleWallpapers(Context context)174 public static boolean existsStyleWallpapers(Context context) { 175 ResolveInfo ri = context.getPackageManager().resolveActivity( 176 PackageManagerHelper.getStyleWallpapersIntent(context), 0); 177 return ri != null; 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 = (View) v.getParent(); 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 * Inverse of {@link #getDescendantCoordRelativeToAncestor(View, View, float[], boolean)}. 264 */ mapCoordInSelfToDescendant(View descendant, View root, float[] coord)265 public static void mapCoordInSelfToDescendant(View descendant, View root, float[] coord) { 266 sMatrix.reset(); 267 View v = descendant; 268 while(v != root) { 269 sMatrix.postTranslate(-v.getScrollX(), -v.getScrollY()); 270 sMatrix.postConcat(v.getMatrix()); 271 sMatrix.postTranslate(v.getLeft(), v.getTop()); 272 v = (View) v.getParent(); 273 } 274 sMatrix.postTranslate(-v.getScrollX(), -v.getScrollY()); 275 sMatrix.invert(sInverseMatrix); 276 sInverseMatrix.mapPoints(coord); 277 } 278 279 /** 280 * Sets {@param out} to be same as {@param in} by rounding individual values 281 */ roundArray(float[] in, int[] out)282 public static void roundArray(float[] in, int[] out) { 283 for (int i = 0; i < in.length; i++) { 284 out[i] = Math.round(in[i]); 285 } 286 } 287 offsetPoints(float[] points, float offsetX, float offsetY)288 public static void offsetPoints(float[] points, float offsetX, float offsetY) { 289 for (int i = 0; i < points.length; i += 2) { 290 points[i] += offsetX; 291 points[i + 1] += offsetY; 292 } 293 } 294 295 /** 296 * Utility method to determine whether the given point, in local coordinates, 297 * is inside the view, where the area of the view is expanded by the slop factor. 298 * This method is called while processing touch-move events to determine if the event 299 * is still within the view. 300 */ pointInView(View v, float localX, float localY, float slop)301 public static boolean pointInView(View v, float localX, float localY, float slop) { 302 return localX >= -slop && localY >= -slop && localX < (v.getWidth() + slop) && 303 localY < (v.getHeight() + slop); 304 } 305 getCenterDeltaInScreenSpace(View v0, View v1)306 public static int[] getCenterDeltaInScreenSpace(View v0, View v1) { 307 v0.getLocationInWindow(sLoc0); 308 v1.getLocationInWindow(sLoc1); 309 310 sLoc0[0] += (v0.getMeasuredWidth() * v0.getScaleX()) / 2; 311 sLoc0[1] += (v0.getMeasuredHeight() * v0.getScaleY()) / 2; 312 sLoc1[0] += (v1.getMeasuredWidth() * v1.getScaleX()) / 2; 313 sLoc1[1] += (v1.getMeasuredHeight() * v1.getScaleY()) / 2; 314 return new int[] {sLoc1[0] - sLoc0[0], sLoc1[1] - sLoc0[1]}; 315 } 316 317 /** 318 * Helper method to set rectOut with rectFSrc. 319 */ setRect(RectF rectFSrc, Rect rectOut)320 public static void setRect(RectF rectFSrc, Rect rectOut) { 321 rectOut.left = (int) rectFSrc.left; 322 rectOut.top = (int) rectFSrc.top; 323 rectOut.right = (int) rectFSrc.right; 324 rectOut.bottom = (int) rectFSrc.bottom; 325 } 326 scaleRectFAboutCenter(RectF r, float scale)327 public static void scaleRectFAboutCenter(RectF r, float scale) { 328 scaleRectFAboutPivot(r, scale, r.centerX(), r.centerY()); 329 } 330 scaleRectFAboutPivot(RectF r, float scale, float px, float py)331 public static void scaleRectFAboutPivot(RectF r, float scale, float px, float py) { 332 if (scale != 1.0f) { 333 r.offset(-px, -py); 334 r.left = r.left * scale; 335 r.top = r.top * scale ; 336 r.right = r.right * scale; 337 r.bottom = r.bottom * scale; 338 r.offset(px, py); 339 } 340 } 341 scaleRectAboutCenter(Rect r, float scale)342 public static void scaleRectAboutCenter(Rect r, float scale) { 343 if (scale != 1.0f) { 344 int cx = r.centerX(); 345 int cy = r.centerY(); 346 r.offset(-cx, -cy); 347 scaleRect(r, scale); 348 r.offset(cx, cy); 349 } 350 } 351 scaleRect(Rect r, float scale)352 public static void scaleRect(Rect r, float scale) { 353 if (scale != 1.0f) { 354 r.left = (int) (r.left * scale + 0.5f); 355 r.top = (int) (r.top * scale + 0.5f); 356 r.right = (int) (r.right * scale + 0.5f); 357 r.bottom = (int) (r.bottom * scale + 0.5f); 358 } 359 } 360 insetRect(Rect r, Rect insets)361 public static void insetRect(Rect r, Rect insets) { 362 r.left = Math.min(r.right, r.left + insets.left); 363 r.top = Math.min(r.bottom, r.top + insets.top); 364 r.right = Math.max(r.left, r.right - insets.right); 365 r.bottom = Math.max(r.top, r.bottom - insets.bottom); 366 } 367 shrinkRect(Rect r, float scaleX, float scaleY)368 public static float shrinkRect(Rect r, float scaleX, float scaleY) { 369 float scale = Math.min(Math.min(scaleX, scaleY), 1.0f); 370 if (scale < 1.0f) { 371 int deltaX = (int) (r.width() * (scaleX - scale) * 0.5f); 372 r.left += deltaX; 373 r.right -= deltaX; 374 375 int deltaY = (int) (r.height() * (scaleY - scale) * 0.5f); 376 r.top += deltaY; 377 r.bottom -= deltaY; 378 } 379 return scale; 380 } 381 382 /** 383 * Maps t from one range to another range. 384 * @param t The value to map. 385 * @param fromMin The lower bound of the range that t is being mapped from. 386 * @param fromMax The upper bound of the range that t is being mapped from. 387 * @param toMin The lower bound of the range that t is being mapped to. 388 * @param toMax The upper bound of the range that t is being mapped to. 389 * @return The mapped value of t. 390 */ mapToRange(float t, float fromMin, float fromMax, float toMin, float toMax, Interpolator interpolator)391 public static float mapToRange(float t, float fromMin, float fromMax, float toMin, float toMax, 392 Interpolator interpolator) { 393 if (fromMin == fromMax || toMin == toMax) { 394 Log.e(TAG, "mapToRange: range has 0 length"); 395 return toMin; 396 } 397 float progress = getProgress(t, fromMin, fromMax); 398 return mapRange(interpolator.getInterpolation(progress), toMin, toMax); 399 } 400 401 /** 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)402 public static float mapBoundToRange(float t, float lowerBound, float upperBound, 403 float toMin, float toMax, Interpolator interpolator) { 404 return mapToRange(boundToRange(t, lowerBound, upperBound), lowerBound, upperBound, 405 toMin, toMax, interpolator); 406 } 407 getProgress(float current, float min, float max)408 public static float getProgress(float current, float min, float max) { 409 return Math.abs(current - min) / Math.abs(max - min); 410 } 411 mapRange(float value, float min, float max)412 public static float mapRange(float value, float min, float max) { 413 return min + (value * (max - min)); 414 } 415 416 /** 417 * Bounds parameter to the range [0, 1] 418 */ saturate(float a)419 public static float saturate(float a) { 420 return boundToRange(a, 0, 1.0f); 421 } 422 423 /** 424 * Returns the compliment (1 - a) of the parameter. 425 */ comp(float a)426 public static float comp(float a) { 427 return 1 - a; 428 } 429 430 /** 431 * Returns the "probabilistic or" of a and b. (a + b - ab). 432 * Useful beyond probability, can be used to combine two unit progresses for example. 433 */ or(float a, float b)434 public static float or(float a, float b) { 435 float satA = saturate(a); 436 float satB = saturate(b); 437 return satA + satB - (satA * satB); 438 } 439 440 /** 441 * Trims the string, removing all whitespace at the beginning and end of the string. 442 * Non-breaking whitespaces are also removed. 443 */ trim(CharSequence s)444 public static String trim(CharSequence s) { 445 if (s == null) { 446 return null; 447 } 448 449 // Just strip any sequence of whitespace or java space characters from the beginning and end 450 Matcher m = sTrimPattern.matcher(s); 451 return m.replaceAll("$1"); 452 } 453 454 /** 455 * Calculates the height of a given string at a specific text size. 456 */ calculateTextHeight(float textSizePx)457 public static int calculateTextHeight(float textSizePx) { 458 Paint p = new Paint(); 459 p.setTextSize(textSizePx); 460 Paint.FontMetrics fm = p.getFontMetrics(); 461 return (int) Math.ceil(fm.bottom - fm.top); 462 } 463 isRtl(Resources res)464 public static boolean isRtl(Resources res) { 465 return res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 466 } 467 dpiFromPx(float size, int densityDpi)468 public static float dpiFromPx(float size, int densityDpi) { 469 float densityRatio = (float) densityDpi / DisplayMetrics.DENSITY_DEFAULT; 470 return (size / densityRatio); 471 } 472 473 /** Converts a dp value to pixels for the current device. */ dpToPx(float dp)474 public static int dpToPx(float dp) { 475 return (int) (dp * Resources.getSystem().getDisplayMetrics().density); 476 } 477 478 pxFromSp(float size, DisplayMetrics metrics)479 public static int pxFromSp(float size, DisplayMetrics metrics) { 480 return pxFromSp(size, metrics, 1f); 481 } 482 pxFromSp(float size, DisplayMetrics metrics, float scale)483 public static int pxFromSp(float size, DisplayMetrics metrics, float scale) { 484 return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 485 size, metrics) * scale); 486 } 487 createDbSelectionQuery(String columnName, IntArray values)488 public static String createDbSelectionQuery(String columnName, IntArray values) { 489 return String.format(Locale.ENGLISH, "%s IN (%s)", columnName, values.toConcatString()); 490 } 491 isBootCompleted()492 public static boolean isBootCompleted() { 493 return "1".equals(getSystemProperty("sys.boot_completed", "1")); 494 } 495 getSystemProperty(String property, String defaultValue)496 public static String getSystemProperty(String property, String defaultValue) { 497 try { 498 Class clazz = Class.forName("android.os.SystemProperties"); 499 Method getter = clazz.getDeclaredMethod("get", String.class); 500 String value = (String) getter.invoke(null, property); 501 if (!TextUtils.isEmpty(value)) { 502 return value; 503 } 504 } catch (Exception e) { 505 Log.d(TAG, "Unable to read system properties"); 506 } 507 return defaultValue; 508 } 509 510 /** 511 * Ensures that a value is within given bounds. Specifically: 512 * If value is less than lowerBound, return lowerBound; else if value is greater than upperBound, 513 * return upperBound; else return value unchanged. 514 */ boundToRange(int value, int lowerBound, int upperBound)515 public static int boundToRange(int value, int lowerBound, int upperBound) { 516 return Math.max(lowerBound, Math.min(value, upperBound)); 517 } 518 519 /** 520 * @see #boundToRange(int, int, int). 521 */ boundToRange(float value, float lowerBound, float upperBound)522 public static float boundToRange(float value, float lowerBound, float upperBound) { 523 return Math.max(lowerBound, Math.min(value, upperBound)); 524 } 525 526 /** 527 * @see #boundToRange(int, int, int). 528 */ boundToRange(long value, long lowerBound, long upperBound)529 public static long boundToRange(long value, long lowerBound, long upperBound) { 530 return Math.max(lowerBound, Math.min(value, upperBound)); 531 } 532 533 /** 534 * Returns an intent for starting the default home activity 535 */ createHomeIntent()536 public static Intent createHomeIntent() { 537 return new Intent(Intent.ACTION_MAIN) 538 .addCategory(Intent.CATEGORY_HOME) 539 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 540 } 541 542 /** 543 * Wraps a message with a TTS span, so that a different message is spoken than 544 * what is getting displayed. 545 * @param msg original message 546 * @param ttsMsg message to be spoken 547 */ wrapForTts(CharSequence msg, String ttsMsg)548 public static CharSequence wrapForTts(CharSequence msg, String ttsMsg) { 549 SpannableString spanned = new SpannableString(msg); 550 spanned.setSpan(new TtsSpan.TextBuilder(ttsMsg).build(), 551 0, spanned.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); 552 return spanned; 553 } 554 555 /** 556 * Prefixes a text with the provided icon 557 */ prefixTextWithIcon(Context context, int iconRes, CharSequence msg)558 public static CharSequence prefixTextWithIcon(Context context, int iconRes, CharSequence msg) { 559 // Update the hint to contain the icon. 560 // Prefix the original hint with two spaces. The first space gets replaced by the icon 561 // using span. The second space is used for a singe space character between the hint 562 // and the icon. 563 SpannableString spanned = new SpannableString(" " + msg); 564 spanned.setSpan(new TintedDrawableSpan(context, iconRes), 565 0, 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); 566 return spanned; 567 } 568 getPrefs(Context context)569 public static SharedPreferences getPrefs(Context context) { 570 // Use application context for shared preferences, so that we use a single cached instance 571 return context.getApplicationContext().getSharedPreferences( 572 LauncherFiles.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE); 573 } 574 getDevicePrefs(Context context)575 public static SharedPreferences getDevicePrefs(Context context) { 576 // Use application context for shared preferences, so that we use a single cached instance 577 return context.getApplicationContext().getSharedPreferences( 578 LauncherFiles.DEVICE_PREFERENCES_KEY, Context.MODE_PRIVATE); 579 } 580 isWallpaperAllowed(Context context)581 public static boolean isWallpaperAllowed(Context context) { 582 return context.getSystemService(WallpaperManager.class).isSetWallpaperAllowed(); 583 } 584 isBinderSizeError(Exception e)585 public static boolean isBinderSizeError(Exception e) { 586 return e.getCause() instanceof TransactionTooLargeException 587 || e.getCause() instanceof DeadObjectException; 588 } 589 isGridOptionsEnabled(Context context)590 public static boolean isGridOptionsEnabled(Context context) { 591 return isComponentEnabled(context.getPackageManager(), 592 context.getPackageName(), 593 GridCustomizationsProvider.class.getName()); 594 } 595 isComponentEnabled(PackageManager pm, String pkgName, String clsName)596 private static boolean isComponentEnabled(PackageManager pm, String pkgName, String clsName) { 597 ComponentName componentName = new ComponentName(pkgName, clsName); 598 int componentEnabledSetting = pm.getComponentEnabledSetting(componentName); 599 600 switch (componentEnabledSetting) { 601 case PackageManager.COMPONENT_ENABLED_STATE_DISABLED: 602 return false; 603 case PackageManager.COMPONENT_ENABLED_STATE_ENABLED: 604 return true; 605 case PackageManager.COMPONENT_ENABLED_STATE_DEFAULT: 606 default: 607 // We need to get the application info to get the component's default state 608 try { 609 PackageInfo packageInfo = pm.getPackageInfo(pkgName, 610 PackageManager.GET_PROVIDERS | PackageManager.GET_DISABLED_COMPONENTS); 611 612 if (packageInfo.providers != null) { 613 return Arrays.stream(packageInfo.providers).anyMatch( 614 pi -> pi.name.equals(clsName) && pi.isEnabled()); 615 } 616 617 // the component is not declared in the AndroidManifest 618 return false; 619 } catch (PackageManager.NameNotFoundException e) { 620 // the package isn't installed on the device 621 return false; 622 } 623 } 624 } 625 626 /** 627 * Utility method to post a runnable on the handler, skipping the synchronization barriers. 628 */ postAsyncCallback(Handler handler, Runnable callback)629 public static void postAsyncCallback(Handler handler, Runnable callback) { 630 Message msg = Message.obtain(handler, callback); 631 msg.setAsynchronous(true); 632 handler.sendMessage(msg); 633 } 634 635 /** 636 * Parses a string encoded using {@link #getPointString(int, int)} 637 */ parsePoint(String point)638 public static Point parsePoint(String point) { 639 String[] split = point.split(","); 640 return new Point(Integer.parseInt(split[0]), Integer.parseInt(split[1])); 641 } 642 643 /** 644 * Encodes a point to string to that it can be persisted atomically. 645 */ getPointString(int x, int y)646 public static String getPointString(int x, int y) { 647 return String.format(Locale.ENGLISH, "%d,%d", x, y); 648 } 649 unregisterReceiverSafely(Context context, BroadcastReceiver receiver)650 public static void unregisterReceiverSafely(Context context, BroadcastReceiver receiver) { 651 try { 652 context.unregisterReceiver(receiver); 653 } catch (IllegalArgumentException e) {} 654 } 655 656 /** 657 * Returns the full drawable for info without any flattening or pre-processing. 658 * 659 * @param outObj this is set to the internal data associated with {@param info}, 660 * eg {@link LauncherActivityInfo} or {@link ShortcutInfo}. 661 */ getFullDrawable(Launcher launcher, ItemInfo info, int width, int height, Object[] outObj)662 public static Drawable getFullDrawable(Launcher launcher, ItemInfo info, int width, int height, 663 Object[] outObj) { 664 Drawable icon = loadFullDrawableWithoutTheme(launcher, info, width, height, outObj); 665 if (icon instanceof BitmapInfo.Extender) { 666 icon = ((BitmapInfo.Extender) icon).getThemedDrawable(launcher); 667 } 668 return icon; 669 } 670 loadFullDrawableWithoutTheme(Launcher launcher, ItemInfo info, int width, int height, Object[] outObj)671 private static Drawable loadFullDrawableWithoutTheme(Launcher launcher, ItemInfo info, 672 int width, int height, Object[] outObj) { 673 LauncherAppState appState = LauncherAppState.getInstance(launcher); 674 if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { 675 LauncherActivityInfo activityInfo = launcher.getSystemService(LauncherApps.class) 676 .resolveActivity(info.getIntent(), info.user); 677 outObj[0] = activityInfo; 678 return activityInfo == null ? null : LauncherAppState.getInstance(launcher) 679 .getIconProvider().getIcon( 680 activityInfo, launcher.getDeviceProfile().inv.fillResIconDpi); 681 } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { 682 if (info instanceof PendingAddShortcutInfo) { 683 ShortcutConfigActivityInfo activityInfo = 684 ((PendingAddShortcutInfo) info).activityInfo; 685 outObj[0] = activityInfo; 686 return activityInfo.getFullResIcon(appState.getIconCache()); 687 } 688 List<ShortcutInfo> si = ShortcutKey.fromItemInfo(info) 689 .buildRequest(launcher) 690 .query(ShortcutRequest.ALL); 691 if (si.isEmpty()) { 692 return null; 693 } else { 694 outObj[0] = si.get(0); 695 return ShortcutCachingLogic.getIcon(launcher, si.get(0), 696 appState.getInvariantDeviceProfile().fillResIconDpi); 697 } 698 } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { 699 FolderAdaptiveIcon icon = FolderAdaptiveIcon.createFolderAdaptiveIcon( 700 launcher, info.id, new Point(width, height)); 701 if (icon == null) { 702 return null; 703 } 704 outObj[0] = icon; 705 return icon; 706 } else { 707 return null; 708 } 709 } 710 711 /** 712 * For apps icons and shortcut icons that have badges, this method creates a drawable that can 713 * later on be rendered on top of the layers for the badges. For app icons, work profile badges 714 * can only be applied. For deep shortcuts, when dragged from the pop up container, there's no 715 * badge. When dragged from workspace or folder, it may contain app AND/OR work profile badge 716 **/ 717 @TargetApi(Build.VERSION_CODES.O) getBadge(Launcher launcher, ItemInfo info, Object obj)718 public static Drawable getBadge(Launcher launcher, ItemInfo info, Object obj) { 719 LauncherAppState appState = LauncherAppState.getInstance(launcher); 720 int iconSize = appState.getInvariantDeviceProfile().iconBitmapSize; 721 if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { 722 boolean iconBadged = (info instanceof ItemInfoWithIcon) 723 && (((ItemInfoWithIcon) info).runtimeStatusFlags & FLAG_ICON_BADGED) > 0; 724 if ((info.id == ItemInfo.NO_ID && !iconBadged) 725 || !(obj instanceof ShortcutInfo)) { 726 // The item is not yet added on home screen. 727 return new FixedSizeEmptyDrawable(iconSize); 728 } 729 ShortcutInfo si = (ShortcutInfo) obj; 730 Bitmap badge = LauncherAppState.getInstance(appState.getContext()) 731 .getIconCache().getShortcutInfoBadge(si).icon; 732 float badgeSize = LauncherIcons.getBadgeSizeForIconSize(iconSize); 733 float insetFraction = (iconSize - badgeSize) / iconSize; 734 return new InsetDrawable(new FastBitmapDrawable(badge), 735 insetFraction, insetFraction, 0, 0); 736 } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { 737 return ((FolderAdaptiveIcon) obj).getBadge(); 738 } else { 739 return launcher.getPackageManager() 740 .getUserBadgedIcon(new FixedSizeEmptyDrawable(iconSize), info.user); 741 } 742 } 743 744 /** 745 * @return true is the extra is either null or is of type {@param type} 746 */ isValidExtraType(Intent intent, String key, Class type)747 public static boolean isValidExtraType(Intent intent, String key, Class type) { 748 Object extra = intent.getParcelableExtra(key); 749 return extra == null || type.isInstance(extra); 750 } 751 squaredHypot(float x, float y)752 public static float squaredHypot(float x, float y) { 753 return x * x + y * y; 754 } 755 squaredTouchSlop(Context context)756 public static float squaredTouchSlop(Context context) { 757 float slop = ViewConfiguration.get(context).getScaledTouchSlop(); 758 return slop * slop; 759 } 760 761 /** 762 * Helper method to create a content provider 763 */ newContentObserver(Handler handler, Consumer<Uri> command)764 public static ContentObserver newContentObserver(Handler handler, Consumer<Uri> command) { 765 return new ContentObserver(handler) { 766 @Override 767 public void onChange(boolean selfChange, Uri uri) { 768 command.accept(uri); 769 } 770 }; 771 } 772 773 /** 774 * Compares the ratio of two quantities and returns whether that ratio is greater than the 775 * provided bound. Order of quantities does not matter. Bound should be a decimal representation 776 * of a percentage. 777 */ 778 public static boolean isRelativePercentDifferenceGreaterThan(float first, float second, 779 float bound) { 780 return (Math.abs(first - second) / Math.abs((first + second) / 2.0f)) > bound; 781 } 782 783 /** 784 * Rotates `inOutBounds` by `delta` 90-degree increments. Rotation is visually CCW. Parent 785 * sizes represent the "space" that will rotate carrying inOutBounds along with it to determine 786 * the final bounds. 787 */ 788 public static void rotateBounds(Rect inOutBounds, int parentWidth, int parentHeight, 789 int delta) { 790 int rdelta = ((delta % 4) + 4) % 4; 791 int origLeft = inOutBounds.left; 792 switch (rdelta) { 793 case 0: 794 return; 795 case 1: 796 inOutBounds.left = inOutBounds.top; 797 inOutBounds.top = parentWidth - inOutBounds.right; 798 inOutBounds.right = inOutBounds.bottom; 799 inOutBounds.bottom = parentWidth - origLeft; 800 return; 801 case 2: 802 inOutBounds.left = parentWidth - inOutBounds.right; 803 inOutBounds.right = parentWidth - origLeft; 804 return; 805 case 3: 806 inOutBounds.left = parentHeight - inOutBounds.bottom; 807 inOutBounds.bottom = inOutBounds.right; 808 inOutBounds.right = parentHeight - inOutBounds.top; 809 inOutBounds.top = origLeft; 810 return; 811 } 812 } 813 814 /** 815 * Make a color filter that blends a color into the destination based on a scalable amout. 816 * 817 * @param color to blend in. 818 * @param tintAmount [0-1] 0 no tinting, 1 full color. 819 * @return ColorFilter for tinting, or {@code null} if no filter is needed. 820 */ 821 public static ColorFilter makeColorTintingColorFilter(int color, float tintAmount) { 822 if (tintAmount == 0f) { 823 return null; 824 } 825 return new LightingColorFilter( 826 // This isn't blending in white, its making a multiplication mask for the base color 827 ColorUtils.blendARGB(Color.WHITE, 0, tintAmount), 828 ColorUtils.blendARGB(0, color, tintAmount)); 829 } 830 831 /** 832 * Sets start margin on the provided {@param view} to be {@param margin}. 833 * Assumes {@param view} is a child of {@link LinearLayout} 834 */ 835 public static void setStartMarginForView(View view, int margin) { 836 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) view.getLayoutParams(); 837 lp.setMarginStart(margin); 838 view.setLayoutParams(lp); 839 } 840 841 private static class FixedSizeEmptyDrawable extends ColorDrawable { 842 843 private final int mSize; 844 845 public FixedSizeEmptyDrawable(int size) { 846 super(Color.TRANSPARENT); 847 mSize = size; 848 } 849 850 @Override 851 public int getIntrinsicHeight() { 852 return mSize; 853 } 854 855 @Override 856 public int getIntrinsicWidth() { 857 return mSize; 858 } 859 } 860 } 861