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