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