• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
getCategories(Context context, Map<Pair<String, String>, Tile> cache)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      */
getCategories(Context context, Map<Pair<String, String>, Tile> cache, boolean categoryDefinedInManifest)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      */
getCategories(Context context, Map<Pair<String, String>, Tile> cache, boolean categoryDefinedInManifest, String extraAction, String settingPkg)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      */
createCategory(Context context, String categoryKey, boolean categoryDefinedInManifest)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 
getTilesForAction(Context context, UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache, String defaultCategory, ArrayList<Tile> outTiles, boolean requireSettings, String settingPkg)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 
getTilesForAction(Context context, UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache, String defaultCategory, ArrayList<Tile> outTiles, boolean requireSettings, boolean usePriority, String settingPkg)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 
getTilesForIntent( Context context, UserHandle user, Intent intent, Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles, boolean usePriority, boolean checkCategory, boolean forceTintExternalIcon)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 
getTilesForIntent( Context context, UserHandle user, Intent intent, Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles, boolean usePriority, boolean checkCategory, boolean forceTintExternalIcon, boolean shouldUpdateTiles)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 
updateTileData(Context context, Tile tile, ActivityInfo activityInfo, ApplicationInfo applicationInfo, PackageManager pm, Map<String, IContentProvider> providerMap, boolean forceTintExternalIcon)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 
updateSummaryAndTitle( Context context, Map<String, IContentProvider> providerMap, Tile tile)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      */
getIconFromUri(Context context, String packageName, String uriString, Map<String, IContentProvider> providerMap)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      */
getTextFromUri(Context context, String uriString, Map<String, IContentProvider> providerMap, String key)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 
getBundleFromUri(Context context, String uriString, Map<String, IContentProvider> providerMap)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 
getString(Bundle bundle, String key)574     private static String getString(Bundle bundle, String key) {
575         return bundle == null ? null : bundle.getString(key);
576     }
577 
getProviderFromUri(Context context, Uri uri, Map<String, IContentProvider> providerMap)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. */
getMethodFromUri(Uri uri)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