Home | History | Annotate | Download | only in launcher3
      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