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