Home | History | Annotate | Download | only in drawer
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 package com.android.settingslib.drawer;
     17 
     18 import android.app.ActivityManager;
     19 import android.content.Context;
     20 import android.content.IContentProvider;
     21 import android.content.Intent;
     22 import android.content.pm.ActivityInfo;
     23 import android.content.pm.ApplicationInfo;
     24 import android.content.pm.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
    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      */
    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      */
    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      */
    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 
    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 
    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 
    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 
    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      */
    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      */
    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 
    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 
    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. */
    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