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 android.app.WallpaperManager; 20 import android.content.ComponentName; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.SharedPreferences; 24 import android.content.pm.ApplicationInfo; 25 import android.content.pm.PackageInfo; 26 import android.content.pm.PackageManager; 27 import android.content.pm.PackageManager.NameNotFoundException; 28 import android.content.pm.ResolveInfo; 29 import android.content.res.Resources; 30 import android.graphics.Bitmap; 31 import android.graphics.Color; 32 import android.graphics.Matrix; 33 import android.graphics.Paint; 34 import android.graphics.Rect; 35 import android.os.Build; 36 import android.os.Bundle; 37 import android.os.DeadObjectException; 38 import android.os.PowerManager; 39 import android.os.TransactionTooLargeException; 40 import android.text.Spannable; 41 import android.text.SpannableString; 42 import android.text.TextUtils; 43 import android.text.style.TtsSpan; 44 import android.util.DisplayMetrics; 45 import android.util.Log; 46 import android.util.Pair; 47 import android.util.SparseArray; 48 import android.util.TypedValue; 49 import android.view.View; 50 import android.view.accessibility.AccessibilityEvent; 51 import android.view.accessibility.AccessibilityManager; 52 53 import com.android.launcher3.config.FeatureFlags; 54 55 import java.io.ByteArrayOutputStream; 56 import java.io.Closeable; 57 import java.io.IOException; 58 import java.lang.reflect.InvocationTargetException; 59 import java.lang.reflect.Method; 60 import java.util.Collection; 61 import java.util.HashSet; 62 import java.util.Locale; 63 import java.util.concurrent.Executor; 64 import java.util.concurrent.LinkedBlockingQueue; 65 import java.util.concurrent.ThreadPoolExecutor; 66 import java.util.concurrent.TimeUnit; 67 import java.util.regex.Matcher; 68 import java.util.regex.Pattern; 69 70 /** 71 * Various utilities shared amongst the Launcher's classes. 72 */ 73 public final class Utilities { 74 75 private static final String TAG = "Launcher.Utilities"; 76 77 private static final Pattern sTrimPattern = 78 Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$"); 79 80 private static final int[] sLoc0 = new int[2]; 81 private static final int[] sLoc1 = new int[2]; 82 private static final float[] sPoint = new float[2]; 83 private static final Matrix sMatrix = new Matrix(); 84 private static final Matrix sInverseMatrix = new Matrix(); 85 86 public static final boolean ATLEAST_OREO_MR1 = 87 Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1; 88 89 public static final boolean ATLEAST_OREO = 90 Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; 91 92 public static final boolean ATLEAST_NOUGAT_MR1 = 93 Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1; 94 95 public static final boolean ATLEAST_NOUGAT = 96 Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; 97 98 public static final boolean ATLEAST_MARSHMALLOW = 99 Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; 100 101 public static final boolean ATLEAST_LOLLIPOP_MR1 = 102 Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1; 103 104 /** 105 * Indicates if the device has a debug build. Should only be used to store additional info or 106 * add extra logging and not for changing the app behavior. 107 */ 108 public static final boolean IS_DEBUG_DEVICE = Build.TYPE.toLowerCase().contains("debug"); 109 110 // An intent extra to indicate the horizontal scroll of the wallpaper. 111 public static final String EXTRA_WALLPAPER_OFFSET = "com.android.launcher3.WALLPAPER_OFFSET"; 112 113 public static final int COLOR_EXTRACTION_JOB_ID = 1; 114 public static final int WALLPAPER_COMPAT_JOB_ID = 2; 115 116 // These values are same as that in {@link AsyncTask}. 117 private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); 118 private static final int CORE_POOL_SIZE = CPU_COUNT + 1; 119 private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; 120 private static final int KEEP_ALIVE = 1; 121 /** 122 * An {@link Executor} to be used with async task with no limit on the queue size. 123 */ 124 public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor( 125 CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, 126 TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); 127 128 public static final String ALLOW_ROTATION_PREFERENCE_KEY = "pref_allowRotation"; 129 isPropertyEnabled(String propertyName)130 public static boolean isPropertyEnabled(String propertyName) { 131 return Log.isLoggable(propertyName, Log.VERBOSE); 132 } 133 isAllowRotationPrefEnabled(Context context)134 public static boolean isAllowRotationPrefEnabled(Context context) { 135 return getPrefs(context).getBoolean(ALLOW_ROTATION_PREFERENCE_KEY, 136 getAllowRotationDefaultValue(context)); 137 } 138 getAllowRotationDefaultValue(Context context)139 public static boolean getAllowRotationDefaultValue(Context context) { 140 if (ATLEAST_NOUGAT) { 141 // If the device was scaled, used the original dimensions to determine if rotation 142 // is allowed of not. 143 Resources res = context.getResources(); 144 int originalSmallestWidth = res.getConfiguration().smallestScreenWidthDp 145 * res.getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEVICE_STABLE; 146 return originalSmallestWidth >= 600; 147 } 148 return false; 149 } 150 151 /** 152 * Given a coordinate relative to the descendant, find the coordinate in a parent view's 153 * coordinates. 154 * 155 * @param descendant The descendant to which the passed coordinate is relative. 156 * @param ancestor The root view to make the coordinates relative to. 157 * @param coord The coordinate that we want mapped. 158 * @param includeRootScroll Whether or not to account for the scroll of the descendant: 159 * sometimes this is relevant as in a child's coordinates within the descendant. 160 * @return The factor by which this descendant is scaled relative to this DragLayer. Caution 161 * this scale factor is assumed to be equal in X and Y, and so if at any point this 162 * assumption fails, we will need to return a pair of scale factors. 163 */ getDescendantCoordRelativeToAncestor( View descendant, View ancestor, int[] coord, boolean includeRootScroll)164 public static float getDescendantCoordRelativeToAncestor( 165 View descendant, View ancestor, int[] coord, boolean includeRootScroll) { 166 sPoint[0] = coord[0]; 167 sPoint[1] = coord[1]; 168 169 float scale = 1.0f; 170 View v = descendant; 171 while(v != ancestor && v != null) { 172 // For TextViews, scroll has a meaning which relates to the text position 173 // which is very strange... ignore the scroll. 174 if (v != descendant || includeRootScroll) { 175 sPoint[0] -= v.getScrollX(); 176 sPoint[1] -= v.getScrollY(); 177 } 178 179 v.getMatrix().mapPoints(sPoint); 180 sPoint[0] += v.getLeft(); 181 sPoint[1] += v.getTop(); 182 scale *= v.getScaleX(); 183 184 v = (View) v.getParent(); 185 } 186 187 coord[0] = Math.round(sPoint[0]); 188 coord[1] = Math.round(sPoint[1]); 189 return scale; 190 } 191 192 /** 193 * Inverse of {@link #getDescendantCoordRelativeToAncestor(View, View, int[], boolean)}. 194 */ mapCoordInSelfToDescendant(View descendant, View root, int[] coord)195 public static void mapCoordInSelfToDescendant(View descendant, View root, int[] coord) { 196 sMatrix.reset(); 197 View v = descendant; 198 while(v != root) { 199 sMatrix.postTranslate(-v.getScrollX(), -v.getScrollY()); 200 sMatrix.postConcat(v.getMatrix()); 201 sMatrix.postTranslate(v.getLeft(), v.getTop()); 202 v = (View) v.getParent(); 203 } 204 sMatrix.postTranslate(-v.getScrollX(), -v.getScrollY()); 205 sMatrix.invert(sInverseMatrix); 206 207 sPoint[0] = coord[0]; 208 sPoint[1] = coord[1]; 209 sInverseMatrix.mapPoints(sPoint); 210 coord[0] = Math.round(sPoint[0]); 211 coord[1] = Math.round(sPoint[1]); 212 } 213 214 /** 215 * Utility method to determine whether the given point, in local coordinates, 216 * is inside the view, where the area of the view is expanded by the slop factor. 217 * This method is called while processing touch-move events to determine if the event 218 * is still within the view. 219 */ pointInView(View v, float localX, float localY, float slop)220 public static boolean pointInView(View v, float localX, float localY, float slop) { 221 return localX >= -slop && localY >= -slop && localX < (v.getWidth() + slop) && 222 localY < (v.getHeight() + slop); 223 } 224 getCenterDeltaInScreenSpace(View v0, View v1)225 public static int[] getCenterDeltaInScreenSpace(View v0, View v1) { 226 v0.getLocationInWindow(sLoc0); 227 v1.getLocationInWindow(sLoc1); 228 229 sLoc0[0] += (v0.getMeasuredWidth() * v0.getScaleX()) / 2; 230 sLoc0[1] += (v0.getMeasuredHeight() * v0.getScaleY()) / 2; 231 sLoc1[0] += (v1.getMeasuredWidth() * v1.getScaleX()) / 2; 232 sLoc1[1] += (v1.getMeasuredHeight() * v1.getScaleY()) / 2; 233 return new int[] {sLoc1[0] - sLoc0[0], sLoc1[1] - sLoc0[1]}; 234 } 235 scaleRectAboutCenter(Rect r, float scale)236 public static void scaleRectAboutCenter(Rect r, float scale) { 237 if (scale != 1.0f) { 238 int cx = r.centerX(); 239 int cy = r.centerY(); 240 r.offset(-cx, -cy); 241 242 r.left = (int) (r.left * scale + 0.5f); 243 r.top = (int) (r.top * scale + 0.5f); 244 r.right = (int) (r.right * scale + 0.5f); 245 r.bottom = (int) (r.bottom * scale + 0.5f); 246 247 r.offset(cx, cy); 248 } 249 } 250 shrinkRect(Rect r, float scaleX, float scaleY)251 public static float shrinkRect(Rect r, float scaleX, float scaleY) { 252 float scale = Math.min(Math.min(scaleX, scaleY), 1.0f); 253 if (scale < 1.0f) { 254 int deltaX = (int) (r.width() * (scaleX - scale) * 0.5f); 255 r.left += deltaX; 256 r.right -= deltaX; 257 258 int deltaY = (int) (r.height() * (scaleY - scale) * 0.5f); 259 r.top += deltaY; 260 r.bottom -= deltaY; 261 } 262 return scale; 263 } 264 isSystemApp(Context context, Intent intent)265 public static boolean isSystemApp(Context context, Intent intent) { 266 PackageManager pm = context.getPackageManager(); 267 ComponentName cn = intent.getComponent(); 268 String packageName = null; 269 if (cn == null) { 270 ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); 271 if ((info != null) && (info.activityInfo != null)) { 272 packageName = info.activityInfo.packageName; 273 } 274 } else { 275 packageName = cn.getPackageName(); 276 } 277 if (packageName != null) { 278 try { 279 PackageInfo info = pm.getPackageInfo(packageName, 0); 280 return (info != null) && (info.applicationInfo != null) && 281 ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0); 282 } catch (NameNotFoundException e) { 283 return false; 284 } 285 } else { 286 return false; 287 } 288 } 289 290 /** 291 * This picks a dominant color, looking for high-saturation, high-value, repeated hues. 292 * @param bitmap The bitmap to scan 293 * @param samples The approximate max number of samples to use. 294 */ findDominantColorByHue(Bitmap bitmap, int samples)295 public static int findDominantColorByHue(Bitmap bitmap, int samples) { 296 final int height = bitmap.getHeight(); 297 final int width = bitmap.getWidth(); 298 int sampleStride = (int) Math.sqrt((height * width) / samples); 299 if (sampleStride < 1) { 300 sampleStride = 1; 301 } 302 303 // This is an out-param, for getting the hsv values for an rgb 304 float[] hsv = new float[3]; 305 306 // First get the best hue, by creating a histogram over 360 hue buckets, 307 // where each pixel contributes a score weighted by saturation, value, and alpha. 308 float[] hueScoreHistogram = new float[360]; 309 float highScore = -1; 310 int bestHue = -1; 311 312 for (int y = 0; y < height; y += sampleStride) { 313 for (int x = 0; x < width; x += sampleStride) { 314 int argb = bitmap.getPixel(x, y); 315 int alpha = 0xFF & (argb >> 24); 316 if (alpha < 0x80) { 317 // Drop mostly-transparent pixels. 318 continue; 319 } 320 // Remove the alpha channel. 321 int rgb = argb | 0xFF000000; 322 Color.colorToHSV(rgb, hsv); 323 // Bucket colors by the 360 integer hues. 324 int hue = (int) hsv[0]; 325 if (hue < 0 || hue >= hueScoreHistogram.length) { 326 // Defensively avoid array bounds violations. 327 continue; 328 } 329 float score = hsv[1] * hsv[2]; 330 hueScoreHistogram[hue] += score; 331 if (hueScoreHistogram[hue] > highScore) { 332 highScore = hueScoreHistogram[hue]; 333 bestHue = hue; 334 } 335 } 336 } 337 338 SparseArray<Float> rgbScores = new SparseArray<Float>(); 339 int bestColor = 0xff000000; 340 highScore = -1; 341 // Go back over the RGB colors that match the winning hue, 342 // creating a histogram of weighted s*v scores, for up to 100*100 [s,v] buckets. 343 // The highest-scoring RGB color wins. 344 for (int y = 0; y < height; y += sampleStride) { 345 for (int x = 0; x < width; x += sampleStride) { 346 int rgb = bitmap.getPixel(x, y) | 0xff000000; 347 Color.colorToHSV(rgb, hsv); 348 int hue = (int) hsv[0]; 349 if (hue == bestHue) { 350 float s = hsv[1]; 351 float v = hsv[2]; 352 int bucket = (int) (s * 100) + (int) (v * 10000); 353 // Score by cumulative saturation * value. 354 float score = s * v; 355 Float oldTotal = rgbScores.get(bucket); 356 float newTotal = oldTotal == null ? score : oldTotal + score; 357 rgbScores.put(bucket, newTotal); 358 if (newTotal > highScore) { 359 highScore = newTotal; 360 // All the colors in the winning bucket are very similar. Last in wins. 361 bestColor = rgb; 362 } 363 } 364 } 365 } 366 return bestColor; 367 } 368 369 /* 370 * Finds a system apk which had a broadcast receiver listening to a particular action. 371 * @param action intent action used to find the apk 372 * @return a pair of apk package name and the resources. 373 */ findSystemApk(String action, PackageManager pm)374 static Pair<String, Resources> findSystemApk(String action, PackageManager pm) { 375 final Intent intent = new Intent(action); 376 for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) { 377 if (info.activityInfo != null && 378 (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { 379 final String packageName = info.activityInfo.packageName; 380 try { 381 final Resources res = pm.getResourcesForApplication(packageName); 382 return Pair.create(packageName, res); 383 } catch (NameNotFoundException e) { 384 Log.w(TAG, "Failed to find resources for " + packageName); 385 } 386 } 387 } 388 return null; 389 } 390 391 /** 392 * Compresses the bitmap to a byte array for serialization. 393 */ flattenBitmap(Bitmap bitmap)394 public static byte[] flattenBitmap(Bitmap bitmap) { 395 // Try go guesstimate how much space the icon will take when serialized 396 // to avoid unnecessary allocations/copies during the write. 397 int size = bitmap.getWidth() * bitmap.getHeight() * 4; 398 ByteArrayOutputStream out = new ByteArrayOutputStream(size); 399 try { 400 bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); 401 out.flush(); 402 out.close(); 403 return out.toByteArray(); 404 } catch (IOException e) { 405 Log.w(TAG, "Could not write bitmap"); 406 return null; 407 } 408 } 409 410 /** 411 * Trims the string, removing all whitespace at the beginning and end of the string. 412 * Non-breaking whitespaces are also removed. 413 */ trim(CharSequence s)414 public static String trim(CharSequence s) { 415 if (s == null) { 416 return null; 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 434 /** 435 * Convenience println with multiple args. 436 */ println(String key, Object... args)437 public static void println(String key, Object... args) { 438 StringBuilder b = new StringBuilder(); 439 b.append(key); 440 b.append(": "); 441 boolean isFirstArgument = true; 442 for (Object arg : args) { 443 if (isFirstArgument) { 444 isFirstArgument = false; 445 } else { 446 b.append(", "); 447 } 448 b.append(arg); 449 } 450 System.out.println(b.toString()); 451 } 452 isRtl(Resources res)453 public static boolean isRtl(Resources res) { 454 return res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 455 } 456 457 /** 458 * Returns true if the intent is a valid launch intent for a launcher activity of an app. 459 * This is used to identify shortcuts which are different from the ones exposed by the 460 * applications' manifest file. 461 * 462 * @param launchIntent The intent that will be launched when the shortcut is clicked. 463 */ isLauncherAppTarget(Intent launchIntent)464 public static boolean isLauncherAppTarget(Intent launchIntent) { 465 if (launchIntent != null 466 && Intent.ACTION_MAIN.equals(launchIntent.getAction()) 467 && launchIntent.getComponent() != null 468 && launchIntent.getCategories() != null 469 && launchIntent.getCategories().size() == 1 470 && launchIntent.hasCategory(Intent.CATEGORY_LAUNCHER) 471 && TextUtils.isEmpty(launchIntent.getDataString())) { 472 // An app target can either have no extra or have ItemInfo.EXTRA_PROFILE. 473 Bundle extras = launchIntent.getExtras(); 474 return extras == null || extras.keySet().isEmpty(); 475 } 476 return false; 477 } 478 dpiFromPx(int size, DisplayMetrics metrics)479 public static float dpiFromPx(int size, DisplayMetrics metrics){ 480 float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT; 481 return (size / densityRatio); 482 } pxFromDp(float size, DisplayMetrics metrics)483 public static int pxFromDp(float size, DisplayMetrics metrics) { 484 return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 485 size, metrics)); 486 } pxFromSp(float size, DisplayMetrics metrics)487 public static int pxFromSp(float size, DisplayMetrics metrics) { 488 return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 489 size, metrics)); 490 } 491 createDbSelectionQuery(String columnName, Iterable<?> values)492 public static String createDbSelectionQuery(String columnName, Iterable<?> values) { 493 return String.format(Locale.ENGLISH, "%s IN (%s)", columnName, TextUtils.join(", ", values)); 494 } 495 isBootCompleted()496 public static boolean isBootCompleted() { 497 return "1".equals(getSystemProperty("sys.boot_completed", "1")); 498 } 499 getSystemProperty(String property, String defaultValue)500 public static String getSystemProperty(String property, String defaultValue) { 501 try { 502 Class clazz = Class.forName("android.os.SystemProperties"); 503 Method getter = clazz.getDeclaredMethod("get", String.class); 504 String value = (String) getter.invoke(null, property); 505 if (!TextUtils.isEmpty(value)) { 506 return value; 507 } 508 } catch (Exception e) { 509 Log.d(TAG, "Unable to read system properties"); 510 } 511 return defaultValue; 512 } 513 514 /** 515 * Ensures that a value is within given bounds. Specifically: 516 * If value is less than lowerBound, return lowerBound; else if value is greater than upperBound, 517 * return upperBound; else return value unchanged. 518 */ boundToRange(int value, int lowerBound, int upperBound)519 public static int boundToRange(int value, int lowerBound, int upperBound) { 520 return Math.max(lowerBound, Math.min(value, upperBound)); 521 } 522 523 /** 524 * @see #boundToRange(int, int, int). 525 */ boundToRange(float value, float lowerBound, float upperBound)526 public static float boundToRange(float value, float lowerBound, float upperBound) { 527 return Math.max(lowerBound, Math.min(value, upperBound)); 528 } 529 530 /** 531 * Wraps a message with a TTS span, so that a different message is spoken than 532 * what is getting displayed. 533 * @param msg original message 534 * @param ttsMsg message to be spoken 535 */ wrapForTts(CharSequence msg, String ttsMsg)536 public static CharSequence wrapForTts(CharSequence msg, String ttsMsg) { 537 SpannableString spanned = new SpannableString(msg); 538 spanned.setSpan(new TtsSpan.TextBuilder(ttsMsg).build(), 539 0, spanned.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); 540 return spanned; 541 } 542 543 /** 544 * Replacement for Long.compare() which was added in API level 19. 545 */ longCompare(long lhs, long rhs)546 public static int longCompare(long lhs, long rhs) { 547 return lhs < rhs ? -1 : (lhs == rhs ? 0 : 1); 548 } 549 getPrefs(Context context)550 public static SharedPreferences getPrefs(Context context) { 551 return context.getSharedPreferences( 552 LauncherFiles.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE); 553 } 554 getDevicePrefs(Context context)555 public static SharedPreferences getDevicePrefs(Context context) { 556 return context.getSharedPreferences( 557 LauncherFiles.DEVICE_PREFERENCES_KEY, Context.MODE_PRIVATE); 558 } 559 isPowerSaverOn(Context context)560 public static boolean isPowerSaverOn(Context context) { 561 PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); 562 return powerManager.isPowerSaveMode(); 563 } 564 isWallpaperAllowed(Context context)565 public static boolean isWallpaperAllowed(Context context) { 566 if (ATLEAST_NOUGAT) { 567 try { 568 WallpaperManager wm = context.getSystemService(WallpaperManager.class); 569 return (Boolean) wm.getClass().getDeclaredMethod("isSetWallpaperAllowed") 570 .invoke(wm); 571 } catch (Exception e) { } 572 } 573 return true; 574 } 575 closeSilently(Closeable c)576 public static void closeSilently(Closeable c) { 577 if (c != null) { 578 try { 579 c.close(); 580 } catch (IOException e) { 581 if (FeatureFlags.IS_DOGFOOD_BUILD) { 582 Log.d(TAG, "Error closing", e); 583 } 584 } 585 } 586 } 587 588 /** 589 * Returns true if {@param original} contains all entries defined in {@param updates} and 590 * have the same value. 591 * The comparison uses {@link Object#equals(Object)} to compare the values. 592 */ containsAll(Bundle original, Bundle updates)593 public static boolean containsAll(Bundle original, Bundle updates) { 594 for (String key : updates.keySet()) { 595 Object value1 = updates.get(key); 596 Object value2 = original.get(key); 597 if (value1 == null) { 598 if (value2 != null) { 599 return false; 600 } 601 } else if (!value1.equals(value2)) { 602 return false; 603 } 604 } 605 return true; 606 } 607 608 /** Returns whether the collection is null or empty. */ isEmpty(Collection c)609 public static boolean isEmpty(Collection c) { 610 return c == null || c.isEmpty(); 611 } 612 sendCustomAccessibilityEvent(View target, int type, String text)613 public static void sendCustomAccessibilityEvent(View target, int type, String text) { 614 AccessibilityManager accessibilityManager = (AccessibilityManager) 615 target.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); 616 if (accessibilityManager.isEnabled()) { 617 AccessibilityEvent event = AccessibilityEvent.obtain(type); 618 target.onInitializeAccessibilityEvent(event); 619 event.getText().add(text); 620 accessibilityManager.sendAccessibilityEvent(event); 621 } 622 } 623 isBinderSizeError(Exception e)624 public static boolean isBinderSizeError(Exception e) { 625 return e.getCause() instanceof TransactionTooLargeException 626 || e.getCause() instanceof DeadObjectException; 627 } 628 getOverrideObject(Class<T> clazz, Context context, int resId)629 public static <T> T getOverrideObject(Class<T> clazz, Context context, int resId) { 630 String className = context.getString(resId); 631 if (!TextUtils.isEmpty(className)) { 632 try { 633 Class<?> cls = Class.forName(className); 634 return (T) cls.getDeclaredConstructor(Context.class).newInstance(context); 635 } catch (ClassNotFoundException | InstantiationException | IllegalAccessException 636 | ClassCastException | NoSuchMethodException | InvocationTargetException e) { 637 Log.e(TAG, "Bad overriden class", e); 638 } 639 } 640 641 try { 642 return clazz.newInstance(); 643 } catch (InstantiationException|IllegalAccessException e) { 644 throw new RuntimeException(e); 645 } 646 } 647 648 /** 649 * Returns a HashSet with a single element. We use this instead of Collections.singleton() 650 * because HashSet ensures all operations, such as remove, are supported. 651 */ singletonHashSet(T elem)652 public static <T> HashSet<T> singletonHashSet(T elem) { 653 HashSet<T> hashSet = new HashSet<>(1); 654 hashSet.add(elem); 655 return hashSet; 656 } 657 658 } 659