1 /* 2 * Copyright (C) 2016 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.car.apps.common; 17 18 import static android.content.pm.PackageManager.MATCH_ALL; 19 20 import android.content.ContentResolver; 21 import android.content.Context; 22 import android.content.Intent.ShortcutIconResource; 23 import android.content.pm.PackageManager; 24 import android.content.pm.PackageManager.NameNotFoundException; 25 import android.content.pm.ProviderInfo; 26 import android.content.res.Resources; 27 import android.graphics.drawable.Drawable; 28 import android.net.Uri; 29 import android.text.TextUtils; 30 31 import androidx.annotation.Nullable; 32 33 /** 34 * Utilities for working with URIs. 35 */ 36 public final class UriUtils { 37 38 private static final String SCHEME_SHORTCUT_ICON_RESOURCE = "shortcut.icon.resource"; 39 private static final String SCHEME_DELIMITER = "://"; 40 private static final String URI_PATH_DELIMITER = "/"; 41 private static final String URI_PACKAGE_DELIMITER = ":"; 42 private static final String HTTP_PREFIX = "http"; 43 private static final String HTTPS_PREFIX = "https"; 44 private static final String SCHEME_ACCOUNT_IMAGE = "image.account"; 45 private static final String ACCOUNT_IMAGE_CHANGE_NOTIFY_URI = "change_notify_uri"; 46 private static final String DETAIL_DIALOG_URI_DIALOG_TITLE = "detail_dialog_title"; 47 private static final String DETAIL_DIALOG_URI_DIALOG_DESCRIPTION = "detail_dialog_description"; 48 private static final String DETAIL_DIALOG_URI_DIALOG_ACTION_START_INDEX = 49 "detail_dialog_action_start_index"; 50 private static final String DETAIL_DIALOG_URI_DIALOG_ACTION_START_NAME = 51 "detail_dialog_action_start_name"; 52 53 /** 54 * Non instantiable. 55 */ UriUtils()56 private UriUtils() {} 57 58 /** Returns true if the uri is null or empty. */ isEmpty(@ullable Uri uri)59 public static boolean isEmpty(@Nullable Uri uri) { 60 return (uri == null || TextUtils.isEmpty(uri.toString())); 61 } 62 63 /** 64 * Gets resource uri representation for a resource of a package 65 */ getAndroidResourceUri(Context context, int resourceId)66 public static String getAndroidResourceUri(Context context, int resourceId) { 67 return getAndroidResourceUri(context.getResources(), resourceId); 68 } 69 70 /** 71 * Gets resource uri representation for a resource 72 */ getAndroidResourceUri(Resources resources, int resourceId)73 public static String getAndroidResourceUri(Resources resources, int resourceId) { 74 return ContentResolver.SCHEME_ANDROID_RESOURCE 75 + SCHEME_DELIMITER + resources.getResourceName(resourceId) 76 .replace(URI_PACKAGE_DELIMITER, URI_PATH_DELIMITER); 77 } 78 79 /** 80 * Loads drawable from resource 81 */ 82 @Nullable getDrawable(Context context, ShortcutIconResource r)83 public static Drawable getDrawable(Context context, ShortcutIconResource r) { 84 Resources resources = null; 85 try { 86 resources = context.getPackageManager().getResourcesForApplication(r.packageName); 87 } catch (NameNotFoundException e) { 88 // Return null below. 89 } 90 if (resources == null) { 91 return null; 92 } 93 final int id = resources.getIdentifier(r.resourceName, null, null); 94 return resources.getDrawable(id, null); 95 } 96 97 /** 98 * Gets a URI with short cut icon scheme. 99 */ getShortcutIconResourceUri(ShortcutIconResource iconResource)100 public static Uri getShortcutIconResourceUri(ShortcutIconResource iconResource) { 101 return Uri.parse(SCHEME_SHORTCUT_ICON_RESOURCE + SCHEME_DELIMITER + iconResource.packageName 102 + URI_PATH_DELIMITER 103 + iconResource.resourceName.replace(URI_PACKAGE_DELIMITER, URI_PATH_DELIMITER)); 104 } 105 106 /** 107 * Gets a URI with scheme = {@link ContentResolver#SCHEME_ANDROID_RESOURCE} for 108 * a full resource name. This name is a single string of the form "package:type/entry". 109 */ getAndroidResourceUri(String resourceName)110 public static Uri getAndroidResourceUri(String resourceName) { 111 Uri uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + SCHEME_DELIMITER 112 + resourceName.replace(URI_PACKAGE_DELIMITER, URI_PATH_DELIMITER)); 113 return uri; 114 } 115 116 /** 117 * Checks if the URI refers to an Android resource. 118 */ isAndroidResourceUri(Uri uri)119 public static boolean isAndroidResourceUri(Uri uri) { 120 return ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme()); 121 } 122 123 /** 124 * Gets a URI with the account image scheme. 125 * @hide 126 */ getAccountImageUri(String accountName)127 public static Uri getAccountImageUri(String accountName) { 128 Uri uri = Uri.parse(SCHEME_ACCOUNT_IMAGE + SCHEME_DELIMITER + accountName); 129 return uri; 130 } 131 132 /** 133 * Gets a URI with the account image scheme, and specifying an URI to be 134 * used in notifyChange() when the image pointed to by the returned URI is 135 * updated. 136 * @hide 137 */ getAccountImageUri(String accountName, Uri changeNotifyUri)138 public static Uri getAccountImageUri(String accountName, Uri changeNotifyUri) { 139 Uri uri = Uri.parse(SCHEME_ACCOUNT_IMAGE + SCHEME_DELIMITER + accountName); 140 if (changeNotifyUri != null) { 141 uri = uri.buildUpon().appendQueryParameter(ACCOUNT_IMAGE_CHANGE_NOTIFY_URI, 142 changeNotifyUri.toString()).build(); 143 } 144 return uri; 145 } 146 147 /** 148 * Checks if the URI refers to an account image. 149 * @hide 150 */ isAccountImageUri(Uri uri)151 public static boolean isAccountImageUri(Uri uri) { 152 return uri == null ? false : SCHEME_ACCOUNT_IMAGE.equals(uri.getScheme()); 153 } 154 155 /** 156 * @hide 157 */ getAccountName(Uri uri)158 public static String getAccountName(Uri uri) { 159 if (isAccountImageUri(uri)) { 160 String accountName = uri.getAuthority() + uri.getPath(); 161 return accountName; 162 } else { 163 throw new IllegalArgumentException("Invalid account image URI. " + uri); 164 } 165 } 166 167 /** 168 * @hide 169 */ getAccountImageChangeNotifyUri(Uri uri)170 public static Uri getAccountImageChangeNotifyUri(Uri uri) { 171 if (isAccountImageUri(uri)) { 172 String notifyUri = uri.getQueryParameter(ACCOUNT_IMAGE_CHANGE_NOTIFY_URI); 173 if (notifyUri == null) { 174 return null; 175 } else { 176 return Uri.parse(notifyUri); 177 } 178 } else { 179 throw new IllegalArgumentException("Invalid account image URI. " + uri); 180 } 181 } 182 183 /** 184 * Finds the packageName of the application to which the content authority of the given uri 185 * belongs to. 186 */ 187 @Nullable getPackageName(Context context, Uri uri)188 public static String getPackageName(Context context, Uri uri) { 189 PackageManager pm = context.getPackageManager(); 190 ProviderInfo info = pm.resolveContentProvider(uri.getAuthority(), MATCH_ALL); 191 // Info can be null when the app doesn't define a provider. 192 return (info != null) ? info.packageName : uri.getAuthority(); 193 } 194 195 /** 196 * Returns {@code true} if the URI refers to a content URI which can be opened via 197 * {@link ContentResolver#openInputStream(Uri)}. 198 */ isContentUri(Uri uri)199 public static boolean isContentUri(Uri uri) { 200 return ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) || 201 ContentResolver.SCHEME_FILE.equals(uri.getScheme()); 202 } 203 204 /** 205 * Checks if the URI refers to an shortcut icon resource. 206 */ isShortcutIconResourceUri(Uri uri)207 public static boolean isShortcutIconResourceUri(Uri uri) { 208 return SCHEME_SHORTCUT_ICON_RESOURCE.equals(uri.getScheme()); 209 } 210 211 /** 212 * Creates a shortcut icon resource object from an Android resource URI. 213 */ getIconResource(Context context, Uri uri)214 public static ShortcutIconResource getIconResource(Context context, Uri uri) { 215 if(isAndroidResourceUri(uri)) { 216 ShortcutIconResource iconResource = new ShortcutIconResource(); 217 iconResource.packageName = getPackageName(context, uri); 218 // Trim off the scheme + 3 extra for "://" + authority, then replace the first "/" 219 // with a ":" and add to packageName. 220 int resStart = ContentResolver.SCHEME_ANDROID_RESOURCE.length() 221 + SCHEME_DELIMITER.length() + uri.getAuthority().length(); 222 iconResource.resourceName = iconResource.packageName 223 + uri.toString().substring(resStart) 224 .replaceFirst(URI_PATH_DELIMITER, URI_PACKAGE_DELIMITER); 225 return iconResource; 226 } else if(isShortcutIconResourceUri(uri)) { 227 ShortcutIconResource iconResource = new ShortcutIconResource(); 228 iconResource.packageName = getPackageName(context, uri); 229 iconResource.resourceName = uri.toString().substring( 230 SCHEME_SHORTCUT_ICON_RESOURCE.length() + SCHEME_DELIMITER.length() 231 + uri.getAuthority().length() + URI_PATH_DELIMITER.length()) 232 .replaceFirst(URI_PATH_DELIMITER, URI_PACKAGE_DELIMITER); 233 return iconResource; 234 } else { 235 throw new IllegalArgumentException("Invalid resource URI. " + uri); 236 } 237 } 238 239 /** 240 * Returns {@code true} if this is a web URI. 241 */ isWebUri(Uri resourceUri)242 public static boolean isWebUri(Uri resourceUri) { 243 String scheme = resourceUri.getScheme() == null ? null 244 : resourceUri.getScheme().toLowerCase(); 245 return HTTP_PREFIX.equals(scheme) || HTTPS_PREFIX.equals(scheme); 246 } 247 248 /** 249 * Build a Uri for canvas details subactions dialog given content uri and optional parameters. 250 * @param uri the subactions ContentUri 251 * @param dialogTitle the custom subactions dialog title. If the value is null, canvas will 252 * fall back to use previous action's name as the subactions dialog title. 253 * @param dialogDescription the custom subactions dialog description. If the value is null, 254 * canvas will fall back to use previous action's subname as the subactions dialog 255 * description. 256 * @hide 257 */ getSubactionDialogUri(Uri uri, String dialogTitle, String dialogDescription)258 public static Uri getSubactionDialogUri(Uri uri, String dialogTitle, String dialogDescription) { 259 return getSubactionDialogUri(uri, dialogTitle, dialogDescription, null, -1); 260 } 261 262 /** 263 * Build a Uri for canvas details subactions dialog given content uri and optional parameters. 264 * @param uri the subactions ContentUri 265 * @param dialogTitle the custom subactions dialog title. If the value is null, canvas will 266 * fall back to use previous action's name as the subactions dialog title. 267 * @param dialogDescription the custom subactions dialog description. If the value is null, 268 * canvas will fall back to use previous action's subname as the subactions dialog 269 * description. 270 * @param startIndex the focused action in actions list when started. 271 * @hide 272 */ getSubactionDialogUri(Uri uri, String dialogTitle, String dialogDescription, int startIndex)273 public static Uri getSubactionDialogUri(Uri uri, String dialogTitle, String dialogDescription, 274 int startIndex) { 275 return getSubactionDialogUri(uri, dialogTitle, dialogDescription, null, startIndex); 276 } 277 278 /** 279 * Build a Uri for canvas details subactions dialog given content uri and optional parameters. 280 * @param uri the subactions ContentUri 281 * @param dialogTitle the custom subactions dialog title. If the value is null, canvas will 282 * fall back to use previous action's name as the subactions dialog title. 283 * @param dialogDescription the custom subactions dialog description. If the value is null, 284 * canvas will fall back to use previous action's subname as the subactions dialog 285 * description. 286 * @param startName the name of action that is focused in actions list when started. 287 * @hide 288 */ getSubactionDialogUri(Uri uri, String dialogTitle, String dialogDescription, String startName)289 public static Uri getSubactionDialogUri(Uri uri, String dialogTitle, String dialogDescription, 290 String startName) { 291 return getSubactionDialogUri(uri, dialogTitle, dialogDescription, startName, -1); 292 } 293 294 /** 295 * Build a Uri for canvas details subactions dialog given content uri and optional parameters. 296 * @param uri the subactions ContentUri 297 * @param dialogTitle the custom subactions dialog title. If the value is null, canvas will 298 * fall back to use previous action's name as the subactions dialog title. 299 * @param dialogDescription the custom subactions dialog description. If the value is null, 300 * canvas will fall back to use previous action's subname as the subactions dialog 301 * description. 302 * @param startIndex the focused action in actions list when started. 303 * @param startName the name of action that is focused in actions list when started. startName 304 * takes priority over start index. 305 * @hide 306 */ getSubactionDialogUri(Uri uri, String dialogTitle, String dialogDescription, String startName, int startIndex)307 public static Uri getSubactionDialogUri(Uri uri, String dialogTitle, String dialogDescription, 308 String startName, int startIndex) { 309 if (uri == null || !isContentUri(uri)) { 310 // If given uri is null, or it is not of contentUri type, return null. 311 return null; 312 } 313 314 Uri.Builder builder = uri.buildUpon(); 315 if (!TextUtils.isEmpty(dialogTitle)) { 316 builder.appendQueryParameter(DETAIL_DIALOG_URI_DIALOG_TITLE, dialogTitle); 317 } 318 319 if (!TextUtils.isEmpty(DETAIL_DIALOG_URI_DIALOG_DESCRIPTION)) { 320 builder.appendQueryParameter(DETAIL_DIALOG_URI_DIALOG_DESCRIPTION, dialogDescription); 321 } 322 323 if (startIndex != -1) { 324 builder.appendQueryParameter(DETAIL_DIALOG_URI_DIALOG_ACTION_START_INDEX, 325 Integer.toString(startIndex)); 326 } 327 328 if (!TextUtils.isEmpty(startName)) { 329 builder.appendQueryParameter(DETAIL_DIALOG_URI_DIALOG_ACTION_START_NAME, startName); 330 } 331 332 return builder.build(); 333 } 334 335 /** 336 * Get subaction dialog title parameter from URI 337 * @param uri ContentUri for canvas details subactions 338 * @return custom dialog title if this parameter is available in URI. Otherwise, return null. 339 * @hide 340 */ getSubactionDialogTitle(Uri uri)341 public static String getSubactionDialogTitle(Uri uri) { 342 if (uri == null || !isContentUri(uri)) { 343 return null; 344 } 345 346 return uri.getQueryParameter(DETAIL_DIALOG_URI_DIALOG_TITLE); 347 } 348 349 /** 350 * Get subaction dialog description parameter from URI 351 * @param uri ContentUri for canvas details subactions 352 * @return custom dialog description if this parameter is available in URI. 353 * Otherwise, return null. 354 * @hide 355 */ getSubactionDialogDescription(Uri uri)356 public static String getSubactionDialogDescription(Uri uri) { 357 if (uri == null || !isContentUri(uri)) { 358 return null; 359 } 360 361 return uri.getQueryParameter(DETAIL_DIALOG_URI_DIALOG_DESCRIPTION); 362 } 363 364 /** 365 * Get subaction dialog action list focused index when started from URI 366 * @param uri ContentUri for canvas details subactions 367 * @return action starting index if this parameter is available in URI. Otherwise, return -1. 368 * @hide 369 */ getSubactionDialogActionStartIndex(Uri uri)370 public static int getSubactionDialogActionStartIndex(Uri uri) { 371 if (uri == null || !isContentUri(uri)) { 372 return -1; 373 } 374 375 String startIndexStr = uri.getQueryParameter(DETAIL_DIALOG_URI_DIALOG_ACTION_START_INDEX); 376 if (!TextUtils.isEmpty(startIndexStr) && TextUtils.isDigitsOnly(startIndexStr)) { 377 return Integer.parseInt(startIndexStr); 378 } else { 379 return -1; 380 } 381 } 382 383 /** 384 * Get subaction dialog action list focused action name when started from URI 385 * @param uri ContentUri for canvas details subactions 386 * @return that name of starting action if this parameter is available in URI. 387 * Otherwise, return null. 388 * @hide 389 */ getSubactionDialogActionStartName(Uri uri)390 public static String getSubactionDialogActionStartName(Uri uri) { 391 if (uri == null || !isContentUri(uri)) { 392 return null; 393 } 394 395 return uri.getQueryParameter(DETAIL_DIALOG_URI_DIALOG_ACTION_START_NAME); 396 } 397 } 398