• 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 
22 import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString;
23 import static com.android.providers.media.util.FileUtils.toFuseFile;
24 
25 import android.content.ContentResolver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.pm.PackageManager.NameNotFoundException;
29 import android.content.res.AssetFileDescriptor;
30 import android.database.Cursor;
31 import android.database.MatrixCursor;
32 import android.net.Uri;
33 import android.os.Bundle;
34 import android.os.CancellationSignal;
35 import android.os.ParcelFileDescriptor;
36 import android.os.UserHandle;
37 import android.provider.CloudMediaProviderContract;
38 import android.provider.MediaStore;
39 import android.util.Log;
40 
41 import androidx.annotation.NonNull;
42 import androidx.annotation.VisibleForTesting;
43 
44 import com.android.modules.utils.build.SdkLevel;
45 import com.android.providers.media.photopicker.data.PickerDbFacade;
46 import com.android.providers.media.photopicker.data.model.UserId;
47 import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger;
48 
49 import java.io.File;
50 import java.io.FileNotFoundException;
51 import java.util.List;
52 import java.util.Set;
53 
54 /**
55  * Utility class for Picker Uris, it handles (includes permission checks, incoming args
56  * validations etc) and redirects picker URIs to the correct resolver.
57  */
58 public class PickerUriResolver {
59     private static final String TAG = "PickerUriResolver";
60 
61     private static final String PICKER_SEGMENT = "picker";
62     private static final String PICKER_INTERNAL_SEGMENT = "picker_internal";
63     /** A uri with prefix "content://media/picker" is considered as a picker uri */
64     public static final Uri PICKER_URI = MediaStore.AUTHORITY_URI.buildUpon().
65             appendPath(PICKER_SEGMENT).build();
66     /**
67      * Internal picker URI with prefix "content://media/picker_internal" to retrieve merged
68      * and deduped cloud and local items.
69      */
70     public static final Uri PICKER_INTERNAL_URI = MediaStore.AUTHORITY_URI.buildUpon().
71             appendPath(PICKER_INTERNAL_SEGMENT).build();
72 
73     public static final String MEDIA_PATH = "media";
74     public static final String ALBUM_PATH = "albums";
75 
76     public static final String LOCAL_PATH = "local";
77     public static final String ALL_PATH = "all";
78 
79     private final Context mContext;
80     private final PickerDbFacade mDbFacade;
81     private final Set<String> mAllValidProjectionColumns;
82     private final String[] mAllValidProjectionColumnsArray;
83 
PickerUriResolver(Context context, PickerDbFacade dbFacade, ProjectionHelper projectionHelper)84     PickerUriResolver(Context context, PickerDbFacade dbFacade, ProjectionHelper projectionHelper) {
85         mContext = context;
86         mDbFacade = dbFacade;
87         mAllValidProjectionColumns = projectionHelper.getProjectionMap(
88                 MediaStore.PickerMediaColumns.class).keySet();
89         mAllValidProjectionColumnsArray = mAllValidProjectionColumns.toArray(new String[0]);
90     }
91 
openFile(Uri uri, String mode, CancellationSignal signal, int callingPid, int callingUid)92     public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal,
93             int callingPid, int callingUid) throws FileNotFoundException {
94         if (ParcelFileDescriptor.parseMode(mode) != ParcelFileDescriptor.MODE_READ_ONLY) {
95             throw new SecurityException("PhotoPicker Uris can only be accessed to read."
96                     + " Uri: " + uri);
97         }
98 
99         checkUriPermission(uri, callingPid, callingUid);
100 
101         final ContentResolver resolver;
102         try {
103             resolver = getContentResolverForUserId(uri);
104         } catch (IllegalStateException e) {
105             // This is to be consistent with MediaProvider's response when a file is not found.
106             Log.e(TAG, "No item at " + uri, e);
107             throw new FileNotFoundException("No item at " + uri);
108         }
109         if (canHandleUriInUser(uri)) {
110             return openPickerFile(uri);
111         }
112         return resolver.openFile(uri, mode, signal);
113     }
114 
openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal, int callingPid, int callingUid)115     public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts,
116             CancellationSignal signal, int callingPid, int callingUid)
117             throws FileNotFoundException {
118         checkUriPermission(uri, callingPid, callingUid);
119 
120         final ContentResolver resolver;
121         try {
122             resolver = getContentResolverForUserId(uri);
123         } catch (IllegalStateException e) {
124             // This is to be consistent with MediaProvider's response when a file is not found.
125             Log.e(TAG, "No item at " + uri, e);
126             throw new FileNotFoundException("No item at " + uri);
127         }
128         if (canHandleUriInUser(uri)) {
129             return new AssetFileDescriptor(openPickerFile(uri), 0,
130                     AssetFileDescriptor.UNKNOWN_LENGTH);
131         }
132         return resolver.openTypedAssetFile(uri, mimeTypeFilter, opts, signal);
133     }
134 
query(Uri uri, String[] projection, int callingPid, int callingUid, String callingPackageName)135     public Cursor query(Uri uri, String[] projection, int callingPid, int callingUid,
136             String callingPackageName) {
137         checkUriPermission(uri, callingPid, callingUid);
138         try {
139             logUnknownProjectionColumns(projection, callingUid, callingPackageName);
140             return queryInternal(uri, projection);
141         } catch (IllegalStateException e) {
142             // This is to be consistent with MediaProvider, it returns an empty cursor if the row
143             // does not exist.
144             Log.e(TAG, "File not found for uri: " + uri, e);
145             return new MatrixCursor(projection == null ? new String[] {} : projection);
146         }
147     }
148 
queryInternal(Uri uri, String[] projection)149     private Cursor queryInternal(Uri uri, String[] projection) {
150         final ContentResolver resolver = getContentResolverForUserId(uri);
151 
152         if (canHandleUriInUser(uri)) {
153             if (projection == null || projection.length == 0) {
154                 projection = mAllValidProjectionColumnsArray;
155             }
156 
157             return queryPickerUri(uri, projection);
158         }
159         return resolver.query(uri, projection, /* queryArgs */ null,
160                 /* cancellationSignal */ null);
161     }
162 
163     /**
164      * getType for Picker Uris
165      */
getType(@onNull Uri uri, int callingPid, int callingUid)166     public String getType(@NonNull Uri uri, int callingPid, int callingUid) {
167         // TODO (b/272265676): Remove system uid check if found unnecessary
168         if (SdkLevel.isAtLeastU() && UserHandle.getAppId(callingUid) != SYSTEM_UID) {
169             // Starting Android 14, there is permission check for getting types requiring query.
170             // System Uid (1000) is allowed to get the types.
171             checkUriPermission(uri, callingPid, callingUid);
172         }
173 
174         try (Cursor cursor = queryInternal(uri, new String[]{MediaStore.MediaColumns.MIME_TYPE})) {
175             if (cursor != null && cursor.getCount() == 1 && cursor.moveToFirst()) {
176                 return getCursorString(cursor,
177                         CloudMediaProviderContract.MediaColumns.MIME_TYPE);
178             }
179         }
180 
181         throw new IllegalArgumentException("Failed to getType for uri: " + uri);
182     }
183 
getMediaUri(String authority)184     public static Uri getMediaUri(String authority) {
185         return Uri.parse("content://" + authority + "/"
186                 + CloudMediaProviderContract.URI_PATH_MEDIA);
187     }
188 
getDeletedMediaUri(String authority)189     public static Uri getDeletedMediaUri(String authority) {
190         return Uri.parse("content://" + authority + "/"
191                 + CloudMediaProviderContract.URI_PATH_DELETED_MEDIA);
192     }
193 
getMediaCollectionInfoUri(String authority)194     public static Uri getMediaCollectionInfoUri(String authority) {
195         return Uri.parse("content://" + authority + "/"
196                 + CloudMediaProviderContract.URI_PATH_MEDIA_COLLECTION_INFO);
197     }
198 
getAlbumUri(String authority)199     public static Uri getAlbumUri(String authority) {
200         return Uri.parse("content://" + authority + "/"
201                 + CloudMediaProviderContract.URI_PATH_ALBUM);
202     }
203 
createSurfaceControllerUri(String authority)204     public static Uri createSurfaceControllerUri(String authority) {
205         return Uri.parse("content://" + authority + "/"
206                 + CloudMediaProviderContract.URI_PATH_SURFACE_CONTROLLER);
207     }
208 
openPickerFile(Uri uri)209     private ParcelFileDescriptor openPickerFile(Uri uri) throws FileNotFoundException {
210         final File file = getPickerFileFromUri(uri);
211         if (file == null) {
212             throw new FileNotFoundException("File not found for uri: " + uri);
213         }
214         return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
215     }
216 
217     @VisibleForTesting
getPickerFileFromUri(Uri uri)218     File getPickerFileFromUri(Uri uri) {
219         final String[] projection = new String[] { MediaStore.PickerMediaColumns.DATA };
220         try (Cursor cursor = queryPickerUri(uri, projection)) {
221             if (cursor != null && cursor.getCount() == 1 && cursor.moveToFirst()) {
222                 String path = getCursorString(cursor, MediaStore.PickerMediaColumns.DATA);
223                 // First replace /sdcard with /storage/emulated path
224                 path = path.replaceFirst("/sdcard", "/storage/emulated/" + MediaStore.MY_USER_ID);
225                 // Then convert /storage/emulated patht to /mnt/user/ path
226                 return toFuseFile(new File(path));
227             }
228         }
229         return null;
230     }
231 
232     @VisibleForTesting
queryPickerUri(Uri uri, String[] projection)233     Cursor queryPickerUri(Uri uri, String[] projection) {
234         uri = unwrapProviderUri(uri);
235         return mDbFacade.queryMediaIdForApps(uri.getHost(), uri.getLastPathSegment(),
236                 projection);
237     }
238 
wrapProviderUri(Uri uri, int userId)239     public static Uri wrapProviderUri(Uri uri, int userId) {
240         final List<String> segments = uri.getPathSegments();
241         if (segments.size() != 2) {
242             throw new IllegalArgumentException("Unexpected provider URI: " + uri);
243         }
244 
245         Uri.Builder builder = initializeUriBuilder(MediaStore.AUTHORITY);
246         builder.appendPath(PICKER_SEGMENT);
247         builder.appendPath(String.valueOf(userId));
248         builder.appendPath(uri.getHost());
249 
250         for (int i = 0; i < segments.size(); i++) {
251             builder.appendPath(segments.get(i));
252         }
253 
254         return builder.build();
255     }
256 
257     @VisibleForTesting
unwrapProviderUri(Uri uri)258     static Uri unwrapProviderUri(Uri uri) {
259         List<String> segments = uri.getPathSegments();
260         if (segments.size() != 5) {
261             throw new IllegalArgumentException("Unexpected picker provider URI: " + uri);
262         }
263 
264         // segments.get(0) == 'picker'
265         final String userId = segments.get(1);
266         final String host = segments.get(2);
267         segments = segments.subList(3, segments.size());
268 
269         Uri.Builder builder = initializeUriBuilder(userId + "@" + host);
270 
271         for (int i = 0; i < segments.size(); i++) {
272             builder.appendPath(segments.get(i));
273         }
274         return builder.build();
275     }
276 
initializeUriBuilder(String authority)277     private static Uri.Builder initializeUriBuilder(String authority) {
278         final Uri.Builder builder = Uri.EMPTY.buildUpon();
279         builder.scheme("content");
280         builder.encodedAuthority(authority);
281 
282         return builder;
283     }
284 
285     @VisibleForTesting
getUserId(Uri uri)286     static int getUserId(Uri uri) {
287         // content://media/picker/<user-id>/<media-id>/...
288         return Integer.parseInt(uri.getPathSegments().get(1));
289     }
290 
checkUriPermission(Uri uri, int pid, int uid)291     private void checkUriPermission(Uri uri, int pid, int uid) {
292         if (!isSelf(uid) && mContext.checkUriPermission(uri, pid, uid,
293                 Intent.FLAG_GRANT_READ_URI_PERMISSION) != PERMISSION_GRANTED) {
294             throw new SecurityException("Calling uid ( " + uid + " ) does not have permission to " +
295                     "access picker uri: " + uri);
296         }
297     }
298 
isSelf(int uid)299     private boolean isSelf(int uid) {
300         return UserHandle.getAppId(android.os.Process.myUid()) == UserHandle.getAppId(uid);
301     }
302 
canHandleUriInUser(Uri uri)303     private boolean canHandleUriInUser(Uri uri) {
304         // If MPs user_id matches the URIs user_id, we can handle this URI in this MP user,
305         // otherwise, we'd have to re-route to MP matching URI user_id
306         return getUserId(uri) == mContext.getUser().getIdentifier();
307     }
308 
logUnknownProjectionColumns(String[] projection, int callingUid, String callingPackageName)309     private void logUnknownProjectionColumns(String[] projection, int callingUid,
310             String callingPackageName) {
311         if (projection == null || callingPackageName.equals(mContext.getPackageName())) {
312             return;
313         }
314 
315         for (String column : projection) {
316             if (!mAllValidProjectionColumns.contains(column)) {
317                 final PhotoPickerUiEventLogger logger = new PhotoPickerUiEventLogger();
318                 logger.logPickerQueriedWithUnknownColumn(callingUid, callingPackageName);
319             }
320         }
321     }
322 
323     @VisibleForTesting
getContentResolverForUserId(Uri uri)324     ContentResolver getContentResolverForUserId(Uri uri) {
325         final UserId userId = UserId.of(UserHandle.of(getUserId(uri)));
326         try {
327             return userId.getContentResolver(mContext);
328         } catch (NameNotFoundException e) {
329             throw new IllegalStateException("Cannot find content resolver for uri: " + uri, e);
330         }
331     }
332 }
333