• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.WallpaperManager;
22 import android.content.ActivityNotFoundException;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.SharedPreferences;
27 import android.content.pm.ApplicationInfo;
28 import android.content.pm.PackageInfo;
29 import android.content.pm.PackageManager;
30 import android.content.pm.PackageManager.NameNotFoundException;
31 import android.content.pm.ResolveInfo;
32 import android.content.res.Resources;
33 import android.content.res.TypedArray;
34 import android.database.Cursor;
35 import android.graphics.Bitmap;
36 import android.graphics.BitmapFactory;
37 import android.graphics.Canvas;
38 import android.graphics.Color;
39 import android.graphics.Matrix;
40 import android.graphics.Paint;
41 import android.graphics.PaintFlagsDrawFilter;
42 import android.graphics.Rect;
43 import android.graphics.RectF;
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.PowerManager;
50 import android.text.Spannable;
51 import android.text.SpannableString;
52 import android.text.TextUtils;
53 import android.text.style.TtsSpan;
54 import android.util.DisplayMetrics;
55 import android.util.Log;
56 import android.util.Pair;
57 import android.util.SparseArray;
58 import android.util.TypedValue;
59 import android.view.MotionEvent;
60 import android.view.View;
61 import android.view.accessibility.AccessibilityEvent;
62 import android.view.accessibility.AccessibilityManager;
63 import android.widget.Toast;
64 
65 import com.android.launcher3.compat.UserHandleCompat;
66 import com.android.launcher3.config.FeatureFlags;
67 import com.android.launcher3.config.ProviderConfig;
68 import com.android.launcher3.graphics.ShadowGenerator;
69 import com.android.launcher3.util.IconNormalizer;
70 
71 import java.io.ByteArrayOutputStream;
72 import java.io.Closeable;
73 import java.io.IOException;
74 import java.lang.reflect.Method;
75 import java.util.ArrayList;
76 import java.util.Collection;
77 import java.util.Locale;
78 import java.util.Set;
79 import java.util.concurrent.Executor;
80 import java.util.concurrent.LinkedBlockingQueue;
81 import java.util.concurrent.ThreadPoolExecutor;
82 import java.util.concurrent.TimeUnit;
83 import java.util.regex.Matcher;
84 import java.util.regex.Pattern;
85 
86 /**
87  * Various utilities shared amongst the Launcher's classes.
88  */
89 public final class Utilities {
90 
91     private static final String TAG = "Launcher.Utilities";
92 
93     private static final Rect sOldBounds = new Rect();
94     private static final Canvas sCanvas = new Canvas();
95 
96     private static final Pattern sTrimPattern =
97             Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$");
98 
99     static {
sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG, Paint.FILTER_BITMAP_FLAG))100         sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG,
101                 Paint.FILTER_BITMAP_FLAG));
102     }
103     static int sColors[] = { 0xffff0000, 0xff00ff00, 0xff0000ff };
104     static int sColorIndex = 0;
105 
106     private static final int[] sLoc0 = new int[2];
107     private static final int[] sLoc1 = new int[2];
108 
isNycMR1OrAbove()109     public static boolean isNycMR1OrAbove() {
110         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1;
111     }
112 
isNycOrAbove()113     public static boolean isNycOrAbove() {
114         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
115     }
116 
117     public static final boolean ATLEAST_MARSHMALLOW =
118             Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
119 
120     public static final boolean ATLEAST_LOLLIPOP_MR1 =
121             Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1;
122 
123     public static final boolean ATLEAST_LOLLIPOP =
124             Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
125 
126     public static final boolean ATLEAST_KITKAT =
127             Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
128 
129     public static final boolean ATLEAST_JB_MR1 =
130             Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1;
131 
132     public static final boolean ATLEAST_JB_MR2 =
133             Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
134 
135     // An intent extra to indicate the horizontal scroll of the wallpaper.
136     public static final String EXTRA_WALLPAPER_OFFSET = "com.android.launcher3.WALLPAPER_OFFSET";
137 
138     // These values are same as that in {@link AsyncTask}.
139     private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
140     private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
141     private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
142     private static final int KEEP_ALIVE = 1;
143     /**
144      * An {@link Executor} to be used with async task with no limit on the queue size.
145      */
146     public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
147             CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
148             TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
149 
150     public static final String ALLOW_ROTATION_PREFERENCE_KEY = "pref_allowRotation";
151 
isPropertyEnabled(String propertyName)152     public static boolean isPropertyEnabled(String propertyName) {
153         return Log.isLoggable(propertyName, Log.VERBOSE);
154     }
155 
isAllowRotationPrefEnabled(Context context)156     public static boolean isAllowRotationPrefEnabled(Context context) {
157         return getPrefs(context).getBoolean(ALLOW_ROTATION_PREFERENCE_KEY,
158                 getAllowRotationDefaultValue(context));
159     }
160 
getAllowRotationDefaultValue(Context context)161     public static boolean getAllowRotationDefaultValue(Context context) {
162         if (isNycOrAbove()) {
163             // If the device was scaled, used the original dimensions to determine if rotation
164             // is allowed of not.
165             Resources res = context.getResources();
166             int originalSmallestWidth = res.getConfiguration().smallestScreenWidthDp
167                     * res.getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEVICE_STABLE;
168             return originalSmallestWidth >= 600;
169         }
170         return false;
171     }
172 
createIconBitmap(Cursor c, int iconIndex, Context context)173     public static Bitmap createIconBitmap(Cursor c, int iconIndex, Context context) {
174         byte[] data = c.getBlob(iconIndex);
175         try {
176             return createIconBitmap(BitmapFactory.decodeByteArray(data, 0, data.length), context);
177         } catch (Exception e) {
178             return null;
179         }
180     }
181 
182     /**
183      * Returns a bitmap suitable for the all apps view. If the package or the resource do not
184      * exist, it returns null.
185      */
createIconBitmap(String packageName, String resourceName, Context context)186     public static Bitmap createIconBitmap(String packageName, String resourceName,
187             Context context) {
188         PackageManager packageManager = context.getPackageManager();
189         // the resource
190         try {
191             Resources resources = packageManager.getResourcesForApplication(packageName);
192             if (resources != null) {
193                 final int id = resources.getIdentifier(resourceName, null, null);
194                 return createIconBitmap(
195                         resources.getDrawableForDensity(id, LauncherAppState.getInstance()
196                                 .getInvariantDeviceProfile().fillResIconDpi), context);
197             }
198         } catch (Exception e) {
199             // Icon not found.
200         }
201         return null;
202     }
203 
getIconBitmapSize()204     private static int getIconBitmapSize() {
205         return LauncherAppState.getInstance().getInvariantDeviceProfile().iconBitmapSize;
206     }
207 
208     /**
209      * Returns a bitmap which is of the appropriate size to be displayed as an icon
210      */
createIconBitmap(Bitmap icon, Context context)211     public static Bitmap createIconBitmap(Bitmap icon, Context context) {
212         final int iconBitmapSize = getIconBitmapSize();
213         if (iconBitmapSize == icon.getWidth() && iconBitmapSize == icon.getHeight()) {
214             return icon;
215         }
216         return createIconBitmap(new BitmapDrawable(context.getResources(), icon), context);
217     }
218 
219     /**
220      * Returns a bitmap suitable for the all apps view. The icon is badged for {@param user}.
221      * The bitmap is also visually normalized with other icons.
222      */
223     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
createBadgedIconBitmap( Drawable icon, UserHandleCompat user, Context context)224     public static Bitmap createBadgedIconBitmap(
225             Drawable icon, UserHandleCompat user, Context context) {
226         float scale = FeatureFlags.LAUNCHER3_DISABLE_ICON_NORMALIZATION ?
227                 1 : IconNormalizer.getInstance().getScale(icon, null);
228         Bitmap bitmap = createIconBitmap(icon, context, scale);
229         return badgeIconForUser(bitmap, user, context);
230     }
231 
232     /**
233      * Badges the provided icon with the user badge if required.
234      */
badgeIconForUser(Bitmap icon, UserHandleCompat user, Context context)235     public static Bitmap badgeIconForUser(Bitmap icon,  UserHandleCompat user, Context context) {
236         if (Utilities.ATLEAST_LOLLIPOP && user != null
237                 && !UserHandleCompat.myUserHandle().equals(user)) {
238             BitmapDrawable drawable = new FixedSizeBitmapDrawable(icon);
239             Drawable badged = context.getPackageManager().getUserBadgedIcon(
240                     drawable, user.getUser());
241             if (badged instanceof BitmapDrawable) {
242                 return ((BitmapDrawable) badged).getBitmap();
243             } else {
244                 return createIconBitmap(badged, context);
245             }
246         } else {
247             return icon;
248         }
249     }
250 
251     /**
252      * Creates a normalized bitmap suitable for the all apps view. The bitmap is also visually
253      * normalized with other icons and has enough spacing to add shadow.
254      */
createScaledBitmapWithoutShadow(Drawable icon, Context context)255     public static Bitmap createScaledBitmapWithoutShadow(Drawable icon, Context context) {
256         RectF iconBounds = new RectF();
257         float scale = FeatureFlags.LAUNCHER3_DISABLE_ICON_NORMALIZATION ?
258                 1 : IconNormalizer.getInstance().getScale(icon, iconBounds);
259         scale = Math.min(scale, ShadowGenerator.getScaleForBounds(iconBounds));
260         return createIconBitmap(icon, context, scale);
261     }
262 
263     /**
264      * Adds a shadow to the provided icon. It assumes that the icon has already been scaled using
265      * {@link #createScaledBitmapWithoutShadow(Drawable, Context)}
266      */
addShadowToIcon(Bitmap icon)267     public static Bitmap addShadowToIcon(Bitmap icon) {
268         return ShadowGenerator.getInstance().recreateIcon(icon);
269     }
270 
271     /**
272      * Adds the {@param badge} on top of {@param srcTgt} using the badge dimensions.
273      */
274     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
badgeWithBitmap(Bitmap srcTgt, Bitmap badge, Context context)275     public static Bitmap badgeWithBitmap(Bitmap srcTgt, Bitmap badge, Context context) {
276         int badgeSize = context.getResources().getDimensionPixelSize(R.dimen.profile_badge_size);
277         synchronized (sCanvas) {
278             sCanvas.setBitmap(srcTgt);
279             sCanvas.drawBitmap(badge, new Rect(0, 0, badge.getWidth(), badge.getHeight()),
280                     new Rect(srcTgt.getWidth() - badgeSize,
281                             srcTgt.getHeight() - badgeSize, srcTgt.getWidth(), srcTgt.getHeight()),
282                     new Paint(Paint.FILTER_BITMAP_FLAG));
283             sCanvas.setBitmap(null);
284         }
285         return srcTgt;
286     }
287 
288     /**
289      * Returns a bitmap suitable for the all apps view.
290      */
createIconBitmap(Drawable icon, Context context)291     public static Bitmap createIconBitmap(Drawable icon, Context context) {
292         return createIconBitmap(icon, context, 1.0f /* scale */);
293     }
294 
295     /**
296      * @param scale the scale to apply before drawing {@param icon} on the canvas
297      */
createIconBitmap(Drawable icon, Context context, float scale)298     public static Bitmap createIconBitmap(Drawable icon, Context context, float scale) {
299         synchronized (sCanvas) {
300             final int iconBitmapSize = getIconBitmapSize();
301 
302             int width = iconBitmapSize;
303             int height = iconBitmapSize;
304 
305             if (icon instanceof PaintDrawable) {
306                 PaintDrawable painter = (PaintDrawable) icon;
307                 painter.setIntrinsicWidth(width);
308                 painter.setIntrinsicHeight(height);
309             } else if (icon instanceof BitmapDrawable) {
310                 // Ensure the bitmap has a density.
311                 BitmapDrawable bitmapDrawable = (BitmapDrawable) icon;
312                 Bitmap bitmap = bitmapDrawable.getBitmap();
313                 if (bitmap != null && bitmap.getDensity() == Bitmap.DENSITY_NONE) {
314                     bitmapDrawable.setTargetDensity(context.getResources().getDisplayMetrics());
315                 }
316             }
317             int sourceWidth = icon.getIntrinsicWidth();
318             int sourceHeight = icon.getIntrinsicHeight();
319             if (sourceWidth > 0 && sourceHeight > 0) {
320                 // Scale the icon proportionally to the icon dimensions
321                 final float ratio = (float) sourceWidth / sourceHeight;
322                 if (sourceWidth > sourceHeight) {
323                     height = (int) (width / ratio);
324                 } else if (sourceHeight > sourceWidth) {
325                     width = (int) (height * ratio);
326                 }
327             }
328 
329             // no intrinsic size --> use default size
330             int textureWidth = iconBitmapSize;
331             int textureHeight = iconBitmapSize;
332 
333             final Bitmap bitmap = Bitmap.createBitmap(textureWidth, textureHeight,
334                     Bitmap.Config.ARGB_8888);
335             final Canvas canvas = sCanvas;
336             canvas.setBitmap(bitmap);
337 
338             final int left = (textureWidth-width) / 2;
339             final int top = (textureHeight-height) / 2;
340 
341             @SuppressWarnings("all") // suppress dead code warning
342             final boolean debug = false;
343             if (debug) {
344                 // draw a big box for the icon for debugging
345                 canvas.drawColor(sColors[sColorIndex]);
346                 if (++sColorIndex >= sColors.length) sColorIndex = 0;
347                 Paint debugPaint = new Paint();
348                 debugPaint.setColor(0xffcccc00);
349                 canvas.drawRect(left, top, left+width, top+height, debugPaint);
350             }
351 
352             sOldBounds.set(icon.getBounds());
353             icon.setBounds(left, top, left+width, top+height);
354             canvas.save(Canvas.MATRIX_SAVE_FLAG);
355             canvas.scale(scale, scale, textureWidth / 2, textureHeight / 2);
356             icon.draw(canvas);
357             canvas.restore();
358             icon.setBounds(sOldBounds);
359             canvas.setBitmap(null);
360 
361             return bitmap;
362         }
363     }
364 
365     /**
366      * Given a coordinate relative to the descendant, find the coordinate in a parent view's
367      * coordinates.
368      *
369      * @param descendant The descendant to which the passed coordinate is relative.
370      * @param ancestor The root view to make the coordinates relative to.
371      * @param coord The coordinate that we want mapped.
372      * @param includeRootScroll Whether or not to account for the scroll of the descendant:
373      *          sometimes this is relevant as in a child's coordinates within the descendant.
374      * @return The factor by which this descendant is scaled relative to this DragLayer. Caution
375      *         this scale factor is assumed to be equal in X and Y, and so if at any point this
376      *         assumption fails, we will need to return a pair of scale factors.
377      */
getDescendantCoordRelativeToAncestor( View descendant, View ancestor, int[] coord, boolean includeRootScroll)378     public static float getDescendantCoordRelativeToAncestor(
379             View descendant, View ancestor, int[] coord, boolean includeRootScroll) {
380         float[] pt = {coord[0], coord[1]};
381         float scale = 1.0f;
382         View v = descendant;
383         while(v != ancestor && v != null) {
384             // For TextViews, scroll has a meaning which relates to the text position
385             // which is very strange... ignore the scroll.
386             if (v != descendant || includeRootScroll) {
387                 pt[0] -= v.getScrollX();
388                 pt[1] -= v.getScrollY();
389             }
390 
391             v.getMatrix().mapPoints(pt);
392             pt[0] += v.getLeft();
393             pt[1] += v.getTop();
394             scale *= v.getScaleX();
395 
396             v = (View) v.getParent();
397         }
398 
399         coord[0] = Math.round(pt[0]);
400         coord[1] = Math.round(pt[1]);
401         return scale;
402     }
403 
404     /**
405      * Inverse of {@link #getDescendantCoordRelativeToAncestor(View, View, int[], boolean)}.
406      */
mapCoordInSelfToDescendent(View descendant, View root, int[] coord)407     public static float mapCoordInSelfToDescendent(View descendant, View root,
408                                                    int[] coord) {
409         ArrayList<View> ancestorChain = new ArrayList<View>();
410 
411         float[] pt = {coord[0], coord[1]};
412 
413         View v = descendant;
414         while(v != root) {
415             ancestorChain.add(v);
416             v = (View) v.getParent();
417         }
418         ancestorChain.add(root);
419 
420         float scale = 1.0f;
421         Matrix inverse = new Matrix();
422         int count = ancestorChain.size();
423         for (int i = count - 1; i >= 0; i--) {
424             View ancestor = ancestorChain.get(i);
425             View next = i > 0 ? ancestorChain.get(i-1) : null;
426 
427             pt[0] += ancestor.getScrollX();
428             pt[1] += ancestor.getScrollY();
429 
430             if (next != null) {
431                 pt[0] -= next.getLeft();
432                 pt[1] -= next.getTop();
433                 next.getMatrix().invert(inverse);
434                 inverse.mapPoints(pt);
435                 scale *= next.getScaleX();
436             }
437         }
438 
439         coord[0] = (int) Math.round(pt[0]);
440         coord[1] = (int) Math.round(pt[1]);
441         return scale;
442     }
443 
444     /**
445      * Utility method to determine whether the given point, in local coordinates,
446      * is inside the view, where the area of the view is expanded by the slop factor.
447      * This method is called while processing touch-move events to determine if the event
448      * is still within the view.
449      */
pointInView(View v, float localX, float localY, float slop)450     public static boolean pointInView(View v, float localX, float localY, float slop) {
451         return localX >= -slop && localY >= -slop && localX < (v.getWidth() + slop) &&
452                 localY < (v.getHeight() + slop);
453     }
454 
455     /** Translates MotionEvents from src's coordinate system to dst's. */
translateEventCoordinates(View src, View dst, MotionEvent dstEvent)456     public static void translateEventCoordinates(View src, View dst, MotionEvent dstEvent) {
457         toGlobalMotionEvent(src, dstEvent);
458         toLocalMotionEvent(dst, dstEvent);
459     }
460 
461     /**
462      * Emulates View.toGlobalMotionEvent(). This implementation does not handle transformations
463      * (scaleX, scaleY, etc).
464      */
toGlobalMotionEvent(View view, MotionEvent event)465     private static void toGlobalMotionEvent(View view, MotionEvent event) {
466         view.getLocationOnScreen(sLoc0);
467         event.offsetLocation(sLoc0[0], sLoc0[1]);
468     }
469 
470     /**
471      * Emulates View.toLocalMotionEvent(). This implementation does not handle transformations
472      * (scaleX, scaleY, etc).
473      */
toLocalMotionEvent(View view, MotionEvent event)474     private static void toLocalMotionEvent(View view, MotionEvent event) {
475         view.getLocationOnScreen(sLoc0);
476         event.offsetLocation(-sLoc0[0], -sLoc0[1]);
477     }
478 
getCenterDeltaInScreenSpace(View v0, View v1, int[] delta)479     public static int[] getCenterDeltaInScreenSpace(View v0, View v1, int[] delta) {
480         v0.getLocationInWindow(sLoc0);
481         v1.getLocationInWindow(sLoc1);
482 
483         sLoc0[0] += (v0.getMeasuredWidth() * v0.getScaleX()) / 2;
484         sLoc0[1] += (v0.getMeasuredHeight() * v0.getScaleY()) / 2;
485         sLoc1[0] += (v1.getMeasuredWidth() * v1.getScaleX()) / 2;
486         sLoc1[1] += (v1.getMeasuredHeight() * v1.getScaleY()) / 2;
487 
488         if (delta == null) {
489             delta = new int[2];
490         }
491 
492         delta[0] = sLoc1[0] - sLoc0[0];
493         delta[1] = sLoc1[1] - sLoc0[1];
494 
495         return delta;
496     }
497 
scaleRectAboutCenter(Rect r, float scale)498     public static void scaleRectAboutCenter(Rect r, float scale) {
499         if (scale != 1.0f) {
500             int cx = r.centerX();
501             int cy = r.centerY();
502             r.offset(-cx, -cy);
503 
504             r.left = (int) (r.left * scale + 0.5f);
505             r.top = (int) (r.top * scale + 0.5f);
506             r.right = (int) (r.right * scale + 0.5f);
507             r.bottom = (int) (r.bottom * scale + 0.5f);
508 
509             r.offset(cx, cy);
510         }
511     }
512 
startActivityForResultSafely( Activity activity, Intent intent, int requestCode)513     public static void startActivityForResultSafely(
514             Activity activity, Intent intent, int requestCode) {
515         try {
516             activity.startActivityForResult(intent, requestCode);
517         } catch (ActivityNotFoundException e) {
518             Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
519         } catch (SecurityException e) {
520             Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
521             Log.e(TAG, "Launcher does not have the permission to launch " + intent +
522                     ". Make sure to create a MAIN intent-filter for the corresponding activity " +
523                     "or use the exported attribute for this activity.", e);
524         }
525     }
526 
isSystemApp(Context context, Intent intent)527     static boolean isSystemApp(Context context, Intent intent) {
528         PackageManager pm = context.getPackageManager();
529         ComponentName cn = intent.getComponent();
530         String packageName = null;
531         if (cn == null) {
532             ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
533             if ((info != null) && (info.activityInfo != null)) {
534                 packageName = info.activityInfo.packageName;
535             }
536         } else {
537             packageName = cn.getPackageName();
538         }
539         if (packageName != null) {
540             try {
541                 PackageInfo info = pm.getPackageInfo(packageName, 0);
542                 return (info != null) && (info.applicationInfo != null) &&
543                         ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0);
544             } catch (NameNotFoundException e) {
545                 return false;
546             }
547         } else {
548             return false;
549         }
550     }
551 
552     /**
553      * This picks a dominant color, looking for high-saturation, high-value, repeated hues.
554      * @param bitmap The bitmap to scan
555      * @param samples The approximate max number of samples to use.
556      */
findDominantColorByHue(Bitmap bitmap, int samples)557     static int findDominantColorByHue(Bitmap bitmap, int samples) {
558         final int height = bitmap.getHeight();
559         final int width = bitmap.getWidth();
560         int sampleStride = (int) Math.sqrt((height * width) / samples);
561         if (sampleStride < 1) {
562             sampleStride = 1;
563         }
564 
565         // This is an out-param, for getting the hsv values for an rgb
566         float[] hsv = new float[3];
567 
568         // First get the best hue, by creating a histogram over 360 hue buckets,
569         // where each pixel contributes a score weighted by saturation, value, and alpha.
570         float[] hueScoreHistogram = new float[360];
571         float highScore = -1;
572         int bestHue = -1;
573 
574         for (int y = 0; y < height; y += sampleStride) {
575             for (int x = 0; x < width; x += sampleStride) {
576                 int argb = bitmap.getPixel(x, y);
577                 int alpha = 0xFF & (argb >> 24);
578                 if (alpha < 0x80) {
579                     // Drop mostly-transparent pixels.
580                     continue;
581                 }
582                 // Remove the alpha channel.
583                 int rgb = argb | 0xFF000000;
584                 Color.colorToHSV(rgb, hsv);
585                 // Bucket colors by the 360 integer hues.
586                 int hue = (int) hsv[0];
587                 if (hue < 0 || hue >= hueScoreHistogram.length) {
588                     // Defensively avoid array bounds violations.
589                     continue;
590                 }
591                 float score = hsv[1] * hsv[2];
592                 hueScoreHistogram[hue] += score;
593                 if (hueScoreHistogram[hue] > highScore) {
594                     highScore = hueScoreHistogram[hue];
595                     bestHue = hue;
596                 }
597             }
598         }
599 
600         SparseArray<Float> rgbScores = new SparseArray<Float>();
601         int bestColor = 0xff000000;
602         highScore = -1;
603         // Go back over the RGB colors that match the winning hue,
604         // creating a histogram of weighted s*v scores, for up to 100*100 [s,v] buckets.
605         // The highest-scoring RGB color wins.
606         for (int y = 0; y < height; y += sampleStride) {
607             for (int x = 0; x < width; x += sampleStride) {
608                 int rgb = bitmap.getPixel(x, y) | 0xff000000;
609                 Color.colorToHSV(rgb, hsv);
610                 int hue = (int) hsv[0];
611                 if (hue == bestHue) {
612                     float s = hsv[1];
613                     float v = hsv[2];
614                     int bucket = (int) (s * 100) + (int) (v * 10000);
615                     // Score by cumulative saturation * value.
616                     float score = s * v;
617                     Float oldTotal = rgbScores.get(bucket);
618                     float newTotal = oldTotal == null ? score : oldTotal + score;
619                     rgbScores.put(bucket, newTotal);
620                     if (newTotal > highScore) {
621                         highScore = newTotal;
622                         // All the colors in the winning bucket are very similar. Last in wins.
623                         bestColor = rgb;
624                     }
625                 }
626             }
627         }
628         return bestColor;
629     }
630 
631     /*
632      * Finds a system apk which had a broadcast receiver listening to a particular action.
633      * @param action intent action used to find the apk
634      * @return a pair of apk package name and the resources.
635      */
findSystemApk(String action, PackageManager pm)636     static Pair<String, Resources> findSystemApk(String action, PackageManager pm) {
637         final Intent intent = new Intent(action);
638         for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) {
639             if (info.activityInfo != null &&
640                     (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
641                 final String packageName = info.activityInfo.packageName;
642                 try {
643                     final Resources res = pm.getResourcesForApplication(packageName);
644                     return Pair.create(packageName, res);
645                 } catch (NameNotFoundException e) {
646                     Log.w(TAG, "Failed to find resources for " + packageName);
647                 }
648             }
649         }
650         return null;
651     }
652 
653     /**
654      * Compresses the bitmap to a byte array for serialization.
655      */
flattenBitmap(Bitmap bitmap)656     public static byte[] flattenBitmap(Bitmap bitmap) {
657         // Try go guesstimate how much space the icon will take when serialized
658         // to avoid unnecessary allocations/copies during the write.
659         int size = bitmap.getWidth() * bitmap.getHeight() * 4;
660         ByteArrayOutputStream out = new ByteArrayOutputStream(size);
661         try {
662             bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
663             out.flush();
664             out.close();
665             return out.toByteArray();
666         } catch (IOException e) {
667             Log.w(TAG, "Could not write bitmap");
668             return null;
669         }
670     }
671 
672     /**
673      * Trims the string, removing all whitespace at the beginning and end of the string.
674      * Non-breaking whitespaces are also removed.
675      */
trim(CharSequence s)676     public static String trim(CharSequence s) {
677         if (s == null) {
678             return null;
679         }
680 
681         // Just strip any sequence of whitespace or java space characters from the beginning and end
682         Matcher m = sTrimPattern.matcher(s);
683         return m.replaceAll("$1");
684     }
685 
686     /**
687      * Calculates the height of a given string at a specific text size.
688      */
calculateTextHeight(float textSizePx)689     public static int calculateTextHeight(float textSizePx) {
690         Paint p = new Paint();
691         p.setTextSize(textSizePx);
692         Paint.FontMetrics fm = p.getFontMetrics();
693         return (int) Math.ceil(fm.bottom - fm.top);
694     }
695 
696     /**
697      * Convenience println with multiple args.
698      */
println(String key, Object... args)699     public static void println(String key, Object... args) {
700         StringBuilder b = new StringBuilder();
701         b.append(key);
702         b.append(": ");
703         boolean isFirstArgument = true;
704         for (Object arg : args) {
705             if (isFirstArgument) {
706                 isFirstArgument = false;
707             } else {
708                 b.append(", ");
709             }
710             b.append(arg);
711         }
712         System.out.println(b.toString());
713     }
714 
715     @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
isRtl(Resources res)716     public static boolean isRtl(Resources res) {
717         return ATLEAST_JB_MR1 &&
718                 (res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
719     }
720 
721     /**
722      * Returns true if the intent is a valid launch intent for a launcher activity of an app.
723      * This is used to identify shortcuts which are different from the ones exposed by the
724      * applications' manifest file.
725      *
726      * @param launchIntent The intent that will be launched when the shortcut is clicked.
727      */
isLauncherAppTarget(Intent launchIntent)728     public static boolean isLauncherAppTarget(Intent launchIntent) {
729         if (launchIntent != null
730                 && Intent.ACTION_MAIN.equals(launchIntent.getAction())
731                 && launchIntent.getComponent() != null
732                 && launchIntent.getCategories() != null
733                 && launchIntent.getCategories().size() == 1
734                 && launchIntent.hasCategory(Intent.CATEGORY_LAUNCHER)
735                 && TextUtils.isEmpty(launchIntent.getDataString())) {
736             // An app target can either have no extra or have ItemInfo.EXTRA_PROFILE.
737             Bundle extras = launchIntent.getExtras();
738             if (extras == null) {
739                 return true;
740             } else {
741                 Set<String> keys = extras.keySet();
742                 return keys.size() == 1 && keys.contains(ItemInfo.EXTRA_PROFILE);
743             }
744         };
745         return false;
746     }
747 
dpiFromPx(int size, DisplayMetrics metrics)748     public static float dpiFromPx(int size, DisplayMetrics metrics){
749         float densityRatio = (float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT;
750         return (size / densityRatio);
751     }
pxFromDp(float size, DisplayMetrics metrics)752     public static int pxFromDp(float size, DisplayMetrics metrics) {
753         return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
754                 size, metrics));
755     }
pxFromSp(float size, DisplayMetrics metrics)756     public static int pxFromSp(float size, DisplayMetrics metrics) {
757         return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
758                 size, metrics));
759     }
760 
createDbSelectionQuery(String columnName, Iterable<?> values)761     public static String createDbSelectionQuery(String columnName, Iterable<?> values) {
762         return String.format(Locale.ENGLISH, "%s IN (%s)", columnName, TextUtils.join(", ", values));
763     }
764 
isBootCompleted()765     public static boolean isBootCompleted() {
766         return "1".equals(getSystemProperty("sys.boot_completed", "1"));
767     }
768 
getSystemProperty(String property, String defaultValue)769     public static String getSystemProperty(String property, String defaultValue) {
770         try {
771             Class clazz = Class.forName("android.os.SystemProperties");
772             Method getter = clazz.getDeclaredMethod("get", String.class);
773             String value = (String) getter.invoke(null, property);
774             if (!TextUtils.isEmpty(value)) {
775                 return value;
776             }
777         } catch (Exception e) {
778             Log.d(TAG, "Unable to read system properties");
779         }
780         return defaultValue;
781     }
782 
783     /**
784      * Ensures that a value is within given bounds. Specifically:
785      * If value is less than lowerBound, return lowerBound; else if value is greater than upperBound,
786      * return upperBound; else return value unchanged.
787      */
boundToRange(int value, int lowerBound, int upperBound)788     public static int boundToRange(int value, int lowerBound, int upperBound) {
789         return Math.max(lowerBound, Math.min(value, upperBound));
790     }
791 
792     /**
793      * @see #boundToRange(int, int, int).
794      */
boundToRange(float value, float lowerBound, float upperBound)795     public static float boundToRange(float value, float lowerBound, float upperBound) {
796         return Math.max(lowerBound, Math.min(value, upperBound));
797     }
798 
799     /**
800      * Wraps a message with a TTS span, so that a different message is spoken than
801      * what is getting displayed.
802      * @param msg original message
803      * @param ttsMsg message to be spoken
804      */
805     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
wrapForTts(CharSequence msg, String ttsMsg)806     public static CharSequence wrapForTts(CharSequence msg, String ttsMsg) {
807         if (Utilities.ATLEAST_LOLLIPOP) {
808             SpannableString spanned = new SpannableString(msg);
809             spanned.setSpan(new TtsSpan.TextBuilder(ttsMsg).build(),
810                     0, spanned.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
811             return spanned;
812         } else {
813             return msg;
814         }
815     }
816 
817     /**
818      * Replacement for Long.compare() which was added in API level 19.
819      */
longCompare(long lhs, long rhs)820     public static int longCompare(long lhs, long rhs) {
821         return lhs < rhs ? -1 : (lhs == rhs ? 0 : 1);
822     }
823 
getPrefs(Context context)824     public static SharedPreferences getPrefs(Context context) {
825         return context.getSharedPreferences(
826                 LauncherFiles.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE);
827     }
828 
829     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
isPowerSaverOn(Context context)830     public static boolean isPowerSaverOn(Context context) {
831         PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
832         return ATLEAST_LOLLIPOP && powerManager.isPowerSaveMode();
833     }
834 
isWallapaperAllowed(Context context)835     public static boolean isWallapaperAllowed(Context context) {
836         if (isNycOrAbove()) {
837             try {
838                 WallpaperManager wm = context.getSystemService(WallpaperManager.class);
839                 return (Boolean) wm.getClass().getDeclaredMethod("isSetWallpaperAllowed")
840                         .invoke(wm);
841             } catch (Exception e) { }
842         }
843         return true;
844     }
845 
closeSilently(Closeable c)846     public static void closeSilently(Closeable c) {
847         if (c != null) {
848             try {
849                 c.close();
850             } catch (IOException e) {
851                 if (ProviderConfig.IS_DOGFOOD_BUILD) {
852                     Log.d(TAG, "Error closing", e);
853                 }
854             }
855         }
856     }
857 
858     /**
859      * Returns true if {@param original} contains all entries defined in {@param updates} and
860      * have the same value.
861      * The comparison uses {@link Object#equals(Object)} to compare the values.
862      */
containsAll(Bundle original, Bundle updates)863     public static boolean containsAll(Bundle original, Bundle updates) {
864         for (String key : updates.keySet()) {
865             Object value1 = updates.get(key);
866             Object value2 = original.get(key);
867             if (value1 == null) {
868                 if (value2 != null) {
869                     return false;
870                 }
871             } else if (!value1.equals(value2)) {
872                 return false;
873             }
874         }
875         return true;
876     }
877 
878     /** Returns whether the collection is null or empty. */
isEmpty(Collection c)879     public static boolean isEmpty(Collection c) {
880         return c == null || c.isEmpty();
881     }
882 
883     /**
884      * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size.
885      * This allows the badging to be done based on the action bitmap size rather than
886      * the scaled bitmap size.
887      */
888     private static class FixedSizeBitmapDrawable extends BitmapDrawable {
889 
FixedSizeBitmapDrawable(Bitmap bitmap)890         public FixedSizeBitmapDrawable(Bitmap bitmap) {
891             super(null, bitmap);
892         }
893 
894         @Override
getIntrinsicHeight()895         public int getIntrinsicHeight() {
896             return getBitmap().getWidth();
897         }
898 
899         @Override
getIntrinsicWidth()900         public int getIntrinsicWidth() {
901             return getBitmap().getWidth();
902         }
903     }
904 
getColorAccent(Context context)905     public static int getColorAccent(Context context) {
906         TypedArray ta = context.obtainStyledAttributes(new int[]{android.R.attr.colorAccent});
907         int colorAccent = ta.getColor(0, 0);
908         ta.recycle();
909         return colorAccent;
910     }
911 
sendCustomAccessibilityEvent(View target, int type, String text)912     public static void sendCustomAccessibilityEvent(View target, int type, String text) {
913         AccessibilityManager accessibilityManager = (AccessibilityManager)
914                 target.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
915         if (accessibilityManager.isEnabled()) {
916             AccessibilityEvent event = AccessibilityEvent.obtain(type);
917             target.onInitializeAccessibilityEvent(event);
918             event.getText().add(text);
919             accessibilityManager.sendAccessibilityEvent(event);
920         }
921     }
922 }
923