Home | History | Annotate | Download | only in drawer
      1 /*
      2  * Copyright (C) 2015 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 package com.android.settingslib.drawer;
     17 
     18 import android.app.ActivityManager;
     19 import android.content.Context;
     20 import android.content.IContentProvider;
     21 import android.content.Intent;
     22 import android.content.pm.ActivityInfo;
     23 import android.content.pm.ApplicationInfo;
     24 import android.content.pm.PackageManager;
     25 import android.content.pm.ResolveInfo;
     26 import android.content.res.Resources;
     27 import android.graphics.drawable.Icon;
     28 import android.net.Uri;
     29 import android.os.Bundle;
     30 import android.os.RemoteException;
     31 import android.os.UserHandle;
     32 import android.os.UserManager;
     33 import android.provider.Settings.Global;
     34 import android.text.TextUtils;
     35 import android.util.Log;
     36 import android.util.Pair;
     37 import android.widget.RemoteViews;
     38 
     39 import java.util.ArrayList;
     40 import java.util.Collections;
     41 import java.util.Comparator;
     42 import java.util.HashMap;
     43 import java.util.List;
     44 import java.util.Map;
     45 
     46 public class TileUtils {
     47 
     48     private static final boolean DEBUG = false;
     49     private static final boolean DEBUG_TIMING = false;
     50 
     51     private static final String LOG_TAG = "TileUtils";
     52 
     53     /**
     54      * Settings will search for system activities of this action and add them as a top level
     55      * settings tile using the following parameters.
     56      *
     57      * <p>A category must be specified in the meta-data for the activity named
     58      * {@link #EXTRA_CATEGORY_KEY}
     59      *
     60      * <p>The title may be defined by meta-data named {@link #META_DATA_PREFERENCE_TITLE}
     61      * otherwise the label for the activity will be used.
     62      *
     63      * <p>The icon may be defined by meta-data named {@link #META_DATA_PREFERENCE_ICON}
     64      * otherwise the icon for the activity will be used.
     65      *
     66      * <p>A summary my be defined by meta-data named {@link #META_DATA_PREFERENCE_SUMMARY}
     67      */
     68     public static final String EXTRA_SETTINGS_ACTION =
     69             "com.android.settings.action.EXTRA_SETTINGS";
     70 
     71     /**
     72      * @See {@link #EXTRA_SETTINGS_ACTION}.
     73      */
     74     private static final String IA_SETTINGS_ACTION =
     75             "com.android.settings.action.IA_SETTINGS";
     76 
     77 
     78     /**
     79      * Same as #EXTRA_SETTINGS_ACTION but used for the platform Settings activities.
     80      */
     81     private static final String SETTINGS_ACTION =
     82             "com.android.settings.action.SETTINGS";
     83 
     84     private static final String OPERATOR_SETTINGS =
     85             "com.android.settings.OPERATOR_APPLICATION_SETTING";
     86 
     87     private static final String OPERATOR_DEFAULT_CATEGORY =
     88             "com.android.settings.category.wireless";
     89 
     90     private static final String MANUFACTURER_SETTINGS =
     91             "com.android.settings.MANUFACTURER_APPLICATION_SETTING";
     92 
     93     private static final String MANUFACTURER_DEFAULT_CATEGORY =
     94             "com.android.settings.category.device";
     95 
     96     /**
     97      * The key used to get the category from metadata of activities of action
     98      * {@link #EXTRA_SETTINGS_ACTION}
     99      * The value must be one of:
    100      * <li>com.android.settings.category.wireless</li>
    101      * <li>com.android.settings.category.device</li>
    102      * <li>com.android.settings.category.personal</li>
    103      * <li>com.android.settings.category.system</li>
    104      */
    105     private static final String EXTRA_CATEGORY_KEY = "com.android.settings.category";
    106 
    107     /**
    108      * The key used to get the package name of the icon resource for the preference.
    109      */
    110     private static final String EXTRA_PREFERENCE_ICON_PACKAGE =
    111         "com.android.settings.icon_package";
    112 
    113     /**
    114      * Name of the meta-data item that should be set in the AndroidManifest.xml
    115      * to specify the key that should be used for the preference.
    116      */
    117     public static final String META_DATA_PREFERENCE_KEYHINT = "com.android.settings.keyhint";
    118 
    119     /**
    120      * Name of the meta-data item that should be set in the AndroidManifest.xml
    121      * to specify the icon that should be displayed for the preference.
    122      */
    123     public static final String META_DATA_PREFERENCE_ICON = "com.android.settings.icon";
    124 
    125     /**
    126      * Name of the meta-data item that should be set in the AndroidManifest.xml
    127      * to specify the icon background color. The value may or may not be used by Settings app.
    128      */
    129     public static final String META_DATA_PREFERENCE_ICON_BACKGROUND_HINT =
    130             "com.android.settings.bg.hint";
    131 
    132     /**
    133      * Name of the meta-data item that should be set in the AndroidManifest.xml
    134      * to specify the content provider providing the icon that should be displayed for
    135      * the preference.
    136      *
    137      * Icon provided by the content provider overrides any static icon.
    138      */
    139     public static final String META_DATA_PREFERENCE_ICON_URI = "com.android.settings.icon_uri";
    140 
    141     /**
    142      * Name of the meta-data item that should be set in the AndroidManifest.xml
    143      * to specify whether the icon is tintable. This should be a boolean value {@code true} or
    144      * {@code false}, set using {@code android:value}
    145      */
    146     public static final String META_DATA_PREFERENCE_ICON_TINTABLE =
    147             "com.android.settings.icon_tintable";
    148 
    149     /**
    150      * Name of the meta-data item that should be set in the AndroidManifest.xml
    151      * to specify the title that should be displayed for the preference.
    152      *
    153      * <p>Note: It is preferred to provide this value using {@code android:resource} with a string
    154      * resource for localization.
    155      */
    156     public static final String META_DATA_PREFERENCE_TITLE = "com.android.settings.title";
    157 
    158     /**
    159      * Name of the meta-data item that should be set in the AndroidManifest.xml
    160      * to specify the summary text that should be displayed for the preference.
    161      */
    162     public static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary";
    163 
    164     /**
    165      * Name of the meta-data item that should be set in the AndroidManifest.xml
    166      * to specify the content provider providing the summary text that should be displayed for the
    167      * preference.
    168      *
    169      * Summary provided by the content provider overrides any static summary.
    170      */
    171     public static final String META_DATA_PREFERENCE_SUMMARY_URI =
    172             "com.android.settings.summary_uri";
    173 
    174     /**
    175      * Name of the meta-data item that should be set in the AndroidManifest.xml to specify the
    176      * custom view which should be displayed for the preference. The custom view will be inflated
    177      * as a remote view.
    178      *
    179      * This also can be used with {@link #META_DATA_PREFERENCE_SUMMARY_URI}, by setting the id
    180      * of the summary TextView to '@android:id/summary'.
    181      */
    182     public static final String META_DATA_PREFERENCE_CUSTOM_VIEW =
    183             "com.android.settings.custom_view";
    184 
    185     public static final String SETTING_PKG = "com.android.settings";
    186 
    187     /**
    188      * Build a list of DashboardCategory. Each category must be defined in manifest.
    189      * eg: .Settings$DeviceSettings
    190      * @deprecated
    191      */
    192     @Deprecated
    193     public static List<DashboardCategory> getCategories(Context context,
    194             Map<Pair<String, String>, Tile> cache) {
    195         return getCategories(context, cache, true /*categoryDefinedInManifest*/);
    196     }
    197 
    198     /**
    199      * Build a list of DashboardCategory.
    200      * @param categoryDefinedInManifest If true, an dummy activity must exists in manifest to
    201      * represent this category (eg: .Settings$DeviceSettings)
    202      */
    203     public static List<DashboardCategory> getCategories(Context context,
    204             Map<Pair<String, String>, Tile> cache, boolean categoryDefinedInManifest) {
    205         return getCategories(context, cache, categoryDefinedInManifest, null, SETTING_PKG);
    206     }
    207 
    208     /**
    209      * Build a list of DashboardCategory.
    210      * @param categoryDefinedInManifest If true, an dummy activity must exists in manifest to
    211      * represent this category (eg: .Settings$DeviceSettings)
    212      * @param extraAction additional intent filter action to be usetileutild to build the dashboard
    213      * categories
    214      */
    215     public static List<DashboardCategory> getCategories(Context context,
    216             Map<Pair<String, String>, Tile> cache, boolean categoryDefinedInManifest,
    217             String extraAction, String settingPkg) {
    218         final long startTime = System.currentTimeMillis();
    219         boolean setup = Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0)
    220                 != 0;
    221         ArrayList<Tile> tiles = new ArrayList<>();
    222         UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
    223         for (UserHandle user : userManager.getUserProfiles()) {
    224             // TODO: Needs much optimization, too many PM queries going on here.
    225             if (user.getIdentifier() == ActivityManager.getCurrentUser()) {
    226                 // Only add Settings for this user.
    227                 getTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true,
    228                         settingPkg);
    229                 getTilesForAction(context, user, OPERATOR_SETTINGS, cache,
    230                         OPERATOR_DEFAULT_CATEGORY, tiles, false, true, settingPkg);
    231                 getTilesForAction(context, user, MANUFACTURER_SETTINGS, cache,
    232                         MANUFACTURER_DEFAULT_CATEGORY, tiles, false, true, settingPkg);
    233             }
    234             if (setup) {
    235                 getTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false,
    236                         settingPkg);
    237                 if (!categoryDefinedInManifest) {
    238                     getTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false,
    239                             settingPkg);
    240                     if (extraAction != null) {
    241                         getTilesForAction(context, user, extraAction, cache, null, tiles, false,
    242                                 settingPkg);
    243                     }
    244                 }
    245             }
    246         }
    247 
    248         HashMap<String, DashboardCategory> categoryMap = new HashMap<>();
    249         for (Tile tile : tiles) {
    250             DashboardCategory category = categoryMap.get(tile.category);
    251             if (category == null) {
    252                 category = createCategory(context, tile.category, categoryDefinedInManifest);
    253                 if (category == null) {
    254                     Log.w(LOG_TAG, "Couldn't find category " + tile.category);
    255                     continue;
    256                 }
    257                 categoryMap.put(category.key, category);
    258             }
    259             category.addTile(tile);
    260         }
    261         ArrayList<DashboardCategory> categories = new ArrayList<>(categoryMap.values());
    262         for (DashboardCategory category : categories) {
    263             category.sortTiles();
    264         }
    265         Collections.sort(categories, CATEGORY_COMPARATOR);
    266         if (DEBUG_TIMING) Log.d(LOG_TAG, "getCategories took "
    267                 + (System.currentTimeMillis() - startTime) + " ms");
    268         return categories;
    269     }
    270 
    271     /**
    272      * Create a new DashboardCategory from key.
    273      *
    274      * @param context Context to query intent
    275      * @param categoryKey The category key
    276      * @param categoryDefinedInManifest If true, an dummy activity must exists in manifest to
    277      * represent this category (eg: .Settings$DeviceSettings)
    278      */
    279     private static DashboardCategory createCategory(Context context, String categoryKey,
    280             boolean categoryDefinedInManifest) {
    281         DashboardCategory category = new DashboardCategory();
    282         category.key = categoryKey;
    283         if (!categoryDefinedInManifest) {
    284             return category;
    285         }
    286         PackageManager pm = context.getPackageManager();
    287         List<ResolveInfo> results = pm.queryIntentActivities(new Intent(categoryKey), 0);
    288         if (results.size() == 0) {
    289             return null;
    290         }
    291         for (ResolveInfo resolved : results) {
    292             if (!resolved.system) {
    293                 // Do not allow any app to add to settings, only system ones.
    294                 continue;
    295             }
    296             category.title = resolved.activityInfo.loadLabel(pm);
    297             category.priority = SETTING_PKG.equals(
    298                     resolved.activityInfo.applicationInfo.packageName) ? resolved.priority : 0;
    299             if (DEBUG) Log.d(LOG_TAG, "Adding category " + category.title);
    300         }
    301 
    302         return category;
    303     }
    304 
    305     private static void getTilesForAction(Context context,
    306             UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,
    307             String defaultCategory, ArrayList<Tile> outTiles, boolean requireSettings,
    308             String settingPkg) {
    309         getTilesForAction(context, user, action, addedCache, defaultCategory, outTiles,
    310                 requireSettings, requireSettings, settingPkg);
    311     }
    312 
    313     private static void getTilesForAction(Context context,
    314             UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,
    315             String defaultCategory, ArrayList<Tile> outTiles, boolean requireSettings,
    316             boolean usePriority, String settingPkg) {
    317         Intent intent = new Intent(action);
    318         if (requireSettings) {
    319             intent.setPackage(settingPkg);
    320         }
    321         getTilesForIntent(context, user, intent, addedCache, defaultCategory, outTiles,
    322                 usePriority, true, true);
    323     }
    324 
    325     public static void getTilesForIntent(
    326             Context context, UserHandle user, Intent intent,
    327             Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles,
    328             boolean usePriority, boolean checkCategory, boolean forceTintExternalIcon) {
    329         getTilesForIntent(context, user, intent, addedCache, defaultCategory, outTiles,
    330                 usePriority, checkCategory, forceTintExternalIcon, false /* shouldUpdateTiles */);
    331     }
    332 
    333     public static void getTilesForIntent(
    334             Context context, UserHandle user, Intent intent,
    335             Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles,
    336             boolean usePriority, boolean checkCategory, boolean forceTintExternalIcon,
    337             boolean shouldUpdateTiles) {
    338         PackageManager pm = context.getPackageManager();
    339         List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
    340                 PackageManager.GET_META_DATA, user.getIdentifier());
    341         Map<String, IContentProvider> providerMap = new HashMap<>();
    342         for (ResolveInfo resolved : results) {
    343             if (!resolved.system) {
    344                 // Do not allow any app to add to settings, only system ones.
    345                 continue;
    346             }
    347             ActivityInfo activityInfo = resolved.activityInfo;
    348             Bundle metaData = activityInfo.metaData;
    349             String categoryKey = defaultCategory;
    350 
    351             // Load category
    352             if (checkCategory && ((metaData == null) || !metaData.containsKey(EXTRA_CATEGORY_KEY))
    353                     && categoryKey == null) {
    354                 Log.w(LOG_TAG, "Found " + resolved.activityInfo.name + " for intent "
    355                         + intent + " missing metadata "
    356                         + (metaData == null ? "" : EXTRA_CATEGORY_KEY));
    357                 continue;
    358             } else {
    359                 categoryKey = metaData.getString(EXTRA_CATEGORY_KEY);
    360             }
    361 
    362             Pair<String, String> key = new Pair<String, String>(activityInfo.packageName,
    363                     activityInfo.name);
    364             Tile tile = addedCache.get(key);
    365             if (tile == null) {
    366                 tile = new Tile();
    367                 tile.intent = new Intent().setClassName(
    368                         activityInfo.packageName, activityInfo.name);
    369                 tile.category = categoryKey;
    370                 tile.priority = usePriority ? resolved.priority : 0;
    371                 tile.metaData = activityInfo.metaData;
    372                 updateTileData(context, tile, activityInfo, activityInfo.applicationInfo,
    373                         pm, providerMap, forceTintExternalIcon);
    374                 if (DEBUG) Log.d(LOG_TAG, "Adding tile " + tile.title);
    375                 addedCache.put(key, tile);
    376             } else if (shouldUpdateTiles) {
    377                 updateSummaryAndTitle(context, providerMap, tile);
    378             }
    379 
    380             if (!tile.userHandle.contains(user)) {
    381                 tile.userHandle.add(user);
    382             }
    383             if (!outTiles.contains(tile)) {
    384                 outTiles.add(tile);
    385             }
    386         }
    387     }
    388 
    389     private static boolean updateTileData(Context context, Tile tile,
    390             ActivityInfo activityInfo, ApplicationInfo applicationInfo, PackageManager pm,
    391             Map<String, IContentProvider> providerMap, boolean forceTintExternalIcon) {
    392         if (applicationInfo.isSystemApp()) {
    393             boolean forceTintIcon = false;
    394             int icon = 0;
    395             Pair<String, Integer> iconFromUri = null;
    396             CharSequence title = null;
    397             String summary = null;
    398             String keyHint = null;
    399             boolean isIconTintable = false;
    400 
    401             // Get the activity's meta-data
    402             try {
    403                 Resources res = pm.getResourcesForApplication(applicationInfo.packageName);
    404                 Bundle metaData = activityInfo.metaData;
    405 
    406                 if (forceTintExternalIcon
    407                         && !context.getPackageName().equals(applicationInfo.packageName)) {
    408                     isIconTintable = true;
    409                     forceTintIcon = true;
    410                 }
    411 
    412                 if (res != null && metaData != null) {
    413                     if (metaData.containsKey(META_DATA_PREFERENCE_ICON)) {
    414                         icon = metaData.getInt(META_DATA_PREFERENCE_ICON);
    415                     }
    416                     if (metaData.containsKey(META_DATA_PREFERENCE_ICON_TINTABLE)) {
    417                         if (forceTintIcon) {
    418                             Log.w(LOG_TAG, "Ignoring icon tintable for " + activityInfo);
    419                         } else {
    420                             isIconTintable =
    421                                     metaData.getBoolean(META_DATA_PREFERENCE_ICON_TINTABLE);
    422                         }
    423                     }
    424                     if (metaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
    425                         if (metaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) {
    426                             title = res.getString(metaData.getInt(META_DATA_PREFERENCE_TITLE));
    427                         } else {
    428                             title = metaData.getString(META_DATA_PREFERENCE_TITLE);
    429                         }
    430                     }
    431                     if (metaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) {
    432                         if (metaData.get(META_DATA_PREFERENCE_SUMMARY) instanceof Integer) {
    433                             summary = res.getString(metaData.getInt(META_DATA_PREFERENCE_SUMMARY));
    434                         } else {
    435                             summary = metaData.getString(META_DATA_PREFERENCE_SUMMARY);
    436                         }
    437                     }
    438                     if (metaData.containsKey(META_DATA_PREFERENCE_KEYHINT)) {
    439                         if (metaData.get(META_DATA_PREFERENCE_KEYHINT) instanceof Integer) {
    440                             keyHint = res.getString(metaData.getInt(META_DATA_PREFERENCE_KEYHINT));
    441                         } else {
    442                             keyHint = metaData.getString(META_DATA_PREFERENCE_KEYHINT);
    443                         }
    444                     }
    445                     if (metaData.containsKey(META_DATA_PREFERENCE_CUSTOM_VIEW)) {
    446                         int layoutId = metaData.getInt(META_DATA_PREFERENCE_CUSTOM_VIEW);
    447                         tile.remoteViews = new RemoteViews(applicationInfo.packageName, layoutId);
    448                         updateSummaryAndTitle(context, providerMap, tile);
    449                     }
    450                 }
    451             } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
    452                 if (DEBUG) Log.d(LOG_TAG, "Couldn't find info", e);
    453             }
    454 
    455             // Set the preference title to the activity's label if no
    456             // meta-data is found
    457             if (TextUtils.isEmpty(title)) {
    458                 title = activityInfo.loadLabel(pm).toString();
    459             }
    460 
    461             // Set the icon
    462             if (icon == 0) {
    463                 // Only fallback to activityinfo.icon if metadata does not contain ICON_URI.
    464                 // ICON_URI should be loaded in app UI when need the icon object.
    465                 if (!tile.metaData.containsKey(META_DATA_PREFERENCE_ICON_URI)) {
    466                     icon = activityInfo.icon;
    467                 }
    468             }
    469             if (icon != 0) {
    470                 tile.icon = Icon.createWithResource(activityInfo.packageName, icon);
    471             }
    472 
    473             // Set title and summary for the preference
    474             tile.title = title;
    475             tile.summary = summary;
    476             // Replace the intent with this specific activity
    477             tile.intent = new Intent().setClassName(activityInfo.packageName,
    478                     activityInfo.name);
    479             // Suggest a key for this tile
    480             tile.key = keyHint;
    481             tile.isIconTintable = isIconTintable;
    482 
    483             return true;
    484         }
    485 
    486         return false;
    487     }
    488 
    489     private static void updateSummaryAndTitle(
    490             Context context, Map<String, IContentProvider> providerMap, Tile tile) {
    491         if (tile == null || tile.metaData == null
    492                 || !tile.metaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) {
    493             return;
    494         }
    495 
    496         String uriString = tile.metaData.getString(META_DATA_PREFERENCE_SUMMARY_URI);
    497         Bundle bundle = getBundleFromUri(context, uriString, providerMap);
    498         String overrideSummary = getString(bundle, META_DATA_PREFERENCE_SUMMARY);
    499         String overrideTitle = getString(bundle, META_DATA_PREFERENCE_TITLE);
    500         if (overrideSummary != null) {
    501             tile.remoteViews.setTextViewText(android.R.id.summary, overrideSummary);
    502         }
    503 
    504         if (overrideTitle != null) {
    505             tile.remoteViews.setTextViewText(android.R.id.title, overrideTitle);
    506         }
    507     }
    508 
    509     /**
    510      * Gets the icon package name and resource id from content provider.
    511      * @param context context
    512      * @param packageName package name of the target activity
    513      * @param uriString URI for the content provider
    514      * @param providerMap Maps URI authorities to providers
    515      * @return package name and resource id of the icon specified
    516      */
    517     public static Pair<String, Integer> getIconFromUri(Context context, String packageName,
    518             String uriString, Map<String, IContentProvider> providerMap) {
    519         Bundle bundle = getBundleFromUri(context, uriString, providerMap);
    520         if (bundle == null) {
    521             return null;
    522         }
    523         String iconPackageName = bundle.getString(EXTRA_PREFERENCE_ICON_PACKAGE);
    524         if (TextUtils.isEmpty(iconPackageName)) {
    525             return null;
    526         }
    527         int resId = bundle.getInt(META_DATA_PREFERENCE_ICON, 0);
    528         if (resId == 0) {
    529             return null;
    530         }
    531         // Icon can either come from the target package or from the Settings app.
    532         if (iconPackageName.equals(packageName)
    533                 || iconPackageName.equals(context.getPackageName())) {
    534             return Pair.create(iconPackageName, bundle.getInt(META_DATA_PREFERENCE_ICON, 0));
    535         }
    536         return null;
    537     }
    538 
    539     /**
    540      * Gets text associated with the input key from the content provider.
    541      * @param context context
    542      * @param uriString URI for the content provider
    543      * @param providerMap Maps URI authorities to providers
    544      * @param key Key mapping to the text in bundle returned by the content provider
    545      * @return Text associated with the key, if returned by the content provider
    546      */
    547     public static String getTextFromUri(Context context, String uriString,
    548             Map<String, IContentProvider> providerMap, String key) {
    549         Bundle bundle = getBundleFromUri(context, uriString, providerMap);
    550         return (bundle != null) ? bundle.getString(key) : null;
    551     }
    552 
    553     private static Bundle getBundleFromUri(Context context, String uriString,
    554             Map<String, IContentProvider> providerMap) {
    555         if (TextUtils.isEmpty(uriString)) {
    556             return null;
    557         }
    558         Uri uri = Uri.parse(uriString);
    559         String method = getMethodFromUri(uri);
    560         if (TextUtils.isEmpty(method)) {
    561             return null;
    562         }
    563         IContentProvider provider = getProviderFromUri(context, uri, providerMap);
    564         if (provider == null) {
    565             return null;
    566         }
    567         try {
    568             return provider.call(context.getPackageName(), method, uriString, null);
    569         } catch (RemoteException e) {
    570             return null;
    571         }
    572     }
    573 
    574     private static String getString(Bundle bundle, String key) {
    575         return bundle == null ? null : bundle.getString(key);
    576     }
    577 
    578     private static IContentProvider getProviderFromUri(Context context, Uri uri,
    579             Map<String, IContentProvider> providerMap) {
    580         if (uri == null) {
    581             return null;
    582         }
    583         String authority = uri.getAuthority();
    584         if (TextUtils.isEmpty(authority)) {
    585             return null;
    586         }
    587         if (!providerMap.containsKey(authority)) {
    588             providerMap.put(authority, context.getContentResolver().acquireUnstableProvider(uri));
    589         }
    590         return providerMap.get(authority);
    591     }
    592 
    593     /** Returns the first path segment of the uri if it exists as the method, otherwise null. */
    594     static String getMethodFromUri(Uri uri) {
    595         if (uri == null) {
    596             return null;
    597         }
    598         List<String> pathSegments = uri.getPathSegments();
    599         if ((pathSegments == null) || pathSegments.isEmpty()) {
    600             return null;
    601         }
    602         return pathSegments.get(0);
    603     }
    604 
    605     private static final Comparator<DashboardCategory> CATEGORY_COMPARATOR =
    606             new Comparator<DashboardCategory>() {
    607         @Override
    608         public int compare(DashboardCategory lhs, DashboardCategory rhs) {
    609             return rhs.priority - lhs.priority;
    610         }
    611     };
    612 }
    613