1 /* 2 * Copyright (C) 2021 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 17 package com.android.providers.media; 18 19 import static android.content.pm.PackageManager.PERMISSION_GRANTED; 20 import static android.os.Process.SYSTEM_UID; 21 import static android.provider.MediaStore.EXTRA_CALLING_PACKAGE_UID; 22 23 import static com.android.providers.media.AccessChecker.isRedactionNeededForPickerUri; 24 import static com.android.providers.media.LocalUriMatcher.PICKER_GET_CONTENT_ID; 25 import static com.android.providers.media.LocalUriMatcher.PICKER_ID; 26 import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_ALBUMS_ALL; 27 import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_ALBUMS_LOCAL; 28 import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_MEDIA_ALL; 29 import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_MEDIA_LOCAL; 30 import static com.android.providers.media.LocalUriMatcher.PICKER_TRANSCODED_ID; 31 import static com.android.providers.media.photopicker.PickerDataLayer.QUERY_CLOUD_ID_SELECTION; 32 import static com.android.providers.media.photopicker.PickerDataLayer.QUERY_ID_SELECTION; 33 import static com.android.providers.media.photopicker.PickerDataLayer.QUERY_LOCAL_ID_SELECTION; 34 import static com.android.providers.media.photopicker.PickerDataLayer.QUERY_SHOULD_SCREEN_SELECTION_URIS; 35 import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString; 36 import static com.android.providers.media.util.FileUtils.toFuseFile; 37 38 import android.content.ContentResolver; 39 import android.content.Context; 40 import android.content.Intent; 41 import android.content.pm.PackageManager.NameNotFoundException; 42 import android.content.res.AssetFileDescriptor; 43 import android.database.Cursor; 44 import android.database.MatrixCursor; 45 import android.net.Uri; 46 import android.os.Binder; 47 import android.os.Bundle; 48 import android.os.CancellationSignal; 49 import android.os.ParcelFileDescriptor; 50 import android.os.Process; 51 import android.os.UserHandle; 52 import android.provider.CloudMediaProviderContract; 53 import android.provider.MediaStore; 54 import android.util.Log; 55 56 import androidx.annotation.NonNull; 57 import androidx.annotation.Nullable; 58 import androidx.annotation.VisibleForTesting; 59 60 import com.android.modules.utils.build.SdkLevel; 61 import com.android.providers.media.photopicker.PickerDataLayer; 62 import com.android.providers.media.photopicker.data.PickerDbFacade; 63 import com.android.providers.media.photopicker.data.model.UserId; 64 import com.android.providers.media.photopicker.metrics.NonUiEventLogger; 65 import com.android.providers.media.util.PermissionUtils; 66 67 import java.io.File; 68 import java.io.FileNotFoundException; 69 import java.util.ArrayList; 70 import java.util.HashSet; 71 import java.util.List; 72 import java.util.Set; 73 import java.util.stream.Collectors; 74 75 /** 76 * Utility class for Picker Uris, it handles (includes permission checks, incoming args 77 * validations etc) and redirects picker URIs to the correct resolver. 78 */ 79 public class PickerUriResolver { 80 private static final String TAG = "PickerUriResolver"; 81 82 public static final String PICKER_SEGMENT = "picker"; 83 84 public static final String PICKER_GET_CONTENT_SEGMENT = "picker_get_content"; 85 private static final String PICKER_INTERNAL_SEGMENT = "picker_internal"; 86 public static final String PICKER_TRANSCODED_SEGMENT = "picker_transcoded"; 87 /** A uri with prefix "content://media/picker" is considered as a picker uri */ 88 public static final Uri PICKER_URI = MediaStore.AUTHORITY_URI.buildUpon(). 89 appendPath(PICKER_SEGMENT).build(); 90 /** 91 * Internal picker URI with prefix "content://media/picker_internal" to retrieve merged 92 * and deduped cloud and local items. 93 */ 94 public static final Uri PICKER_INTERNAL_URI = MediaStore.AUTHORITY_URI.buildUpon(). 95 appendPath(PICKER_INTERNAL_SEGMENT).build(); 96 97 public static final String REFRESH_PICKER_UI_PATH = "refresh_ui"; 98 public static final Uri REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI = 99 PICKER_INTERNAL_URI.buildUpon().appendPath(REFRESH_PICKER_UI_PATH).build(); 100 public static final String INIT_PATH = "init"; 101 102 public static final String MEDIA_PATH = "media"; 103 public static final String ALBUM_PATH = "albums"; 104 105 public static final String LOCAL_PATH = "local"; 106 public static final String ALL_PATH = "all"; 107 public static final List<Integer> PICKER_INTERNAL_TABLES = List.of( 108 PICKER_INTERNAL_MEDIA_ALL, 109 PICKER_INTERNAL_MEDIA_LOCAL, 110 PICKER_INTERNAL_ALBUMS_ALL, 111 PICKER_INTERNAL_ALBUMS_LOCAL); 112 // use this uid for when the uid is eventually going to be ignored or a test for invalid uid. 113 public static final Integer DEFAULT_UID = -1; 114 115 private final Context mContext; 116 private final PickerDbFacade mDbFacade; 117 private final Set<String> mAllValidProjectionColumns; 118 private final String[] mAllValidProjectionColumnsArray; 119 private final LocalUriMatcher mLocalUriMatcher; 120 PickerUriResolver(Context context, PickerDbFacade dbFacade, ProjectionHelper projectionHelper, LocalUriMatcher localUriMatcher)121 PickerUriResolver(Context context, PickerDbFacade dbFacade, ProjectionHelper projectionHelper, 122 LocalUriMatcher localUriMatcher) { 123 mContext = context; 124 mDbFacade = dbFacade; 125 mAllValidProjectionColumns = projectionHelper.getProjectionMap( 126 MediaStore.PickerMediaColumns.class).keySet(); 127 mAllValidProjectionColumnsArray = mAllValidProjectionColumns.toArray(new String[0]); 128 mLocalUriMatcher = localUriMatcher; 129 } 130 openFile(Uri uri, String mode, CancellationSignal signal, LocalCallingIdentity localCallingIdentity)131 public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal, 132 LocalCallingIdentity localCallingIdentity) 133 throws FileNotFoundException { 134 if (ParcelFileDescriptor.parseMode(mode) != ParcelFileDescriptor.MODE_READ_ONLY) { 135 throw new SecurityException("PhotoPicker Uris can only be accessed to read." 136 + " Uri: " + uri); 137 } 138 139 checkPermissionForRequireOriginalQueryParam(uri, localCallingIdentity); 140 checkUriPermission(uri, localCallingIdentity.pid, localCallingIdentity.uid); 141 142 final ContentResolver resolver; 143 try { 144 resolver = getContentResolverForUserId(uri); 145 } catch (IllegalStateException e) { 146 // This is to be consistent with MediaProvider's response when a file is not found. 147 Log.e(TAG, "No item at " + uri, e); 148 throw new FileNotFoundException("No item at " + uri); 149 } 150 if (canHandleUriInUser(uri)) { 151 return openPickerFile(uri); 152 } 153 return resolver.openFile(uri, mode, signal); 154 } 155 openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal, LocalCallingIdentity localCallingIdentity, boolean wantsThumb)156 public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts, 157 CancellationSignal signal, LocalCallingIdentity localCallingIdentity, 158 boolean wantsThumb) 159 throws FileNotFoundException { 160 checkPermissionForRequireOriginalQueryParam(uri, localCallingIdentity); 161 checkUriPermission(uri, localCallingIdentity.pid, localCallingIdentity.uid); 162 163 final ContentResolver resolver; 164 try { 165 resolver = getContentResolverForUserId(uri); 166 } catch (IllegalStateException e) { 167 // This is to be consistent with MediaProvider's response when a file is not found. 168 Log.e(TAG, "No item at " + uri, e); 169 throw new FileNotFoundException("No item at " + uri); 170 } 171 172 if (wantsThumb) { 173 Log.d(TAG, "Thumbnail is requested for " + uri); 174 // If thumbnail is requested, forward the thumbnail request to the provider 175 // rather than requesting the full media file 176 return openThumbnailFromProvider(resolver, uri, mimeTypeFilter, opts, signal); 177 } 178 179 if (canHandleUriInUser(uri)) { 180 return new AssetFileDescriptor(openPickerFile(uri), 0, 181 AssetFileDescriptor.UNKNOWN_LENGTH); 182 } 183 return resolver.openTypedAssetFile(uri, mimeTypeFilter, opts, signal); 184 } 185 186 /** 187 * Returns result of the query operations that can be performed on the internal picker tables 188 * as a cursor. 189 * 190 * <p>This also caters to the filtering of queryArgs parameter for id selection if required for 191 * pre-selection. 192 */ query(Integer table, Bundle queryArgs, String localProvider, String cloudProvider, PickerDataLayer pickerDataLayer)193 public Cursor query(Integer table, Bundle queryArgs, String localProvider, 194 String cloudProvider, PickerDataLayer pickerDataLayer) { 195 Bundle screenedQueryArgs; 196 if (table == PICKER_INTERNAL_MEDIA_ALL || table == PICKER_INTERNAL_MEDIA_LOCAL) { 197 screenedQueryArgs = processUrisForSelection(queryArgs, 198 localProvider, 199 cloudProvider, 200 /* isLocalOnly */ table == PICKER_INTERNAL_MEDIA_LOCAL); 201 if (table == PICKER_INTERNAL_MEDIA_ALL) { 202 return pickerDataLayer.fetchAllMedia(screenedQueryArgs); 203 } else if (table == PICKER_INTERNAL_MEDIA_LOCAL) { 204 return pickerDataLayer.fetchLocalMedia(screenedQueryArgs); 205 } 206 } 207 if (table == PICKER_INTERNAL_ALBUMS_ALL) { 208 return pickerDataLayer.fetchAllAlbums(queryArgs); 209 } else if (table == PICKER_INTERNAL_ALBUMS_LOCAL) { 210 return pickerDataLayer.fetchLocalAlbums(queryArgs); 211 } 212 return null; 213 } 214 query(Uri uri, String[] projection, int callingPid, int callingUid, String callingPackageName)215 public Cursor query(Uri uri, String[] projection, int callingPid, int callingUid, 216 String callingPackageName) { 217 checkUriPermission(uri, callingPid, callingUid); 218 try { 219 logUnknownProjectionColumns(projection, callingUid, callingPackageName); 220 return queryInternal(uri, projection); 221 } catch (IllegalStateException e) { 222 // This is to be consistent with MediaProvider, it returns an empty cursor if the row 223 // does not exist. 224 Log.e(TAG, "File not found for uri: " + uri, e); 225 return new MatrixCursor(projection == null ? new String[] {} : projection); 226 } 227 } 228 queryInternal(Uri uri, String[] projection)229 private Cursor queryInternal(Uri uri, String[] projection) { 230 final ContentResolver resolver = getContentResolverForUserId(uri); 231 232 if (canHandleUriInUser(uri)) { 233 if (projection == null || projection.length == 0) { 234 projection = mAllValidProjectionColumnsArray; 235 } 236 237 return queryPickerUri(uri, projection); 238 } 239 return resolver.query(uri, projection, /* queryArgs */ null, 240 /* cancellationSignal */ null); 241 } 242 243 /** 244 * getType for Picker Uris 245 */ getType(@onNull Uri uri, int callingPid, int callingUid)246 public String getType(@NonNull Uri uri, int callingPid, int callingUid) { 247 // TODO (b/272265676): Remove system uid check if found unnecessary 248 if (SdkLevel.isAtLeastU() && UserHandle.getAppId(callingUid) != SYSTEM_UID) { 249 // Starting Android 14, there is permission check for getting types requiring query. 250 // System Uid (1000) is allowed to get the types. 251 checkUriPermission(uri, callingPid, callingUid); 252 } 253 254 try (Cursor cursor = queryInternal(uri, new String[]{MediaStore.MediaColumns.MIME_TYPE})) { 255 if (cursor != null && cursor.getCount() == 1 && cursor.moveToFirst()) { 256 return getCursorString(cursor, 257 CloudMediaProviderContract.MediaColumns.MIME_TYPE); 258 } 259 } 260 261 throw new IllegalArgumentException("Failed to getType for uri: " + uri); 262 } 263 getMediaUri(String authority)264 public static Uri getMediaUri(String authority) { 265 return Uri.parse("content://" + authority + "/" 266 + CloudMediaProviderContract.URI_PATH_MEDIA); 267 } 268 getDeletedMediaUri(String authority)269 public static Uri getDeletedMediaUri(String authority) { 270 return Uri.parse("content://" + authority + "/" 271 + CloudMediaProviderContract.URI_PATH_DELETED_MEDIA); 272 } 273 getMediaCollectionInfoUri(String authority)274 public static Uri getMediaCollectionInfoUri(String authority) { 275 return Uri.parse("content://" + authority + "/" 276 + CloudMediaProviderContract.URI_PATH_MEDIA_COLLECTION_INFO); 277 } 278 getAlbumUri(String authority)279 public static Uri getAlbumUri(String authority) { 280 return Uri.parse("content://" + authority + "/" 281 + CloudMediaProviderContract.URI_PATH_ALBUM); 282 } 283 createSurfaceControllerUri(String authority)284 public static Uri createSurfaceControllerUri(String authority) { 285 return Uri.parse("content://" + authority + "/" 286 + CloudMediaProviderContract.URI_PATH_SURFACE_CONTROLLER); 287 } 288 openPickerFile(Uri uri)289 private ParcelFileDescriptor openPickerFile(Uri uri) 290 throws FileNotFoundException { 291 final File file = getPickerFileFromUri(uri); 292 if (file == null) { 293 throw new FileNotFoundException("File not found for uri: " + uri); 294 } 295 return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); 296 } 297 298 @VisibleForTesting getPickerFileFromUri(Uri uri)299 File getPickerFileFromUri(Uri uri) { 300 final String[] projection = new String[] { MediaStore.PickerMediaColumns.DATA }; 301 try (Cursor cursor = queryPickerUri(uri, projection)) { 302 if (cursor != null && cursor.getCount() == 1 && cursor.moveToFirst()) { 303 String path = getCursorString(cursor, MediaStore.PickerMediaColumns.DATA); 304 // First replace /sdcard with /storage/emulated path 305 path = path.replaceFirst("/sdcard", "/storage/emulated/" + MediaStore.MY_USER_ID); 306 // Then convert /storage/emulated patht to /mnt/user/ path 307 return toFuseFile(new File(path)); 308 } 309 } 310 return null; 311 } 312 313 @VisibleForTesting queryPickerUri(Uri uri, String[] projection)314 Cursor queryPickerUri(Uri uri, String[] projection) { 315 String pickerSegmentType = getPickerSegmentType(uri); 316 uri = unwrapProviderUri(uri); 317 return mDbFacade.queryMediaIdForApps(pickerSegmentType, uri.getHost(), 318 uri.getLastPathSegment(), projection); 319 } 320 getPickerSegmentType(Uri uri)321 private String getPickerSegmentType(Uri uri) { 322 switch (mLocalUriMatcher.matchUri(uri, /* allowHidden */ false)) { 323 case PICKER_ID: 324 return PICKER_SEGMENT; 325 case PICKER_GET_CONTENT_ID: 326 return PICKER_GET_CONTENT_SEGMENT; 327 case PICKER_TRANSCODED_ID: 328 return PICKER_TRANSCODED_SEGMENT; 329 } 330 331 return null; 332 } 333 334 /** 335 * @param intentAction The intent action associated with the Picker session. Note that the 336 * intent action could be null in case of embedded picker. 337 * @return The Picker URI path segment. 338 */ getPickerSegmentFromIntentAction(@ullable String intentAction)339 public static String getPickerSegmentFromIntentAction(@Nullable String intentAction) { 340 if (intentAction != null && intentAction.equals(Intent.ACTION_GET_CONTENT)) { 341 return PICKER_GET_CONTENT_SEGMENT; 342 } 343 return PICKER_SEGMENT; 344 } 345 346 /** 347 * Creates a picker uri incorporating authority, user id and cloud provider. 348 */ wrapProviderUri(Uri uri, String action, int userId)349 public static Uri wrapProviderUri(Uri uri, String action, int userId) { 350 final List<String> segments = uri.getPathSegments(); 351 if (segments.size() != 2) { 352 throw new IllegalArgumentException("Unexpected provider URI: " + uri); 353 } 354 355 Uri.Builder builder = initializeUriBuilder(MediaStore.AUTHORITY); 356 builder.appendPath(getPickerSegmentFromIntentAction(action)); 357 builder.appendPath(String.valueOf(userId)); 358 builder.appendPath(uri.getHost()); 359 360 for (int i = 0; i < segments.size(); i++) { 361 builder.appendPath(segments.get(i)); 362 } 363 364 return builder.build(); 365 } 366 367 /** 368 * Filters URIs received for preSelection based on permission, authority and validity checks. 369 */ processUrisForSelection(Bundle queryArgs, String localProvider, String cloudProvider, boolean isLocalOnly)370 public Bundle processUrisForSelection(Bundle queryArgs, String localProvider, 371 String cloudProvider, boolean isLocalOnly) { 372 373 List<String> inputUrisAsStrings = queryArgs.getStringArrayList(QUERY_ID_SELECTION); 374 if (inputUrisAsStrings == null) { 375 // If no input selection is present then return; 376 return queryArgs; 377 } 378 379 boolean shouldScreenSelectionUris = queryArgs.getBoolean( 380 QUERY_SHOULD_SCREEN_SELECTION_URIS); 381 382 if (shouldScreenSelectionUris) { 383 Set<Uri> inputUris = screenArgsForPermissionCheckIfAny(queryArgs, inputUrisAsStrings); 384 385 SelectionIdsSegregationResult result = populateLocalAndCloudIdListsForSelection( 386 inputUris, localProvider, cloudProvider, isLocalOnly); 387 if (!result.getLocalIds().isEmpty()) { 388 queryArgs.putStringArrayList(QUERY_LOCAL_ID_SELECTION, result.getLocalIds()); 389 } 390 if (!result.getCloudIds().isEmpty()) { 391 queryArgs.putStringArrayList(QUERY_CLOUD_ID_SELECTION, result.getCloudIds()); 392 } 393 if (!result.getCloudIds().isEmpty() || !result.getLocalIds().isEmpty()) { 394 Log.d(TAG, "Id selection has been enabled in the current query operation."); 395 } else { 396 Log.d(TAG, "Id selection has not been enabled in the current query operation."); 397 } 398 } else if (isLocalOnly) { 399 Set<Uri> inputUris = inputUrisAsStrings.stream().map(Uri::parse).collect( 400 Collectors.toSet()); 401 402 Log.d(TAG, "Local id selection has been enabled in the current query operation."); 403 queryArgs.putStringArrayList(QUERY_LOCAL_ID_SELECTION, 404 new ArrayList<>(inputUris.stream().map(Uri::getLastPathSegment) 405 .collect(Collectors.toList()))); 406 } else { 407 Log.wtf(TAG, "Expected the uris to be local uris when screening is disabled"); 408 } 409 410 return queryArgs; 411 } 412 screenArgsForPermissionCheckIfAny(Bundle queryArgs, List<String> inputUris)413 private Set<Uri> screenArgsForPermissionCheckIfAny(Bundle queryArgs, List<String> inputUris) { 414 int callingUid = queryArgs.getInt(EXTRA_CALLING_PACKAGE_UID); 415 416 if (/* uid not found */ callingUid == 0 || /* uid is invalid */ callingUid == DEFAULT_UID) { 417 // if calling uid is absent or is invalid then throw an error 418 throw new IllegalArgumentException("Filtering Uris for Selection: " 419 + "Uid absent or invalid"); 420 } 421 422 Set<Uri> accessibleUris = new HashSet<>(); 423 // perform checks and filtration. 424 for (String uriAsString : inputUris) { 425 Uri uriForSelection = Uri.parse(uriAsString); 426 try { 427 // verify if the calling package have permission to the requested uri. 428 checkUriPermission(uriForSelection, /* pid */ -1, callingUid); 429 accessibleUris.add(uriForSelection); 430 } catch (SecurityException se) { 431 Log.d(TAG, 432 "Filtering Uris for Selection: package does not have permission for " 433 + "the uri: " 434 + uriAsString); 435 } 436 } 437 return accessibleUris; 438 } 439 populateLocalAndCloudIdListsForSelection( Set<Uri> inputUris, String localProvider, String cloudProvider, boolean isLocalOnly)440 private SelectionIdsSegregationResult populateLocalAndCloudIdListsForSelection( 441 Set<Uri> inputUris, String localProvider, 442 String cloudProvider, boolean isLocalOnly) { 443 ArrayList<String> localIds = new ArrayList<>(); 444 ArrayList<String> cloudIds = new ArrayList<>(); 445 for (Uri uriForSelection : inputUris) { 446 try { 447 // unwrap picker uri to get host and id. 448 Uri uri = PickerUriResolver.unwrapProviderUri(uriForSelection); 449 if (localProvider.equals(uri.getHost())) { 450 // Adds the last segment (id) to localIds if the authority matches the 451 // local authority. 452 localIds.add(uri.getLastPathSegment()); 453 } else if (!isLocalOnly && cloudProvider != null && cloudProvider.equals( 454 uri.getHost())) { 455 // Adds the last segment (id) to cloudIds if the authority matches the 456 // current cloud authority. 457 cloudIds.add(uri.getLastPathSegment()); 458 } else { 459 Log.d(TAG, 460 "Filtering Uris for Selection: Unknown authority/host for the uri: " 461 + uriForSelection); 462 } 463 } catch (IllegalArgumentException illegalArgumentException) { 464 Log.d(TAG, "Filtering Uris for Selection: Input uri: " + uriForSelection 465 + " is not valid."); 466 } 467 } 468 return new SelectionIdsSegregationResult(localIds, cloudIds); 469 } 470 471 private static class SelectionIdsSegregationResult { 472 private final ArrayList<String> mLocalIds; 473 private final ArrayList<String> mCloudIds; 474 SelectionIdsSegregationResult(ArrayList<String> localIds, ArrayList<String> cloudIds)475 SelectionIdsSegregationResult(ArrayList<String> localIds, ArrayList<String> cloudIds) { 476 mLocalIds = localIds; 477 mCloudIds = cloudIds; 478 } 479 getLocalIds()480 public ArrayList<String> getLocalIds() { 481 return mLocalIds; 482 } 483 getCloudIds()484 public ArrayList<String> getCloudIds() { 485 return mCloudIds; 486 } 487 } 488 489 /** 490 * Unwraps picker uri for processing host and id. 491 */ unwrapProviderUri(Uri uri)492 public static Uri unwrapProviderUri(Uri uri) { 493 return unwrapProviderUri(uri, true); 494 } 495 unwrapProviderUri(Uri uri, boolean addUserId)496 private static Uri unwrapProviderUri(Uri uri, boolean addUserId) { 497 List<String> segments = uri.getPathSegments(); 498 if (segments.size() != 5) { 499 throw new IllegalArgumentException("Unexpected picker provider URI: " + uri); 500 } 501 502 // segments.get(0) == 'picker' 503 final String userId = segments.get(1); 504 final String host = segments.get(2); 505 segments = segments.subList(3, segments.size()); 506 507 Uri.Builder builder = initializeUriBuilder(addUserId ? (userId + "@" + host) : host); 508 509 for (int i = 0; i < segments.size(); i++) { 510 builder.appendPath(segments.get(i)); 511 } 512 return builder.build(); 513 } 514 initializeUriBuilder(String authority)515 private static Uri.Builder initializeUriBuilder(String authority) { 516 final Uri.Builder builder = Uri.EMPTY.buildUpon(); 517 builder.scheme("content"); 518 builder.encodedAuthority(authority); 519 520 return builder; 521 } 522 523 /** 524 * Gets the user id of the picker uri. 525 * 526 * @param uri The picker URI. 527 */ getUserId(Uri uri)528 static int getUserId(Uri uri) { 529 // content://media/picker/<user-id>/<media-id>/... 530 return Integer.parseInt(uri.getPathSegments().get(1)); 531 } 532 533 /** 534 * Checks if the package represented by input uid and pid have access to the uri. 535 */ checkUriPermission(Uri uri, int pid, int uid)536 public void checkUriPermission(Uri uri, int pid, int uid) { 537 checkUriPermission(mContext, uri, pid, uid); 538 } 539 540 /** 541 * Checks if the package represented by input uid and pid have access to the uri. 542 */ checkUriPermission(Context context, Uri uri, int pid, int uid)543 public static void checkUriPermission(Context context, Uri uri, int pid, int uid) { 544 // Clear query parameters to check for URI permissions, apps can add requireOriginal 545 // query parameter to URI, URI grants will not be present in that case. 546 Uri uriWithoutQueryParams = uri.buildUpon().clearQuery().build(); 547 if (!isSelf(uid) 548 && !PermissionUtils.checkManageCloudMediaProvidersPermission(context, pid, uid) 549 && context.checkUriPermission(uriWithoutQueryParams, pid, uid, 550 Intent.FLAG_GRANT_READ_URI_PERMISSION) != PERMISSION_GRANTED) { 551 throw new SecurityException("Calling uid ( " + uid + " ) does not have permission to " + 552 "access picker uri: " + uriWithoutQueryParams); 553 } 554 } 555 556 /** 557 * Checks if the caller has the required permission to require original for the picker URI. 558 */ checkPermissionForRequireOriginalQueryParam(Uri uri, LocalCallingIdentity localCallingIdentity)559 public void checkPermissionForRequireOriginalQueryParam(Uri uri, 560 LocalCallingIdentity localCallingIdentity) { 561 String value = uri.getQueryParameter(MediaStore.PARAM_REQUIRE_ORIGINAL); 562 if (value == null || value.isEmpty()) { 563 return; 564 } 565 566 // Check if requireOriginal is set 567 if (Integer.parseInt(value) == 1) { 568 if (mLocalUriMatcher.matchUri(uri, /* allowHidden */ false) == PICKER_ID) { 569 throw new UnsupportedOperationException( 570 "Require Original is not supported for Picker URI " + uri); 571 } 572 573 if (mLocalUriMatcher.matchUri(uri, /* allowHidden */ false) == PICKER_GET_CONTENT_ID 574 && isRedactionNeededForPickerUri(localCallingIdentity)) { 575 throw new UnsupportedOperationException("Calling uid ( " + Binder.getCallingUid() 576 + " ) does not have ACCESS_MEDIA_LOCATION permission for requesting " 577 + "original file"); 578 } 579 } 580 } 581 isSelf(int uid)582 private static boolean isSelf(int uid) { 583 return UserHandle.getAppId(Process.myUid()) == UserHandle.getAppId(uid); 584 } 585 canHandleUriInUser(Uri uri)586 private boolean canHandleUriInUser(Uri uri) { 587 // If MPs user_id matches the URIs user_id, we can handle this URI in this MP user, 588 // otherwise, we'd have to re-route to MP matching URI user_id 589 return getUserId(uri) == mContext.getUser().getIdentifier(); 590 } 591 logUnknownProjectionColumns(String[] projection, int callingUid, String callingPackageName)592 private void logUnknownProjectionColumns(String[] projection, int callingUid, 593 String callingPackageName) { 594 if (projection == null || callingPackageName.equals(mContext.getPackageName())) { 595 return; 596 } 597 598 for (String column : projection) { 599 if (!mAllValidProjectionColumns.contains(column)) { 600 final String callingPackageAndColumn = callingPackageName + ":" + column; 601 NonUiEventLogger.logPickerQueriedWithUnknownColumn( 602 callingUid, callingPackageAndColumn); 603 } 604 } 605 } 606 openThumbnailFromProvider(ContentResolver resolver, Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)607 private AssetFileDescriptor openThumbnailFromProvider(ContentResolver resolver, Uri uri, 608 String mimeTypeFilter, Bundle opts, 609 CancellationSignal signal) throws FileNotFoundException { 610 Bundle newOpts = opts == null ? new Bundle() : (Bundle) opts.clone(); 611 newOpts.putBoolean(CloudMediaProviderContract.EXTRA_PREVIEW_THUMBNAIL, true); 612 newOpts.putBoolean(CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB, true); 613 614 final Uri unwrappedUri = unwrapProviderUri(uri, false); 615 final long callingIdentity = Binder.clearCallingIdentity(); 616 try { 617 return resolver.openTypedAssetFile(unwrappedUri, mimeTypeFilter, newOpts, signal); 618 } finally { 619 Binder.restoreCallingIdentity(callingIdentity); 620 } 621 } 622 623 @VisibleForTesting getContentResolverForUserId(Uri uri)624 ContentResolver getContentResolverForUserId(Uri uri) { 625 final UserId userId = UserId.of(UserHandle.of(getUserId(uri))); 626 try { 627 return userId.getContentResolver(mContext); 628 } catch (NameNotFoundException e) { 629 throw new IllegalStateException("Cannot find content resolver for uri: " + uri, e); 630 } 631 } 632 } 633