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