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.PackageInfo;
25 import android.content.pm.PackageManager;
26 import android.content.pm.ResolveInfo;
27 import android.content.res.Resources;
28 import android.graphics.drawable.Icon;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.RemoteException;
32 import android.os.UserHandle;
33 import android.os.UserManager;
34 import android.provider.Settings.Global;
35 import android.text.TextUtils;
36 import android.util.Log;
37 import android.util.Pair;
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     private 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 content provider providing the icon that should be displayed for
128      * the preference.
129      *
130      * Icon provided by the content provider overrides any static icon.
131      */
132     public static final String META_DATA_PREFERENCE_ICON_URI = "com.android.settings.icon_uri";
133 
134     /**
135      * Name of the meta-data item that should be set in the AndroidManifest.xml
136      * to specify the title that should be displayed for the preference.
137      */
138     @Deprecated
139     public static final String META_DATA_PREFERENCE_TITLE = "com.android.settings.title";
140 
141     /**
142      * Name of the meta-data item that should be set in the AndroidManifest.xml
143      * to specify the title that should be displayed for the preference.
144      */
145     public static final String META_DATA_PREFERENCE_TITLE_RES_ID =
146             "com.android.settings.title.resid";
147 
148     /**
149      * Name of the meta-data item that should be set in the AndroidManifest.xml
150      * to specify the summary text that should be displayed for the preference.
151      */
152     public static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary";
153 
154     /**
155      * Name of the meta-data item that should be set in the AndroidManifest.xml
156      * to specify the content provider providing the summary text that should be displayed for the
157      * preference.
158      *
159      * Summary provided by the content provider overrides any static summary.
160      */
161     public static final String META_DATA_PREFERENCE_SUMMARY_URI =
162             "com.android.settings.summary_uri";
163 
164     public static final String SETTING_PKG = "com.android.settings";
165 
166     /**
167      * Build a list of DashboardCategory. Each category must be defined in manifest.
168      * eg: .Settings$DeviceSettings
169      * @deprecated
170      */
171     @Deprecated
getCategories(Context context, Map<Pair<String, String>, Tile> cache)172     public static List<DashboardCategory> getCategories(Context context,
173             Map<Pair<String, String>, Tile> cache) {
174         return getCategories(context, cache, true /*categoryDefinedInManifest*/);
175     }
176 
177     /**
178      * Build a list of DashboardCategory.
179      * @param categoryDefinedInManifest If true, an dummy activity must exists in manifest to
180      * represent this category (eg: .Settings$DeviceSettings)
181      */
getCategories(Context context, Map<Pair<String, String>, Tile> cache, boolean categoryDefinedInManifest)182     public static List<DashboardCategory> getCategories(Context context,
183             Map<Pair<String, String>, Tile> cache, boolean categoryDefinedInManifest) {
184         return getCategories(context, cache, categoryDefinedInManifest, null, SETTING_PKG);
185     }
186 
187     /**
188      * Build a list of DashboardCategory.
189      * @param categoryDefinedInManifest If true, an dummy activity must exists in manifest to
190      * represent this category (eg: .Settings$DeviceSettings)
191      * @param extraAction additional intent filter action to be usetileutild to build the dashboard
192      * categories
193      */
getCategories(Context context, Map<Pair<String, String>, Tile> cache, boolean categoryDefinedInManifest, String extraAction, String settingPkg)194     public static List<DashboardCategory> getCategories(Context context,
195             Map<Pair<String, String>, Tile> cache, boolean categoryDefinedInManifest,
196             String extraAction, String settingPkg) {
197         final long startTime = System.currentTimeMillis();
198         boolean setup = Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0)
199                 != 0;
200         ArrayList<Tile> tiles = new ArrayList<>();
201         UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
202         for (UserHandle user : userManager.getUserProfiles()) {
203             // TODO: Needs much optimization, too many PM queries going on here.
204             if (user.getIdentifier() == ActivityManager.getCurrentUser()) {
205                 // Only add Settings for this user.
206                 getTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true,
207                         settingPkg);
208                 getTilesForAction(context, user, OPERATOR_SETTINGS, cache,
209                         OPERATOR_DEFAULT_CATEGORY, tiles, false, true, settingPkg);
210                 getTilesForAction(context, user, MANUFACTURER_SETTINGS, cache,
211                         MANUFACTURER_DEFAULT_CATEGORY, tiles, false, true, settingPkg);
212             }
213             if (setup) {
214                 getTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false,
215                         settingPkg);
216                 if (!categoryDefinedInManifest) {
217                     getTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false,
218                             settingPkg);
219                     if (extraAction != null) {
220                         getTilesForAction(context, user, extraAction, cache, null, tiles, false,
221                                 settingPkg);
222                     }
223                 }
224             }
225         }
226 
227         HashMap<String, DashboardCategory> categoryMap = new HashMap<>();
228         for (Tile tile : tiles) {
229             DashboardCategory category = categoryMap.get(tile.category);
230             if (category == null) {
231                 category = createCategory(context, tile.category, categoryDefinedInManifest);
232                 if (category == null) {
233                     Log.w(LOG_TAG, "Couldn't find category " + tile.category);
234                     continue;
235                 }
236                 categoryMap.put(category.key, category);
237             }
238             category.addTile(tile);
239         }
240         ArrayList<DashboardCategory> categories = new ArrayList<>(categoryMap.values());
241         for (DashboardCategory category : categories) {
242             Collections.sort(category.tiles, TILE_COMPARATOR);
243         }
244         Collections.sort(categories, CATEGORY_COMPARATOR);
245         if (DEBUG_TIMING) Log.d(LOG_TAG, "getCategories took "
246                 + (System.currentTimeMillis() - startTime) + " ms");
247         return categories;
248     }
249 
250     /**
251      * Create a new DashboardCategory from key.
252      *
253      * @param context Context to query intent
254      * @param categoryKey The category key
255      * @param categoryDefinedInManifest If true, an dummy activity must exists in manifest to
256      * represent this category (eg: .Settings$DeviceSettings)
257      */
createCategory(Context context, String categoryKey, boolean categoryDefinedInManifest)258     private static DashboardCategory createCategory(Context context, String categoryKey,
259             boolean categoryDefinedInManifest) {
260         DashboardCategory category = new DashboardCategory();
261         category.key = categoryKey;
262         if (!categoryDefinedInManifest) {
263             return category;
264         }
265         PackageManager pm = context.getPackageManager();
266         List<ResolveInfo> results = pm.queryIntentActivities(new Intent(categoryKey), 0);
267         if (results.size() == 0) {
268             return null;
269         }
270         for (ResolveInfo resolved : results) {
271             if (!resolved.system) {
272                 // Do not allow any app to add to settings, only system ones.
273                 continue;
274             }
275             category.title = resolved.activityInfo.loadLabel(pm);
276             category.priority = SETTING_PKG.equals(
277                     resolved.activityInfo.applicationInfo.packageName) ? resolved.priority : 0;
278             if (DEBUG) Log.d(LOG_TAG, "Adding category " + category.title);
279         }
280 
281         return category;
282     }
283 
getTilesForAction(Context context, UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache, String defaultCategory, ArrayList<Tile> outTiles, boolean requireSettings, String settingPkg)284     private static void getTilesForAction(Context context,
285             UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,
286             String defaultCategory, ArrayList<Tile> outTiles, boolean requireSettings,
287             String settingPkg) {
288         getTilesForAction(context, user, action, addedCache, defaultCategory, outTiles,
289                 requireSettings, requireSettings, settingPkg);
290     }
291 
getTilesForAction(Context context, UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache, String defaultCategory, ArrayList<Tile> outTiles, boolean requireSettings, boolean usePriority, String settingPkg)292     private static void getTilesForAction(Context context,
293             UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,
294             String defaultCategory, ArrayList<Tile> outTiles, boolean requireSettings,
295             boolean usePriority, String settingPkg) {
296         Intent intent = new Intent(action);
297         if (requireSettings) {
298             intent.setPackage(settingPkg);
299         }
300         getTilesForIntent(context, user, intent, addedCache, defaultCategory, outTiles,
301                 usePriority, true);
302     }
303 
getTilesForIntent(Context context, UserHandle user, Intent intent, Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles, boolean usePriority, boolean checkCategory)304     public static void getTilesForIntent(Context context, UserHandle user, Intent intent,
305             Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles,
306             boolean usePriority, boolean checkCategory) {
307         PackageManager pm = context.getPackageManager();
308         List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
309                 PackageManager.GET_META_DATA, user.getIdentifier());
310         for (ResolveInfo resolved : results) {
311             if (!resolved.system) {
312                 // Do not allow any app to add to settings, only system ones.
313                 continue;
314             }
315             ActivityInfo activityInfo = resolved.activityInfo;
316             Bundle metaData = activityInfo.metaData;
317             String categoryKey = defaultCategory;
318 
319             // Load category
320             if (checkCategory && ((metaData == null) || !metaData.containsKey(EXTRA_CATEGORY_KEY))
321                     && categoryKey == null) {
322                 Log.w(LOG_TAG, "Found " + resolved.activityInfo.name + " for intent "
323                         + intent + " missing metadata "
324                         + (metaData == null ? "" : EXTRA_CATEGORY_KEY));
325                 continue;
326             } else {
327                 categoryKey = metaData.getString(EXTRA_CATEGORY_KEY);
328             }
329 
330             Pair<String, String> key = new Pair<String, String>(activityInfo.packageName,
331                     activityInfo.name);
332             Tile tile = addedCache.get(key);
333             if (tile == null) {
334                 tile = new Tile();
335                 tile.intent = new Intent().setClassName(
336                         activityInfo.packageName, activityInfo.name);
337                 tile.category = categoryKey;
338                 tile.priority = usePriority ? resolved.priority : 0;
339                 tile.metaData = activityInfo.metaData;
340                 updateTileData(context, tile, activityInfo, activityInfo.applicationInfo,
341                         pm);
342                 if (DEBUG) Log.d(LOG_TAG, "Adding tile " + tile.title);
343 
344                 addedCache.put(key, tile);
345             }
346             if (!tile.userHandle.contains(user)) {
347                 tile.userHandle.add(user);
348             }
349             if (!outTiles.contains(tile)) {
350                 outTiles.add(tile);
351             }
352         }
353     }
354 
updateTileData(Context context, Tile tile, ActivityInfo activityInfo, ApplicationInfo applicationInfo, PackageManager pm)355     private static boolean updateTileData(Context context, Tile tile,
356             ActivityInfo activityInfo, ApplicationInfo applicationInfo, PackageManager pm) {
357         if (applicationInfo.isSystemApp()) {
358             int icon = 0;
359             Pair<String, Integer> iconFromUri = null;
360             CharSequence title = null;
361             String summary = null;
362             String keyHint = null;
363             Uri uri = null;
364 
365             // Get the activity's meta-data
366             try {
367                 Resources res = pm.getResourcesForApplication(
368                         applicationInfo.packageName);
369                 Bundle metaData = activityInfo.metaData;
370 
371                 if (res != null && metaData != null) {
372                     if (metaData.containsKey(META_DATA_PREFERENCE_ICON)) {
373                         icon = metaData.getInt(META_DATA_PREFERENCE_ICON);
374                     }
375                     int resId = 0;
376                     if (metaData.containsKey(META_DATA_PREFERENCE_TITLE_RES_ID)) {
377                         resId = metaData.getInt(META_DATA_PREFERENCE_TITLE_RES_ID);
378                         if (resId != 0) {
379                             title = res.getString(resId);
380                         }
381                     }
382                     // Fallback to legacy title extraction if we couldn't get the title through
383                     // res id.
384                     if ((resId == 0) && metaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
385                         if (metaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) {
386                             title = res.getString(metaData.getInt(META_DATA_PREFERENCE_TITLE));
387                         } else {
388                             title = metaData.getString(META_DATA_PREFERENCE_TITLE);
389                         }
390                     }
391                     if (metaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) {
392                         if (metaData.get(META_DATA_PREFERENCE_SUMMARY) instanceof Integer) {
393                             summary = res.getString(metaData.getInt(META_DATA_PREFERENCE_SUMMARY));
394                         } else {
395                             summary = metaData.getString(META_DATA_PREFERENCE_SUMMARY);
396                         }
397                     }
398                     if (metaData.containsKey(META_DATA_PREFERENCE_KEYHINT)) {
399                         if (metaData.get(META_DATA_PREFERENCE_KEYHINT) instanceof Integer) {
400                             keyHint = res.getString(metaData.getInt(META_DATA_PREFERENCE_KEYHINT));
401                         } else {
402                             keyHint = metaData.getString(META_DATA_PREFERENCE_KEYHINT);
403                         }
404                     }
405                 }
406             } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
407                 if (DEBUG) Log.d(LOG_TAG, "Couldn't find info", e);
408             }
409 
410             // Set the preference title to the activity's label if no
411             // meta-data is found
412             if (TextUtils.isEmpty(title)) {
413                 title = activityInfo.loadLabel(pm).toString();
414             }
415 
416             // Set the icon
417             if (iconFromUri != null) {
418                 tile.icon = Icon.createWithResource(iconFromUri.first, iconFromUri.second);
419             } else {
420                 if (icon == 0) {
421                     icon = activityInfo.icon;
422                 }
423                 tile.icon = Icon.createWithResource(activityInfo.packageName, icon);
424             }
425 
426             // Set title and summary for the preference
427             tile.title = title;
428             tile.summary = summary;
429             // Replace the intent with this specific activity
430             tile.intent = new Intent().setClassName(activityInfo.packageName,
431                     activityInfo.name);
432             // Suggest a key for this tile
433             tile.key = keyHint;
434 
435             return true;
436         }
437 
438         return false;
439     }
440 
441     /**
442      * Gets the icon package name and resource id from content provider.
443      * @param Context context
444      * @param packageName package name of the target activity
445      * @param uriString URI for the content provider
446      * @param providerMap Maps URI authorities to providers
447      * @return package name and resource id of the icon specified
448      */
getIconFromUri(Context context, String packageName, String uriString, Map<String, IContentProvider> providerMap)449     public static Pair<String, Integer> getIconFromUri(Context context, String packageName,
450             String uriString, Map<String, IContentProvider> providerMap) {
451         Bundle bundle = getBundleFromUri(context, uriString, providerMap);
452         if (bundle == null) {
453             return null;
454         }
455         String iconPackageName = bundle.getString(EXTRA_PREFERENCE_ICON_PACKAGE);
456         if (TextUtils.isEmpty(iconPackageName)) {
457             return null;
458         }
459         int resId = bundle.getInt(META_DATA_PREFERENCE_ICON, 0);
460         if (resId == 0) {
461             return null;
462         }
463         // Icon can either come from the target package or from the Settings app.
464         if (iconPackageName.equals(packageName)
465                 || iconPackageName.equals(context.getPackageName())) {
466             return Pair.create(iconPackageName, bundle.getInt(META_DATA_PREFERENCE_ICON, 0));
467         }
468         return null;
469     }
470 
471     /**
472      * Gets text associated with the input key from the content provider.
473      * @param Context context
474      * @param uriString URI for the content provider
475      * @param providerMap Maps URI authorities to providers
476      * @param key Key mapping to the text in bundle returned by the content provider
477      * @return Text associated with the key, if returned by the content provider
478      */
getTextFromUri(Context context, String uriString, Map<String, IContentProvider> providerMap, String key)479     public static String getTextFromUri(Context context, String uriString,
480             Map<String, IContentProvider> providerMap, String key) {
481         Bundle bundle = getBundleFromUri(context, uriString, providerMap);
482         return (bundle != null) ? bundle.getString(key) : null;
483     }
484 
getBundleFromUri(Context context, String uriString, Map<String, IContentProvider> providerMap)485     private static Bundle getBundleFromUri(Context context, String uriString,
486             Map<String, IContentProvider> providerMap) {
487         if (TextUtils.isEmpty(uriString)) {
488             return null;
489         }
490         Uri uri = Uri.parse(uriString);
491         String method = getMethodFromUri(uri);
492         if (TextUtils.isEmpty(method)) {
493             return null;
494         }
495         IContentProvider provider = getProviderFromUri(context, uri, providerMap);
496         if (provider == null) {
497             return null;
498         }
499         try {
500             return provider.call(context.getPackageName(), method, uriString, null);
501         } catch (RemoteException e) {
502             return null;
503         }
504     }
505 
getProviderFromUri(Context context, Uri uri, Map<String, IContentProvider> providerMap)506     private static IContentProvider getProviderFromUri(Context context, Uri uri,
507             Map<String, IContentProvider> providerMap) {
508         if (uri == null) {
509             return null;
510         }
511         String authority = uri.getAuthority();
512         if (TextUtils.isEmpty(authority)) {
513             return null;
514         }
515         if (!providerMap.containsKey(authority)) {
516             providerMap.put(authority, context.getContentResolver().acquireUnstableProvider(uri));
517         }
518         return providerMap.get(authority);
519     }
520 
521     /** Returns the first path segment of the uri if it exists as the method, otherwise null. */
getMethodFromUri(Uri uri)522     static String getMethodFromUri(Uri uri) {
523         if (uri == null) {
524             return null;
525         }
526         List<String> pathSegments = uri.getPathSegments();
527         if ((pathSegments == null) || pathSegments.isEmpty()) {
528             return null;
529         }
530         return pathSegments.get(0);
531     }
532 
533     public static final Comparator<Tile> TILE_COMPARATOR =
534             new Comparator<Tile>() {
535         @Override
536         public int compare(Tile lhs, Tile rhs) {
537             return rhs.priority - lhs.priority;
538         }
539     };
540 
541     private static final Comparator<DashboardCategory> CATEGORY_COMPARATOR =
542             new Comparator<DashboardCategory>() {
543         @Override
544         public int compare(DashboardCategory lhs, DashboardCategory rhs) {
545             return rhs.priority - lhs.priority;
546         }
547     };
548 }
549