1 /*
2  * Copyright (C) 2019 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.ContentResolver;
20 import android.content.Context;
21 import android.content.IContentProvider;
22 import android.content.Intent;
23 import android.content.pm.ActivityInfo;
24 import android.content.pm.ComponentInfo;
25 import android.content.pm.PackageManager;
26 import android.content.pm.ProviderInfo;
27 import android.content.pm.ResolveInfo;
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.ArrayMap;
36 import android.util.Log;
37 import android.util.Pair;
38 
39 import androidx.annotation.VisibleForTesting;
40 
41 import java.util.ArrayList;
42 import java.util.HashMap;
43 import java.util.List;
44 import java.util.Map;
45 
46 /**
47  * Utils is a helper class that contains profile key, meta data, settings action
48  * and static methods for get icon or text from uri.
49  */
50 public class TileUtils {
51 
52     private static final boolean DEBUG_TIMING = false;
53 
54     private static final String LOG_TAG = "TileUtils";
55     @VisibleForTesting
56     static final String SETTING_PKG = "com.android.settings";
57 
58     /**
59      * Settings will search for system activities of this action and add them as a top level
60      * settings tile using the following parameters.
61      *
62      * <p>A category must be specified in the meta-data for the activity named
63      * {@link #EXTRA_CATEGORY_KEY}
64      *
65      * <p>The title may be defined by meta-data named {@link #META_DATA_PREFERENCE_TITLE}
66      * otherwise the label for the activity will be used.
67      *
68      * <p>The icon may be defined by meta-data named {@link #META_DATA_PREFERENCE_ICON}
69      * otherwise the icon for the activity will be used.
70      *
71      * <p>A summary my be defined by meta-data named {@link #META_DATA_PREFERENCE_SUMMARY}
72      */
73     public static final String EXTRA_SETTINGS_ACTION = "com.android.settings.action.EXTRA_SETTINGS";
74 
75     /**
76      * @See {@link #EXTRA_SETTINGS_ACTION}.
77      */
78     public static final String IA_SETTINGS_ACTION = "com.android.settings.action.IA_SETTINGS";
79 
80     /**
81      * Same as #EXTRA_SETTINGS_ACTION but used for the platform Settings activities.
82      */
83     private static final String SETTINGS_ACTION = "com.android.settings.action.SETTINGS";
84 
85     private static final String OPERATOR_SETTINGS =
86             "com.android.settings.OPERATOR_APPLICATION_SETTING";
87 
88     private static final String OPERATOR_DEFAULT_CATEGORY =
89             "com.android.settings.category.wireless";
90 
91     private static final String MANUFACTURER_SETTINGS =
92             "com.android.settings.MANUFACTURER_APPLICATION_SETTING";
93 
94     private static final String MANUFACTURER_DEFAULT_CATEGORY =
95             "com.android.settings.category.device";
96 
97     /**
98      * The key used to get the category from metadata of activities of action
99      * {@link #EXTRA_SETTINGS_ACTION}
100      * The value must be from {@link CategoryKey}.
101      */
102     static final String EXTRA_CATEGORY_KEY = "com.android.settings.category";
103 
104     /**
105      * The key used to get the package name of the icon resource for the preference.
106      */
107     static final String EXTRA_PREFERENCE_ICON_PACKAGE = "com.android.settings.icon_package";
108 
109     /**
110      * Name of the meta-data item that should be set in the AndroidManifest.xml
111      * to specify the key that should be used for the preference.
112      */
113     public static final String META_DATA_PREFERENCE_KEYHINT = "com.android.settings.keyhint";
114 
115     /**
116      * Name of the meta-data item that can be set in the AndroidManifest.xml or in the content
117      * provider to specify the key of a group / category where this preference belongs to.
118      */
119     public static final String META_DATA_PREFERENCE_GROUP_KEY = "com.android.settings.group_key";
120 
121     /**
122      * Order of the item that should be displayed on screen. Bigger value items displays closer on
123      * top.
124      */
125     public static final String META_DATA_KEY_ORDER = "com.android.settings.order";
126 
127     /**
128      * Name of the meta-data item that should be set in the AndroidManifest.xml
129      * to specify the icon that should be displayed for the preference.
130      */
131     public static final String META_DATA_PREFERENCE_ICON = "com.android.settings.icon";
132 
133     /**
134      * Name of the meta-data item that should be set in the AndroidManifest.xml
135      * to specify the icon background color. The value may or may not be used by Settings app.
136      */
137     public static final String META_DATA_PREFERENCE_ICON_BACKGROUND_HINT =
138             "com.android.settings.bg.hint";
139 
140     /**
141      * Name of the meta-data item that should be set in the AndroidManifest.xml
142      * to specify the icon background color as raw ARGB.
143      */
144     public static final String META_DATA_PREFERENCE_ICON_BACKGROUND_ARGB =
145             "com.android.settings.bg.argb";
146 
147     /**
148      * Name of the meta-data item that should be set in the AndroidManifest.xml
149      * to specify the content provider providing the icon that should be displayed for
150      * the preference.
151      *
152      * Icon provided by the content provider overrides any static icon.
153      */
154     public static final String META_DATA_PREFERENCE_ICON_URI = "com.android.settings.icon_uri";
155 
156     /**
157      * Name of the meta-data item that should be set in the AndroidManifest.xml
158      * to specify whether the icon is tintable. This should be a boolean value {@code true} or
159      * {@code false}, set using {@code android:value}
160      */
161     public static final String META_DATA_PREFERENCE_ICON_TINTABLE =
162             "com.android.settings.icon_tintable";
163 
164     /**
165      * Name of the meta-data item that should be set in the AndroidManifest.xml
166      * to specify the title that should be displayed for the preference.
167      *
168      * <p>Note: It is preferred to provide this value using {@code android:resource} with a string
169      * resource for localization.
170      */
171     public static final String META_DATA_PREFERENCE_TITLE = "com.android.settings.title";
172 
173     /**
174      * Name of the meta-data item that should be set in the AndroidManifest.xml
175      * to specify the content provider providing the title text that should be displayed for the
176      * preference.
177      *
178      * Title provided by the content provider overrides any static title.
179      */
180     public static final String META_DATA_PREFERENCE_TITLE_URI =
181             "com.android.settings.title_uri";
182 
183     /**
184      * Name of the meta-data item that should be set in the AndroidManifest.xml
185      * to specify the summary text that should be displayed for the preference.
186      */
187     public static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary";
188 
189     /**
190      * Name of the meta-data item that should be set in the AndroidManifest.xml
191      * to specify the content provider providing the summary text that should be displayed for the
192      * preference.
193      *
194      * Summary provided by the content provider overrides any static summary.
195      */
196     public static final String META_DATA_PREFERENCE_SUMMARY_URI =
197             "com.android.settings.summary_uri";
198 
199     /**
200      * Name of the meta-data item that should be set in the AndroidManifest.xml
201      * to specify the content provider providing the switch that should be displayed for the
202      * preference.
203      *
204      * This works with {@link #META_DATA_PREFERENCE_KEYHINT} which should also be set in the
205      * AndroidManifest.xml
206      */
207     public static final String META_DATA_PREFERENCE_SWITCH_URI =
208             "com.android.settings.switch_uri";
209 
210     /**
211      * Name of the meta-data item that can be set from the content provider providing the intent
212      * that will be executed when the user taps on the preference.
213      */
214     public static final String META_DATA_PREFERENCE_PENDING_INTENT =
215             "com.android.settings.pending_intent";
216 
217     /**
218      * Value for {@link #META_DATA_KEY_PROFILE}. When the device has a managed profile,
219      * the app will always be run in the primary profile.
220      *
221      * @see #META_DATA_KEY_PROFILE
222      */
223     public static final String PROFILE_PRIMARY = "primary_profile_only";
224 
225     /**
226      * Value for {@link #META_DATA_KEY_PROFILE}. When the device has a managed profile, the user
227      * will be presented with a dialog to choose the profile the app will be run in.
228      *
229      * @see #META_DATA_KEY_PROFILE
230      */
231     public static final String PROFILE_ALL = "all_profiles";
232 
233     /**
234      * Name of the meta-data item that should be set in the AndroidManifest.xml
235      * to specify the profile in which the app should be run when the device has a managed profile.
236      * The default value is {@link #PROFILE_ALL} which means the user will be presented with a
237      * dialog to choose the profile. If set to {@link #PROFILE_PRIMARY} the app will always be
238      * run in the primary profile.
239      *
240      * @see #PROFILE_PRIMARY
241      * @see #PROFILE_ALL
242      */
243     public static final String META_DATA_KEY_PROFILE = "com.android.settings.profile";
244 
245     /**
246      * Name of the meta-data item that should be set in the AndroidManifest.xml
247      * to specify whether the {@link android.app.Activity} should be launched in a separate task.
248      * This should be a boolean value {@code true} or {@code false}, set using {@code android:value}
249      */
250     public static final String META_DATA_NEW_TASK = "com.android.settings.new_task";
251 
252     /**
253      * If the entry should be shown in settings search results. Defaults to true.
254      */
255     public static final String META_DATA_PREFERENCE_SEARCHABLE = "com.android.settings.searchable";
256 
257     /**
258      * Build a list of DashboardCategory.
259      */
getCategories(Context context, Map<Pair<String, String>, Tile> cache)260     public static List<DashboardCategory> getCategories(Context context,
261             Map<Pair<String, String>, Tile> cache) {
262         final long startTime = System.currentTimeMillis();
263         final boolean setup =
264                 Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0) != 0;
265         final ArrayList<Tile> tiles = new ArrayList<>();
266         final UserManager userManager = (UserManager) context.getSystemService(
267                 Context.USER_SERVICE);
268         for (UserHandle user : userManager.getUserProfiles()) {
269             // TODO: Needs much optimization, too many PM queries going on here.
270             if (user.getIdentifier() == ActivityManager.getCurrentUser()) {
271                 // Only add Settings for this user.
272                 loadTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true);
273                 loadTilesForAction(context, user, OPERATOR_SETTINGS, cache,
274                         OPERATOR_DEFAULT_CATEGORY, tiles, false);
275                 loadTilesForAction(context, user, MANUFACTURER_SETTINGS, cache,
276                         MANUFACTURER_DEFAULT_CATEGORY, tiles, false);
277             }
278             if (setup) {
279                 loadTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false);
280                 loadTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false);
281             }
282         }
283 
284         final HashMap<String, DashboardCategory> categoryMap = new HashMap<>();
285         for (Tile tile : tiles) {
286             final String categoryKey = tile.getCategory();
287             DashboardCategory category = categoryMap.get(categoryKey);
288             if (category == null) {
289                 category = new DashboardCategory(categoryKey);
290 
291                 if (category == null) {
292                     Log.w(LOG_TAG, "Couldn't find category " + categoryKey);
293                     continue;
294                 }
295                 categoryMap.put(categoryKey, category);
296             }
297             category.addTile(tile);
298         }
299         final ArrayList<DashboardCategory> categories = new ArrayList<>(categoryMap.values());
300         for (DashboardCategory category : categories) {
301             category.sortTiles();
302         }
303 
304         if (DEBUG_TIMING) {
305             Log.d(LOG_TAG, "getCategories took "
306                     + (System.currentTimeMillis() - startTime) + " ms");
307         }
308         return categories;
309     }
310 
311     @VisibleForTesting
loadTilesForAction(Context context, UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles, boolean requireSettings)312     static void loadTilesForAction(Context context,
313             UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,
314             String defaultCategory, List<Tile> outTiles, boolean requireSettings) {
315         final Intent intent = new Intent(action);
316         if (requireSettings) {
317             intent.setPackage(SETTING_PKG);
318         }
319         loadActivityTiles(context, user, addedCache, defaultCategory, outTiles, intent);
320         loadProviderTiles(context, user, addedCache, defaultCategory, outTiles, intent);
321     }
322 
loadActivityTiles(Context context, UserHandle user, Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles, Intent intent)323     private static void loadActivityTiles(Context context,
324             UserHandle user, Map<Pair<String, String>, Tile> addedCache,
325             String defaultCategory, List<Tile> outTiles, Intent intent) {
326         final PackageManager pm = context.getPackageManager();
327         final List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
328                 PackageManager.GET_META_DATA, user.getIdentifier());
329         for (ResolveInfo resolved : results) {
330             if (!resolved.system) {
331                 // Do not allow any app to add to settings, only system ones.
332                 continue;
333             }
334             final ActivityInfo activityInfo = resolved.activityInfo;
335             final Bundle metaData = activityInfo.metaData;
336             loadTile(user, addedCache, defaultCategory, outTiles, intent, metaData, activityInfo);
337         }
338     }
339 
loadProviderTiles(Context context, UserHandle user, Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles, Intent intent)340     private static void loadProviderTiles(Context context,
341             UserHandle user, Map<Pair<String, String>, Tile> addedCache,
342             String defaultCategory, List<Tile> outTiles, Intent intent) {
343         final PackageManager pm = context.getPackageManager();
344         final List<ResolveInfo> results = pm.queryIntentContentProvidersAsUser(intent,
345                 0 /* flags */, user.getIdentifier());
346         for (ResolveInfo resolved : results) {
347             if (!resolved.system) {
348                 // Do not allow any app to add to settings, only system ones.
349                 continue;
350             }
351             final ProviderInfo providerInfo = resolved.providerInfo;
352             final List<Bundle> entryData = getEntryDataFromProvider(
353                     // Build new context so the entry data is retrieved for the queried user.
354                     context.createContextAsUser(user, 0 /* flags */),
355                     providerInfo.authority);
356             if (entryData == null || entryData.isEmpty()) {
357                 continue;
358             }
359             for (Bundle metaData : entryData) {
360                 loadTile(user, addedCache, defaultCategory, outTiles, intent, metaData,
361                         providerInfo);
362             }
363         }
364     }
365 
loadTile(UserHandle user, Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles, Intent intent, Bundle metaData, ComponentInfo componentInfo)366     private static void loadTile(UserHandle user, Map<Pair<String, String>, Tile> addedCache,
367             String defaultCategory, List<Tile> outTiles, Intent intent, Bundle metaData,
368             ComponentInfo componentInfo) {
369         // Skip loading tile if the component is tagged primary_profile_only but not running on
370         // the current user.
371         if (user.getIdentifier() != ActivityManager.getCurrentUser()
372                 && Tile.isPrimaryProfileOnly(componentInfo.metaData)) {
373             Log.w(LOG_TAG, "Found " + componentInfo.name + " for intent "
374                     + intent + " is primary profile only, skip loading tile for uid "
375                     + user.getIdentifier());
376             return;
377         }
378 
379         String categoryKey = defaultCategory;
380         // Load category
381         if ((metaData == null || !metaData.containsKey(EXTRA_CATEGORY_KEY))
382                 && categoryKey == null) {
383             Log.w(LOG_TAG, "Found " + componentInfo.name + " for intent "
384                     + intent + " missing metadata "
385                     + (metaData == null ? "" : EXTRA_CATEGORY_KEY));
386             return;
387         } else {
388             categoryKey = metaData.getString(EXTRA_CATEGORY_KEY);
389         }
390 
391         final boolean isProvider = componentInfo instanceof ProviderInfo;
392         final Pair<String, String> key = isProvider
393                 ? new Pair<>(((ProviderInfo) componentInfo).authority,
394                         metaData.getString(META_DATA_PREFERENCE_KEYHINT))
395                 : new Pair<>(componentInfo.packageName, componentInfo.name);
396         Tile tile = addedCache.get(key);
397         if (tile == null) {
398             tile = isProvider
399                     ? new ProviderTile((ProviderInfo) componentInfo, categoryKey, metaData)
400                     : new ActivityTile((ActivityInfo) componentInfo, categoryKey);
401             addedCache.put(key, tile);
402         } else {
403             tile.setMetaData(metaData);
404         }
405 
406         if (!tile.userHandle.contains(user)) {
407             tile.userHandle.add(user);
408         }
409         if (metaData.containsKey(META_DATA_PREFERENCE_PENDING_INTENT)) {
410             tile.pendingIntentMap.put(
411                     user, metaData.getParcelable(META_DATA_PREFERENCE_PENDING_INTENT));
412         }
413         if (!outTiles.contains(tile)) {
414             outTiles.add(tile);
415         }
416     }
417 
418     /** Returns the entry data of the key specified from the provider */
419     // TODO(b/144732809): rearrange methods by access level modifiers
getEntryDataFromProvider(Context context, String authority, String key)420     static Bundle getEntryDataFromProvider(Context context, String authority, String key) {
421         final Map<String, IContentProvider> providerMap = new ArrayMap<>();
422         final Uri uri = buildUri(authority, EntriesProvider.METHOD_GET_ENTRY_DATA, key);
423         Bundle result = getBundleFromUri(context, uri, providerMap, null /* bundle */);
424         if (result == null) {
425             Uri fallbackUri = buildUri(authority, EntriesProvider.METHOD_GET_SWITCH_DATA, key);
426             result = getBundleFromUri(context, fallbackUri, providerMap, null /* bundle */);
427         }
428         return result;
429     }
430 
431     /** Returns all entry data from the provider */
getEntryDataFromProvider(Context context, String authority)432     private static List<Bundle> getEntryDataFromProvider(Context context, String authority) {
433         final Map<String, IContentProvider> providerMap = new ArrayMap<>();
434         final Uri uri = buildUri(authority, EntriesProvider.METHOD_GET_ENTRY_DATA);
435         final Bundle result = getBundleFromUri(context, uri, providerMap, null /* bundle */);
436         if (result != null) {
437             return result.getParcelableArrayList(EntriesProvider.EXTRA_ENTRY_DATA);
438         } else {
439             Uri fallbackUri = buildUri(authority, EntriesProvider.METHOD_GET_SWITCH_DATA);
440             Bundle fallbackResult =
441                     getBundleFromUri(context, fallbackUri, providerMap, null /* bundle */);
442             return fallbackResult != null
443                     ? fallbackResult.getParcelableArrayList(EntriesProvider.EXTRA_SWITCH_DATA)
444                     : null;
445         }
446     }
447 
448     /**
449      * Returns the complete uri from the meta data key of the tile.
450      *
451      * A complete uri should contain at least one path segment and be one of the following types:
452      *      content://authority/method
453      *      content://authority/method/key
454      *
455      * If the uri from the tile is not complete, build a uri by the default method and the
456      * preference key.
457      *
458      * @param tile          Tile which contains meta data
459      * @param metaDataKey   Key mapping to the uri in meta data
460      * @param defaultMethod Method to be attached to the uri by default if it has no path segment
461      * @return Uri associated with the key
462      */
getCompleteUri(Tile tile, String metaDataKey, String defaultMethod)463     public static Uri getCompleteUri(Tile tile, String metaDataKey, String defaultMethod) {
464         final String uriString = tile.getMetaData().getString(metaDataKey);
465         if (TextUtils.isEmpty(uriString)) {
466             return null;
467         }
468 
469         final Uri uri = Uri.parse(uriString);
470         final List<String> pathSegments = uri.getPathSegments();
471         if (pathSegments != null && !pathSegments.isEmpty()) {
472             return uri;
473         }
474 
475         final String key = tile.getMetaData().getString(META_DATA_PREFERENCE_KEYHINT);
476         if (TextUtils.isEmpty(key)) {
477             Log.w(LOG_TAG, "Please specify the meta-data " + META_DATA_PREFERENCE_KEYHINT
478                     + " in AndroidManifest.xml for " + uriString);
479             return buildUri(uri.getAuthority(), defaultMethod);
480         }
481         return buildUri(uri.getAuthority(), defaultMethod, key);
482     }
483 
buildUri(String authority, String method, String key)484     static Uri buildUri(String authority, String method, String key) {
485         return new Uri.Builder()
486                 .scheme(ContentResolver.SCHEME_CONTENT)
487                 .authority(authority)
488                 .appendPath(method)
489                 .appendPath(key)
490                 .build();
491     }
492 
buildUri(String authority, String method)493     private static Uri buildUri(String authority, String method) {
494         return new Uri.Builder()
495                 .scheme(ContentResolver.SCHEME_CONTENT)
496                 .authority(authority)
497                 .appendPath(method)
498                 .build();
499     }
500 
501     /**
502      * Gets the icon package name and resource id from content provider.
503      *
504      * @param context     context
505      * @param packageName package name of the target activity
506      * @param uri         URI for the content provider
507      * @param providerMap Maps URI authorities to providers
508      * @return package name and resource id of the icon specified
509      */
getIconFromUri(Context context, String packageName, Uri uri, Map<String, IContentProvider> providerMap)510     public static Pair<String, Integer> getIconFromUri(Context context, String packageName,
511             Uri uri, Map<String, IContentProvider> providerMap) {
512         final Bundle bundle = getBundleFromUri(context, uri, providerMap, null /* bundle */);
513         if (bundle == null) {
514             return null;
515         }
516         final String iconPackageName = bundle.getString(EXTRA_PREFERENCE_ICON_PACKAGE);
517         if (TextUtils.isEmpty(iconPackageName)) {
518             return null;
519         }
520         int resId = bundle.getInt(META_DATA_PREFERENCE_ICON, 0);
521         if (resId == 0) {
522             return null;
523         }
524         // Icon can either come from the target package or from the Settings app.
525         if (iconPackageName.equals(packageName)
526                 || iconPackageName.equals(context.getPackageName())) {
527             return Pair.create(iconPackageName, resId);
528         }
529         return null;
530     }
531 
532     /**
533      * Gets text associated with the input key from the content provider.
534      *
535      * @param context     context
536      * @param uri         URI for the content provider
537      * @param providerMap Maps URI authorities to providers
538      * @param key         Key mapping to the text in bundle returned by the content provider
539      * @return Text associated with the key, if returned by the content provider
540      */
getTextFromUri(Context context, Uri uri, Map<String, IContentProvider> providerMap, String key)541     public static String getTextFromUri(Context context, Uri uri,
542             Map<String, IContentProvider> providerMap, String key) {
543         final Bundle bundle = getBundleFromUri(context, uri, providerMap, null /* bundle */);
544         return (bundle != null) ? bundle.getString(key) : null;
545     }
546 
547     /**
548      * Gets boolean associated with the input key from the content provider.
549      *
550      * @param context     context
551      * @param uri         URI for the content provider
552      * @param providerMap Maps URI authorities to providers
553      * @param key         Key mapping to the text in bundle returned by the content provider
554      * @return Boolean associated with the key, if returned by the content provider
555      */
getBooleanFromUri(Context context, Uri uri, Map<String, IContentProvider> providerMap, String key)556     public static boolean getBooleanFromUri(Context context, Uri uri,
557             Map<String, IContentProvider> providerMap, String key) {
558         final Bundle bundle = getBundleFromUri(context, uri, providerMap, null /* bundle */);
559         return (bundle != null) ? bundle.getBoolean(key) : false;
560     }
561 
562     /**
563      * Puts boolean associated with the input key to the content provider.
564      *
565      * @param context     context
566      * @param uri         URI for the content provider
567      * @param providerMap Maps URI authorities to providers
568      * @param key         Key mapping to the text in bundle returned by the content provider
569      * @param value       Boolean associated with the key
570      * @return Bundle associated with the action, if returned by the content provider
571      */
putBooleanToUriAndGetResult(Context context, Uri uri, Map<String, IContentProvider> providerMap, String key, boolean value)572     public static Bundle putBooleanToUriAndGetResult(Context context, Uri uri,
573             Map<String, IContentProvider> providerMap, String key, boolean value) {
574         final Bundle bundle = new Bundle();
575         bundle.putBoolean(key, value);
576         return getBundleFromUri(context, uri, providerMap, bundle);
577     }
578 
getBundleFromUri(Context context, Uri uri, Map<String, IContentProvider> providerMap, Bundle bundle)579     private static Bundle getBundleFromUri(Context context, Uri uri,
580             Map<String, IContentProvider> providerMap, Bundle bundle) {
581         final Pair<String, String> args = getMethodAndKey(uri);
582         if (args == null) {
583             return null;
584         }
585         final String method = args.first;
586         final String key = args.second;
587         if (TextUtils.isEmpty(method)) {
588             return null;
589         }
590         final IContentProvider provider = getProviderFromUri(context, uri, providerMap);
591         if (provider == null) {
592             return null;
593         }
594         if (!TextUtils.isEmpty(key)) {
595             if (bundle == null) {
596                 bundle = new Bundle();
597             }
598             bundle.putString(META_DATA_PREFERENCE_KEYHINT, key);
599         }
600         try {
601             return provider.call(context.getAttributionSource(),
602                     uri.getAuthority(), method, uri.toString(), bundle);
603         } catch (RemoteException e) {
604             return null;
605         }
606     }
607 
getProviderFromUri(Context context, Uri uri, Map<String, IContentProvider> providerMap)608     private static IContentProvider getProviderFromUri(Context context, Uri uri,
609             Map<String, IContentProvider> providerMap) {
610         if (uri == null) {
611             return null;
612         }
613         final String authority = uri.getAuthority();
614         if (TextUtils.isEmpty(authority)) {
615             return null;
616         }
617         if (!providerMap.containsKey(authority)) {
618             providerMap.put(authority, context.getContentResolver().acquireUnstableProvider(uri));
619         }
620         return providerMap.get(authority);
621     }
622 
623     /** Returns method and key of the complete uri. */
getMethodAndKey(Uri uri)624     private static Pair<String, String> getMethodAndKey(Uri uri) {
625         if (uri == null) {
626             return null;
627         }
628         final List<String> pathSegments = uri.getPathSegments();
629         if (pathSegments == null || pathSegments.isEmpty()) {
630             return null;
631         }
632         final String method = pathSegments.get(0);
633         final String key = pathSegments.size() > 1 ? pathSegments.get(1) : null;
634         return Pair.create(method, key);
635     }
636 }
637