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