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.annotation.TargetApi; 20 import android.app.Activity; 21 import android.app.SearchManager; 22 import android.appwidget.AppWidgetManager; 23 import android.appwidget.AppWidgetProviderInfo; 24 import android.content.ActivityNotFoundException; 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.SharedPreferences; 29 import android.content.pm.ApplicationInfo; 30 import android.content.pm.PackageInfo; 31 import android.content.pm.PackageManager; 32 import android.content.pm.PackageManager.NameNotFoundException; 33 import android.content.pm.ResolveInfo; 34 import android.content.res.Resources; 35 import android.database.Cursor; 36 import android.graphics.Bitmap; 37 import android.graphics.BitmapFactory; 38 import android.graphics.Canvas; 39 import android.graphics.Color; 40 import android.graphics.Matrix; 41 import android.graphics.Paint; 42 import android.graphics.PaintFlagsDrawFilter; 43 import android.graphics.Rect; 44 import android.graphics.drawable.BitmapDrawable; 45 import android.graphics.drawable.Drawable; 46 import android.graphics.drawable.PaintDrawable; 47 import android.os.Build; 48 import android.os.Bundle; 49 import android.os.Process; 50 import android.text.TextUtils; 51 import android.util.DisplayMetrics; 52 import android.util.Log; 53 import android.util.Pair; 54 import android.util.SparseArray; 55 import android.util.TypedValue; 56 import android.view.View; 57 import android.widget.Toast; 58 59 import java.io.ByteArrayOutputStream; 60 import java.io.IOException; 61 import java.util.ArrayList; 62 import java.util.Locale; 63 import java.util.Set; 64 import java.util.regex.Matcher; 65 import java.util.regex.Pattern; 66 67 /** 68 * Various utilities shared amongst the Launcher's classes. 69 */ 70 public final class Utilities { 71 72 private static final String TAG = "Launcher.Utilities"; 73 74 private static final Rect sOldBounds = new Rect(); 75 private static final Canvas sCanvas = new Canvas(); 76 77 private static final Pattern sTrimPattern = 78 Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$"); 79 80 static { 81 sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG, 82 Paint.FILTER_BITMAP_FLAG)); 83 } 84 static int sColors[] = { 0xffff0000, 0xff00ff00, 0xff0000ff }; 85 static int sColorIndex = 0; 86 87 private static final int[] sLoc0 = new int[2]; 88 private static final int[] sLoc1 = new int[2]; 89 90 // To turn on these properties, type 91 // adb shell setprop log.tag.PROPERTY_NAME [VERBOSE | SUPPRESS] 92 private static final String FORCE_ENABLE_ROTATION_PROPERTY = "launcher_force_rotate"; 93 private static boolean sForceEnableRotation = isPropertyEnabled(FORCE_ENABLE_ROTATION_PROPERTY); 94 95 public static final String ALLOW_ROTATION_PREFERENCE_KEY = "pref_allowRotation"; 96 97 public static boolean isPropertyEnabled(String propertyName) { 98 return Log.isLoggable(propertyName, Log.VERBOSE); 99 } 100 101 public static boolean isAllowRotationPrefEnabled(Context context, boolean multiProcess) { 102 SharedPreferences sharedPrefs = context.getSharedPreferences( 103 LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE | (multiProcess ? 104 Context.MODE_MULTI_PROCESS : 0)); 105 boolean allowRotationPref = sharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY, false); 106 return sForceEnableRotation || allowRotationPref; 107 } 108 109 public static boolean isRotationAllowedForDevice(Context context) { 110 return sForceEnableRotation || context.getResources().getBoolean(R.bool.allow_rotation); 111 } 112 113 /** 114 * Indicates if the device is running LMP or higher. 115 */ 116 public static boolean isLmpOrAbove() { 117 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; 118 } 119 120 public static boolean isLmpMR1OrAbove() { 121 // TODO(adamcohen): update to Build.VERSION_CODES.LOLLIPOP_MR1 once building against 22; 122 return Build.VERSION.SDK_INT >= 22; 123 } 124 125 public static boolean isLmpMR1() { 126 // TODO(adamcohen): update to Build.VERSION_CODES.LOLLIPOP_MR1 once building against 22; 127 return Build.VERSION.SDK_INT == 22; 128 } 129 130 public static Bitmap createIconBitmap(Cursor c, int iconIndex, Context context) { 131 byte[] data = c.getBlob(iconIndex); 132 try { 133 return createIconBitmap(BitmapFactory.decodeByteArray(data, 0, data.length), context); 134 } catch (Exception e) { 135 return null; 136 } 137 } 138 139 /** 140 * Returns a bitmap suitable for the all apps view. If the package or the resource do not 141 * exist, it returns null. 142 */ 143 public static Bitmap createIconBitmap(String packageName, String resourceName, 144 Context context) { 145 PackageManager packageManager = context.getPackageManager(); 146 // the resource 147 try { 148 Resources resources = packageManager.getResourcesForApplication(packageName); 149 if (resources != null) { 150 final int id = resources.getIdentifier(resourceName, null, null); 151 return createIconBitmap( 152 resources.getDrawableForDensity(id, LauncherAppState.getInstance() 153 .getInvariantDeviceProfile().fillResIconDpi), context); 154 } 155 } catch (Exception e) { 156 // Icon not found. 157 } 158 return null; 159 } 160 161 private static int getIconBitmapSize() { 162 return LauncherAppState.getInstance().getInvariantDeviceProfile().iconBitmapSize; 163 } 164 165 /** 166 * Returns a bitmap which is of the appropriate size to be displayed as an icon 167 */ 168 public static Bitmap createIconBitmap(Bitmap icon, Context context) { 169 final int iconBitmapSize = getIconBitmapSize(); 170 if (iconBitmapSize == icon.getWidth() && iconBitmapSize == icon.getHeight()) { 171 return icon; 172 } 173 return createIconBitmap(new BitmapDrawable(context.getResources(), icon), context); 174 } 175 176 /** 177 * Returns a bitmap suitable for the all apps view. 178 */ 179 public static Bitmap createIconBitmap(Drawable icon, Context context) { 180 synchronized (sCanvas) { 181 final int iconBitmapSize = getIconBitmapSize(); 182 183 int width = iconBitmapSize; 184 int height = iconBitmapSize; 185 186 if (icon instanceof PaintDrawable) { 187 PaintDrawable painter = (PaintDrawable) icon; 188 painter.setIntrinsicWidth(width); 189 painter.setIntrinsicHeight(height); 190 } else if (icon instanceof BitmapDrawable) { 191 // Ensure the bitmap has a density. 192 BitmapDrawable bitmapDrawable = (BitmapDrawable) icon; 193 Bitmap bitmap = bitmapDrawable.getBitmap(); 194 if (bitmap.getDensity() == Bitmap.DENSITY_NONE) { 195 bitmapDrawable.setTargetDensity(context.getResources().getDisplayMetrics()); 196 } 197 } 198 int sourceWidth = icon.getIntrinsicWidth(); 199 int sourceHeight = icon.getIntrinsicHeight(); 200 if (sourceWidth > 0 && sourceHeight > 0) { 201 // Scale the icon proportionally to the icon dimensions 202 final float ratio = (float) sourceWidth / sourceHeight; 203 if (sourceWidth > sourceHeight) { 204 height = (int) (width / ratio); 205 } else if (sourceHeight > sourceWidth) { 206 width = (int) (height * ratio); 207 } 208 } 209 210 // no intrinsic size --> use default size 211 int textureWidth = iconBitmapSize; 212 int textureHeight = iconBitmapSize; 213 214 final Bitmap bitmap = Bitmap.createBitmap(textureWidth, textureHeight, 215 Bitmap.Config.ARGB_8888); 216 final Canvas canvas = sCanvas; 217 canvas.setBitmap(bitmap); 218 219 final int left = (textureWidth-width) / 2; 220 final int top = (textureHeight-height) / 2; 221 222 @SuppressWarnings("all") // suppress dead code warning 223 final boolean debug = false; 224 if (debug) { 225 // draw a big box for the icon for debugging 226 canvas.drawColor(sColors[sColorIndex]); 227 if (++sColorIndex >= sColors.length) sColorIndex = 0; 228 Paint debugPaint = new Paint(); 229 debugPaint.setColor(0xffcccc00); 230 canvas.drawRect(left, top, left+width, top+height, debugPaint); 231 } 232 233 sOldBounds.set(icon.getBounds()); 234 icon.setBounds(left, top, left+width, top+height); 235 icon.draw(canvas); 236 icon.setBounds(sOldBounds); 237 canvas.setBitmap(null); 238 239 return bitmap; 240 } 241 } 242 243 /** 244 * Given a coordinate relative to the descendant, find the coordinate in a parent view's 245 * coordinates. 246 * 247 * @param descendant The descendant to which the passed coordinate is relative. 248 * @param root The root view to make the coordinates relative to. 249 * @param coord The coordinate that we want mapped. 250 * @param includeRootScroll Whether or not to account for the scroll of the descendant: 251 * sometimes this is relevant as in a child's coordinates within the descendant. 252 * @return The factor by which this descendant is scaled relative to this DragLayer. Caution 253 * this scale factor is assumed to be equal in X and Y, and so if at any point this 254 * assumption fails, we will need to return a pair of scale factors. 255 */ 256 public static float getDescendantCoordRelativeToParent(View descendant, View root, 257 int[] coord, boolean includeRootScroll) { 258 ArrayList<View> ancestorChain = new ArrayList<View>(); 259 260 float[] pt = {coord[0], coord[1]}; 261 262 View v = descendant; 263 while(v != root && v != null) { 264 ancestorChain.add(v); 265 v = (View) v.getParent(); 266 } 267 ancestorChain.add(root); 268 269 float scale = 1.0f; 270 int count = ancestorChain.size(); 271 for (int i = 0; i < count; i++) { 272 View v0 = ancestorChain.get(i); 273 // For TextViews, scroll has a meaning which relates to the text position 274 // which is very strange... ignore the scroll. 275 if (v0 != descendant || includeRootScroll) { 276 pt[0] -= v0.getScrollX(); 277 pt[1] -= v0.getScrollY(); 278 } 279 280 v0.getMatrix().mapPoints(pt); 281 pt[0] += v0.getLeft(); 282 pt[1] += v0.getTop(); 283 scale *= v0.getScaleX(); 284 } 285 286 coord[0] = (int) Math.round(pt[0]); 287 coord[1] = (int) Math.round(pt[1]); 288 return scale; 289 } 290 291 /** 292 * Inverse of {@link #getDescendantCoordRelativeToSelf(View, int[])}. 293 */ 294 public static float mapCoordInSelfToDescendent(View descendant, View root, 295 int[] coord) { 296 ArrayList<View> ancestorChain = new ArrayList<View>(); 297 298 float[] pt = {coord[0], coord[1]}; 299 300 View v = descendant; 301 while(v != root) { 302 ancestorChain.add(v); 303 v = (View) v.getParent(); 304 } 305 ancestorChain.add(root); 306 307 float scale = 1.0f; 308 Matrix inverse = new Matrix(); 309 int count = ancestorChain.size(); 310 for (int i = count - 1; i >= 0; i--) { 311 View ancestor = ancestorChain.get(i); 312 View next = i > 0 ? ancestorChain.get(i-1) : null; 313 314 pt[0] += ancestor.getScrollX(); 315 pt[1] += ancestor.getScrollY(); 316 317 if (next != null) { 318 pt[0] -= next.getLeft(); 319 pt[1] -= next.getTop(); 320 next.getMatrix().invert(inverse); 321 inverse.mapPoints(pt); 322 scale *= next.getScaleX(); 323 } 324 } 325 326 coord[0] = (int) Math.round(pt[0]); 327 coord[1] = (int) Math.round(pt[1]); 328 return scale; 329 } 330 331 /** 332 * Utility method to determine whether the given point, in local coordinates, 333 * is inside the view, where the area of the view is expanded by the slop factor. 334 * This method is called while processing touch-move events to determine if the event 335 * is still within the view. 336 */ 337 public static boolean pointInView(View v, float localX, float localY, float slop) { 338 return localX >= -slop && localY >= -slop && localX < (v.getWidth() + slop) && 339 localY < (v.getHeight() + slop); 340 } 341 342 public static void scaleRect(Rect r, float scale) { 343 if (scale != 1.0f) { 344 r.left = (int) (r.left * scale + 0.5f); 345 r.top = (int) (r.top * scale + 0.5f); 346 r.right = (int) (r.right * scale + 0.5f); 347 r.bottom = (int) (r.bottom * scale + 0.5f); 348 } 349 } 350 351 public static int[] getCenterDeltaInScreenSpace(View v0, View v1, int[] delta) { 352 v0.getLocationInWindow(sLoc0); 353 v1.getLocationInWindow(sLoc1); 354 355 sLoc0[0] += (v0.getMeasuredWidth() * v0.getScaleX()) / 2; 356 sLoc0[1] += (v0.getMeasuredHeight() * v0.getScaleY()) / 2; 357 sLoc1[0] += (v1.getMeasuredWidth() * v1.getScaleX()) / 2; 358 sLoc1[1] += (v1.getMeasuredHeight() * v1.getScaleY()) / 2; 359 360 if (delta == null) { 361 delta = new int[2]; 362 } 363 364 delta[0] = sLoc1[0] - sLoc0[0]; 365 delta[1] = sLoc1[1] - sLoc0[1]; 366 367 return delta; 368 } 369 370 public static void scaleRectAboutCenter(Rect r, float scale) { 371 int cx = r.centerX(); 372 int cy = r.centerY(); 373 r.offset(-cx, -cy); 374 Utilities.scaleRect(r, scale); 375 r.offset(cx, cy); 376 } 377 378 public static void startActivityForResultSafely( 379 Activity activity, Intent intent, int requestCode) { 380 try { 381 activity.startActivityForResult(intent, requestCode); 382 } catch (ActivityNotFoundException e) { 383 Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); 384 } catch (SecurityException e) { 385 Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show(); 386 Log.e(TAG, "Launcher does not have the permission to launch " + intent + 387 ". Make sure to create a MAIN intent-filter for the corresponding activity " + 388 "or use the exported attribute for this activity.", e); 389 } 390 } 391 392 static boolean isSystemApp(Context context, Intent intent) { 393 PackageManager pm = context.getPackageManager(); 394 ComponentName cn = intent.getComponent(); 395 String packageName = null; 396 if (cn == null) { 397 ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); 398 if ((info != null) && (info.activityInfo != null)) { 399 packageName = info.activityInfo.packageName; 400 } 401 } else { 402 packageName = cn.getPackageName(); 403 } 404 if (packageName != null) { 405 try { 406 PackageInfo info = pm.getPackageInfo(packageName, 0); 407 return (info != null) && (info.applicationInfo != null) && 408 ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0); 409 } catch (NameNotFoundException e) { 410 return false; 411 } 412 } else { 413 return false; 414 } 415 } 416 417 /** 418 * This picks a dominant color, looking for high-saturation, high-value, repeated hues. 419 * @param bitmap The bitmap to scan 420 * @param samples The approximate max number of samples to use. 421 */ 422 static int findDominantColorByHue(Bitmap bitmap, int samples) { 423 final int height = bitmap.getHeight(); 424 final int width = bitmap.getWidth(); 425 int sampleStride = (int) Math.sqrt((height * width) / samples); 426 if (sampleStride < 1) { 427 sampleStride = 1; 428 } 429 430 // This is an out-param, for getting the hsv values for an rgb 431 float[] hsv = new float[3]; 432 433 // First get the best hue, by creating a histogram over 360 hue buckets, 434 // where each pixel contributes a score weighted by saturation, value, and alpha. 435 float[] hueScoreHistogram = new float[360]; 436 float highScore = -1; 437 int bestHue = -1; 438 439 for (int y = 0; y < height; y += sampleStride) { 440 for (int x = 0; x < width; x += sampleStride) { 441 int argb = bitmap.getPixel(x, y); 442 int alpha = 0xFF & (argb >> 24); 443 if (alpha < 0x80) { 444 // Drop mostly-transparent pixels. 445 continue; 446 } 447 // Remove the alpha channel. 448 int rgb = argb | 0xFF000000; 449 Color.colorToHSV(rgb, hsv); 450 // Bucket colors by the 360 integer hues. 451 int hue = (int) hsv[0]; 452 if (hue < 0 || hue >= hueScoreHistogram.length) { 453 // Defensively avoid array bounds violations. 454 continue; 455 } 456 float score = hsv[1] * hsv[2]; 457 hueScoreHistogram[hue] += score; 458 if (hueScoreHistogram[hue] > highScore) { 459 highScore = hueScoreHistogram[hue]; 460 bestHue = hue; 461 } 462 } 463 } 464 465 SparseArray<Float> rgbScores = new SparseArray<Float>(); 466 int bestColor = 0xff000000; 467 highScore = -1; 468 // Go back over the RGB colors that match the winning hue, 469 // creating a histogram of weighted s*v scores, for up to 100*100 [s,v] buckets. 470 // The highest-scoring RGB color wins. 471 for (int y = 0; y < height; y += sampleStride) { 472 for (int x = 0; x < width; x += sampleStride) { 473 int rgb = bitmap.getPixel(x, y) | 0xff000000; 474 Color.colorToHSV(rgb, hsv); 475 int hue = (int) hsv[0]; 476 if (hue == bestHue) { 477 float s = hsv[1]; 478 float v = hsv[2]; 479 int bucket = (int) (s * 100) + (int) (v * 10000); 480 // Score by cumulative saturation * value. 481 float score = s * v; 482 Float oldTotal = rgbScores.get(bucket); 483 float newTotal = oldTotal == null ? score : oldTotal + score; 484 rgbScores.put(bucket, newTotal); 485 if (newTotal > highScore) { 486 highScore = newTotal; 487 // All the colors in the winning bucket are very similar. Last in wins. 488 bestColor = rgb; 489 } 490 } 491 } 492 } 493 return bestColor; 494 } 495 496 /* 497 * Finds a system apk which had a broadcast receiver listening to a particular action. 498 * @param action intent action used to find the apk 499 * @return a pair of apk package name and the resources. 500 */ 501 static Pair<String, Resources> findSystemApk(String action, PackageManager pm) { 502 final Intent intent = new Intent(action); 503 for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) { 504 if (info.activityInfo != null && 505 (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { 506 final String packageName = info.activityInfo.packageName; 507 try { 508 final Resources res = pm.getResourcesForApplication(packageName); 509 return Pair.create(packageName, res); 510 } catch (NameNotFoundException e) { 511 Log.w(TAG, "Failed to find resources for " + packageName); 512 } 513 } 514 } 515 return null; 516 } 517 518 @TargetApi(Build.VERSION_CODES.KITKAT) 519 public static boolean isViewAttachedToWindow(View v) { 520 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 521 return v.isAttachedToWindow(); 522 } else { 523 // A proxy call which returns null, if the view is not attached to the window. 524 return v.getKeyDispatcherState() != null; 525 } 526 } 527 528 /** 529 * Returns a widget with category {@link AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX} 530 * provided by the same package which is set to be global search activity. 531 * If widgetCategory is not supported, or no such widget is found, returns the first widget 532 * provided by the package. 533 */ 534 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) 535 public static AppWidgetProviderInfo getSearchWidgetProvider(Context context) { 536 SearchManager searchManager = 537 (SearchManager) context.getSystemService(Context.SEARCH_SERVICE); 538 ComponentName searchComponent = searchManager.getGlobalSearchActivity(); 539 if (searchComponent == null) return null; 540 String providerPkg = searchComponent.getPackageName(); 541 542 AppWidgetProviderInfo defaultWidgetForSearchPackage = null; 543 544 AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); 545 for (AppWidgetProviderInfo info : appWidgetManager.getInstalledProviders()) { 546 if (info.provider.getPackageName().equals(providerPkg)) { 547 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 548 if ((info.widgetCategory & AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX) != 0) { 549 return info; 550 } else if (defaultWidgetForSearchPackage == null) { 551 defaultWidgetForSearchPackage = info; 552 } 553 } else { 554 return info; 555 } 556 } 557 } 558 return defaultWidgetForSearchPackage; 559 } 560 561 /** 562 * Compresses the bitmap to a byte array for serialization. 563 */ 564 public static byte[] flattenBitmap(Bitmap bitmap) { 565 // Try go guesstimate how much space the icon will take when serialized 566 // to avoid unnecessary allocations/copies during the write. 567 int size = bitmap.getWidth() * bitmap.getHeight() * 4; 568 ByteArrayOutputStream out = new ByteArrayOutputStream(size); 569 try { 570 bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); 571 out.flush(); 572 out.close(); 573 return out.toByteArray(); 574 } catch (IOException e) { 575 Log.w(TAG, "Could not write bitmap"); 576 return null; 577 } 578 } 579 580 /** 581 * Find the first vacant cell, if there is one. 582 * 583 * @param vacant Holds the x and y coordinate of the vacant cell 584 * @param spanX Horizontal cell span. 585 * @param spanY Vertical cell span. 586 * 587 * @return true if a vacant cell was found 588 */ 589 public static boolean findVacantCell(int[] vacant, int spanX, int spanY, 590 int xCount, int yCount, boolean[][] occupied) { 591 592 for (int y = 0; (y + spanY) <= yCount; y++) { 593 for (int x = 0; (x + spanX) <= xCount; x++) { 594 boolean available = !occupied[x][y]; 595 out: for (int i = x; i < x + spanX; i++) { 596 for (int j = y; j < y + spanY; j++) { 597 available = available && !occupied[i][j]; 598 if (!available) break out; 599 } 600 } 601 602 if (available) { 603 vacant[0] = x; 604 vacant[1] = y; 605 return true; 606 } 607 } 608 } 609 610 return false; 611 } 612 613 /** 614 * Trims the string, removing all whitespace at the beginning and end of the string. 615 * Non-breaking whitespaces are also removed. 616 */ 617 public static String trim(CharSequence s) { 618 if (s == null) { 619 return null; 620 } 621 622 // Just strip any sequence of whitespace or java space characters from the beginning and end 623 Matcher m = sTrimPattern.matcher(s); 624 return m.replaceAll("$1"); 625 } 626 627 /** 628 * Calculates the height of a given string at a specific text size. 629 */ 630 public static float calculateTextHeight(float textSizePx) { 631 Paint p = new Paint(); 632 p.setTextSize(textSizePx); 633 Paint.FontMetrics fm = p.getFontMetrics(); 634 return -fm.top + fm.bottom; 635 } 636 637 /** 638 * Convenience println with multiple args. 639 */ 640 public static void println(String key, Object... args) { 641 StringBuilder b = new StringBuilder(); 642 b.append(key); 643 b.append(": "); 644 boolean isFirstArgument = true; 645 for (Object arg : args) { 646 if (isFirstArgument) { 647 isFirstArgument = false; 648 } else { 649 b.append(", "); 650 } 651 b.append(arg); 652 } 653 System.out.println(b.toString()); 654 } 655 656 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) 657 public static boolean isRtl(Resources res) { 658 return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) && 659 (res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); 660 } 661 662 public static void assertWorkerThread() { 663 if (LauncherAppState.isDogfoodBuild() && 664 (LauncherModel.sWorkerThread.getThreadId() != Process.myTid())) { 665 throw new IllegalStateException(); 666 } 667 } 668 669 /** 670 * Returns true if the intent is a valid launch intent for a launcher activity of an app. 671 * This is used to identify shortcuts which are different from the ones exposed by the 672 * applications' manifest file. 673 * 674 * @param launchIntent The intent that will be launched when the shortcut is clicked. 675 */ 676 public static boolean isLauncherAppTarget(Intent launchIntent) { 677 if (launchIntent != null 678 && Intent.ACTION_MAIN.equals(launchIntent.getAction()) 679 && launchIntent.getComponent() != null 680 && launchIntent.getCategories() != null 681 && launchIntent.getCategories().size() == 1 682 && launchIntent.hasCategory(Intent.CATEGORY_LAUNCHER) 683 && TextUtils.isEmpty(launchIntent.getDataString())) { 684 // An app target can either have no extra or have ItemInfo.EXTRA_PROFILE. 685 Bundle extras = launchIntent.getExtras(); 686 if (extras == null) { 687 return true; 688 } else { 689 Set<String> keys = extras.keySet(); 690 return keys.size() == 1 && keys.contains(ItemInfo.EXTRA_PROFILE); 691 } 692 }; 693 return false; 694 } 695 696 public static float dpiFromPx(int size, DisplayMetrics metrics){ 697 float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT; 698 return (size / densityRatio); 699 } 700 public static int pxFromDp(float size, DisplayMetrics metrics) { 701 return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 702 size, metrics)); 703 } 704 public static int pxFromSp(float size, DisplayMetrics metrics) { 705 return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 706 size, metrics)); 707 } 708 709 public static String createDbSelectionQuery(String columnName, Iterable<?> values) { 710 return String.format(Locale.ENGLISH, "%s IN (%s)", columnName, TextUtils.join(", ", values)); 711 } 712 } 713