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