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