/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.settingslib.drawer; import android.app.ActivityManager; import android.content.Context; import android.content.IContentProvider; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Bundle; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings.Global; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; public class TileUtils { private static final boolean DEBUG = false; private static final boolean DEBUG_TIMING = false; private static final String LOG_TAG = "TileUtils"; /** * Settings will search for system activities of this action and add them as a top level * settings tile using the following parameters. * *

A category must be specified in the meta-data for the activity named * {@link #EXTRA_CATEGORY_KEY} * *

The title may be defined by meta-data named {@link #META_DATA_PREFERENCE_TITLE} * otherwise the label for the activity will be used. * *

The icon may be defined by meta-data named {@link #META_DATA_PREFERENCE_ICON} * otherwise the icon for the activity will be used. * *

A summary my be defined by meta-data named {@link #META_DATA_PREFERENCE_SUMMARY} */ private static final String EXTRA_SETTINGS_ACTION = "com.android.settings.action.EXTRA_SETTINGS"; /** * @See {@link #EXTRA_SETTINGS_ACTION}. */ private static final String IA_SETTINGS_ACTION = "com.android.settings.action.IA_SETTINGS"; /** * Same as #EXTRA_SETTINGS_ACTION but used for the platform Settings activities. */ private static final String SETTINGS_ACTION = "com.android.settings.action.SETTINGS"; private static final String OPERATOR_SETTINGS = "com.android.settings.OPERATOR_APPLICATION_SETTING"; private static final String OPERATOR_DEFAULT_CATEGORY = "com.android.settings.category.wireless"; private static final String MANUFACTURER_SETTINGS = "com.android.settings.MANUFACTURER_APPLICATION_SETTING"; private static final String MANUFACTURER_DEFAULT_CATEGORY = "com.android.settings.category.device"; /** * The key used to get the category from metadata of activities of action * {@link #EXTRA_SETTINGS_ACTION} * The value must be one of: *

  • com.android.settings.category.wireless
  • *
  • com.android.settings.category.device
  • *
  • com.android.settings.category.personal
  • *
  • com.android.settings.category.system
  • */ private static final String EXTRA_CATEGORY_KEY = "com.android.settings.category"; /** * The key used to get the package name of the icon resource for the preference. */ private static final String EXTRA_PREFERENCE_ICON_PACKAGE = "com.android.settings.icon_package"; /** * Name of the meta-data item that should be set in the AndroidManifest.xml * to specify the key that should be used for the preference. */ public static final String META_DATA_PREFERENCE_KEYHINT = "com.android.settings.keyhint"; /** * Name of the meta-data item that should be set in the AndroidManifest.xml * to specify the icon that should be displayed for the preference. */ public static final String META_DATA_PREFERENCE_ICON = "com.android.settings.icon"; /** * Name of the meta-data item that should be set in the AndroidManifest.xml * to specify the content provider providing the icon that should be displayed for * the preference. * * Icon provided by the content provider overrides any static icon. */ public static final String META_DATA_PREFERENCE_ICON_URI = "com.android.settings.icon_uri"; /** * Name of the meta-data item that should be set in the AndroidManifest.xml * to specify the title that should be displayed for the preference. */ @Deprecated public static final String META_DATA_PREFERENCE_TITLE = "com.android.settings.title"; /** * Name of the meta-data item that should be set in the AndroidManifest.xml * to specify the title that should be displayed for the preference. */ public static final String META_DATA_PREFERENCE_TITLE_RES_ID = "com.android.settings.title.resid"; /** * Name of the meta-data item that should be set in the AndroidManifest.xml * to specify the summary text that should be displayed for the preference. */ public static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary"; /** * Name of the meta-data item that should be set in the AndroidManifest.xml * to specify the content provider providing the summary text that should be displayed for the * preference. * * Summary provided by the content provider overrides any static summary. */ public static final String META_DATA_PREFERENCE_SUMMARY_URI = "com.android.settings.summary_uri"; public static final String SETTING_PKG = "com.android.settings"; /** * Build a list of DashboardCategory. Each category must be defined in manifest. * eg: .Settings$DeviceSettings * @deprecated */ @Deprecated public static List getCategories(Context context, Map, Tile> cache) { return getCategories(context, cache, true /*categoryDefinedInManifest*/); } /** * Build a list of DashboardCategory. * @param categoryDefinedInManifest If true, an dummy activity must exists in manifest to * represent this category (eg: .Settings$DeviceSettings) */ public static List getCategories(Context context, Map, Tile> cache, boolean categoryDefinedInManifest) { return getCategories(context, cache, categoryDefinedInManifest, null, SETTING_PKG); } /** * Build a list of DashboardCategory. * @param categoryDefinedInManifest If true, an dummy activity must exists in manifest to * represent this category (eg: .Settings$DeviceSettings) * @param extraAction additional intent filter action to be usetileutild to build the dashboard * categories */ public static List getCategories(Context context, Map, Tile> cache, boolean categoryDefinedInManifest, String extraAction, String settingPkg) { final long startTime = System.currentTimeMillis(); boolean setup = Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0) != 0; ArrayList tiles = new ArrayList<>(); UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); for (UserHandle user : userManager.getUserProfiles()) { // TODO: Needs much optimization, too many PM queries going on here. if (user.getIdentifier() == ActivityManager.getCurrentUser()) { // Only add Settings for this user. getTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true, settingPkg); getTilesForAction(context, user, OPERATOR_SETTINGS, cache, OPERATOR_DEFAULT_CATEGORY, tiles, false, true, settingPkg); getTilesForAction(context, user, MANUFACTURER_SETTINGS, cache, MANUFACTURER_DEFAULT_CATEGORY, tiles, false, true, settingPkg); } if (setup) { getTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false, settingPkg); if (!categoryDefinedInManifest) { getTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false, settingPkg); if (extraAction != null) { getTilesForAction(context, user, extraAction, cache, null, tiles, false, settingPkg); } } } } HashMap categoryMap = new HashMap<>(); for (Tile tile : tiles) { DashboardCategory category = categoryMap.get(tile.category); if (category == null) { category = createCategory(context, tile.category, categoryDefinedInManifest); if (category == null) { Log.w(LOG_TAG, "Couldn't find category " + tile.category); continue; } categoryMap.put(category.key, category); } category.addTile(tile); } ArrayList categories = new ArrayList<>(categoryMap.values()); for (DashboardCategory category : categories) { Collections.sort(category.tiles, TILE_COMPARATOR); } Collections.sort(categories, CATEGORY_COMPARATOR); if (DEBUG_TIMING) Log.d(LOG_TAG, "getCategories took " + (System.currentTimeMillis() - startTime) + " ms"); return categories; } /** * Create a new DashboardCategory from key. * * @param context Context to query intent * @param categoryKey The category key * @param categoryDefinedInManifest If true, an dummy activity must exists in manifest to * represent this category (eg: .Settings$DeviceSettings) */ private static DashboardCategory createCategory(Context context, String categoryKey, boolean categoryDefinedInManifest) { DashboardCategory category = new DashboardCategory(); category.key = categoryKey; if (!categoryDefinedInManifest) { return category; } PackageManager pm = context.getPackageManager(); List results = pm.queryIntentActivities(new Intent(categoryKey), 0); if (results.size() == 0) { return null; } for (ResolveInfo resolved : results) { if (!resolved.system) { // Do not allow any app to add to settings, only system ones. continue; } category.title = resolved.activityInfo.loadLabel(pm); category.priority = SETTING_PKG.equals( resolved.activityInfo.applicationInfo.packageName) ? resolved.priority : 0; if (DEBUG) Log.d(LOG_TAG, "Adding category " + category.title); } return category; } private static void getTilesForAction(Context context, UserHandle user, String action, Map, Tile> addedCache, String defaultCategory, ArrayList outTiles, boolean requireSettings, String settingPkg) { getTilesForAction(context, user, action, addedCache, defaultCategory, outTiles, requireSettings, requireSettings, settingPkg); } private static void getTilesForAction(Context context, UserHandle user, String action, Map, Tile> addedCache, String defaultCategory, ArrayList outTiles, boolean requireSettings, boolean usePriority, String settingPkg) { Intent intent = new Intent(action); if (requireSettings) { intent.setPackage(settingPkg); } getTilesForIntent(context, user, intent, addedCache, defaultCategory, outTiles, usePriority, true); } public static void getTilesForIntent(Context context, UserHandle user, Intent intent, Map, Tile> addedCache, String defaultCategory, List outTiles, boolean usePriority, boolean checkCategory) { PackageManager pm = context.getPackageManager(); List results = pm.queryIntentActivitiesAsUser(intent, PackageManager.GET_META_DATA, user.getIdentifier()); for (ResolveInfo resolved : results) { if (!resolved.system) { // Do not allow any app to add to settings, only system ones. continue; } ActivityInfo activityInfo = resolved.activityInfo; Bundle metaData = activityInfo.metaData; String categoryKey = defaultCategory; // Load category if (checkCategory && ((metaData == null) || !metaData.containsKey(EXTRA_CATEGORY_KEY)) && categoryKey == null) { Log.w(LOG_TAG, "Found " + resolved.activityInfo.name + " for intent " + intent + " missing metadata " + (metaData == null ? "" : EXTRA_CATEGORY_KEY)); continue; } else { categoryKey = metaData.getString(EXTRA_CATEGORY_KEY); } Pair key = new Pair(activityInfo.packageName, activityInfo.name); Tile tile = addedCache.get(key); if (tile == null) { tile = new Tile(); tile.intent = new Intent().setClassName( activityInfo.packageName, activityInfo.name); tile.category = categoryKey; tile.priority = usePriority ? resolved.priority : 0; tile.metaData = activityInfo.metaData; updateTileData(context, tile, activityInfo, activityInfo.applicationInfo, pm); if (DEBUG) Log.d(LOG_TAG, "Adding tile " + tile.title); addedCache.put(key, tile); } if (!tile.userHandle.contains(user)) { tile.userHandle.add(user); } if (!outTiles.contains(tile)) { outTiles.add(tile); } } } private static boolean updateTileData(Context context, Tile tile, ActivityInfo activityInfo, ApplicationInfo applicationInfo, PackageManager pm) { if (applicationInfo.isSystemApp()) { int icon = 0; Pair iconFromUri = null; CharSequence title = null; String summary = null; String keyHint = null; Uri uri = null; // Get the activity's meta-data try { Resources res = pm.getResourcesForApplication( applicationInfo.packageName); Bundle metaData = activityInfo.metaData; if (res != null && metaData != null) { if (metaData.containsKey(META_DATA_PREFERENCE_ICON)) { icon = metaData.getInt(META_DATA_PREFERENCE_ICON); } int resId = 0; if (metaData.containsKey(META_DATA_PREFERENCE_TITLE_RES_ID)) { resId = metaData.getInt(META_DATA_PREFERENCE_TITLE_RES_ID); if (resId != 0) { title = res.getString(resId); } } // Fallback to legacy title extraction if we couldn't get the title through // res id. if ((resId == 0) && metaData.containsKey(META_DATA_PREFERENCE_TITLE)) { if (metaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) { title = res.getString(metaData.getInt(META_DATA_PREFERENCE_TITLE)); } else { title = metaData.getString(META_DATA_PREFERENCE_TITLE); } } if (metaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) { if (metaData.get(META_DATA_PREFERENCE_SUMMARY) instanceof Integer) { summary = res.getString(metaData.getInt(META_DATA_PREFERENCE_SUMMARY)); } else { summary = metaData.getString(META_DATA_PREFERENCE_SUMMARY); } } if (metaData.containsKey(META_DATA_PREFERENCE_KEYHINT)) { if (metaData.get(META_DATA_PREFERENCE_KEYHINT) instanceof Integer) { keyHint = res.getString(metaData.getInt(META_DATA_PREFERENCE_KEYHINT)); } else { keyHint = metaData.getString(META_DATA_PREFERENCE_KEYHINT); } } } } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) { if (DEBUG) Log.d(LOG_TAG, "Couldn't find info", e); } // Set the preference title to the activity's label if no // meta-data is found if (TextUtils.isEmpty(title)) { title = activityInfo.loadLabel(pm).toString(); } // Set the icon if (iconFromUri != null) { tile.icon = Icon.createWithResource(iconFromUri.first, iconFromUri.second); } else { if (icon == 0) { icon = activityInfo.icon; } tile.icon = Icon.createWithResource(activityInfo.packageName, icon); } // Set title and summary for the preference tile.title = title; tile.summary = summary; // Replace the intent with this specific activity tile.intent = new Intent().setClassName(activityInfo.packageName, activityInfo.name); // Suggest a key for this tile tile.key = keyHint; return true; } return false; } /** * Gets the icon package name and resource id from content provider. * @param Context context * @param packageName package name of the target activity * @param uriString URI for the content provider * @param providerMap Maps URI authorities to providers * @return package name and resource id of the icon specified */ public static Pair getIconFromUri(Context context, String packageName, String uriString, Map providerMap) { Bundle bundle = getBundleFromUri(context, uriString, providerMap); if (bundle == null) { return null; } String iconPackageName = bundle.getString(EXTRA_PREFERENCE_ICON_PACKAGE); if (TextUtils.isEmpty(iconPackageName)) { return null; } int resId = bundle.getInt(META_DATA_PREFERENCE_ICON, 0); if (resId == 0) { return null; } // Icon can either come from the target package or from the Settings app. if (iconPackageName.equals(packageName) || iconPackageName.equals(context.getPackageName())) { return Pair.create(iconPackageName, bundle.getInt(META_DATA_PREFERENCE_ICON, 0)); } return null; } /** * Gets text associated with the input key from the content provider. * @param Context context * @param uriString URI for the content provider * @param providerMap Maps URI authorities to providers * @param key Key mapping to the text in bundle returned by the content provider * @return Text associated with the key, if returned by the content provider */ public static String getTextFromUri(Context context, String uriString, Map providerMap, String key) { Bundle bundle = getBundleFromUri(context, uriString, providerMap); return (bundle != null) ? bundle.getString(key) : null; } private static Bundle getBundleFromUri(Context context, String uriString, Map providerMap) { if (TextUtils.isEmpty(uriString)) { return null; } Uri uri = Uri.parse(uriString); String method = getMethodFromUri(uri); if (TextUtils.isEmpty(method)) { return null; } IContentProvider provider = getProviderFromUri(context, uri, providerMap); if (provider == null) { return null; } try { return provider.call(context.getPackageName(), method, uriString, null); } catch (RemoteException e) { return null; } } private static IContentProvider getProviderFromUri(Context context, Uri uri, Map providerMap) { if (uri == null) { return null; } String authority = uri.getAuthority(); if (TextUtils.isEmpty(authority)) { return null; } if (!providerMap.containsKey(authority)) { providerMap.put(authority, context.getContentResolver().acquireUnstableProvider(uri)); } return providerMap.get(authority); } /** Returns the first path segment of the uri if it exists as the method, otherwise null. */ static String getMethodFromUri(Uri uri) { if (uri == null) { return null; } List pathSegments = uri.getPathSegments(); if ((pathSegments == null) || pathSegments.isEmpty()) { return null; } return pathSegments.get(0); } public static final Comparator TILE_COMPARATOR = new Comparator() { @Override public int compare(Tile lhs, Tile rhs) { return rhs.priority - lhs.priority; } }; private static final Comparator CATEGORY_COMPARATOR = new Comparator() { @Override public int compare(DashboardCategory lhs, DashboardCategory rhs) { return rhs.priority - lhs.priority; } }; }