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