• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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