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.Activity; 20 import android.content.ActivityNotFoundException; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 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.Canvas; 32 import android.graphics.Color; 33 import android.graphics.Matrix; 34 import android.graphics.Paint; 35 import android.graphics.PaintFlagsDrawFilter; 36 import android.graphics.Rect; 37 import android.graphics.drawable.BitmapDrawable; 38 import android.graphics.drawable.Drawable; 39 import android.graphics.drawable.PaintDrawable; 40 import android.os.Build; 41 import android.util.Log; 42 import android.util.Pair; 43 import android.util.SparseArray; 44 import android.view.View; 45 import android.widget.Toast; 46 47 import java.util.ArrayList; 48 49 /** 50 * Various utilities shared amongst the Launcher's classes. 51 */ 52 public final class Utilities { 53 private static final String TAG = "Launcher.Utilities"; 54 55 private static int sIconWidth = -1; 56 private static int sIconHeight = -1; 57 public static int sIconTextureWidth = -1; 58 public static int sIconTextureHeight = -1; 59 60 private static final Rect sOldBounds = new Rect(); 61 private static final Canvas sCanvas = new Canvas(); 62 63 static { 64 sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG, 65 Paint.FILTER_BITMAP_FLAG)); 66 } 67 static int sColors[] = { 0xffff0000, 0xff00ff00, 0xff0000ff }; 68 static int sColorIndex = 0; 69 70 static int[] sLoc0 = new int[2]; 71 static int[] sLoc1 = new int[2]; 72 73 // To turn on these properties, type 74 // adb shell setprop log.tag.PROPERTY_NAME [VERBOSE | SUPPRESS] 75 static final String FORCE_ENABLE_ROTATION_PROPERTY = "launcher_force_rotate"; 76 public static boolean sForceEnableRotation = isPropertyEnabled(FORCE_ENABLE_ROTATION_PROPERTY); 77 78 /** 79 * Returns a FastBitmapDrawable with the icon, accurately sized. 80 */ 81 public static FastBitmapDrawable createIconDrawable(Bitmap icon) { 82 FastBitmapDrawable d = new FastBitmapDrawable(icon); 83 d.setFilterBitmap(true); 84 resizeIconDrawable(d); 85 return d; 86 } 87 88 /** 89 * Resizes an icon drawable to the correct icon size. 90 */ 91 static void resizeIconDrawable(Drawable icon) { 92 icon.setBounds(0, 0, sIconTextureWidth, sIconTextureHeight); 93 } 94 95 private static boolean isPropertyEnabled(String propertyName) { 96 return Log.isLoggable(propertyName, Log.VERBOSE); 97 } 98 99 public static boolean isRotationEnabled(Context c) { 100 boolean enableRotation = sForceEnableRotation || 101 c.getResources().getBoolean(R.bool.allow_rotation); 102 return enableRotation; 103 } 104 105 /** 106 * Indicates if the device is running LMP or higher. 107 */ 108 public static boolean isLmpOrAbove() { 109 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.L; 110 } 111 112 /** 113 * Returns a bitmap suitable for the all apps view. Used to convert pre-ICS 114 * icon bitmaps that are stored in the database (which were 74x74 pixels at hdpi size) 115 * to the proper size (48dp) 116 */ 117 static Bitmap createIconBitmap(Bitmap icon, Context context) { 118 int textureWidth = sIconTextureWidth; 119 int textureHeight = sIconTextureHeight; 120 int sourceWidth = icon.getWidth(); 121 int sourceHeight = icon.getHeight(); 122 if (sourceWidth > textureWidth && sourceHeight > textureHeight) { 123 // Icon is bigger than it should be; clip it (solves the GB->ICS migration case) 124 return Bitmap.createBitmap(icon, 125 (sourceWidth - textureWidth) / 2, 126 (sourceHeight - textureHeight) / 2, 127 textureWidth, textureHeight); 128 } else if (sourceWidth == textureWidth && sourceHeight == textureHeight) { 129 // Icon is the right size, no need to change it 130 return icon; 131 } else { 132 // Icon is too small, render to a larger bitmap 133 final Resources resources = context.getResources(); 134 return createIconBitmap(new BitmapDrawable(resources, icon), context); 135 } 136 } 137 138 /** 139 * Returns a bitmap suitable for the all apps view. 140 */ 141 public static Bitmap createIconBitmap(Drawable icon, Context context) { 142 synchronized (sCanvas) { // we share the statics :-( 143 if (sIconWidth == -1) { 144 initStatics(context); 145 } 146 147 int width = sIconWidth; 148 int height = sIconHeight; 149 150 if (icon instanceof PaintDrawable) { 151 PaintDrawable painter = (PaintDrawable) icon; 152 painter.setIntrinsicWidth(width); 153 painter.setIntrinsicHeight(height); 154 } else if (icon instanceof BitmapDrawable) { 155 // Ensure the bitmap has a density. 156 BitmapDrawable bitmapDrawable = (BitmapDrawable) icon; 157 Bitmap bitmap = bitmapDrawable.getBitmap(); 158 if (bitmap.getDensity() == Bitmap.DENSITY_NONE) { 159 bitmapDrawable.setTargetDensity(context.getResources().getDisplayMetrics()); 160 } 161 } 162 int sourceWidth = icon.getIntrinsicWidth(); 163 int sourceHeight = icon.getIntrinsicHeight(); 164 if (sourceWidth > 0 && sourceHeight > 0) { 165 // Scale the icon proportionally to the icon dimensions 166 final float ratio = (float) sourceWidth / sourceHeight; 167 if (sourceWidth > sourceHeight) { 168 height = (int) (width / ratio); 169 } else if (sourceHeight > sourceWidth) { 170 width = (int) (height * ratio); 171 } 172 } 173 174 // no intrinsic size --> use default size 175 int textureWidth = sIconTextureWidth; 176 int textureHeight = sIconTextureHeight; 177 178 final Bitmap bitmap = Bitmap.createBitmap(textureWidth, textureHeight, 179 Bitmap.Config.ARGB_8888); 180 final Canvas canvas = sCanvas; 181 canvas.setBitmap(bitmap); 182 183 final int left = (textureWidth-width) / 2; 184 final int top = (textureHeight-height) / 2; 185 186 @SuppressWarnings("all") // suppress dead code warning 187 final boolean debug = false; 188 if (debug) { 189 // draw a big box for the icon for debugging 190 canvas.drawColor(sColors[sColorIndex]); 191 if (++sColorIndex >= sColors.length) sColorIndex = 0; 192 Paint debugPaint = new Paint(); 193 debugPaint.setColor(0xffcccc00); 194 canvas.drawRect(left, top, left+width, top+height, debugPaint); 195 } 196 197 sOldBounds.set(icon.getBounds()); 198 icon.setBounds(left, top, left+width, top+height); 199 icon.draw(canvas); 200 icon.setBounds(sOldBounds); 201 canvas.setBitmap(null); 202 203 return bitmap; 204 } 205 } 206 207 /** 208 * Returns a Bitmap representing the thumbnail of the specified Bitmap. 209 * 210 * @param bitmap The bitmap to get a thumbnail of. 211 * @param context The application's context. 212 * 213 * @return A thumbnail for the specified bitmap or the bitmap itself if the 214 * thumbnail could not be created. 215 */ 216 static Bitmap resampleIconBitmap(Bitmap bitmap, Context context) { 217 synchronized (sCanvas) { // we share the statics :-( 218 if (sIconWidth == -1) { 219 initStatics(context); 220 } 221 222 if (bitmap.getWidth() == sIconWidth && bitmap.getHeight() == sIconHeight) { 223 return bitmap; 224 } else { 225 final Resources resources = context.getResources(); 226 return createIconBitmap(new BitmapDrawable(resources, bitmap), context); 227 } 228 } 229 } 230 231 /** 232 * Given a coordinate relative to the descendant, find the coordinate in a parent view's 233 * coordinates. 234 * 235 * @param descendant The descendant to which the passed coordinate is relative. 236 * @param root The root view to make the coordinates relative to. 237 * @param coord The coordinate that we want mapped. 238 * @param includeRootScroll Whether or not to account for the scroll of the descendant: 239 * sometimes this is relevant as in a child's coordinates within the descendant. 240 * @return The factor by which this descendant is scaled relative to this DragLayer. Caution 241 * this scale factor is assumed to be equal in X and Y, and so if at any point this 242 * assumption fails, we will need to return a pair of scale factors. 243 */ 244 public static float getDescendantCoordRelativeToParent(View descendant, View root, 245 int[] coord, boolean includeRootScroll) { 246 ArrayList<View> ancestorChain = new ArrayList<View>(); 247 248 float[] pt = {coord[0], coord[1]}; 249 250 View v = descendant; 251 while(v != root && v != null) { 252 ancestorChain.add(v); 253 v = (View) v.getParent(); 254 } 255 ancestorChain.add(root); 256 257 float scale = 1.0f; 258 int count = ancestorChain.size(); 259 for (int i = 0; i < count; i++) { 260 View v0 = ancestorChain.get(i); 261 // For TextViews, scroll has a meaning which relates to the text position 262 // which is very strange... ignore the scroll. 263 if (v0 != descendant || includeRootScroll) { 264 pt[0] -= v0.getScrollX(); 265 pt[1] -= v0.getScrollY(); 266 } 267 268 v0.getMatrix().mapPoints(pt); 269 pt[0] += v0.getLeft(); 270 pt[1] += v0.getTop(); 271 scale *= v0.getScaleX(); 272 } 273 274 coord[0] = (int) Math.round(pt[0]); 275 coord[1] = (int) Math.round(pt[1]); 276 return scale; 277 } 278 279 /** 280 * Inverse of {@link #getDescendantCoordRelativeToSelf(View, int[])}. 281 */ 282 public static float mapCoordInSelfToDescendent(View descendant, View root, 283 int[] coord) { 284 ArrayList<View> ancestorChain = new ArrayList<View>(); 285 286 float[] pt = {coord[0], coord[1]}; 287 288 View v = descendant; 289 while(v != root) { 290 ancestorChain.add(v); 291 v = (View) v.getParent(); 292 } 293 ancestorChain.add(root); 294 295 float scale = 1.0f; 296 Matrix inverse = new Matrix(); 297 int count = ancestorChain.size(); 298 for (int i = count - 1; i >= 0; i--) { 299 View ancestor = ancestorChain.get(i); 300 View next = i > 0 ? ancestorChain.get(i-1) : null; 301 302 pt[0] += ancestor.getScrollX(); 303 pt[1] += ancestor.getScrollY(); 304 305 if (next != null) { 306 pt[0] -= next.getLeft(); 307 pt[1] -= next.getTop(); 308 next.getMatrix().invert(inverse); 309 inverse.mapPoints(pt); 310 scale *= next.getScaleX(); 311 } 312 } 313 314 coord[0] = (int) Math.round(pt[0]); 315 coord[1] = (int) Math.round(pt[1]); 316 return scale; 317 } 318 319 /** 320 * Utility method to determine whether the given point, in local coordinates, 321 * is inside the view, where the area of the view is expanded by the slop factor. 322 * This method is called while processing touch-move events to determine if the event 323 * is still within the view. 324 */ 325 public static boolean pointInView(View v, float localX, float localY, float slop) { 326 return localX >= -slop && localY >= -slop && localX < (v.getWidth() + slop) && 327 localY < (v.getHeight() + slop); 328 } 329 330 private static void initStatics(Context context) { 331 final Resources resources = context.getResources(); 332 sIconWidth = sIconHeight = (int) resources.getDimension(R.dimen.app_icon_size); 333 sIconTextureWidth = sIconTextureHeight = sIconWidth; 334 } 335 336 public static void setIconSize(int widthPx) { 337 sIconWidth = sIconHeight = widthPx; 338 sIconTextureWidth = sIconTextureHeight = widthPx; 339 } 340 341 public static void scaleRect(Rect r, float scale) { 342 if (scale != 1.0f) { 343 r.left = (int) (r.left * scale + 0.5f); 344 r.top = (int) (r.top * scale + 0.5f); 345 r.right = (int) (r.right * scale + 0.5f); 346 r.bottom = (int) (r.bottom * scale + 0.5f); 347 } 348 } 349 350 public static int[] getCenterDeltaInScreenSpace(View v0, View v1, int[] delta) { 351 v0.getLocationInWindow(sLoc0); 352 v1.getLocationInWindow(sLoc1); 353 354 sLoc0[0] += (v0.getMeasuredWidth() * v0.getScaleX()) / 2; 355 sLoc0[1] += (v0.getMeasuredHeight() * v0.getScaleY()) / 2; 356 sLoc1[0] += (v1.getMeasuredWidth() * v1.getScaleX()) / 2; 357 sLoc1[1] += (v1.getMeasuredHeight() * v1.getScaleY()) / 2; 358 359 if (delta == null) { 360 delta = new int[2]; 361 } 362 363 delta[0] = sLoc1[0] - sLoc0[0]; 364 delta[1] = sLoc1[1] - sLoc0[1]; 365 366 return delta; 367 } 368 369 public static void scaleRectAboutCenter(Rect r, float scale) { 370 int cx = r.centerX(); 371 int cy = r.centerY(); 372 r.offset(-cx, -cy); 373 Utilities.scaleRect(r, scale); 374 r.offset(cx, cy); 375 } 376 377 public static void startActivityForResultSafely( 378 Activity activity, Intent intent, int requestCode) { 379 try { 380 activity.startActivityForResult(intent, requestCode); 381 } catch (ActivityNotFoundException e) { 382 Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); 383 } catch (SecurityException e) { 384 Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); 385 Log.e(TAG, "Launcher does not have the permission to launch " + intent + 386 ". Make sure to create a MAIN intent-filter for the corresponding activity " + 387 "or use the exported attribute for this activity.", e); 388 } 389 } 390 391 static boolean isSystemApp(Context context, Intent intent) { 392 PackageManager pm = context.getPackageManager(); 393 ComponentName cn = intent.getComponent(); 394 String packageName = null; 395 if (cn == null) { 396 ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); 397 if ((info != null) && (info.activityInfo != null)) { 398 packageName = info.activityInfo.packageName; 399 } 400 } else { 401 packageName = cn.getPackageName(); 402 } 403 if (packageName != null) { 404 try { 405 PackageInfo info = pm.getPackageInfo(packageName, 0); 406 return (info != null) && (info.applicationInfo != null) && 407 ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0); 408 } catch (NameNotFoundException e) { 409 return false; 410 } 411 } else { 412 return false; 413 } 414 } 415 416 /** 417 * This picks a dominant color, looking for high-saturation, high-value, repeated hues. 418 * @param bitmap The bitmap to scan 419 * @param samples The approximate max number of samples to use. 420 */ 421 static int findDominantColorByHue(Bitmap bitmap, int samples) { 422 final int height = bitmap.getHeight(); 423 final int width = bitmap.getWidth(); 424 int sampleStride = (int) Math.sqrt((height * width) / samples); 425 if (sampleStride < 1) { 426 sampleStride = 1; 427 } 428 429 // This is an out-param, for getting the hsv values for an rgb 430 float[] hsv = new float[3]; 431 432 // First get the best hue, by creating a histogram over 360 hue buckets, 433 // where each pixel contributes a score weighted by saturation, value, and alpha. 434 float[] hueScoreHistogram = new float[360]; 435 float highScore = -1; 436 int bestHue = -1; 437 438 for (int y = 0; y < height; y += sampleStride) { 439 for (int x = 0; x < width; x += sampleStride) { 440 int argb = bitmap.getPixel(x, y); 441 int alpha = 0xFF & (argb >> 24); 442 if (alpha < 0x80) { 443 // Drop mostly-transparent pixels. 444 continue; 445 } 446 // Remove the alpha channel. 447 int rgb = argb | 0xFF000000; 448 Color.colorToHSV(rgb, hsv); 449 // Bucket colors by the 360 integer hues. 450 int hue = (int) hsv[0]; 451 if (hue < 0 || hue >= hueScoreHistogram.length) { 452 // Defensively avoid array bounds violations. 453 continue; 454 } 455 float score = hsv[1] * hsv[2]; 456 hueScoreHistogram[hue] += score; 457 if (hueScoreHistogram[hue] > highScore) { 458 highScore = hueScoreHistogram[hue]; 459 bestHue = hue; 460 } 461 } 462 } 463 464 SparseArray<Float> rgbScores = new SparseArray<Float>(); 465 int bestColor = 0xff000000; 466 highScore = -1; 467 // Go back over the RGB colors that match the winning hue, 468 // creating a histogram of weighted s*v scores, for up to 100*100 [s,v] buckets. 469 // The highest-scoring RGB color wins. 470 for (int y = 0; y < height; y += sampleStride) { 471 for (int x = 0; x < width; x += sampleStride) { 472 int rgb = bitmap.getPixel(x, y) | 0xff000000; 473 Color.colorToHSV(rgb, hsv); 474 int hue = (int) hsv[0]; 475 if (hue == bestHue) { 476 float s = hsv[1]; 477 float v = hsv[2]; 478 int bucket = (int) (s * 100) + (int) (v * 10000); 479 // Score by cumulative saturation * value. 480 float score = s * v; 481 Float oldTotal = rgbScores.get(bucket); 482 float newTotal = oldTotal == null ? score : oldTotal + score; 483 rgbScores.put(bucket, newTotal); 484 if (newTotal > highScore) { 485 highScore = newTotal; 486 // All the colors in the winning bucket are very similar. Last in wins. 487 bestColor = rgb; 488 } 489 } 490 } 491 } 492 return bestColor; 493 } 494 495 /* 496 * Finds a system apk which had a broadcast receiver listening to a particular action. 497 * @param action intent action used to find the apk 498 * @return a pair of apk package name and the resources. 499 */ 500 static Pair<String, Resources> findSystemApk(String action, PackageManager pm) { 501 final Intent intent = new Intent(action); 502 for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) { 503 if (info.activityInfo != null && 504 (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { 505 final String packageName = info.activityInfo.packageName; 506 try { 507 final Resources res = pm.getResourcesForApplication(packageName); 508 return Pair.create(packageName, res); 509 } catch (NameNotFoundException e) { 510 Log.w(TAG, "Failed to find resources for " + packageName); 511 } 512 } 513 } 514 return null; 515 } 516 } 517