1 /* 2 * Copyright (C) 2006 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.Manifest.permission.ACCESS_MEDIA_LOCATION; 20 import static android.app.AppOpsManager.permissionToOp; 21 import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; 22 import static android.app.PendingIntent.FLAG_IMMUTABLE; 23 import static android.app.PendingIntent.FLAG_ONE_SHOT; 24 import static android.content.ContentResolver.QUERY_ARG_SQL_GROUP_BY; 25 import static android.content.ContentResolver.QUERY_ARG_SQL_HAVING; 26 import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION; 27 import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS; 28 import static android.content.ContentResolver.QUERY_ARG_SQL_SORT_ORDER; 29 import static android.content.pm.PackageManager.PERMISSION_GRANTED; 30 import static android.database.Cursor.FIELD_TYPE_BLOB; 31 import static android.provider.CloudMediaProviderContract.EXTRA_ASYNC_CONTENT_PROVIDER; 32 import static android.provider.CloudMediaProviderContract.MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION; 33 import static android.provider.CloudMediaProviderContract.METHOD_GET_ASYNC_CONTENT_PROVIDER; 34 import static android.provider.MediaStore.EXTRA_IS_STABLE_URIS_ENABLED; 35 import static android.provider.MediaStore.EXTRA_OPEN_ASSET_FILE_REQUEST; 36 import static android.provider.MediaStore.EXTRA_OPEN_FILE_REQUEST; 37 import static android.provider.MediaStore.EXTRA_URI_LIST; 38 import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE; 39 import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE; 40 import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO; 41 import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT; 42 import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_NONE; 43 import static android.provider.MediaStore.GET_BACKUP_FILES; 44 import static android.provider.MediaStore.GET_OWNER_PACKAGE_NAME; 45 import static android.provider.MediaStore.Images.ImageColumns.LATITUDE; 46 import static android.provider.MediaStore.Images.ImageColumns.LONGITUDE; 47 import static android.provider.MediaStore.MATCH_DEFAULT; 48 import static android.provider.MediaStore.MATCH_EXCLUDE; 49 import static android.provider.MediaStore.MATCH_INCLUDE; 50 import static android.provider.MediaStore.MATCH_ONLY; 51 import static android.provider.MediaStore.MEDIA_IGNORE_FILENAME; 52 import static android.provider.MediaStore.MY_UID; 53 import static android.provider.MediaStore.MediaColumns.OEM_METADATA; 54 import static android.provider.MediaStore.MediaColumns.OWNER_PACKAGE_NAME; 55 import static android.provider.MediaStore.PER_USER_RANGE; 56 import static android.provider.MediaStore.QUERY_ARG_DEFER_SCAN; 57 import static android.provider.MediaStore.QUERY_ARG_LATEST_SELECTION_ONLY; 58 import static android.provider.MediaStore.QUERY_ARG_MATCH_FAVORITE; 59 import static android.provider.MediaStore.QUERY_ARG_MATCH_PENDING; 60 import static android.provider.MediaStore.QUERY_ARG_MATCH_TRASHED; 61 import static android.provider.MediaStore.QUERY_ARG_MEDIA_STANDARD_SORT_ORDER; 62 import static android.provider.MediaStore.QUERY_ARG_REDACTED_URI; 63 import static android.provider.MediaStore.QUERY_ARG_RELATED_URI; 64 import static android.provider.MediaStore.READ_BACKUP; 65 import static android.provider.MediaStore.REVOKED_ALL_READ_GRANTS_FOR_PACKAGE_CALL; 66 import static android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY; 67 import static android.provider.MediaStore.getVolumeName; 68 import static android.system.OsConstants.F_GETFL; 69 70 import static com.android.providers.media.AccessChecker.getWhereForConstrainedAccess; 71 import static com.android.providers.media.AccessChecker.getWhereForLatestSelection; 72 import static com.android.providers.media.AccessChecker.getWhereForOwnerPackageMatch; 73 import static com.android.providers.media.AccessChecker.getWhereForUserSelectedAccess; 74 import static com.android.providers.media.AccessChecker.hasAccessToCollection; 75 import static com.android.providers.media.AccessChecker.hasUserSelectedAccess; 76 import static com.android.providers.media.AccessChecker.isRedactionNeededForPickerUri; 77 import static com.android.providers.media.DatabaseHelper.EXTERNAL_DATABASE_NAME; 78 import static com.android.providers.media.DatabaseHelper.INTERNAL_DATABASE_NAME; 79 import static com.android.providers.media.LocalCallingIdentity.APPOP_REQUEST_INSTALL_PACKAGES_FOR_SHARED_UID; 80 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_ACCESS_MTP; 81 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_INSTALL_PACKAGES; 82 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_DELEGATOR; 83 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_GRANTED; 84 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_READ; 85 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE; 86 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_MANAGER; 87 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED; 88 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SELF; 89 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SHELL; 90 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SYSTEM_GALLERY; 91 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_IMAGES; 92 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_VIDEO; 93 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_EXTERNAL_STORAGE; 94 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMART; 95 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMART_FILE_ID; 96 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMART_ID; 97 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMS; 98 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMS_ID; 99 import static com.android.providers.media.LocalUriMatcher.AUDIO_ARTISTS; 100 import static com.android.providers.media.LocalUriMatcher.AUDIO_ARTISTS_ID; 101 import static com.android.providers.media.LocalUriMatcher.AUDIO_ARTISTS_ID_ALBUMS; 102 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES; 103 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES_ALL_MEMBERS; 104 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES_ID; 105 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES_ID_MEMBERS; 106 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA; 107 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA_ID; 108 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA_ID_GENRES; 109 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA_ID_GENRES_ID; 110 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS; 111 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS_ID; 112 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS_ID_MEMBERS; 113 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS_ID_MEMBERS_ID; 114 import static com.android.providers.media.LocalUriMatcher.CLI; 115 import static com.android.providers.media.LocalUriMatcher.DOWNLOADS; 116 import static com.android.providers.media.LocalUriMatcher.DOWNLOADS_ID; 117 import static com.android.providers.media.LocalUriMatcher.FILES; 118 import static com.android.providers.media.LocalUriMatcher.FILES_ID; 119 import static com.android.providers.media.LocalUriMatcher.FS_ID; 120 import static com.android.providers.media.LocalUriMatcher.IMAGES_MEDIA; 121 import static com.android.providers.media.LocalUriMatcher.IMAGES_MEDIA_ID; 122 import static com.android.providers.media.LocalUriMatcher.IMAGES_MEDIA_ID_THUMBNAIL; 123 import static com.android.providers.media.LocalUriMatcher.IMAGES_THUMBNAILS; 124 import static com.android.providers.media.LocalUriMatcher.IMAGES_THUMBNAILS_ID; 125 import static com.android.providers.media.LocalUriMatcher.MEDIA_GRANTS; 126 import static com.android.providers.media.LocalUriMatcher.MEDIA_SCANNER; 127 import static com.android.providers.media.LocalUriMatcher.PICKER_GET_CONTENT_ID; 128 import static com.android.providers.media.LocalUriMatcher.PICKER_ID; 129 import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_V2; 130 import static com.android.providers.media.LocalUriMatcher.PICKER_TRANSCODED_ID; 131 import static com.android.providers.media.LocalUriMatcher.VERSION; 132 import static com.android.providers.media.LocalUriMatcher.VIDEO_MEDIA; 133 import static com.android.providers.media.LocalUriMatcher.VIDEO_MEDIA_ID; 134 import static com.android.providers.media.LocalUriMatcher.VIDEO_MEDIA_ID_THUMBNAIL; 135 import static com.android.providers.media.LocalUriMatcher.VIDEO_THUMBNAILS; 136 import static com.android.providers.media.LocalUriMatcher.VIDEO_THUMBNAILS_ID; 137 import static com.android.providers.media.LocalUriMatcher.VOLUMES; 138 import static com.android.providers.media.LocalUriMatcher.VOLUMES_ID; 139 import static com.android.providers.media.PickerUriResolver.PICKER_GET_CONTENT_SEGMENT; 140 import static com.android.providers.media.PickerUriResolver.PICKER_SEGMENT; 141 import static com.android.providers.media.PickerUriResolver.PICKER_TRANSCODED_SEGMENT; 142 import static com.android.providers.media.PickerUriResolver.getMediaUri; 143 import static com.android.providers.media.flags.Flags.indexMediaLatitudeLongitude; 144 import static com.android.providers.media.flags.Flags.versionLockdown; 145 import static com.android.providers.media.photopicker.data.ItemsProvider.EXTRA_MIME_TYPE_SELECTION; 146 import static com.android.providers.media.scan.MediaScanner.REASON_DEMAND; 147 import static com.android.providers.media.scan.MediaScanner.REASON_IDLE; 148 import static com.android.providers.media.util.DatabaseUtils.bindList; 149 import static com.android.providers.media.util.FileUtils.DEFAULT_FOLDER_NAMES; 150 import static com.android.providers.media.util.FileUtils.PATTERN_PENDING_FILEPATH_FOR_SQL; 151 import static com.android.providers.media.util.FileUtils.buildPrimaryVolumeFile; 152 import static com.android.providers.media.util.FileUtils.extractDisplayName; 153 import static com.android.providers.media.util.FileUtils.extractFileExtension; 154 import static com.android.providers.media.util.FileUtils.extractFileName; 155 import static com.android.providers.media.util.FileUtils.extractOwnerPackageNameFromRelativePath; 156 import static com.android.providers.media.util.FileUtils.extractPathOwnerPackageName; 157 import static com.android.providers.media.util.FileUtils.extractRelativePath; 158 import static com.android.providers.media.util.FileUtils.extractRelativePathWithDisplayName; 159 import static com.android.providers.media.util.FileUtils.extractTopLevelDir; 160 import static com.android.providers.media.util.FileUtils.extractVolumeName; 161 import static com.android.providers.media.util.FileUtils.extractVolumePath; 162 import static com.android.providers.media.util.FileUtils.fromFuseFile; 163 import static com.android.providers.media.util.FileUtils.getAbsoluteSanitizedPath; 164 import static com.android.providers.media.util.FileUtils.isCrossUserEnabled; 165 import static com.android.providers.media.util.FileUtils.isDataOrObbPath; 166 import static com.android.providers.media.util.FileUtils.isDataOrObbRelativePath; 167 import static com.android.providers.media.util.FileUtils.isDownload; 168 import static com.android.providers.media.util.FileUtils.isExternalMediaDirectory; 169 import static com.android.providers.media.util.FileUtils.isObbOrChildRelativePath; 170 import static com.android.providers.media.util.FileUtils.sanitizePath; 171 import static com.android.providers.media.util.FileUtils.toFuseFile; 172 import static com.android.providers.media.util.Logging.LOGV; 173 import static com.android.providers.media.util.Logging.TAG; 174 import static com.android.providers.media.util.PermissionUtils.checkPermissionSelf; 175 import static com.android.providers.media.util.PermissionUtils.checkPermissionShell; 176 import static com.android.providers.media.util.PermissionUtils.checkPermissionSystem; 177 import static com.android.providers.media.util.StringUtils.componentStateToString; 178 import static com.android.providers.media.util.SyntheticPathUtils.REDACTED_URI_ID_PREFIX; 179 import static com.android.providers.media.util.SyntheticPathUtils.REDACTED_URI_ID_SIZE; 180 import static com.android.providers.media.util.SyntheticPathUtils.createSparseFile; 181 import static com.android.providers.media.util.SyntheticPathUtils.extractSyntheticRelativePathSegements; 182 import static com.android.providers.media.util.SyntheticPathUtils.getRedactedRelativePath; 183 import static com.android.providers.media.util.SyntheticPathUtils.isPickerPath; 184 import static com.android.providers.media.util.SyntheticPathUtils.isRedactedPath; 185 import static com.android.providers.media.util.SyntheticPathUtils.isSyntheticPath; 186 187 import android.Manifest; 188 import android.annotation.IntDef; 189 import android.app.ActivityOptions; 190 import android.app.AppOpsManager; 191 import android.app.AppOpsManager.OnOpActiveChangedListener; 192 import android.app.AppOpsManager.OnOpChangedListener; 193 import android.app.DownloadManager; 194 import android.app.PendingIntent; 195 import android.app.RecoverableSecurityException; 196 import android.app.RemoteAction; 197 import android.app.compat.CompatChanges; 198 import android.compat.annotation.ChangeId; 199 import android.compat.annotation.EnabledAfter; 200 import android.compat.annotation.EnabledSince; 201 import android.content.BroadcastReceiver; 202 import android.content.ClipData; 203 import android.content.ClipDescription; 204 import android.content.ComponentName; 205 import android.content.ContentProvider; 206 import android.content.ContentProviderClient; 207 import android.content.ContentProviderOperation; 208 import android.content.ContentProviderResult; 209 import android.content.ContentResolver; 210 import android.content.ContentUris; 211 import android.content.ContentValues; 212 import android.content.Context; 213 import android.content.Intent; 214 import android.content.IntentFilter; 215 import android.content.OperationApplicationException; 216 import android.content.SharedPreferences; 217 import android.content.pm.ApplicationInfo; 218 import android.content.pm.PackageInstaller.SessionInfo; 219 import android.content.pm.PackageManager; 220 import android.content.pm.PackageManager.NameNotFoundException; 221 import android.content.pm.PermissionGroupInfo; 222 import android.content.pm.ProviderInfo; 223 import android.content.res.AssetFileDescriptor; 224 import android.content.res.Configuration; 225 import android.content.res.Resources; 226 import android.database.Cursor; 227 import android.database.MatrixCursor; 228 import android.database.sqlite.SQLiteConstraintException; 229 import android.database.sqlite.SQLiteDatabase; 230 import android.graphics.Bitmap; 231 import android.graphics.BitmapFactory; 232 import android.graphics.drawable.Icon; 233 import android.icu.util.ULocale; 234 import android.media.ThumbnailUtils; 235 import android.mtp.MtpConstants; 236 import android.net.Uri; 237 import android.os.Binder; 238 import android.os.Binder.ProxyTransactListener; 239 import android.os.Build; 240 import android.os.Bundle; 241 import android.os.CancellationSignal; 242 import android.os.Environment; 243 import android.os.IBinder; 244 import android.os.ParcelFileDescriptor; 245 import android.os.ParcelFileDescriptor.OnCloseListener; 246 import android.os.Parcelable; 247 import android.os.Process; 248 import android.os.RemoteException; 249 import android.os.SystemClock; 250 import android.os.Trace; 251 import android.os.UserHandle; 252 import android.os.UserManager; 253 import android.os.storage.StorageManager; 254 import android.os.storage.StorageManager.StorageVolumeCallback; 255 import android.os.storage.StorageVolume; 256 import android.preference.PreferenceManager; 257 import android.provider.AsyncContentProvider; 258 import android.provider.BaseColumns; 259 import android.provider.Column; 260 import android.provider.DocumentsContract; 261 import android.provider.ExportedSince; 262 import android.provider.IAsyncContentProvider; 263 import android.provider.MediaStore; 264 import android.provider.MediaStore.Audio; 265 import android.provider.MediaStore.Audio.AudioColumns; 266 import android.provider.MediaStore.Audio.Playlists; 267 import android.provider.MediaStore.Downloads; 268 import android.provider.MediaStore.Files; 269 import android.provider.MediaStore.Files.FileColumns; 270 import android.provider.MediaStore.Images; 271 import android.provider.MediaStore.MediaColumns; 272 import android.provider.MediaStore.Video; 273 import android.provider.OpenAssetFileRequest; 274 import android.provider.OpenFileRequest; 275 import android.provider.Settings; 276 import android.system.ErrnoException; 277 import android.system.Os; 278 import android.system.OsConstants; 279 import android.system.StructStat; 280 import android.text.TextUtils; 281 import android.text.format.DateUtils; 282 import android.util.ArrayMap; 283 import android.util.ArraySet; 284 import android.util.DisplayMetrics; 285 import android.util.Log; 286 import android.util.LongSparseArray; 287 import android.util.Pair; 288 import android.util.Size; 289 import android.util.SparseArray; 290 import android.webkit.MimeTypeMap; 291 292 import androidx.annotation.ChecksSdkIntAtLeast; 293 import androidx.annotation.GuardedBy; 294 import androidx.annotation.Keep; 295 import androidx.annotation.NonNull; 296 import androidx.annotation.Nullable; 297 import androidx.annotation.RequiresApi; 298 import androidx.annotation.VisibleForTesting; 299 300 import com.android.modules.utils.BackgroundThread; 301 import com.android.modules.utils.build.SdkLevel; 302 import com.android.providers.media.DatabaseHelper.OnFilesChangeListener; 303 import com.android.providers.media.DatabaseHelper.OnLegacyMigrationListener; 304 import com.android.providers.media.backupandrestore.BackupAndRestoreUtils; 305 import com.android.providers.media.backupandrestore.BackupExecutor; 306 import com.android.providers.media.dao.FileRow; 307 import com.android.providers.media.flags.Flags; 308 import com.android.providers.media.fuse.ExternalStorageServiceImpl; 309 import com.android.providers.media.fuse.FuseDaemon; 310 import com.android.providers.media.metrics.PulledMetrics; 311 import com.android.providers.media.photopicker.PhotoPickerActivity; 312 import com.android.providers.media.photopicker.PickerDataLayer; 313 import com.android.providers.media.photopicker.PickerSyncController; 314 import com.android.providers.media.photopicker.data.ExternalDbFacade; 315 import com.android.providers.media.photopicker.data.PickerDbFacade; 316 import com.android.providers.media.photopicker.data.PickerSyncRequestExtras; 317 import com.android.providers.media.photopicker.sync.PickerSyncLockManager; 318 import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException; 319 import com.android.providers.media.photopicker.v2.PickerDataLayerV2; 320 import com.android.providers.media.photopicker.v2.PickerUriResolverV2; 321 import com.android.providers.media.playlist.Playlist; 322 import com.android.providers.media.scan.MediaScanner; 323 import com.android.providers.media.scan.MediaScanner.ScanReason; 324 import com.android.providers.media.scan.ModernMediaScanner; 325 import com.android.providers.media.stableuris.dao.BackupIdRow; 326 import com.android.providers.media.util.CachedSupplier; 327 import com.android.providers.media.util.DatabaseUtils; 328 import com.android.providers.media.util.FileUtils; 329 import com.android.providers.media.util.ForegroundThread; 330 import com.android.providers.media.util.Logging; 331 import com.android.providers.media.util.LongArray; 332 import com.android.providers.media.util.Metrics; 333 import com.android.providers.media.util.MimeTypeFixHandler; 334 import com.android.providers.media.util.MimeUtils; 335 import com.android.providers.media.util.PermissionUtils; 336 import com.android.providers.media.util.Preconditions; 337 import com.android.providers.media.util.RedactionUtils; 338 import com.android.providers.media.util.SQLiteQueryBuilder; 339 import com.android.providers.media.util.SpecialFormatDetector; 340 import com.android.providers.media.util.StringUtils; 341 import com.android.providers.media.util.UserCache; 342 343 import com.google.common.hash.HashCode; 344 import com.google.common.hash.Hashing; 345 346 import org.jetbrains.annotations.NotNull; 347 348 import java.io.File; 349 import java.io.FileDescriptor; 350 import java.io.FileInputStream; 351 import java.io.FileNotFoundException; 352 import java.io.FileOutputStream; 353 import java.io.IOException; 354 import java.io.OutputStream; 355 import java.io.PrintWriter; 356 import java.lang.annotation.Retention; 357 import java.lang.annotation.RetentionPolicy; 358 import java.lang.reflect.InvocationTargetException; 359 import java.lang.reflect.Method; 360 import java.nio.charset.StandardCharsets; 361 import java.nio.file.Path; 362 import java.util.ArrayList; 363 import java.util.Arrays; 364 import java.util.Collection; 365 import java.util.Collections; 366 import java.util.HashSet; 367 import java.util.LinkedHashMap; 368 import java.util.List; 369 import java.util.Locale; 370 import java.util.Map; 371 import java.util.Objects; 372 import java.util.Optional; 373 import java.util.Set; 374 import java.util.UUID; 375 import java.util.concurrent.CountDownLatch; 376 import java.util.concurrent.ExecutionException; 377 import java.util.concurrent.TimeUnit; 378 import java.util.concurrent.TimeoutException; 379 import java.util.function.Consumer; 380 import java.util.function.Supplier; 381 import java.util.function.UnaryOperator; 382 import java.util.regex.Matcher; 383 import java.util.regex.Pattern; 384 import java.util.stream.Collectors; 385 386 /** 387 * Media content provider. See {@link android.provider.MediaStore} for details. 388 * A single database keep track of media files on external storage 389 * The content visible at content://media/external/... is a combined view of all media files on all 390 * available external storage devices 391 */ 392 public class MediaProvider extends ContentProvider { 393 /** 394 * Enables checks to stop apps from inserting and updating to private files via media provider. 395 */ 396 @ChangeId 397 @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.R) 398 static final long ENABLE_CHECKS_FOR_PRIVATE_FILES = 172100307L; 399 400 /** 401 * Regex of a selection string that matches a specific ID. 402 */ 403 static final Pattern PATTERN_SELECTION_ID = Pattern.compile( 404 "(?:image_id|video_id)\\s*=\\s*(\\d+)"); 405 406 /** File access by uid requires the transcoding transform */ 407 private static final int FLAG_TRANSFORM_TRANSCODING = 1 << 0; 408 409 /** File access by uid is a synthetic path corresponding to a redacted URI */ 410 private static final int FLAG_TRANSFORM_REDACTION = 1 << 1; 411 412 /** File access by uid is a synthetic path corresponding to a picker URI */ 413 private static final int FLAG_TRANSFORM_PICKER = 1 << 2; 414 415 /** 416 * These directory names aren't declared in Environment as final variables, and so we need to 417 * have the same values in separate final variables in order to have them considered constant 418 * expressions. 419 * These directory names are intentionally in lower case to ease the case insensitive path 420 * comparison. 421 */ 422 private static final String DIRECTORY_MUSIC_LOWER_CASE = "music"; 423 private static final String DIRECTORY_PODCASTS_LOWER_CASE = "podcasts"; 424 private static final String DIRECTORY_RINGTONES_LOWER_CASE = "ringtones"; 425 private static final String DIRECTORY_ALARMS_LOWER_CASE = "alarms"; 426 private static final String DIRECTORY_NOTIFICATIONS_LOWER_CASE = "notifications"; 427 private static final String DIRECTORY_PICTURES_LOWER_CASE = "pictures"; 428 private static final String DIRECTORY_MOVIES_LOWER_CASE = "movies"; 429 private static final String DIRECTORY_DOWNLOADS_LOWER_CASE = "download"; 430 private static final String DIRECTORY_DCIM_LOWER_CASE = "dcim"; 431 private static final String DIRECTORY_DOCUMENTS_LOWER_CASE = "documents"; 432 private static final String DIRECTORY_AUDIOBOOKS_LOWER_CASE = "audiobooks"; 433 private static final String DIRECTORY_RECORDINGS_LOWER_CASE = "recordings"; 434 private static final String DIRECTORY_ANDROID_LOWER_CASE = "android"; 435 436 private static final String DIRECTORY_MEDIA = "media"; 437 private static final String DIRECTORY_THUMBNAILS = ".thumbnails"; 438 439 /** 440 * Hard-coded filename where the current value of 441 * {@link DatabaseHelper#getOrCreateUuid} is persisted on a physical SD card 442 * to help identify stale thumbnail collections. 443 */ 444 private static final String FILE_DATABASE_UUID = ".database_uuid"; 445 446 /** 447 * Specify what default directories the caller gets full access to. By default, the caller 448 * shouldn't get full access to any default dirs. 449 * But for example, we do an exception for System Gallery apps and allow them full access to: 450 * DCIM, Pictures, Movies. 451 */ 452 static final String INCLUDED_DEFAULT_DIRECTORIES = 453 "android:included-default-directories"; 454 455 /** 456 * Value indicating that operations should include database rows matching the criteria defined 457 * by this key only when calling package has write permission to the database row or column is 458 * {@column MediaColumns#IS_PENDING} and is set by FUSE. 459 * <p> 460 * Note that items <em>not</em> matching the criteria will also be included, and as part of this 461 * match no additional write permission checks are carried out for those items. 462 */ 463 private static final int MATCH_VISIBLE_FOR_FILEPATH = 32; 464 465 private static final int NON_HIDDEN_CACHE_SIZE = 50; 466 467 /** 468 * This is required as idle maintenance maybe stopped anytime; we do not want to query 469 * and accumulate values to update for a long time, instead we want to batch query and update 470 * by a limited number. 471 */ 472 private static final int IDLE_MAINTENANCE_ROWS_LIMIT = 1000; 473 474 /** 475 * Where clause to match pending files from FUSE. Pending files from FUSE will not have 476 * PATTERN_PENDING_FILEPATH_FOR_SQL pattern. 477 */ 478 private static final String MATCH_PENDING_FROM_FUSE = String.format("lower(%s) NOT REGEXP '%s'", 479 MediaColumns.DATA, PATTERN_PENDING_FILEPATH_FOR_SQL); 480 481 /** 482 * This flag is replaced with {@link MediaStore#QUERY_ARG_DEFER_SCAN} from S onwards and only 483 * kept around for app compatibility in R. 484 */ 485 private static final String QUERY_ARG_DO_ASYNC_SCAN = "android:query-arg-do-async-scan"; 486 487 /** 488 * Time between two polling attempts for availability of FuseDaemon thread. 489 */ 490 private static final long POLLING_TIME_IN_MILLIS = 100; 491 492 /** 493 * Enable option to defer the scan triggered as part of MediaProvider#update() 494 */ 495 @ChangeId 496 @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.R) 497 static final long ENABLE_DEFERRED_SCAN = 180326732L; 498 499 /** 500 * Enable option to include database rows of files from recently unmounted 501 * volume in MediaProvider#query 502 */ 503 @ChangeId 504 @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.R) 505 static final long ENABLE_INCLUDE_ALL_VOLUMES = 182734110L; 506 507 /** 508 * Enables allowing the user to revoke the app access to its created photos and videos. 509 * If the app target sdk is >= {@link android.os.Build.VERSION_CODES#BAKLAVA}, 510 * then they should expect that they may lose access to photos or videos they have created 511 * while they could still be on the device. 512 */ 513 @ChangeId 514 @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) 515 public static final long ENABLE_OWNED_PHOTOS = 310703690L; 516 517 518 /** 519 * Excludes unreliable storage volumes from being included in 520 * {@link MediaStore#getExternalVolumeNames(Context)}. 521 */ 522 @ChangeId 523 @EnabledSince(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT) 524 @VisibleForTesting 525 // TODO: b/402623169 Set CUR_DEVELOPMENT as the latest version once available 526 static final long EXCLUDE_UNRELIABLE_STORAGE_VOLUMES = 391360514L; 527 528 /** 529 * Set of {@link Cursor} columns that refer to raw filesystem paths. 530 */ 531 private static final ArrayMap<String, Object> sDataColumns = new ArrayMap<>(); 532 533 static { sDataColumns.put(MediaStore.MediaColumns.DATA, null)534 sDataColumns.put(MediaStore.MediaColumns.DATA, null); sDataColumns.put(MediaStore.Images.Thumbnails.DATA, null)535 sDataColumns.put(MediaStore.Images.Thumbnails.DATA, null); sDataColumns.put(MediaStore.Video.Thumbnails.DATA, null)536 sDataColumns.put(MediaStore.Video.Thumbnails.DATA, null); sDataColumns.put(MediaStore.Audio.PlaylistsColumns.DATA, null)537 sDataColumns.put(MediaStore.Audio.PlaylistsColumns.DATA, null); sDataColumns.put(MediaStore.Audio.AlbumColumns.ALBUM_ART, null)538 sDataColumns.put(MediaStore.Audio.AlbumColumns.ALBUM_ART, null); 539 } 540 541 private static final int sUserId = UserHandle.myUserId(); 542 543 /** 544 * Please use {@link getDownloadsProviderAuthority()} instead of using this directly. 545 */ 546 private static final String DOWNLOADS_PROVIDER_AUTHORITY = "downloads"; 547 548 private static final String DEFAULT_FOLDER_CREATED_KEY_PREFIX = "created_default_folders_"; 549 550 /** 551 * This value should match android.os.Trace.MAX_SECTION_NAME_LEN , not accessible from this 552 * class 553 */ 554 private static final int MAX_SECTION_NAME_LEN = 127; 555 556 /** 557 * This string is a copy of 558 * {@link com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY} 559 */ 560 private static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary"; 561 562 private static final String MEDIAPROVIDER_PREFS = "mediaprovider_prefs"; 563 564 private static final String IS_MIME_TYPE_FIXED_IN_ANDROID_15 = 565 "is_mime_type_fixed_in_android_15"; 566 567 /** 568 * Updates the MediaStore versioning schema and format to reduce identifying properties. 569 */ 570 @ChangeId 571 @EnabledSince(targetSdkVersion = Build.VERSION_CODES.BAKLAVA) 572 static final long LOCKDOWN_MEDIASTORE_VERSION = 343977174L; 573 574 /** 575 * Number of uris sent to bulk write/delete/trash/favorite requests restricted at 2000. 576 * Attempting to send more than 2000 uris will result in an IllegalArgumentException. 577 */ 578 @ChangeId 579 @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) 580 static final long LIMIT_CREATE_REQUEST_URIS = 203408344L; 581 582 @GuardedBy("mPendingOpenInfo") 583 private final Map<Integer, PendingOpenInfo> mPendingOpenInfo = new ArrayMap<>(); 584 585 @GuardedBy("mNonHiddenPaths") 586 private final LRUCache<String, Integer> mNonHiddenPaths = new LRUCache<>(NON_HIDDEN_CACHE_SIZE); 587 updateVolumes()588 public void updateVolumes() { 589 mVolumeCache.update(); 590 // Update filters to reflect mounted volumes so users don't get 591 // confused by metadata from ejected volumes 592 ForegroundThread.getExecutor().execute(() -> { 593 mExternalDatabase.setFilterVolumeNames(mVolumeCache.getExternalVolumeNames()); 594 }); 595 } 596 597 @NonNull getVolume(@onNull String volumeName)598 public MediaVolume getVolume(@NonNull String volumeName) throws FileNotFoundException { 599 return mVolumeCache.findVolume(volumeName, mCallingIdentity.get().getUser()); 600 } 601 602 @NonNull getVolumePath(@onNull String volumeName)603 public File getVolumePath(@NonNull String volumeName) throws FileNotFoundException { 604 // Ugly hack to keep unit tests passing, where we don't always have a 605 // Context to discover volumes with 606 if (getContext() == null) { 607 return Environment.getExternalStorageDirectory(); 608 } 609 610 return mVolumeCache.getVolumePath(volumeName, mCallingIdentity.get().getUser()); 611 } 612 613 @NonNull getAllowedVolumePaths(String volumeName)614 private Collection<File> getAllowedVolumePaths(String volumeName) 615 throws FileNotFoundException { 616 // This method is used to verify whether a path belongs to a certain volume name; 617 // we can't always use the calling user's identity here to determine exactly which 618 // volume is meant, because the MediaScanner may scan paths belonging to another user, 619 // eg a clone user. 620 // So, for volumes like external_primary, just return allowed paths for all users. 621 List<UserHandle> users = mUserCache.getUsersCached(); 622 ArrayList<File> allowedPaths = new ArrayList<>(); 623 for (UserHandle user : users) { 624 try { 625 Collection<File> volumeScanPaths = mVolumeCache.getVolumeScanPaths(volumeName, 626 user); 627 allowedPaths.addAll(volumeScanPaths); 628 } catch (FileNotFoundException e) { 629 Log.e(TAG, volumeName + " has no associated path for user: " + user); 630 } 631 } 632 633 return allowedPaths; 634 } 635 636 /** 637 * Frees any cache held by MediaProvider. 638 * 639 * @param bytes number of bytes which need to be freed 640 */ freeCache(long bytes)641 public void freeCache(long bytes) { 642 bytes -= mPhotoPickerTranscodeHelper.freeCache(bytes); 643 if (bytes > 0) { 644 mTranscodeHelper.freeCache(bytes); 645 } 646 } 647 onAnrDelayStarted(@onNull String packageName, int uid, int tid, int reason)648 public void onAnrDelayStarted(@NonNull String packageName, int uid, int tid, int reason) { 649 mTranscodeHelper.onAnrDelayStarted(packageName, uid, tid, reason); 650 } 651 652 private volatile Locale mLastLocale = Locale.getDefault(); 653 654 private StorageManager mStorageManager; 655 private PackageManager mPackageManager; 656 private UserManager mUserManager; 657 private PickerUriResolver mPickerUriResolver; 658 private AsyncPickerFileOpener mAsyncPickerFileOpener; 659 660 private UserCache mUserCache; 661 private VolumeCache mVolumeCache; 662 663 private int mExternalStorageAuthorityAppId; 664 private int mDownloadsAuthorityAppId; 665 private Size mThumbSize; 666 private MaliciousAppDetector mMaliciousAppDetector; 667 668 /** 669 * Map from UID to cached {@link LocalCallingIdentity}. Values are only 670 * maintained in this map while the UID is actively working with a 671 * performance-critical component, such as camera. 672 */ 673 @GuardedBy("mCachedCallingIdentity") 674 private final SparseArray<LocalCallingIdentity> mCachedCallingIdentity = new SparseArray<>(); 675 676 private final OnOpActiveChangedListener mActiveListener = (code, uid, packageName, active) -> { 677 synchronized (mCachedCallingIdentity) { 678 if (active) { 679 // TODO moltmann: Set correct featureId 680 mCachedCallingIdentity.put(uid, 681 LocalCallingIdentity.fromExternal(getContext(), mUserCache, uid, 682 packageName, null)); 683 } else { 684 mCachedCallingIdentity.remove(uid); 685 } 686 } 687 }; 688 689 /** 690 * Utility function if owned photos features is enabled. 691 * @return boolean value indicating whether feature is enabled or not 692 */ isOwnedPhotosEnabled(int uid)693 public static boolean isOwnedPhotosEnabled(int uid) { 694 // TODO change this to SdkLevel.isAtLeastB() once method is available 695 return ((Build.VERSION.CODENAME.equals("Baklava") 696 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) 697 && CompatChanges.isChangeEnabled(ENABLE_OWNED_PHOTOS, uid) 698 && Flags.revokeAccessOwnedPhotos()); 699 } 700 701 /** 702 * Map from UID to cached {@link LocalCallingIdentity}. Values are only 703 * maintained in this map until there's any change in the appops needed or packages 704 * used in the {@link LocalCallingIdentity}. 705 */ 706 @GuardedBy("mCachedCallingIdentityForFuse") 707 private final SparseArray<LocalCallingIdentity> mCachedCallingIdentityForFuse = 708 new SparseArray<>(); 709 710 private final OnOpChangedListener mModeListener = new OnOpChangedListener() { 711 712 /** 713 * Callback method called as part of {@link OnOpChangedListener}. 714 * Calls {@link #onOpChanged(String, String, int)} with cached userId(s). 715 * 716 * @param packageName - package for which AppOp changed 717 * @param op - AppOp for which the mode changed. 718 */ 719 public void onOpChanged(String op, String packageName) { 720 // In case no userId is supplied, we drop grants for all cached users. 721 List<UserHandle> userHandles = mUserCache.getUsersCached(); 722 for (UserHandle user : userHandles) { 723 onOpChanged(op, packageName, user.getIdentifier()); 724 } 725 } 726 727 /** 728 * Callback method called as part of {@link OnOpChangedListener}. 729 * When an AppOp is written - 730 * 1. We invalidate saved LocalCallingIdentity object for the package. This 731 * is needed to ensure we read the new permission state 732 * 2. If the AppOp change was on the read media appOps, we clear any stale 733 * grants, 734 * 735 * @param packageName - package for which AppOp changed 736 * @param op - AppOp for which the mode changed. 737 * @param userId - userSpace where the package is located 738 */ 739 public void onOpChanged(String op, String packageName, int userId) { 740 invalidateLocalCallingIdentityCache(packageName, "op " + op /* reason */); 741 removeMediaGrantsOnModeChange(packageName, op, userId); 742 } 743 }; 744 745 /** 746 * Removes media_grants for the given {@code packageName} and {@code userId} if the AppOp 747 * change resulted in a state of "Allow All" or "Deny All" for read 748 * permission. 749 */ removeMediaGrantsOnModeChange(String packageName, String op, int userId)750 private void removeMediaGrantsOnModeChange(String packageName, String op, int userId) { 751 // b/265963379: onModeChanged is always called with op=OPSTR_READ_EXTERNAL_STORAGE even if 752 // the appOp mode changed for other read media app ops. Handle all read media app op changes 753 // until the bug is fixed. 754 if (!SdkLevel.isAtLeastU() || !isReadMediaAppOp(op)) { 755 return; 756 } 757 Context context = getContext(); 758 PackageManager packageManager = context.getPackageManager(); 759 try { 760 int uid = 761 packageManager.getPackageUidAsUser( 762 packageName, PackageManager.PackageInfoFlags.of(0), userId); 763 LocalCallingIdentity lci = LocalCallingIdentity.fromExternal(context, mUserCache, uid); 764 if (!lci.checkCallingPermissionUserSelected(/* forDataDelivery */ false)) { 765 String[] packages = lci.getSharedPackageNamesArray(); 766 mMediaGrants.removeAllMediaGrantsForPackages( 767 packages, /* reason= */ "Mode changed: " + op, userId); 768 } 769 } catch (NameNotFoundException e) { 770 Log.d( 771 TAG, 772 "Unable to resolve uid. Ignoring the AppOp change for " 773 + packageName 774 + ", User : " 775 + userId); 776 } 777 } 778 779 /** 780 * Returns {@code true} if the given {@code op} is one of the appOp 781 * related to read media appOps 782 */ isReadMediaAppOp(String op)783 private boolean isReadMediaAppOp(String op) { 784 return AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE.equals(op) 785 || AppOpsManager.OPSTR_READ_MEDIA_IMAGES.equals(op) 786 || AppOpsManager.OPSTR_READ_MEDIA_VIDEO.equals(op) 787 || AppOpsManager.OPSTR_READ_MEDIA_VISUAL_USER_SELECTED.equals(op); 788 } 789 790 /** 791 * Retrieves a cached calling identity or creates a new one. Also, always sets the app-op 792 * description for the calling identity. 793 */ getCachedCallingIdentityForFuse(int uid)794 private LocalCallingIdentity getCachedCallingIdentityForFuse(int uid) { 795 synchronized (mCachedCallingIdentityForFuse) { 796 PermissionUtils.setOpDescription("via FUSE"); 797 LocalCallingIdentity identity = mCachedCallingIdentityForFuse.get(uid); 798 if (identity == null) { 799 identity = LocalCallingIdentity.fromExternal(getContext(), mUserCache, uid); 800 if (uidToUserId(uid) == sUserId) { 801 mCachedCallingIdentityForFuse.put(uid, identity); 802 } else { 803 // In some app cloning designs, MediaProvider user 0 may 804 // serve requests for apps running as a "clone" user; in 805 // those cases, don't keep a cache for the clone user, since 806 // we don't get any invalidation events for these users. 807 } 808 } 809 return identity; 810 } 811 } 812 813 /** 814 * Calling identity state about on the current thread. Populated on demand, 815 * and invalidated by {@link #onCallingPackageChanged()} when each remote 816 * call is finished. 817 */ 818 private final ThreadLocal<LocalCallingIdentity> mCallingIdentity = ThreadLocal 819 .withInitial(() -> { 820 PermissionUtils.setOpDescription("via MediaProvider"); 821 synchronized (mCachedCallingIdentity) { 822 final LocalCallingIdentity cached = mCachedCallingIdentity 823 .get(Binder.getCallingUid()); 824 return (cached != null) ? cached 825 : LocalCallingIdentity.fromBinder(getContext(), this, mUserCache); 826 } 827 }); 828 829 /** 830 * We simply propagate the UID that is being tracked by 831 * {@link LocalCallingIdentity}, which means we accurately blame both 832 * incoming Binder calls and FUSE calls. 833 */ 834 private final ProxyTransactListener mTransactListener = new ProxyTransactListener() { 835 @Override 836 public Object onTransactStarted(IBinder binder, int transactionCode) { 837 if (LOGV) Trace.beginSection(Thread.currentThread().getStackTrace()[5].getMethodName()); 838 // Check if mCallindIdentity was created within a fuse or content provider transaction 839 if (mCallingIdentity.get().isValidProviderOrFuseCallingIdentity()) { 840 return Binder.setCallingWorkSourceUid(mCallingIdentity.get().uid); 841 } 842 // If mCallingIdentity was not created for a fuse or content provider transaction, 843 // we should reset it, the next time it is retrieved it will be created for the 844 // appropriate caller. 845 mCallingIdentity.remove(); 846 return Binder.setCallingWorkSourceUid(Binder.getCallingUid()); 847 } 848 849 @Override 850 public void onTransactEnded(Object session) { 851 final long token = (long) session; 852 Binder.restoreCallingWorkSource(token); 853 if (LOGV) Trace.endSection(); 854 } 855 }; 856 857 // In memory cache of path<->id mappings, to speed up inserts during media scan 858 @GuardedBy("mDirectoryCache") 859 private final ArrayMap<String, Long> mDirectoryCache = new ArrayMap<>(); 860 861 private static final String[] sDataOnlyColumn = new String[] { 862 FileColumns.DATA 863 }; 864 865 private static final String ID_NOT_PARENT_CLAUSE = 866 "_id NOT IN (SELECT parent FROM files WHERE parent IS NOT NULL)"; 867 868 private static final String CANONICAL = "canonical"; 869 870 private static final String ALL_VOLUMES = "all_volumes"; 871 872 private final BroadcastReceiver mPackageReceiver = new BroadcastReceiver() { 873 @Override 874 public void onReceive(Context context, Intent intent) { 875 switch (intent.getAction()) { 876 case Intent.ACTION_PACKAGE_REMOVED: 877 case Intent.ACTION_PACKAGE_CHANGED: 878 case Intent.ACTION_PACKAGE_ADDED: 879 Uri uri = intent.getData(); 880 String pkg = uri != null ? uri.getSchemeSpecificPart() : null; 881 int uid = intent.getIntExtra(Intent.EXTRA_UID, 0); 882 if (pkg != null) { 883 invalidateLocalCallingIdentityCache(uid, "package " + intent.getAction()); 884 if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) { 885 mUserCache.invalidateWorkProfileOwnerApps(pkg); 886 mPickerSyncController.notifyPackageRemoval(pkg); 887 invalidateDentryForExternalStorage(pkg); 888 } else if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())) { 889 try { 890 // If package has been modified e.g. has been enabled or disabled, 891 // it should be checked against current set of providers. 892 // Hence if a modified package is disable, attempt to remove it from 893 // pickerSyncController. 894 if (!getContext().getPackageManager().getApplicationInfo(pkg, 895 /* flags */ 0).enabled) { 896 Log.d(TAG, "Removing disabled package: " + pkg 897 + " from providers list if required."); 898 mPickerSyncController.notifyPackageRemoval(pkg); 899 } 900 } catch (NameNotFoundException ignored) { 901 // no-op 902 } 903 } 904 } else { 905 Log.w(TAG, "Failed to retrieve package from intent: " + intent.getAction()); 906 } 907 break; 908 } 909 } 910 }; 911 invalidateDentryForExternalStorage(String packageName)912 private void invalidateDentryForExternalStorage(String packageName) { 913 for (MediaVolume vol : mVolumeCache.getExternalVolumes()) { 914 try { 915 invalidateFuseDentry(String.format(Locale.ROOT, 916 "%s/Android/media/%s/", getVolumePath(vol.getName()).getAbsolutePath(), 917 packageName)); 918 } catch (FileNotFoundException e) { 919 Log.e(TAG, "External volume path not found for " + vol.getName(), e); 920 } 921 } 922 } 923 924 private final BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() { 925 @Override 926 public void onReceive(Context context, Intent intent) { 927 switch (intent.getAction()) { 928 case Intent.ACTION_USER_REMOVED: 929 /** 930 * Removing media files for user being deleted. This would impact if the deleted 931 * user have been using same MediaProvider as the current user i.e. when 932 * isMediaSharedWithParent is true.On removal of such user profile, 933 * the owner's MediaProvider would need to clean any media files stored 934 * by the removed user profile. 935 * We also remove the default folder key for the cloned user (just removed) 936 * from user 0's SharedPreferences. Usually, the next clone user would be 937 * created with a different key (as user-id would be incremented), however, if 938 * device is restarted, the next clone-user can use the user-id previously 939 * assigned, causing stale entries in user 0's SharedPreferences 940 */ 941 UserHandle userToBeRemoved = intent.getParcelableExtra(Intent.EXTRA_USER); 942 if(userToBeRemoved.getIdentifier() != sUserId){ 943 mExternalDatabase.runWithTransaction((db) -> { 944 db.execSQL("delete from files where _user_id=?", 945 new String[]{String.valueOf(userToBeRemoved.getIdentifier())}); 946 return null ; 947 }); 948 String userToBeRemovedVolId = null; 949 synchronized (mAttachedVolumes) { 950 for (MediaVolume volume : mAttachedVolumes) { 951 if (userToBeRemoved.equals(volume.getUser())) { 952 userToBeRemovedVolId = volume.getId(); 953 break; 954 } 955 } 956 } 957 //The clone user volume may be unmounted at this time (userToBeRemovedVolId 958 // will be null then), we construct the volId of unmounted vol from userId. 959 String key = DEFAULT_FOLDER_CREATED_KEY_PREFIX 960 + getPrimaryVolumeId(userToBeRemovedVolId, userToBeRemoved); 961 final SharedPreferences prefs = PreferenceManager 962 .getDefaultSharedPreferences(getContext()); 963 if (prefs.getInt(key, /* default */ 0) == 1) { 964 SharedPreferences.Editor editor = prefs.edit(); 965 editor.remove(key); 966 editor.commit(); 967 } 968 } 969 970 boolean isDeviceInDemoMode = false; 971 try { 972 isDeviceInDemoMode = Settings.Global.getInt( 973 getContext().getContentResolver(), Settings.Global.DEVICE_DEMO_MODE) 974 > 0; 975 } catch (Settings.SettingNotFoundException e) { 976 Log.w(TAG, "Exception in reading DEVICE_DEMO_MODE setting", e); 977 } 978 979 Log.i(TAG, "isDeviceInDemoMode: " + isDeviceInDemoMode); 980 // Only allow default system user 0 to update xattrs on /data/media/0 and 981 // only on retail demo devices 982 if (sUserId == UserHandle.SYSTEM.getIdentifier() && isDeviceInDemoMode) { 983 mDatabaseBackupAndRecovery.removeRecoveryDataForUserId( 984 userToBeRemoved.getIdentifier()); 985 } 986 break; 987 } 988 } 989 }; 990 invalidateLocalCallingIdentityCache(String packageName, String reason)991 private void invalidateLocalCallingIdentityCache(String packageName, String reason) { 992 try { 993 int packageUid = getContext().getPackageManager().getPackageUid(packageName, 0); 994 invalidateLocalCallingIdentityCache(packageUid, reason); 995 } catch (NameNotFoundException e) { 996 Log.d(TAG, "Couldn't get uid for package: " + packageName); 997 } 998 } 999 invalidateLocalCallingIdentityCache(int packageUid, String reason)1000 private void invalidateLocalCallingIdentityCache(int packageUid, String reason) { 1001 synchronized (mCachedCallingIdentityForFuse) { 1002 if (mCachedCallingIdentityForFuse.contains(packageUid)) { 1003 mCachedCallingIdentityForFuse.get(packageUid).dump(reason); 1004 mCachedCallingIdentityForFuse.remove(packageUid); 1005 } 1006 } 1007 } 1008 updateQuotaTypeForUri(@onNull FileRow row)1009 protected void updateQuotaTypeForUri(@NonNull FileRow row) { 1010 final String volumeName = row.getVolumeName(); 1011 final String path = row.getPath(); 1012 1013 // Quota type is only updated for external primary volume 1014 if (!MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)) { 1015 return; 1016 } 1017 1018 int mediaType = row.getMediaType(); 1019 Trace.beginSection("MP.updateQuotaTypeForUri"); 1020 File file; 1021 try { 1022 if (path != null) { 1023 file = new File(path); 1024 } else { 1025 // This can happen in case of renames, where the path isn't 1026 // part of the 'new' FileRow data. Fall back to querying 1027 // the path directly. 1028 final Uri uri = MediaStore.Files.getContentUri(row.getVolumeName(), 1029 row.getId()); 1030 if (uri == null) { 1031 // Row could have been deleted 1032 return; 1033 } 1034 file = queryForDataFile(uri, null); 1035 } 1036 if (!file.exists()) { 1037 // This can happen if an item is inserted in MediaStore before it is created 1038 return; 1039 } 1040 1041 if (mediaType == FileColumns.MEDIA_TYPE_NONE) { 1042 // This might be because the file is hidden; but we still want to 1043 // attribute its quota to the correct type, so get the type from 1044 // the extension instead. 1045 mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(file)); 1046 } 1047 1048 updateQuotaTypeForFileInternal(file, mediaType); 1049 } catch (FileNotFoundException | IllegalArgumentException e) { 1050 // Ignore 1051 Log.w(TAG, "Failed to update quota", e); 1052 } finally { 1053 Trace.endSection(); 1054 } 1055 } 1056 updateQuotaTypeForFileInternal(File file, int mediaType)1057 private void updateQuotaTypeForFileInternal(File file, int mediaType) { 1058 try { 1059 switch (mediaType) { 1060 case FileColumns.MEDIA_TYPE_AUDIO: 1061 mStorageManager.updateExternalStorageFileQuotaType(file, 1062 StorageManager.QUOTA_TYPE_MEDIA_AUDIO); 1063 break; 1064 case FileColumns.MEDIA_TYPE_VIDEO: 1065 mStorageManager.updateExternalStorageFileQuotaType(file, 1066 StorageManager.QUOTA_TYPE_MEDIA_VIDEO); 1067 break; 1068 case FileColumns.MEDIA_TYPE_IMAGE: 1069 mStorageManager.updateExternalStorageFileQuotaType(file, 1070 StorageManager.QUOTA_TYPE_MEDIA_IMAGE); 1071 break; 1072 default: 1073 mStorageManager.updateExternalStorageFileQuotaType(file, 1074 StorageManager.QUOTA_TYPE_MEDIA_NONE); 1075 break; 1076 } 1077 } catch (IOException e) { 1078 Log.w(TAG, "Failed to update quota type for " + file.getPath(), e); 1079 } 1080 } 1081 1082 /** 1083 * Since these operations are in the critical path of apps working with 1084 * media, we only collect the {@link Uri} that need to be notified, and all 1085 * other side-effect operations are delegated to {@link BackgroundThread} so 1086 * that we return as quickly as possible. 1087 */ 1088 private final OnFilesChangeListener mFilesListener = new OnFilesChangeListener() { 1089 @Override 1090 public void onInsert(@NonNull DatabaseHelper helper, @NonNull FileRow insertedRow) { 1091 if (helper.isDatabaseRecovering()) { 1092 // Do not perform any trigger operation if database is recovering 1093 return; 1094 } 1095 1096 handleInsertedRowForFuse(insertedRow.getId()); 1097 acceptWithExpansion(helper::notifyInsert, insertedRow.getVolumeName(), 1098 insertedRow.getId(), insertedRow.getMediaType(), insertedRow.isDownload()); 1099 1100 mDatabaseBackupAndRecovery.updateNextRowIdXattr(helper, insertedRow.getId()); 1101 1102 helper.postBackground(() -> { 1103 if (helper.isExternal() && !isFuseThread()) { 1104 // Update the quota type on the filesystem 1105 Uri fileUri = MediaStore.Files.getContentUri(insertedRow.getVolumeName(), 1106 insertedRow.getId()); 1107 updateQuotaTypeForUri(insertedRow); 1108 } 1109 1110 // Tell our SAF provider so it knows when views are no longer empty 1111 MediaDocumentsProvider.onMediaStoreInsert(getContext(), insertedRow.getVolumeName(), 1112 insertedRow.getMediaType(), insertedRow.getId()); 1113 1114 if (mExternalDbFacade.onFileInserted(insertedRow.getMediaType(), 1115 insertedRow.isPending())) { 1116 mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ true, 1117 PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY, null); 1118 } 1119 1120 mDatabaseBackupAndRecovery.backupVolumeDbData(helper, insertedRow); 1121 1122 1123 // check for potentially malicious file creation activity 1124 // to prevent excessive file creation that could exhaust system inodes, 1125 // this check periodically monitors the number of files created by an app. 1126 // if an app exceeds a defined threshold, it is flagged as potentially malicious 1127 if (shouldCheckForMaliciousActivity() 1128 && insertedRow.getVolumeName().equals(MediaStore.VOLUME_EXTERNAL_PRIMARY) 1129 && insertedRow.getId() 1130 % mMaliciousAppDetector.getFrequencyOfMaliciousInsertionCheck() 1131 == 0) { 1132 mMaliciousAppDetector.detectFileCreationByMaliciousApp(getContext(), helper, 1133 insertedRow.getOwnerPackageName()); 1134 } 1135 }); 1136 } 1137 1138 @Override 1139 public void onUpdate(@NonNull DatabaseHelper helper, @NonNull FileRow oldRow, 1140 @NonNull FileRow newRow) { 1141 if (helper.isDatabaseRecovering()) { 1142 // Do not perform any trigger operation if database is recovering 1143 return; 1144 } 1145 1146 final boolean isDownload = oldRow.isDownload() || newRow.isDownload(); 1147 final Uri fileUri = MediaStore.Files.getContentUri(oldRow.getVolumeName(), 1148 oldRow.getId()); 1149 handleUpdatedRowForFuse(oldRow.getPath(), oldRow.getOwnerPackageName(), oldRow.getId(), 1150 newRow.getId()); 1151 handleOwnerPackageNameChange(oldRow.getPath(), oldRow.getOwnerPackageName(), 1152 newRow.getOwnerPackageName()); 1153 acceptWithExpansion(helper::notifyUpdate, oldRow.getVolumeName(), oldRow.getId(), 1154 oldRow.getMediaType(), isDownload); 1155 1156 mDatabaseBackupAndRecovery.updateNextRowIdAndSetDirty(helper, oldRow, newRow); 1157 1158 helper.postBackground(() -> { 1159 if (helper.isExternal()) { 1160 // Update the quota type on the filesystem 1161 updateQuotaTypeForUri(newRow); 1162 } 1163 1164 if (mExternalDbFacade.onFileUpdated(oldRow.getId(), 1165 oldRow.getMediaType(), newRow.getMediaType(), 1166 oldRow.isTrashed(), newRow.isTrashed(), 1167 oldRow.isPending(), newRow.isPending(), 1168 oldRow.isFavorite(), newRow.isFavorite(), 1169 oldRow.getSpecialFormat(), newRow.getSpecialFormat())) { 1170 mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ true, 1171 PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY, null); 1172 } 1173 1174 mDatabaseBackupAndRecovery.updateBackup(helper, oldRow, newRow); 1175 }); 1176 1177 if (newRow.getMediaType() != oldRow.getMediaType()) { 1178 acceptWithExpansion(helper::notifyUpdate, oldRow.getVolumeName(), oldRow.getId(), 1179 newRow.getMediaType(), isDownload); 1180 1181 helper.postBackground(() -> { 1182 // Invalidate any thumbnails when the media type changes 1183 invalidateThumbnails(fileUri); 1184 }); 1185 } 1186 } 1187 1188 @Override 1189 public void onDelete(@NonNull DatabaseHelper helper, @NonNull FileRow deletedRow) { 1190 if (helper.isDatabaseRecovering()) { 1191 // Do not perform any trigger operation if database is recovering 1192 return; 1193 } 1194 1195 handleDeletedRowForFuse(deletedRow.getPath(), deletedRow.getOwnerPackageName(), 1196 deletedRow.getId()); 1197 acceptWithExpansion(helper::notifyDelete, deletedRow.getVolumeName(), 1198 deletedRow.getId(), deletedRow.getMediaType(), deletedRow.isDownload()); 1199 1200 // Remove cached transcoded file if any 1201 mTranscodeHelper.deleteCachedTranscodeFile(deletedRow.getId()); 1202 mPhotoPickerTranscodeHelper.deleteCachedTranscodedFile( 1203 PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY, deletedRow.getId()); 1204 1205 helper.postBackground(() -> { 1206 // Item no longer exists, so revoke all access to it 1207 Trace.beginSection("MP.revokeUriPermission"); 1208 try { 1209 acceptWithExpansion((uri) -> getContext().revokeUriPermission(uri, ~0), 1210 deletedRow.getVolumeName(), deletedRow.getId(), 1211 deletedRow.getMediaType(), deletedRow.isDownload()); 1212 } finally { 1213 Trace.endSection(); 1214 } 1215 1216 switch (deletedRow.getMediaType()) { 1217 case FileColumns.MEDIA_TYPE_PLAYLIST: 1218 case FileColumns.MEDIA_TYPE_AUDIO: 1219 if (helper.isExternal()) { 1220 removePlaylistMembers(deletedRow.getMediaType(), deletedRow.getId()); 1221 } 1222 } 1223 1224 // Invalidate any thumbnails now that media is gone 1225 invalidateThumbnails(MediaStore.Files.getContentUri(deletedRow.getVolumeName(), 1226 deletedRow.getId())); 1227 1228 // Tell our SAF provider so it can revoke too 1229 MediaDocumentsProvider.onMediaStoreDelete(getContext(), deletedRow.getVolumeName(), 1230 deletedRow.getMediaType(), deletedRow.getId()); 1231 1232 if (mExternalDbFacade.onFileDeleted(deletedRow.getId(), 1233 deletedRow.getMediaType())) { 1234 mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ true, 1235 PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY, null); 1236 } 1237 1238 mDatabaseBackupAndRecovery.deleteFromDbBackup(helper, deletedRow); 1239 if (deletedRow.getVolumeName() != null 1240 && deletedRow.getVolumeName().equalsIgnoreCase(VOLUME_EXTERNAL_PRIMARY)) { 1241 mExternalPrimaryBackupExecutor.deleteBackupForPath(deletedRow.getPath()); 1242 } 1243 }); 1244 } 1245 }; 1246 1247 private final UnaryOperator<String> mIdGenerator = path -> { 1248 final long rowId = mCallingIdentity.get().getDeletedRowId(path); 1249 if (rowId != -1 && isFuseThread()) { 1250 return String.valueOf(rowId); 1251 } 1252 return null; 1253 }; 1254 1255 /** {@hide} */ 1256 public static final OnLegacyMigrationListener MIGRATION_LISTENER = 1257 new OnLegacyMigrationListener() { 1258 @Override 1259 public void onStarted(ContentProviderClient client, String volumeName) { 1260 MediaStore.startLegacyMigration(ContentResolver.wrap(client), volumeName); 1261 } 1262 1263 @Override 1264 public void onProgress(ContentProviderClient client, String volumeName, 1265 long progress, long total) { 1266 // TODO: notify blocked threads of progress once we can change APIs 1267 } 1268 1269 @Override 1270 public void onFinished(ContentProviderClient client, String volumeName) { 1271 MediaStore.finishLegacyMigration(ContentResolver.wrap(client), volumeName); 1272 } 1273 }; 1274 1275 /** 1276 * Apply {@link Consumer#accept} to the given item. 1277 * <p> 1278 * Since media items can be exposed through multiple collections or views, 1279 * this method expands the single item being accepted to also accept all 1280 * relevant views. 1281 */ acceptWithExpansion(@onNull Consumer<Uri> consumer, @NonNull String volumeName, long id, int mediaType, boolean isDownload)1282 private void acceptWithExpansion(@NonNull Consumer<Uri> consumer, @NonNull String volumeName, 1283 long id, int mediaType, boolean isDownload) { 1284 switch (mediaType) { 1285 case FileColumns.MEDIA_TYPE_AUDIO: 1286 consumer.accept(MediaStore.Audio.Media.getContentUri(volumeName, id)); 1287 1288 // Any changing audio items mean we probably need to invalidate all 1289 // indexed views built from that media 1290 consumer.accept(Audio.Genres.getContentUri(volumeName)); 1291 consumer.accept(Audio.Playlists.getContentUri(volumeName)); 1292 consumer.accept(Audio.Artists.getContentUri(volumeName)); 1293 consumer.accept(Audio.Albums.getContentUri(volumeName)); 1294 break; 1295 1296 case FileColumns.MEDIA_TYPE_VIDEO: 1297 consumer.accept(MediaStore.Video.Media.getContentUri(volumeName, id)); 1298 break; 1299 1300 case FileColumns.MEDIA_TYPE_IMAGE: 1301 consumer.accept(MediaStore.Images.Media.getContentUri(volumeName, id)); 1302 break; 1303 1304 case FileColumns.MEDIA_TYPE_PLAYLIST: 1305 consumer.accept(ContentUris.withAppendedId( 1306 MediaStore.Audio.Playlists.getContentUri(volumeName), id)); 1307 break; 1308 } 1309 1310 // Also notify through any generic views 1311 consumer.accept(MediaStore.Files.getContentUri(volumeName, id)); 1312 if (isDownload) { 1313 consumer.accept(MediaStore.Downloads.getContentUri(volumeName, id)); 1314 } 1315 1316 // Rinse and repeat through any synthetic views 1317 switch (volumeName) { 1318 case MediaStore.VOLUME_INTERNAL: 1319 case MediaStore.VOLUME_EXTERNAL: 1320 // Already a top-level view, no need to expand 1321 break; 1322 default: 1323 acceptWithExpansion(consumer, MediaStore.VOLUME_EXTERNAL, 1324 id, mediaType, isDownload); 1325 break; 1326 } 1327 } 1328 1329 @VisibleForTesting getDefaultFolderNames()1330 protected String[] getDefaultFolderNames() { 1331 return DEFAULT_FOLDER_NAMES; 1332 } 1333 1334 @VisibleForTesting getFoldersToSkipInDefaultCreation()1335 protected List<String> getFoldersToSkipInDefaultCreation() { 1336 return StringUtils.getStringArrayConfig(getContext(), 1337 R.array.config_foldersToSkipInDefaultCreation); 1338 } 1339 1340 /** 1341 * Ensure that default folders are created on mounted storage devices. 1342 * We only do this once per volume so we don't annoy the user if deleted 1343 * manually. Folders in the exclusion list are not created. 1344 */ 1345 @VisibleForTesting ensureDefaultFolders(@onNull MediaVolume volume, @NonNull SQLiteDatabase db)1346 protected void ensureDefaultFolders(@NonNull MediaVolume volume, @NonNull SQLiteDatabase db) { 1347 if (volume.shouldSkipDefaultDirCreation()) { 1348 // Default folders should not be automatically created inside volumes managed from 1349 // outside Android. 1350 return; 1351 } 1352 final String volumeName = volume.getName(); 1353 String key; 1354 if (volumeName.equals(MediaStore.VOLUME_EXTERNAL_PRIMARY)) { 1355 // For the primary volume, we use the ID, because we may be handling 1356 // the primary volume for multiple users 1357 key = DEFAULT_FOLDER_CREATED_KEY_PREFIX 1358 + getPrimaryVolumeId(volume.getId(), volume.getUser()); 1359 } else { 1360 // For others, like public volumes, just use the name, because the id 1361 // might not change when re-formatted 1362 key = DEFAULT_FOLDER_CREATED_KEY_PREFIX + volumeName; 1363 } 1364 1365 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); 1366 if (prefs.getInt(key, 0) == 0) { 1367 // Get case insensitive exclusion list. 1368 List<String> exclusionList = 1369 Flags.enableExclusionListForDefaultFolders() 1370 ? getFoldersToSkipInDefaultCreation().stream().map( 1371 String::toLowerCase).collect(Collectors.toList()) 1372 : List.of(); 1373 if (exclusionList.size() > getDefaultFolderNames().length) { 1374 Log.e(TAG, "Exclusion list has " + exclusionList.size() 1375 + " items which exceeds the size of default folders list which has size " 1376 + getDefaultFolderNames().length); 1377 exclusionList = List.of(); 1378 } 1379 for (String folderName : getDefaultFolderNames()) { 1380 final File folder = new File(volume.getPath(), folderName); 1381 if (folder.exists()) { 1382 continue; 1383 } 1384 if (Flags.enableExclusionListForDefaultFolders() && exclusionList.contains( 1385 folderName.toLowerCase(Locale.ROOT))) { 1386 // Do not create mobile-centric folders for PC. 1387 Log.d(TAG, "Excluding " + folder + " from default creation"); 1388 continue; 1389 } 1390 folder.mkdirs(); 1391 insertDirectory(db, folder.getAbsolutePath()); 1392 } 1393 1394 SharedPreferences.Editor editor = prefs.edit(); 1395 editor.putInt(key, 1); 1396 editor.commit(); 1397 } 1398 } 1399 1400 /** 1401 * Returns the volume id for Primary External Volumes. 1402 * If volId is supplied, it is returned as-is, in case it is not, user-id is used to 1403 * construct the id for Primary External Volume. 1404 * 1405 * @param volId the id of the Volume in consideration. 1406 * @param userId userId for which primary volume id needs to be determined. 1407 * @return the primary volume id. 1408 */ getPrimaryVolumeId(String volId, UserHandle userId)1409 private String getPrimaryVolumeId(String volId, UserHandle userId) { 1410 if (volId == null) { 1411 // The construction is based upon system/vold/model/EmulatedVolume.cpp 1412 // Should be kept in sync with the same. 1413 return "emulated;" + userId.getIdentifier(); 1414 } 1415 return volId; 1416 } 1417 1418 /** 1419 * Ensure that any thumbnail collections on the given storage volume can be 1420 * used with the given {@link DatabaseHelper}. If the 1421 * {@link DatabaseHelper#getOrCreateUuid} doesn't match the UUID found on 1422 * disk, then all thumbnails will be considered stable and will be deleted. 1423 */ ensureThumbnailsValid(@onNull MediaVolume volume, @NonNull SQLiteDatabase db)1424 private void ensureThumbnailsValid(@NonNull MediaVolume volume, @NonNull SQLiteDatabase db) { 1425 if (volume.shouldSkipDefaultDirCreation()) { 1426 // Default folders and thumbnail directories should not be automatically created inside 1427 // volumes managed from outside Android, and there is no need to ensure the validity of 1428 // their thumbnails here. 1429 return; 1430 } 1431 final String uuidFromDatabase = DatabaseHelper.getOrCreateUuid(db); 1432 try { 1433 for (File dir : getThumbnailDirectories(volume)) { 1434 if (!dir.exists()) { 1435 dir.mkdirs(); 1436 } 1437 1438 final File file = new File(dir, FILE_DATABASE_UUID); 1439 final Optional<String> uuidFromDisk = FileUtils.readString(file); 1440 1441 final boolean updateUuid; 1442 if (!uuidFromDisk.isPresent()) { 1443 // For newly inserted volumes or upgrading of existing volumes, 1444 // assume that our current UUID is valid 1445 updateUuid = true; 1446 } else if (!Objects.equals(uuidFromDatabase, uuidFromDisk.get())) { 1447 // The UUID of database disagrees with the one on disk, 1448 // which means we can't trust any thumbnails 1449 Log.d(TAG, "Invalidating all thumbnails under " + dir); 1450 FileUtils.walkFileTreeContents(dir.toPath(), this::deleteAndInvalidate); 1451 updateUuid = true; 1452 } else { 1453 updateUuid = false; 1454 } 1455 1456 if (updateUuid) { 1457 FileUtils.writeString(file, Optional.of(uuidFromDatabase)); 1458 } 1459 } 1460 } catch (IOException e) { 1461 Log.w(TAG, "Failed to ensure thumbnails valid for " + volume.getName(), e); 1462 } 1463 } 1464 1465 @Override attachInfo(Context context, ProviderInfo info)1466 public void attachInfo(Context context, ProviderInfo info) { 1467 Log.v(TAG, "Attached " + info.authority + " from " + info.applicationInfo.packageName); 1468 1469 mUriMatcher = new LocalUriMatcher(info.authority); 1470 1471 super.attachInfo(context, info); 1472 } 1473 1474 @Nullable 1475 private static MediaProvider sInstance; 1476 1477 @Nullable getInstance()1478 static synchronized MediaProvider getInstance() { 1479 return sInstance; 1480 } 1481 1482 @Override onCreate()1483 public boolean onCreate() { 1484 synchronized (MediaProvider.class) { 1485 sInstance = this; 1486 } 1487 1488 final Context context = getContext(); 1489 1490 mUserCache = new UserCache(context); 1491 1492 // Shift call statistics back to the original caller 1493 Binder.setProxyTransactListener(mTransactListener); 1494 1495 mStorageManager = context.getSystemService(StorageManager.class); 1496 AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class); 1497 mPackageManager = context.getPackageManager(); 1498 mUserManager = context.getSystemService(UserManager.class); 1499 mVolumeCache = new VolumeCache(context, mUserCache); 1500 1501 // Reasonable thumbnail size is half of the smallest screen edge width 1502 final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); 1503 final int thumbSize = Math.min(metrics.widthPixels, metrics.heightPixels) / 2; 1504 mThumbSize = new Size(thumbSize, thumbSize); 1505 1506 mConfigStore = createConfigStore(); 1507 mDatabaseBackupAndRecovery = createDatabaseBackupAndRecovery(); 1508 1509 mMediaScanner = new ModernMediaScanner(context, mConfigStore); 1510 mProjectionHelper = new ProjectionHelper(Column.class, ExportedSince.class); 1511 mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, false, false, 1512 mProjectionHelper, Metrics::logSchemaChange, mFilesListener, 1513 MIGRATION_LISTENER, mIdGenerator, true, mDatabaseBackupAndRecovery); 1514 mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME, false, false, 1515 mProjectionHelper, Metrics::logSchemaChange, mFilesListener, 1516 MIGRATION_LISTENER, mIdGenerator, true, mDatabaseBackupAndRecovery); 1517 mExternalDbFacade = new ExternalDbFacade(getContext(), mExternalDatabase, mVolumeCache); 1518 1519 mMediaGrants = new MediaGrants(mExternalDatabase); 1520 mFilesOwnershipUtils = new FilesOwnershipUtils(mExternalDatabase); 1521 1522 PickerSyncLockManager pickerSyncLockManager = new PickerSyncLockManager(); 1523 mPickerDbFacade = new PickerDbFacade(context, pickerSyncLockManager); 1524 mPickerSyncController = PickerSyncController.initialize(context, mPickerDbFacade, 1525 mConfigStore, pickerSyncLockManager); 1526 mPickerDataLayer = PickerDataLayer.create(context, mPickerDbFacade, mPickerSyncController, 1527 mConfigStore); 1528 mPhotoPickerTranscodeHelper = new PhotoPickerTranscodeHelper(); 1529 mPickerUriResolver = new PickerUriResolver(context, mPickerDbFacade, mProjectionHelper, 1530 mUriMatcher); 1531 mAsyncPickerFileOpener = new AsyncPickerFileOpener(this, mPickerUriResolver); 1532 1533 mExternalPrimaryBackupExecutor = new BackupExecutor(getContext(), mExternalDatabase); 1534 1535 if (SdkLevel.isAtLeastS()) { 1536 mTranscodeHelper = new TranscodeHelperImpl(context, this, mConfigStore); 1537 } else { 1538 mTranscodeHelper = new TranscodeHelperNoOp(); 1539 } 1540 1541 final IntentFilter packageFilter = new IntentFilter(); 1542 packageFilter.setPriority(10); 1543 packageFilter.addDataScheme("package"); 1544 packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 1545 packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); 1546 packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); 1547 context.registerReceiver(mPackageReceiver, packageFilter); 1548 1549 // Creating intent broadcast receiver for user actions like Intent.ACTION_USER_REMOVED, 1550 // where we would need to remove files stored by removed user. 1551 final IntentFilter userIntentFilter = new IntentFilter(); 1552 userIntentFilter.addAction(Intent.ACTION_USER_REMOVED); 1553 context.registerReceiver(mUserIntentReceiver, userIntentFilter); 1554 1555 // Watch for invalidation of cached volumes 1556 mStorageManager.registerStorageVolumeCallback(context.getMainExecutor(), 1557 new StorageVolumeCallback() { 1558 @Override 1559 public void onStateChanged(@NonNull StorageVolume volume) { 1560 updateVolumes(); 1561 } 1562 }); 1563 1564 if (SdkLevel.isAtLeastT()) { 1565 try { 1566 mStorageManager.setCloudMediaProvider(mPickerSyncController.getCloudProvider()); 1567 } catch (SecurityException e) { 1568 // This can happen in unit tests 1569 Log.w(TAG, "Failed to update the system_server with the latest cloud provider", e); 1570 } 1571 } 1572 1573 updateVolumes(); 1574 attachVolume(MediaVolume.fromInternal(), /* validate */ false, /* volumeState */ null); 1575 for (MediaVolume volume : mVolumeCache.getExternalVolumes()) { 1576 attachVolume(volume, /* validate */ false, /* volumeState */ null); 1577 } 1578 1579 // Watch for performance-sensitive activity 1580 appOpsManager.startWatchingActive(new String[] { 1581 AppOpsManager.OPSTR_CAMERA 1582 }, context.getMainExecutor(), mActiveListener); 1583 1584 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE, 1585 null /* all packages */, mModeListener); 1586 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_AUDIO, 1587 null /* all packages */, mModeListener); 1588 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_IMAGES, 1589 null /* all packages */, mModeListener); 1590 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_VIDEO, 1591 null /* all packages */, mModeListener); 1592 if (SdkLevel.isAtLeastU()) { 1593 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_VISUAL_USER_SELECTED, 1594 null /* all packages */, mModeListener); 1595 } 1596 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_EXTERNAL_STORAGE, 1597 null /* all packages */, mModeListener); 1598 appOpsManager.startWatchingMode(permissionToOp(ACCESS_MEDIA_LOCATION), 1599 null /* all packages */, mModeListener); 1600 // Legacy apps 1601 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_LEGACY_STORAGE, 1602 null /* all packages */, mModeListener); 1603 // File managers 1604 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_MANAGE_EXTERNAL_STORAGE, 1605 null /* all packages */, mModeListener); 1606 // Default gallery changes 1607 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES, 1608 null /* all packages */, mModeListener); 1609 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO, 1610 null /* all packages */, mModeListener); 1611 try { 1612 // Here we are forced to depend on the non-public API of AppOpsManager. If 1613 // OPSTR_NO_ISOLATED_STORAGE app op is not defined in AppOpsManager, then this call will 1614 // throw an IllegalArgumentException during MediaProvider startup. In combination with 1615 // MediaProvider's CTS tests it should give us guarantees that OPSTR_NO_ISOLATED_STORAGE 1616 // is defined. 1617 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_NO_ISOLATED_STORAGE, 1618 null /* all packages */, mModeListener); 1619 } catch (IllegalArgumentException e) { 1620 Log.w(TAG, "Failed to start watching " + AppOpsManager.OPSTR_NO_ISOLATED_STORAGE, e); 1621 } 1622 1623 ProviderInfo provider = mPackageManager.resolveContentProvider( 1624 getDownloadsProviderAuthority(), PackageManager.MATCH_DIRECT_BOOT_AWARE 1625 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE); 1626 if (provider != null) { 1627 mDownloadsAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid); 1628 } 1629 1630 provider = mPackageManager.resolveContentProvider(getExternalStorageProviderAuthority(), 1631 PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE); 1632 if (provider != null) { 1633 mExternalStorageAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid); 1634 } 1635 1636 storageNativeBootPropertyChangeListener(); 1637 mConfigStore.addOnChangeListener( 1638 BackgroundThread.getExecutor(), this::storageNativeBootPropertyChangeListener); 1639 1640 PulledMetrics.initialize(context); 1641 mMaliciousAppDetector = createMaliciousAppDetector(); 1642 1643 initializeMimeTypeFixHandlerForAndroid15(getContext()); 1644 1645 return true; 1646 } 1647 1648 @VisibleForTesting storageNativeBootPropertyChangeListener()1649 protected void storageNativeBootPropertyChangeListener() { 1650 1651 // Notify the Photopicker that DeviceConfig has changed for T+ devices. 1652 Intent intent = new Intent(Intent.ACTION_MAIN); 1653 if (SdkLevel.isAtLeastT()) { 1654 getContext().sendBroadcast(intent, MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION); 1655 } 1656 1657 boolean isGetContentTakeoverEnabled = false; 1658 1659 if (SdkLevel.isAtLeastT()) { 1660 isGetContentTakeoverEnabled = true; 1661 } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { 1662 isGetContentTakeoverEnabled = true; 1663 } else { 1664 isGetContentTakeoverEnabled = mConfigStore.isGetContentTakeOverEnabled(); 1665 } 1666 setComponentEnabledSetting( 1667 "PhotoPickerGetContentActivity", isGetContentTakeoverEnabled); 1668 1669 // Always make sure PhotoPickerActivity is enabled. 1670 setComponentEnabledSetting( 1671 "PhotoPickerActivity", true); 1672 1673 // Always make sure PhotoPickerUserSelectActivity is enabled. 1674 setComponentEnabledSetting( 1675 "PhotoPickerUserSelectActivity", true); 1676 } 1677 getDatabaseBackupAndRecovery()1678 public DatabaseBackupAndRecovery getDatabaseBackupAndRecovery() { 1679 return mDatabaseBackupAndRecovery; 1680 } 1681 setComponentEnabledSetting(@onNull String activityName, boolean isEnabled)1682 private void setComponentEnabledSetting(@NonNull String activityName, boolean isEnabled) { 1683 final String activityFullName = 1684 PhotoPickerActivity.class.getPackage().getName() + "." + activityName; 1685 final ComponentName componentName = new ComponentName(getContext().getPackageName(), 1686 activityFullName); 1687 1688 final int expectedState = isEnabled 1689 ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED 1690 : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; 1691 1692 Log.i(TAG, "Changed " + activityName + " component state to " 1693 + componentStateToString(expectedState)); 1694 1695 getContext().getPackageManager().setComponentEnabledSetting(componentName, expectedState, 1696 PackageManager.DONT_KILL_APP); 1697 } 1698 getDatabaseHelper(String dbName)1699 Optional<DatabaseHelper> getDatabaseHelper(String dbName) { 1700 if (dbName.equalsIgnoreCase(INTERNAL_DATABASE_NAME)) { 1701 return Optional.of(mInternalDatabase); 1702 } else if (dbName.equalsIgnoreCase(EXTERNAL_DATABASE_NAME)) { 1703 return Optional.of(mExternalDatabase); 1704 } 1705 1706 return Optional.empty(); 1707 } 1708 1709 @Override onCallingPackageChanged()1710 public void onCallingPackageChanged() { 1711 // Identity of the current thread has changed, so invalidate caches 1712 mCallingIdentity.remove(); 1713 } 1714 clearLocalCallingIdentity()1715 public LocalCallingIdentity clearLocalCallingIdentity() { 1716 // We retain the user part of the calling identity, since we are executing 1717 // the call on behalf of that user, and we need to maintain the user context 1718 // to correctly resolve things like volumes 1719 UserHandle user = mCallingIdentity.get().getUser(); 1720 return clearLocalCallingIdentity(LocalCallingIdentity.fromSelfAsUser(getContext(), user)); 1721 } 1722 clearLocalCallingIdentity(LocalCallingIdentity replacement)1723 public LocalCallingIdentity clearLocalCallingIdentity(LocalCallingIdentity replacement) { 1724 final LocalCallingIdentity token = mCallingIdentity.get(); 1725 mCallingIdentity.set(replacement); 1726 return token; 1727 } 1728 restoreLocalCallingIdentity(LocalCallingIdentity token)1729 public void restoreLocalCallingIdentity(LocalCallingIdentity token) { 1730 mCallingIdentity.set(token); 1731 } 1732 1733 /** 1734 * Adds the mapping from thread id to uid in PendingOpen map. 1735 */ addToPendingOpenMap(int tid, int uid)1736 public void addToPendingOpenMap(int tid, int uid) { 1737 synchronized (mPendingOpenInfo) { 1738 mPendingOpenInfo.put(tid, new PendingOpenInfo(uid, /* mediaCapabilitiesUid */ 0, 1739 /* shouldRedact */ false, /* transcodeReason */ 0)); 1740 } 1741 } 1742 1743 /** 1744 * Removes the pending open info for the passed thread i from PendingOpen map. 1745 */ removeFromPendingOpenMap(int tid)1746 public void removeFromPendingOpenMap(int tid) { 1747 synchronized (mPendingOpenInfo) { 1748 mPendingOpenInfo.remove(tid); 1749 } 1750 } 1751 isPackageKnown(@onNull String packageName, int userId)1752 private boolean isPackageKnown(@NonNull String packageName, int userId) { 1753 final Context context = mUserCache.getContextForUser(UserHandle.of(userId)); 1754 final PackageManager pm = context.getPackageManager(); 1755 1756 // First, is the app actually installed? 1757 try { 1758 pm.getPackageInfo(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES); 1759 return true; 1760 } catch (NameNotFoundException ignored) { 1761 } 1762 1763 // Second, is the app pending, probably from a backup/restore operation? 1764 // Cloned app installations do not have a linked install session, so skipping the check in 1765 // case the user-id is a clone profile. 1766 if (!isAppCloneUserForFuse(userId)) { 1767 if (sUserId != userId) { 1768 // Skip the package check and ensure media provider doesn't crash 1769 // Returning true since we are unsure what caused the cross-user entries to be in 1770 // the database and want to avoid deleting data that might be required. 1771 Log.e(TAG, "Skip pruning cross-user entries stored in database for package: " 1772 + packageName + " userId: " + userId + " processUserId: " + sUserId); 1773 return true; 1774 } 1775 for (SessionInfo si : pm.getPackageInstaller().getAllSessions()) { 1776 if (Objects.equals(packageName, si.getAppPackageName())) { 1777 return true; 1778 } 1779 } 1780 } else { 1781 Log.e(TAG, "Cross-user entries found in database for package " + packageName 1782 + " userId: " + userId + " processUserId: " + sUserId); 1783 } 1784 1785 // I've never met this package in my life 1786 return false; 1787 } 1788 onIdleMaintenance(@onNull CancellationSignal signal)1789 public void onIdleMaintenance(@NonNull CancellationSignal signal) { 1790 final long startTime = SystemClock.elapsedRealtime(); 1791 1792 // Print # of deleted files 1793 synchronized (mCachedCallingIdentityForFuse) { 1794 for (int i = 0; i < mCachedCallingIdentityForFuse.size(); i++) { 1795 mCachedCallingIdentityForFuse.valueAt(i).dump("Idle maintenance"); 1796 } 1797 } 1798 1799 // Trim any stale log files before we emit new events below 1800 Logging.trimPersistent(); 1801 1802 // Scan all volumes to resolve any staleness 1803 for (MediaVolume volume : mVolumeCache.getExternalVolumes()) { 1804 // Possibly bail before digging into each volume 1805 signal.throwIfCanceled(); 1806 1807 try { 1808 MediaService.onScanVolume(getContext(), volume, REASON_IDLE); 1809 } catch (IOException | IllegalArgumentException e) { 1810 Log.w(TAG, "Failure in " + volume.getName() + " volume scan", e); 1811 } 1812 1813 // Ensure that our thumbnails are valid 1814 mExternalDatabase.runWithTransaction((db) -> { 1815 ensureThumbnailsValid(volume, db); 1816 return null; 1817 }); 1818 } 1819 BackupAndRestoreUtils.doCleanUpAfterRestoreIfRequired(getContext()); 1820 1821 // Delete any stale thumbnails 1822 final int staleThumbnails = mExternalDatabase.runWithTransaction((db) -> { 1823 return pruneThumbnails(db, signal); 1824 }); 1825 Log.d(TAG, "Pruned " + staleThumbnails + " unknown thumbnails"); 1826 1827 // Finished orphaning any content whose package no longer exists 1828 pruneStalePackages(signal); 1829 1830 // Delete the expired items or extend them on mounted volumes 1831 final int[] result = deleteOrExtendExpiredItems(signal); 1832 final int deletedExpiredMedia = result[0]; 1833 Log.d(TAG, "Deleted " + deletedExpiredMedia + " expired items"); 1834 Log.d(TAG, "Extended " + result[1] + " expired items"); 1835 1836 // Forget any stale volumes 1837 deleteStaleVolumes(signal); 1838 1839 final long itemCount = mExternalDatabase.runWithTransaction(DatabaseHelper::getItemCount); 1840 1841 // Clean picker transcoded media cache. 1842 mPhotoPickerTranscodeHelper.cleanAllTranscodedFiles(signal); 1843 1844 // Cleaning media files for users that have been removed 1845 cleanMediaFilesForRemovedUser(signal); 1846 1847 // Calculate standard_mime_type_extension column for files which have SPECIAL_FORMAT column 1848 // value as NULL, and update the same in the picker db 1849 detectSpecialFormat(signal); 1850 1851 mExternalPrimaryBackupExecutor.doBackup(signal); 1852 1853 // In Android 15, certain MIME types were introduced that are not supported, this fixes 1854 // existing data with these unsupported MIME types 1855 fixUnsupportedMimeTypesForAndroid15(getContext()); 1856 1857 final long durationMillis = (SystemClock.elapsedRealtime() - startTime); 1858 Metrics.logIdleMaintenance(MediaStore.VOLUME_EXTERNAL, itemCount, 1859 durationMillis, staleThumbnails, deletedExpiredMedia); 1860 } 1861 1862 /** 1863 * This function find and clean the files related to user who have been removed 1864 */ cleanMediaFilesForRemovedUser(CancellationSignal signal)1865 private void cleanMediaFilesForRemovedUser(CancellationSignal signal) { 1866 //Finding userIds that are available in database 1867 final List<String> userIds = mExternalDatabase.runWithTransaction((db) -> { 1868 final List<String> userIdsPresent = new ArrayList<>(); 1869 try (Cursor c = db.query(true, "files", new String[] { "_user_id" }, 1870 null, null, null, null, null, 1871 null, signal)) { 1872 while (c.moveToNext()) { 1873 final String userId = c.getString(0); 1874 userIdsPresent.add(userId); 1875 } 1876 } 1877 return userIdsPresent; 1878 }); 1879 1880 // removing calling userId 1881 userIds.remove(String.valueOf(sUserId)); 1882 1883 List<String> validUserProfiles = mUserManager.getEnabledProfiles().stream() 1884 .map(userHandle -> String.valueOf(userHandle.getIdentifier())).collect( 1885 Collectors.toList()); 1886 // removing all the valid/existing user, remaining userIds would be users who would have 1887 // been removed 1888 userIds.removeAll(validUserProfiles); 1889 1890 // Cleaning media files of users who have been removed 1891 mExternalDatabase.runWithTransaction((db) -> { 1892 userIds.stream().forEach(userId ->{ 1893 Log.d(TAG, "Removing media files associated with user : " + userId); 1894 db.execSQL("delete from files where _user_id=?", 1895 new String[]{String.valueOf(userId)}); 1896 }); 1897 return null ; 1898 }); 1899 1900 boolean isDeviceInDemoMode = false; 1901 try { 1902 isDeviceInDemoMode = Settings.Global.getInt(getContext().getContentResolver(), 1903 Settings.Global.DEVICE_DEMO_MODE) > 0; 1904 } catch (Settings.SettingNotFoundException e) { 1905 Log.w(TAG, "Exception in reading DEVICE_DEMO_MODE setting", e); 1906 } 1907 1908 Log.i(TAG, "isDeviceInDemoMode: " + isDeviceInDemoMode); 1909 // Only allow default system user 0 to update xattrs on /data/media/0 and only when 1910 // device is in retail mode 1911 if (sUserId == UserHandle.SYSTEM.getIdentifier() && isDeviceInDemoMode) { 1912 List<String> validUsers = mUserManager.getUserHandles(/* excludeDying */ true).stream() 1913 .map(userHandle -> String.valueOf(userHandle.getIdentifier())).collect( 1914 Collectors.toList()); 1915 Log.i(TAG, "Active user ids are:" + validUsers); 1916 mDatabaseBackupAndRecovery.removeRecoveryDataExceptValidUsers(validUsers); 1917 } 1918 } 1919 pruneStalePackages(CancellationSignal signal)1920 private void pruneStalePackages(CancellationSignal signal) { 1921 final int stalePackages = mExternalDatabase.runWithTransaction((db) -> { 1922 final ArraySet<Pair<String, Integer>> unknownPackages = new ArraySet<>(); 1923 try (Cursor c = db.query(true, "files", 1924 new String[] { "owner_package_name", "_user_id" }, 1925 null, null, null, null, null, null, signal)) { 1926 while (c.moveToNext()) { 1927 final String packageName = c.getString(0); 1928 if (TextUtils.isEmpty(packageName)) continue; 1929 1930 final int userId = c.getInt(1); 1931 1932 if (!isPackageKnown(packageName, userId)) { 1933 unknownPackages.add(Pair.create(packageName, userId)); 1934 } 1935 } 1936 } 1937 for (Pair<String, Integer> pair : unknownPackages) { 1938 onPackageOrphaned(db, pair.first, pair.second); 1939 } 1940 return unknownPackages.size(); 1941 }); 1942 Log.d(TAG, "Pruned " + stalePackages + " unknown packages"); 1943 } 1944 deleteStaleVolumes(CancellationSignal signal)1945 private void deleteStaleVolumes(CancellationSignal signal) { 1946 mExternalDatabase.runWithTransaction((db) -> { 1947 final Set<String> recentVolumeNames = MediaStore 1948 .getRecentExternalVolumeNames(getContext()); 1949 final Set<String> knownVolumeNames = new ArraySet<>(); 1950 try (Cursor c = db.query(true, "files", new String[] { MediaColumns.VOLUME_NAME }, 1951 null, null, null, null, null, null, signal)) { 1952 while (c.moveToNext()) { 1953 knownVolumeNames.add(c.getString(0)); 1954 } 1955 } 1956 final Set<String> staleVolumeNames = new ArraySet<>(); 1957 staleVolumeNames.addAll(knownVolumeNames); 1958 staleVolumeNames.removeAll(recentVolumeNames); 1959 for (String staleVolumeName : staleVolumeNames) { 1960 final int num = db.delete("files", FileColumns.VOLUME_NAME + "=?", 1961 new String[] { staleVolumeName }); 1962 Log.d(TAG, "Forgot " + num + " stale items from " + staleVolumeName); 1963 mDatabaseBackupAndRecovery.deleteBackupForVolume(staleVolumeName); 1964 } 1965 return null; 1966 }); 1967 1968 synchronized (mDirectoryCache) { 1969 mDirectoryCache.clear(); 1970 } 1971 } 1972 1973 @VisibleForTesting setUriResolver(PickerUriResolver resolver)1974 public void setUriResolver(PickerUriResolver resolver) { 1975 Log.w(TAG, "Changing the PickerUriResolver!!! Should only be called during test"); 1976 mPickerUriResolver = resolver; 1977 } 1978 1979 @VisibleForTesting detectSpecialFormat(@onNull CancellationSignal signal)1980 void detectSpecialFormat(@NonNull CancellationSignal signal) { 1981 // Picker sync and special format update can execute concurrently and run into a deadlock. 1982 // Acquiring a lock before execution of each flow to avoid this. 1983 PickerSyncController.sIdleMaintenanceSyncLock.lock(); 1984 try { 1985 mExternalDatabase.runWithTransaction((db) -> { 1986 updateSpecialFormatColumn(db, signal); 1987 return null; 1988 }); 1989 } finally { 1990 PickerSyncController.sIdleMaintenanceSyncLock.unlock(); 1991 } 1992 } 1993 fixUnsupportedMimeTypesForAndroid15(Context context)1994 private void fixUnsupportedMimeTypesForAndroid15(Context context) { 1995 if (!Flags.enableMimeTypeFixForAndroid15()) { 1996 return; 1997 } 1998 1999 if (context == null) { 2000 return; 2001 } 2002 2003 if (Build.VERSION.SDK_INT != Build.VERSION_CODES.VANILLA_ICE_CREAM) { 2004 return; 2005 } 2006 2007 SharedPreferences prefs = context.getSharedPreferences(MEDIAPROVIDER_PREFS, 2008 Context.MODE_PRIVATE); 2009 if (prefs.getBoolean(IS_MIME_TYPE_FIXED_IN_ANDROID_15, false)) { 2010 Log.v(TAG, "Mime type already corrected"); 2011 return; 2012 } 2013 2014 mExternalDatabase.runWithTransaction(db -> { 2015 boolean isSuccess = MimeTypeFixHandler.updateUnsupportedMimeTypes(db); 2016 // if success then update the shared pref value 2017 if (isSuccess) { 2018 SharedPreferences.Editor editor = prefs.edit(); 2019 editor.putBoolean(IS_MIME_TYPE_FIXED_IN_ANDROID_15, true); 2020 editor.apply(); 2021 } 2022 return null; 2023 }); 2024 } 2025 updateSpecialFormatColumn(SQLiteDatabase db, @NonNull CancellationSignal signal)2026 private void updateSpecialFormatColumn(SQLiteDatabase db, @NonNull CancellationSignal signal) { 2027 // This is to ensure we only do a bounded iteration over the rows as updates can fail, and 2028 // we don't want to keep running the query/update indefinitely. 2029 final int totalRowsToUpdate = getPendingSpecialFormatRowsCount(db, signal); 2030 for (int i = 0; i < totalRowsToUpdate; i += IDLE_MAINTENANCE_ROWS_LIMIT) { 2031 try (PickerDbFacade.UpdateMediaOperation operation = 2032 mPickerDbFacade.beginUpdateMediaOperation( 2033 PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)) { 2034 updateSpecialFormatForLimitedRows(db, signal, operation); 2035 operation.setSuccess(); 2036 } 2037 } 2038 } 2039 getPendingSpecialFormatRowsCount(SQLiteDatabase db, @NonNull CancellationSignal signal)2040 private int getPendingSpecialFormatRowsCount(SQLiteDatabase db, 2041 @NonNull CancellationSignal signal) { 2042 try (Cursor c = queryForPendingSpecialFormatColumns(db, /* limit */ null, signal)) { 2043 if (c == null) { 2044 return 0; 2045 } 2046 return c.getCount(); 2047 } 2048 } 2049 updateSpecialFormatForLimitedRows(SQLiteDatabase externalDb, @NonNull CancellationSignal signal, PickerDbFacade.UpdateMediaOperation operation)2050 private void updateSpecialFormatForLimitedRows(SQLiteDatabase externalDb, 2051 @NonNull CancellationSignal signal, PickerDbFacade.UpdateMediaOperation operation) { 2052 // Accumulate all the new SPECIAL_FORMAT updates with their ids 2053 ArrayMap<Long, Integer> newSpecialFormatValues = new ArrayMap<>(); 2054 final String limit = String.valueOf(IDLE_MAINTENANCE_ROWS_LIMIT); 2055 try (Cursor c = queryForPendingSpecialFormatColumns(externalDb, limit, signal)) { 2056 while (c.moveToNext() && !signal.isCanceled()) { 2057 final long id = c.getLong(0); 2058 final String path = c.getString(1); 2059 newSpecialFormatValues.put(id, getSpecialFormatValue(path)); 2060 } 2061 } 2062 2063 // Now, update all the new SPECIAL_FORMAT values in both external db and picker db. 2064 final ContentValues pickerDbValues = new ContentValues(); 2065 final ContentValues externalDbValues = new ContentValues(); 2066 int count = 0; 2067 for (long id : newSpecialFormatValues.keySet()) { 2068 if (signal.isCanceled()) { 2069 return; 2070 } 2071 2072 int specialFormat = newSpecialFormatValues.get(id); 2073 2074 pickerDbValues.clear(); 2075 pickerDbValues.put(PickerDbFacade.KEY_STANDARD_MIME_TYPE_EXTENSION, specialFormat); 2076 boolean pickerDbWriteSuccess = operation.execute(String.valueOf(id), pickerDbValues); 2077 2078 externalDbValues.clear(); 2079 externalDbValues.put(_SPECIAL_FORMAT, specialFormat); 2080 final String externalDbSelection = MediaColumns._ID + "=?"; 2081 final String[] externalDbSelectionArgs = new String[]{String.valueOf(id)}; 2082 boolean externalDbWriteSuccess = 2083 externalDb.update("files", externalDbValues, externalDbSelection, 2084 externalDbSelectionArgs) 2085 == 1; 2086 2087 if (pickerDbWriteSuccess && externalDbWriteSuccess) { 2088 count++; 2089 } 2090 } 2091 Log.d(TAG, "Updated standard_mime_type_extension for " + count + " items"); 2092 } 2093 getSpecialFormatValue(String path)2094 private int getSpecialFormatValue(String path) { 2095 final File file = new File(path); 2096 if (!file.exists()) { 2097 // We always update special format to none if the file is not found or there is an 2098 // error, this is so that we do not repeat over the same column again and again. 2099 return _SPECIAL_FORMAT_NONE; 2100 } 2101 2102 try { 2103 return SpecialFormatDetector.detect(file); 2104 } catch (Exception e) { 2105 // we tried our best, no need to run special detection again and again if it 2106 // throws exception once, it is likely to do so everytime. 2107 Log.d(TAG, "Failed to detect special format for file: " + file, e); 2108 return _SPECIAL_FORMAT_NONE; 2109 } 2110 } 2111 queryForPendingSpecialFormatColumns(SQLiteDatabase db, String limit, @NonNull CancellationSignal signal)2112 private Cursor queryForPendingSpecialFormatColumns(SQLiteDatabase db, String limit, 2113 @NonNull CancellationSignal signal) { 2114 // Run special detection for images only 2115 final String selection = _SPECIAL_FORMAT + " IS NULL AND " 2116 + MEDIA_TYPE + "=" + MEDIA_TYPE_IMAGE; 2117 final String[] projection = new String[] { MediaColumns._ID, MediaColumns.DATA }; 2118 return db.query(/* distinct */ true, "files", projection, selection, null, null, null, 2119 null, limit, signal); 2120 } 2121 2122 /** 2123 * Delete any expired content on mounted volumes. The expired content on unmounted 2124 * volumes will be deleted when we forget any stale volumes; we're cautious about 2125 * wildly changing clocks, so only delete items within the last week. 2126 * If the items are expired more than one week, extend the expired time of them 2127 * another one week to avoid data loss with incorrect time zone data. We will 2128 * delete it when it is expired next time. 2129 * 2130 * @param signal the cancellation signal 2131 * @return the integer array includes total deleted count and total extended count 2132 */ 2133 @NonNull deleteOrExtendExpiredItems(@onNull CancellationSignal signal)2134 private int[] deleteOrExtendExpiredItems(@NonNull CancellationSignal signal) { 2135 final long expiredOneWeek = 2136 ((System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS) / 1000); 2137 final long now = (System.currentTimeMillis() / 1000); 2138 final long expiredTime = now + (FileUtils.DEFAULT_DURATION_EXTENDED / 1000); 2139 return mExternalDatabase.runWithTransaction((db) -> { 2140 String selection = FileColumns.DATE_EXPIRES + " < " + now; 2141 selection += " AND (IS_PENDING=1 OR IS_TRASHED=1)"; 2142 selection += " AND volume_name in " + bindList(MediaStore.getExternalVolumeNames( 2143 getContext()).toArray()); 2144 String[] projection = new String[]{"volume_name", "_id", 2145 FileColumns.DATE_EXPIRES, FileColumns.DATA}; 2146 final class TrashItem { 2147 final String mVolumeName; 2148 final long mId; 2149 final long mDateExpires; 2150 final String mOriginalPath; 2151 2152 TrashItem(String volumeName, long id, long dateExpires, String oriPath) { 2153 this.mVolumeName = volumeName; 2154 this.mId = id; 2155 this.mDateExpires = dateExpires; 2156 this.mOriginalPath = oriPath; 2157 } 2158 } 2159 2160 final List<TrashItem> items = new ArrayList<>(); 2161 try (Cursor c = db.query(true, "files", projection, selection, 2162 null, null, null, null, null, signal)) { 2163 while (c.moveToNext()) { 2164 items.add(new TrashItem( 2165 c.getString(0), // volumeName 2166 c.getLong(1), // id 2167 c.getLong(2), // dateExpires 2168 c.getString(3) // oriPath 2169 )); 2170 } 2171 } 2172 2173 int totalDeleteCount = 0; 2174 int totalExtendedCount = 0; 2175 int index = 0; 2176 2177 for (TrashItem item : items) { 2178 if (item.mDateExpires > expiredOneWeek) { 2179 totalDeleteCount += delete(Files.getContentUri(item.mVolumeName, item.mId), 2180 null, null); 2181 } else { 2182 boolean success = extendExpiredItem(db, item.mOriginalPath, item.mId, 2183 expiredTime, expiredTime + index); 2184 if (success) { 2185 totalExtendedCount++; 2186 } 2187 index++; 2188 } 2189 } 2190 2191 return new int[]{totalDeleteCount, totalExtendedCount}; 2192 }); 2193 } 2194 2195 /** 2196 * Extend the expired items by renaming the file to new path with new timestamp and updating the 2197 * database for {@link FileColumns#DATA} and {@link FileColumns#DATE_EXPIRES}. If there is 2198 * UNIQUE constraint error for FileColumns.DATA, use adjustedExpiredTime and generate the new 2199 * path by adjustedExpiredTime. 2200 */ extendExpiredItem(@onNull SQLiteDatabase db, @NonNull String originalPath, long id, long newExpiredTime, long adjustedExpiredTime)2201 private boolean extendExpiredItem(@NonNull SQLiteDatabase db, @NonNull String originalPath, 2202 long id, long newExpiredTime, long adjustedExpiredTime) { 2203 String newPath = FileUtils.getAbsoluteExtendedPath(originalPath, newExpiredTime); 2204 if (newPath == null) { 2205 Log.e(TAG, "Couldn't compute path for " + originalPath + " and expired time " 2206 + newExpiredTime); 2207 return false; 2208 } 2209 2210 try { 2211 if (updateDatabaseForExpiredItem(db, newPath, id, newExpiredTime)) { 2212 return renameInLowerFsAndInvalidateFuseDentry(originalPath, newPath); 2213 } 2214 return false; 2215 } catch (SQLiteConstraintException e) { 2216 final String errorMessage = 2217 "Update database _data from " + originalPath + " to " + newPath + " failed."; 2218 Log.d(TAG, errorMessage, e); 2219 } 2220 2221 // When we update the database for newPath with newExpiredTime, if the new path already 2222 // exists in the database, it may raise SQLiteConstraintException. 2223 // If there are two expired items that have the same display name in the same directory, 2224 // but they have different expired time. E.g. .trashed-123-A.jpg and .trashed-456-A.jpg. 2225 // After we rename .trashed-123-A.jpg to .trashed-newExpiredTime-A.jpg, then we rename 2226 // .trashed-456-A.jpg to .trashed-newExpiredTime-A.jpg, it raises the exception. For 2227 // this case, we will retry it with the adjustedExpiredTime again. 2228 newPath = FileUtils.getAbsoluteExtendedPath(originalPath, adjustedExpiredTime); 2229 Log.i(TAG, "Retrying to extend expired item with the new path = " + newPath); 2230 try { 2231 if (updateDatabaseForExpiredItem(db, newPath, id, adjustedExpiredTime)) { 2232 return renameInLowerFsAndInvalidateFuseDentry(originalPath, newPath); 2233 } 2234 } catch (SQLiteConstraintException e) { 2235 // If we want to rename one expired item E.g. .trashed-123-A.jpg., and there is another 2236 // non-expired trashed/pending item has the same name. E.g. 2237 // .trashed-adjustedExpiredTime-A.jpg. When we rename .trashed-123-A.jpg to 2238 // .trashed-adjustedExpiredTime-A.jpg, it raises the SQLiteConstraintException. 2239 // The smallest unit of the expired time we use is second. It is a very rare case. 2240 // When this case is happened, we can handle it in next idle maintenance. 2241 final String errorMessage = 2242 "Update database _data from " + originalPath + " to " + newPath + " failed."; 2243 Log.d(TAG, errorMessage, e); 2244 } 2245 2246 return false; 2247 } 2248 updateDatabaseForExpiredItem(@onNull SQLiteDatabase db, @NonNull String path, long id, long expiredTime)2249 private boolean updateDatabaseForExpiredItem(@NonNull SQLiteDatabase db, 2250 @NonNull String path, long id, long expiredTime) { 2251 final String table = "files"; 2252 final String whereClause = MediaColumns._ID + "=?"; 2253 final String[] whereArgs = new String[]{String.valueOf(id)}; 2254 final ContentValues values = new ContentValues(); 2255 values.put(FileColumns.DATA, path); 2256 values.put(FileColumns.DATE_EXPIRES, expiredTime); 2257 final int count = db.update(table, values, whereClause, whereArgs); 2258 return count == 1; 2259 } 2260 renameInLowerFsAndInvalidateFuseDentry(@onNull String originalPath, @NonNull String newPath)2261 private boolean renameInLowerFsAndInvalidateFuseDentry(@NonNull String originalPath, 2262 @NonNull String newPath) { 2263 try { 2264 Os.rename(originalPath, newPath); 2265 invalidateFuseDentry(originalPath); 2266 invalidateFuseDentry(newPath); 2267 return true; 2268 } catch (ErrnoException e) { 2269 final String errorMessage = "Rename " + originalPath + " to " + newPath 2270 + " in lower file system for extending item failed."; 2271 Log.e(TAG, errorMessage, e); 2272 } 2273 return false; 2274 } 2275 onIdleMaintenanceStopped()2276 public void onIdleMaintenanceStopped() { 2277 mMediaScanner.onIdleScanStopped(); 2278 } 2279 2280 /** 2281 * Orphan any content of the given package. This will delete Android/media orphaned files from 2282 * the database. 2283 */ onPackageOrphaned(String packageName, int uid)2284 public void onPackageOrphaned(String packageName, int uid) { 2285 mExternalDatabase.runWithTransaction((db) -> { 2286 final int userId = uid / PER_USER_RANGE; 2287 onPackageOrphaned(db, packageName, userId); 2288 2289 if (SdkLevel.isAtLeastU()) { 2290 removeAllMediaGrantsForUid(uid, userId, packageName); 2291 } 2292 return null; 2293 }); 2294 } 2295 2296 /** 2297 * Orphan any content of the given package from the given database. This will delete 2298 * Android/media files from the database if the underlying file no longer exists. 2299 */ onPackageOrphaned(@onNull SQLiteDatabase db, @NonNull String packageName, int userId)2300 public void onPackageOrphaned(@NonNull SQLiteDatabase db, 2301 @NonNull String packageName, int userId) { 2302 // Delete Android/media entries. 2303 deleteAndroidMediaEntriesAndInvalidateDentryCache(db, packageName, userId); 2304 // Orphan rest of entries. 2305 orphanEntries(db, packageName, userId); 2306 mDatabaseBackupAndRecovery.removeOwnerIdToPackageRelation(packageName, userId); 2307 2308 } 2309 2310 /** 2311 * Removes all media_grants for all packages with the given UID. (i.e. shared packages.) 2312 * 2313 * @param uid the package uid. (will use this to query all shared packages that use this uid) 2314 * @param userId the user id, since packages can be installed by multiple users. 2315 * @param additionalPackageName An optional additional package name in the event that the 2316 * package has been removed at won't be returned by the PackageManager APIs. 2317 */ removeAllMediaGrantsForUid( int uid, int userId, @Nullable String additionalPackageName)2318 private void removeAllMediaGrantsForUid( 2319 int uid, int userId, @Nullable String additionalPackageName) { 2320 2321 String[] packages; 2322 try { 2323 LocalCallingIdentity lci = 2324 LocalCallingIdentity.fromExternal(getContext(), mUserCache, uid); 2325 packages = lci.getSharedPackageNamesArray(); 2326 } catch (IllegalArgumentException notFound) { 2327 // If there are no packages found, this means the specified UID has no packages 2328 // remaining on the system. 2329 packages = new String[]{}; 2330 } 2331 if (additionalPackageName != null) { 2332 // Include the passed additional package in the list LocalCallingIdentity returns. 2333 List<String> packageList = new ArrayList<>(); 2334 packageList.addAll(Arrays.asList(packages)); 2335 packageList.add(additionalPackageName); 2336 packages = packageList.toArray(new String[packageList.size()]); 2337 } 2338 2339 // TODO(b/260685885): Add e2e tests to ensure these are cleared when a package 2340 // is removed. 2341 mMediaGrants.removeAllMediaGrantsForPackages( 2342 packages, /* reason */ "Package orphaned", userId); 2343 } 2344 deleteAndroidMediaEntriesAndInvalidateDentryCache(SQLiteDatabase db, String packageName, int userId)2345 private void deleteAndroidMediaEntriesAndInvalidateDentryCache(SQLiteDatabase db, 2346 String packageName, int userId) { 2347 String relativePath = "Android/media/" + DatabaseUtils.escapeForLike(packageName) + "/%"; 2348 try (Cursor cursor = db.query( 2349 "files", 2350 new String[] { MediaColumns._ID, MediaColumns.DATA }, 2351 "relative_path LIKE ? ESCAPE '\\' AND owner_package_name=? AND _user_id=?", 2352 new String[] { relativePath, packageName, "" + userId }, 2353 /* groupBy= */ null, 2354 /* having= */ null, 2355 /* orderBy= */null, 2356 /* limit= */ null)) { 2357 int countDeleted = 0; 2358 if (cursor != null) { 2359 while (cursor.moveToNext()) { 2360 File file = new File(cursor.getString(1)); 2361 // We check for existence to be sure we don't delete files that still exist. 2362 // This can happen even if the pair (package, userid) is unknown, 2363 // since some framework implementations may rely on special userids. 2364 if (!file.exists()) { 2365 countDeleted += 2366 db.delete("files", "_id=?", new String[]{cursor.getString(0)}); 2367 } 2368 } 2369 } 2370 Log.d(TAG, "Deleted " + countDeleted + " Android/media items belonging to " 2371 + packageName + " on " + db.getPath()); 2372 } 2373 2374 // Invalidate Dentry cache for Android/media/<package-name> directories 2375 invalidateDentryForExternalStorage(packageName); 2376 } 2377 orphanEntries( @onNull SQLiteDatabase db, @NonNull String packageName, int userId)2378 private void orphanEntries( 2379 @NonNull SQLiteDatabase db, @NonNull String packageName, int userId) { 2380 final ContentValues values = new ContentValues(); 2381 values.putNull(FileColumns.OWNER_PACKAGE_NAME); 2382 2383 final int countOrphaned = db.update("files", values, 2384 "owner_package_name=? AND _user_id=?", new String[] { packageName, "" + userId }); 2385 if (countOrphaned > 0) { 2386 Log.d(TAG, "Orphaned " + countOrphaned + " items belonging to " 2387 + packageName + " on " + db.getPath()); 2388 } 2389 } 2390 scanDirectory(@onNull File dir, @ScanReason int reason)2391 public void scanDirectory(@NonNull File dir, @ScanReason int reason) { 2392 mMediaScanner.scanDirectory(dir, reason); 2393 } 2394 scanFile(@onNull File file, @ScanReason int reason)2395 public Uri scanFile(@NonNull File file, @ScanReason int reason) { 2396 return mMediaScanner.scanFile(file, reason); 2397 } 2398 scanFileAsMediaProvider(File file)2399 private Uri scanFileAsMediaProvider(File file) { 2400 final LocalCallingIdentity tokenInner = clearLocalCallingIdentity(); 2401 try { 2402 return scanFile(file, REASON_DEMAND); 2403 } finally { 2404 restoreLocalCallingIdentity(tokenInner); 2405 } 2406 } 2407 2408 /** 2409 * Called when a new file is created through FUSE 2410 * 2411 * @param path path of the file that was created 2412 * 2413 * Called from JNI in jni/MediaProviderWrapper.cpp 2414 */ 2415 @Keep onFileCreatedForFuse(String path)2416 public void onFileCreatedForFuse(String path) { 2417 // Make sure we update the quota type of the file 2418 BackgroundThread.getExecutor().execute(() -> { 2419 File file = new File(path); 2420 int mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(file)); 2421 updateQuotaTypeForFileInternal(file, mediaType); 2422 }); 2423 } 2424 isAppCloneUserPair(int userId1, int userId2)2425 private boolean isAppCloneUserPair(int userId1, int userId2) { 2426 UserHandle user1 = UserHandle.of(userId1); 2427 UserHandle user2 = UserHandle.of(userId2); 2428 if (SdkLevel.isAtLeastS()) { 2429 if (mUserCache.userSharesMediaWithParent(user1) 2430 || mUserCache.userSharesMediaWithParent(user2)) { 2431 return true; 2432 } 2433 if (Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.S) { 2434 // If we're on S or higher, and we shipped with S or higher, only allow the new 2435 // app cloning functionality 2436 return false; 2437 } 2438 // else, fall back to deprecated solution below on updating devices 2439 } 2440 try { 2441 Method isAppCloneUserPair = StorageManager.class.getMethod("isAppCloneUserPair", 2442 int.class, int.class); 2443 return (Boolean) isAppCloneUserPair.invoke(mStorageManager, userId1, userId2); 2444 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 2445 Log.w(TAG, "isAppCloneUserPair failed. Users: " + userId1 + " and " + userId2); 2446 return false; 2447 } 2448 } 2449 2450 /** 2451 * Determines whether the passed in userId forms an app clone user pair with user 0. 2452 * 2453 * @param userId user ID to check 2454 * 2455 * Called from JNI in jni/MediaProviderWrapper.cpp 2456 */ 2457 @Keep isAppCloneUserForFuse(int userId)2458 public boolean isAppCloneUserForFuse(int userId) { 2459 if (!isCrossUserEnabled()) { 2460 Log.d(TAG, "CrossUser not enabled."); 2461 return false; 2462 } 2463 boolean result = isAppCloneUserPair(0, userId); 2464 2465 Log.w(TAG, "isAppCloneUserPair for user " + userId + ": " + result); 2466 2467 return result; 2468 } 2469 2470 /** 2471 * Determines if to allow FUSE_LOOKUP for uid. Might allow uids that don't belong to the 2472 * MediaProvider user, depending on OEM configuration. 2473 * 2474 * @param uid linux uid to check 2475 * 2476 * Called from JNI in jni/MediaProviderWrapper.cpp 2477 */ 2478 @Keep shouldAllowLookupForFuse(int uid, int pathUserId)2479 public boolean shouldAllowLookupForFuse(int uid, int pathUserId) { 2480 int callingUserId = uidToUserId(uid); 2481 if (!isCrossUserEnabled()) { 2482 Log.d(TAG, "CrossUser not enabled. Users: " + callingUserId + " and " + pathUserId); 2483 return false; 2484 } 2485 2486 if (callingUserId != pathUserId && callingUserId != 0 && pathUserId != 0) { 2487 Log.w(TAG, "CrossUser at least one user is 0 check failed. Users: " + callingUserId 2488 + " and " + pathUserId); 2489 return false; 2490 } 2491 2492 if (mUserCache.isWorkProfile(callingUserId) || mUserCache.isWorkProfile(pathUserId)) { 2493 // Cross-user lookup not allowed if one user in the pair has a profile owner app 2494 Log.w(TAG, "CrossUser work profile check failed. Users: " + callingUserId + " and " 2495 + pathUserId); 2496 return false; 2497 } 2498 2499 boolean result = isAppCloneUserPair(pathUserId, callingUserId); 2500 if (result) { 2501 Log.i(TAG, "CrossUser allowed. Users: " + callingUserId + " and " + pathUserId); 2502 } else { 2503 Log.w(TAG, "CrossUser isAppCloneUserPair check failed. Users: " + callingUserId 2504 + " and " + pathUserId); 2505 } 2506 2507 return result; 2508 } 2509 2510 /** 2511 * Called from FUSE to transform a file 2512 * 2513 * A transform can change the file contents for {@code uid} from {@code src} to {@code dst} 2514 * depending on {@code flags}. This allows the FUSE daemon serve different file contents for 2515 * the same file to different apps. 2516 * 2517 * The only supported transform for now is transcoding which re-encodes a file taken in a modern 2518 * format like HEVC to a legacy format like AVC. 2519 * 2520 * @param src file path to transform 2521 * @param dst file path to save transformed file 2522 * @param flags determines the kind of transform 2523 * @param readUid app that called us requesting transform 2524 * @param openUid app that originally made the open call 2525 * @param mediaCapabilitiesUid app for which the transform decision was made, 2526 * 0 if decision was made with openUid 2527 * 2528 * Called from JNI in jni/MediaProviderWrapper.cpp 2529 */ 2530 @Keep transformForFuse(String src, String dst, int transforms, int transformsReason, int readUid, int openUid, int mediaCapabilitiesUid)2531 public boolean transformForFuse(String src, String dst, int transforms, int transformsReason, 2532 int readUid, int openUid, int mediaCapabilitiesUid) { 2533 if ((transforms & FLAG_TRANSFORM_TRANSCODING) != 0) { 2534 if (mTranscodeHelper.isTranscodeFileCached(src, dst)) { 2535 Log.d(TAG, "Using transcode cache for " + src); 2536 return true; 2537 } 2538 2539 // In general we always mark the opener as causing transcoding. 2540 // However, if the mediaCapabilitiesUid is available then we mark the reader as causing 2541 // transcoding. This handles the case where a malicious app might want to take 2542 // advantage of mediaCapabilitiesUid by setting it to another app's uid and reading the 2543 // media contents itself; in such cases we'd mark the reader (malicious app) for the 2544 // cost of transcoding. 2545 // 2546 // openUid readUid mediaCapabilitiesUid 2547 // ------------------------------------------------------------------------------------- 2548 // using picker SAF app app 2549 // abusive case bad app bad app victim 2550 // modern to lega- 2551 // -cy sharing modern legacy legacy 2552 // 2553 // we'd not be here in the below case. 2554 // legacy to mode- 2555 // -rn sharing legacy modern modern 2556 2557 int transcodeUid = openUid; 2558 if (mediaCapabilitiesUid > 0) { 2559 Log.d(TAG, "Fix up transcodeUid to " + readUid + ". openUid " + openUid 2560 + ", mediaCapabilitiesUid " + mediaCapabilitiesUid); 2561 transcodeUid = readUid; 2562 } 2563 return mTranscodeHelper.transcode(src, dst, transcodeUid, transformsReason); 2564 } 2565 return true; 2566 } 2567 2568 /** 2569 * Called from FUSE to get {@link FileLookupResult} for a {@code path} and {@code uid} 2570 * 2571 * {@link FileLookupResult} contains transforms, transforms completion status and ioPath 2572 * for transform lookup query for a file and uid. 2573 * 2574 * @param path file path to get transforms for 2575 * @param uid app requesting IO form kernel 2576 * @param tid FUSE thread id handling IO request from kernel 2577 * 2578 * Called from JNI in jni/MediaProviderWrapper.cpp 2579 */ 2580 @Keep onFileLookupForFuse(String path, int uid, int tid)2581 public FileLookupResult onFileLookupForFuse(String path, int uid, int tid) { 2582 uid = getBinderUidForFuse(uid, tid); 2583 // Use MediaProviders UserId as the caller might be calling cross profile. 2584 final int userId = UserHandle.myUserId(); 2585 2586 if (isSyntheticPath(path, userId)) { 2587 if (isRedactedPath(path, userId)) { 2588 return handleRedactedFileLookup(uid, path); 2589 } else if (isPickerPath(path, userId)) { 2590 return handlePickerFileLookup(userId, uid, path); 2591 } 2592 2593 throw new IllegalStateException("Unexpected synthetic path: " + path); 2594 } 2595 2596 if (mTranscodeHelper.supportsTranscode(path)) { 2597 return handleTranscodedFileLookup(path, uid, tid); 2598 } 2599 2600 return new FileLookupResult(/* transforms */ 0, uid, /* ioPath */ ""); 2601 } 2602 handleTranscodedFileLookup(String path, int uid, int tid)2603 private FileLookupResult handleTranscodedFileLookup(String path, int uid, int tid) { 2604 final int transformsReason; 2605 final PendingOpenInfo info; 2606 2607 synchronized (mPendingOpenInfo) { 2608 info = mPendingOpenInfo.get(tid); 2609 } 2610 2611 if (info != null && info.uid == uid) { 2612 transformsReason = info.transcodeReason; 2613 } else { 2614 transformsReason = mTranscodeHelper.shouldTranscode(path, uid, null /* bundle */); 2615 } 2616 2617 if (transformsReason > 0) { 2618 final String ioPath = mTranscodeHelper.prepareIoPath(path, uid); 2619 final boolean transformsComplete = mTranscodeHelper.isTranscodeFileCached(path, ioPath); 2620 2621 return new FileLookupResult(FLAG_TRANSFORM_TRANSCODING, transformsReason, uid, 2622 transformsComplete, /* transformsSupported */ true, ioPath); 2623 } 2624 2625 return new FileLookupResult(/* transforms */ 0, transformsReason, uid, 2626 /* transformsComplete */ true, /* transformsSupported */ true, ""); 2627 } 2628 handleRedactedFileLookup(int uid, @NonNull String path)2629 private FileLookupResult handleRedactedFileLookup(int uid, @NonNull String path) { 2630 final LocalCallingIdentity token = clearLocalCallingIdentity(); 2631 final String fileName = extractFileName(path); 2632 2633 final DatabaseHelper helper; 2634 try { 2635 helper = getDatabaseForUri(FileUtils.getContentUriForPath(path)); 2636 } catch (VolumeNotFoundException e) { 2637 throw new IllegalStateException("Volume not found for file: " + path); 2638 } 2639 2640 try (final Cursor c = helper.runWithoutTransaction( 2641 (db) -> db.query("files", new String[]{MediaColumns.DATA}, 2642 FileColumns.REDACTED_URI_ID + "=?", new String[]{fileName}, null, null, 2643 null))) { 2644 if (c.moveToFirst()) { 2645 return new FileLookupResult(FLAG_TRANSFORM_REDACTION, uid, c.getString(0)); 2646 } 2647 2648 throw new IllegalStateException("Failed to fetch synthetic redacted path: " + path); 2649 } finally { 2650 restoreLocalCallingIdentity(token); 2651 } 2652 } 2653 2654 /** TODO(b/242153950) :Add negative tests for permission check of file lookup of synthetic 2655 * paths. */ handlePickerFileLookup(int userId, int uid, @NonNull String path)2656 private FileLookupResult handlePickerFileLookup(int userId, int uid, @NonNull String path) { 2657 final File file = new File(path); 2658 final List<String> syntheticRelativePathSegments = 2659 extractSyntheticRelativePathSegements(path, userId); 2660 final int segmentCount = syntheticRelativePathSegments.size(); 2661 2662 if (segmentCount < 1 || segmentCount > 5) { 2663 throw new IllegalStateException("Unexpected synthetic picker path: " + file); 2664 } 2665 2666 final String lastSegment = syntheticRelativePathSegments.get(segmentCount - 1); 2667 2668 boolean result = false; 2669 switch (segmentCount) { 2670 case 1: 2671 // .../picker or .../picker_get_content or .../picker_transcoded 2672 if (lastSegment.equals(PICKER_SEGMENT) 2673 || lastSegment.equals(PICKER_GET_CONTENT_SEGMENT) 2674 || lastSegment.equals(PICKER_TRANSCODED_SEGMENT)) { 2675 result = file.exists() || file.mkdir(); 2676 } 2677 break; 2678 case 2: 2679 // .../picker/<user-id> or .../picker_get_content/<user-id> or 2680 // .../picker_transcoded/<user-id> 2681 try { 2682 Integer.parseInt(lastSegment); 2683 result = file.exists() || file.mkdir(); 2684 } catch (NumberFormatException e) { 2685 Log.w(TAG, "Invalid user id for picker file lookup: " + lastSegment 2686 + ". File: " + file); 2687 } 2688 break; 2689 case 3: 2690 // .../picker/<user-id>/<authority> or 2691 // .../picker_get_content/<user-id>/<authority> or 2692 // .../picker_transcoded/<user-id>/<authority> 2693 result = preparePickerAuthorityPathSegment(file, lastSegment, uid); 2694 break; 2695 case 4: 2696 // .../picker/<user-id>/<authority>/media or 2697 // .../picker_get_content/<user-id>/<authority>/media or 2698 // .../picker_transcoded/<user-id>/<authority>/media 2699 if (lastSegment.equals("media")) { 2700 result = file.exists() || file.mkdir(); 2701 } 2702 break; 2703 case 5: 2704 // .../picker/<user-id>/<authority>/media/<media-id.extension> or 2705 // .../picker_get_content/<user-id>/<authority>/media/<media-id.extension> or 2706 // .../picker_transcoded/<user-id>/<authority>/media/<media-id.extension> 2707 final String pickerSegmentType = syntheticRelativePathSegments.get(0); 2708 final String fileUserId = syntheticRelativePathSegments.get(1); 2709 final String authority = syntheticRelativePathSegments.get(2); 2710 result = preparePickerMediaIdPathSegment(file, pickerSegmentType, authority, 2711 lastSegment, fileUserId, uid); 2712 break; 2713 } 2714 2715 if (result) { 2716 return new FileLookupResult(FLAG_TRANSFORM_PICKER, uid, path); 2717 } 2718 throw new IllegalStateException("Failed to prepare synthetic picker path: " + file); 2719 } 2720 handlePickerFileOpen(String path, int uid)2721 private FileOpenResult handlePickerFileOpen(String path, int uid) { 2722 final String[] segments = path.split("/"); 2723 if (segments.length != 11) { 2724 Log.e(TAG, "Picker file open failed. Unexpected segments: " + path); 2725 return new FileOpenResult(OsConstants.ENOENT /* status */, uid, /* transformsUid */ 0, 2726 new long[0]); 2727 } 2728 2729 // ['', 'storage', 'emulated', '0', 'transforms', 'synthetic', 2730 // 'picker' or 'picker_get_content' or 'picker_transcoded', 2731 // '<user-id>', '<host>', 'media', '<fileName>'] 2732 final String pickerSegmentType = segments[6]; 2733 final String userId = segments[7]; 2734 final String fileName = segments[10]; 2735 final String host = segments[8]; 2736 final String authority = userId + "@" + host; 2737 final int lastDotIndex = fileName.lastIndexOf('.'); 2738 2739 if (lastDotIndex == -1) { 2740 Log.e(TAG, "Picker file open failed. No file extension: " + path); 2741 return FileOpenResult.createError(OsConstants.ENOENT, uid); 2742 } 2743 2744 final String mediaId = fileName.substring(0, lastDotIndex); 2745 final ParcelFileDescriptor pfd; 2746 if (pickerSegmentType.equalsIgnoreCase(PICKER_TRANSCODED_SEGMENT)) { 2747 try { 2748 pfd = mPhotoPickerTranscodeHelper.openTranscodedFile(host, mediaId); 2749 } catch (FileNotFoundException e) { 2750 Log.e(TAG, "Picker transcoded file open failed. No cached transcoded file found."); 2751 return FileOpenResult.createError(OsConstants.ENOENT, uid); 2752 } 2753 } else { 2754 final Uri uri = getMediaUri(authority).buildUpon().appendPath(mediaId).build(); 2755 IBinder binder = getContext().getContentResolver() 2756 .call(uri, METHOD_GET_ASYNC_CONTENT_PROVIDER, null, null) 2757 .getBinder(EXTRA_ASYNC_CONTENT_PROVIDER); 2758 if (binder == null) { 2759 Log.e(TAG, "Picker file open failed. No cloud media provider found."); 2760 return FileOpenResult.createError(OsConstants.ENOENT, uid); 2761 } 2762 IAsyncContentProvider iAsyncProvider = IAsyncContentProvider.Stub.asInterface(binder); 2763 AsyncContentProvider asyncContentProvider = new AsyncContentProvider(iAsyncProvider); 2764 try { 2765 pfd = asyncContentProvider.openMedia(uri, "r"); 2766 } catch (FileNotFoundException | ExecutionException | InterruptedException 2767 | TimeoutException | RemoteException e) { 2768 Log.e(TAG, "Picker file open failed. Failed to open URI: " + uri, e); 2769 return FileOpenResult.createError(OsConstants.ENOENT, uid); 2770 } 2771 } 2772 2773 try (FileInputStream fis = new FileInputStream(pfd.getFileDescriptor())) { 2774 final String mimeType = MimeUtils.resolveMimeType(new File(path)); 2775 // Picker segment indicates we need to force redact location metadata. 2776 // Picker_get_content indicates that we need to check A_M_L permission to decide if the 2777 // metadata needs to be redacted 2778 LocalCallingIdentity callingIdentityForOriginalUid = getCachedCallingIdentityForFuse( 2779 uid); 2780 final boolean isRedactionNeeded = pickerSegmentType.equalsIgnoreCase(PICKER_SEGMENT) 2781 || pickerSegmentType.equalsIgnoreCase(PICKER_TRANSCODED_SEGMENT) 2782 || callingIdentityForOriginalUid == null 2783 || isRedactionNeededForPickerUri(callingIdentityForOriginalUid); 2784 Log.v(TAG, "Redaction needed for file open: " + isRedactionNeeded); 2785 long[] redactionRanges = new long[0]; 2786 if (isRedactionNeeded) { 2787 redactionRanges = RedactionUtils.getRedactionRanges(fis, mimeType); 2788 Log.v(TAG, "Redaction ranges: " + Arrays.toString(redactionRanges)); 2789 } 2790 return new FileOpenResult(0 /* status */, uid, /* transformsUid */ 0, 2791 /* nativeFd */ pfd.detachFd(), redactionRanges); 2792 } catch (IOException e) { 2793 Log.e(TAG, "Picker file open failed. No file extension: " + path, e); 2794 return FileOpenResult.createError(OsConstants.ENOENT, uid); 2795 } 2796 } 2797 preparePickerAuthorityPathSegment(File file, String authority, int uid)2798 private boolean preparePickerAuthorityPathSegment(File file, String authority, int uid) { 2799 if (mPickerSyncController.isProviderEnabled(authority)) { 2800 return file.exists() || file.mkdir(); 2801 } 2802 2803 return false; 2804 } 2805 preparePickerMediaIdPathSegment(File file, String pickerSegmentType, String authority, String fileName, String userId, int uid)2806 private boolean preparePickerMediaIdPathSegment(File file, String pickerSegmentType, 2807 String authority, String fileName, String userId, int uid) { 2808 final String mediaId = extractFileName(fileName); 2809 final String[] projection = new String[]{MediaStore.PickerMediaColumns.SIZE}; 2810 2811 final Uri uri = Uri.parse( 2812 "content://media/" + pickerSegmentType + "/" + userId + "/" + authority + "/media/" 2813 + mediaId); 2814 try (Cursor cursor = mPickerUriResolver.query(uri, projection, /* callingPid */0, uid, 2815 mCallingIdentity.get().getPackageName())) { 2816 if (cursor != null && cursor.moveToFirst()) { 2817 // For picker transcoded files, get their actual size, as ths value may differ from 2818 // the source file. The code is put after the query operation to make sure that 2819 // the app accessing the file have required permissions. 2820 if (pickerSegmentType.equalsIgnoreCase(PICKER_TRANSCODED_SEGMENT)) { 2821 long size = mPhotoPickerTranscodeHelper.getTranscodedFileSize(authority, 2822 mediaId); 2823 if (size > 0) { 2824 return createSparseFile(file, size); 2825 } 2826 return false; 2827 } 2828 2829 final int sizeBytesIdx = cursor.getColumnIndex(MediaStore.PickerMediaColumns.SIZE); 2830 if (sizeBytesIdx != -1) { 2831 2832 return createSparseFile(file, cursor.getLong(sizeBytesIdx)); 2833 } 2834 } 2835 } 2836 2837 return false; 2838 } 2839 getBinderUidForFuse(int uid, int tid)2840 public int getBinderUidForFuse(int uid, int tid) { 2841 if (uid != MY_UID) { 2842 return uid; 2843 } 2844 2845 synchronized (mPendingOpenInfo) { 2846 PendingOpenInfo info = mPendingOpenInfo.get(tid); 2847 if (info == null) { 2848 return uid; 2849 } 2850 return info.uid; 2851 } 2852 } 2853 uidToUserId(int uid)2854 private static int uidToUserId(int uid) { 2855 return uid / PER_USER_RANGE; 2856 } 2857 2858 /** 2859 * Returns true if the app denoted by the given {@code uid} and {@code packageName} is allowed 2860 * to clear other apps' cache directories. 2861 */ hasPermissionToClearCaches(Context context, ApplicationInfo ai)2862 static boolean hasPermissionToClearCaches(Context context, ApplicationInfo ai) { 2863 PermissionUtils.setOpDescription("clear app cache"); 2864 try { 2865 return PermissionUtils.checkPermissionManager(context, /* pid */ -1, ai.uid, 2866 ai.packageName, /* attributionTag */ null); 2867 } finally { 2868 PermissionUtils.clearOpDescription(); 2869 } 2870 } 2871 2872 @VisibleForTesting computeAudioLocalizedValues(ContentValues values)2873 void computeAudioLocalizedValues(ContentValues values) { 2874 try { 2875 final String title = values.getAsString(AudioColumns.TITLE); 2876 final String titleRes = values.getAsString(AudioColumns.TITLE_RESOURCE_URI); 2877 2878 if (!TextUtils.isEmpty(titleRes)) { 2879 final String localized = getLocalizedTitle(titleRes); 2880 if (!TextUtils.isEmpty(localized)) { 2881 values.put(AudioColumns.TITLE, localized); 2882 } 2883 } else { 2884 final String localized = getLocalizedTitle(title); 2885 if (!TextUtils.isEmpty(localized)) { 2886 values.put(AudioColumns.TITLE, localized); 2887 values.put(AudioColumns.TITLE_RESOURCE_URI, title); 2888 } 2889 } 2890 } catch (Exception e) { 2891 Log.w(TAG, "Failed to localize title", e); 2892 } 2893 } 2894 2895 @VisibleForTesting computeAudioKeyValues(ContentValues values)2896 static void computeAudioKeyValues(ContentValues values) { 2897 computeAudioKeyValue(values, AudioColumns.TITLE, AudioColumns.TITLE_KEY, /* focusId */ 2898 null, /* hashValue */ 0); 2899 computeAudioKeyValue(values, AudioColumns.ARTIST, AudioColumns.ARTIST_KEY, 2900 AudioColumns.ARTIST_ID, /* hashValue */ 0); 2901 computeAudioKeyValue(values, AudioColumns.GENRE, AudioColumns.GENRE_KEY, 2902 AudioColumns.GENRE_ID, /* hashValue */ 0); 2903 computeAudioAlbumKeyValue(values); 2904 } 2905 2906 /** 2907 * To distinguish same-named albums, we append a hash. The hash is 2908 * based on the "album artist" tag if present, otherwise on the path of 2909 * the parent directory of the audio file. 2910 */ computeAudioAlbumKeyValue(ContentValues values)2911 private static void computeAudioAlbumKeyValue(ContentValues values) { 2912 int hashCode = 0; 2913 2914 final String albumArtist = values.getAsString(MediaColumns.ALBUM_ARTIST); 2915 if (!TextUtils.isEmpty(albumArtist)) { 2916 hashCode = albumArtist.hashCode(); 2917 } else { 2918 final String path = values.getAsString(MediaColumns.DATA); 2919 if (!TextUtils.isEmpty(path)) { 2920 hashCode = path.substring(0, path.lastIndexOf('/')).hashCode(); 2921 } 2922 } 2923 2924 computeAudioKeyValue(values, AudioColumns.ALBUM, AudioColumns.ALBUM_KEY, 2925 AudioColumns.ALBUM_ID, hashCode); 2926 } 2927 computeAudioKeyValue(@onNull ContentValues values, @NonNull String focus, @Nullable String focusKey, @Nullable String focusId, int hashValue)2928 private static void computeAudioKeyValue(@NonNull ContentValues values, @NonNull String focus, 2929 @Nullable String focusKey, @Nullable String focusId, int hashValue) { 2930 if (focusKey != null) values.remove(focusKey); 2931 if (focusId != null) values.remove(focusId); 2932 2933 final String value = values.getAsString(focus); 2934 if (TextUtils.isEmpty(value)) return; 2935 2936 final String key = Audio.keyFor(value); 2937 if (key == null) return; 2938 2939 if (focusKey != null) { 2940 values.put(focusKey, key); 2941 } 2942 if (focusId != null) { 2943 // Many apps break if we generate negative IDs, so trim off the 2944 // highest bit to ensure we're always unsigned 2945 final long id = Hashing.farmHashFingerprint64().hashString(key + hashValue, 2946 StandardCharsets.UTF_8).asLong() & ~(1L << 63); 2947 values.put(focusId, id); 2948 } 2949 } 2950 2951 @Override canonicalize(@onNull Uri uri)2952 public Uri canonicalize(@NonNull Uri uri) { 2953 // Skip when we have nothing to canonicalize 2954 if ("1".equals(uri.getQueryParameter(CANONICAL))) { 2955 return uri; 2956 } 2957 2958 final boolean allowHidden = mCallingIdentity.get().hasPermission(PERMISSION_IS_SELF); 2959 final int match = matchUri(uri, allowHidden); 2960 2961 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) { 2962 switch (match) { 2963 case AUDIO_MEDIA_ID: { 2964 final String title = getDefaultTitleFromCursor(c); 2965 if (!TextUtils.isEmpty(title)) { 2966 final Uri.Builder builder = uri.buildUpon(); 2967 builder.appendQueryParameter(AudioColumns.TITLE, title); 2968 builder.appendQueryParameter(CANONICAL, "1"); 2969 return builder.build(); 2970 } 2971 break; 2972 } 2973 case VIDEO_MEDIA_ID: 2974 case IMAGES_MEDIA_ID: { 2975 final String documentId = c 2976 .getString(c.getColumnIndexOrThrow(MediaColumns.DOCUMENT_ID)); 2977 if (!TextUtils.isEmpty(documentId)) { 2978 final Uri.Builder builder = uri.buildUpon(); 2979 builder.appendQueryParameter(MediaColumns.DOCUMENT_ID, documentId); 2980 builder.appendQueryParameter(CANONICAL, "1"); 2981 return builder.build(); 2982 } 2983 break; 2984 } 2985 } 2986 } catch (FileNotFoundException e) { 2987 Log.w(TAG, e.getMessage()); 2988 } 2989 return null; 2990 } 2991 2992 @Override uncanonicalize(@onNull Uri uri)2993 public Uri uncanonicalize(@NonNull Uri uri) { 2994 // Skip when we have nothing to uncanonicalize 2995 if (!"1".equals(uri.getQueryParameter(CANONICAL))) { 2996 return uri; 2997 } 2998 final boolean allowHidden = mCallingIdentity.get().hasPermission(PERMISSION_IS_SELF); 2999 final int match = matchUri(uri, allowHidden); 3000 3001 // Extract values and then clear to avoid recursive lookups 3002 final String title = uri.getQueryParameter(AudioColumns.TITLE); 3003 final String documentId = uri.getQueryParameter(MediaColumns.DOCUMENT_ID); 3004 uri = uri.buildUpon().clearQuery().build(); 3005 3006 switch (match) { 3007 case AUDIO_MEDIA_ID: { 3008 // First check for an exact match 3009 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) { 3010 if (Objects.equals(title, getDefaultTitleFromCursor(c))) { 3011 return uri; 3012 } 3013 } catch (FileNotFoundException e) { 3014 Log.w(TAG, "Trouble resolving " + uri + "; falling back to search: " + e); 3015 } 3016 3017 // Otherwise fallback to searching 3018 final Uri baseUri = ContentUris.removeId(uri); 3019 try (Cursor c = queryForSingleItem(baseUri, 3020 new String[] { BaseColumns._ID }, 3021 AudioColumns.TITLE + "=?", new String[] { title }, null)) { 3022 return ContentUris.withAppendedId(baseUri, c.getLong(0)); 3023 } catch (FileNotFoundException e) { 3024 Log.w(TAG, "Failed to resolve " + uri + ": " + e); 3025 return null; 3026 } 3027 } 3028 case VIDEO_MEDIA_ID: 3029 case IMAGES_MEDIA_ID: { 3030 // First check for an exact match 3031 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) { 3032 if (Objects.equals(title, getDefaultTitleFromCursor(c))) { 3033 return uri; 3034 } 3035 } catch (FileNotFoundException e) { 3036 Log.w(TAG, "Trouble resolving " + uri + "; falling back to search: " + e); 3037 } 3038 3039 // Otherwise fallback to searching 3040 final Uri baseUri = ContentUris.removeId(uri); 3041 try (Cursor c = queryForSingleItem(baseUri, 3042 new String[] { BaseColumns._ID }, 3043 MediaColumns.DOCUMENT_ID + "=?", new String[] { documentId }, null)) { 3044 return ContentUris.withAppendedId(baseUri, c.getLong(0)); 3045 } catch (FileNotFoundException e) { 3046 Log.w(TAG, "Failed to resolve " + uri + ": " + e); 3047 return null; 3048 } 3049 } 3050 } 3051 3052 return uri; 3053 } 3054 safeUncanonicalize(Uri uri)3055 private Uri safeUncanonicalize(Uri uri) { 3056 Uri newUri = uncanonicalize(uri); 3057 if (newUri != null) { 3058 return newUri; 3059 } 3060 return uri; 3061 } 3062 safeTraceSectionNameWithUri(String operation, Uri uri)3063 private static String safeTraceSectionNameWithUri(String operation, Uri uri) { 3064 String sectionName = "MP." + operation + " [" + uri + "]"; 3065 if (sectionName.length() > MAX_SECTION_NAME_LEN) { 3066 return sectionName.substring(0, MAX_SECTION_NAME_LEN); 3067 } 3068 return sectionName; 3069 } 3070 3071 /** 3072 * @return where clause to exclude database rows where 3073 * <ul> 3074 * <li> {@code column} is set or 3075 * <li> {@code column} is {@link MediaColumns#IS_PENDING} and is set by FUSE and not owned by 3076 * calling package. 3077 * <li> {@code column} is {@link MediaColumns#IS_PENDING}, is unset and is waiting for 3078 * metadata update from a deferred scan. 3079 * </ul> 3080 */ getWhereClauseForMatchExclude(@onNull String column)3081 private String getWhereClauseForMatchExclude(@NonNull String column) { 3082 if (column.equalsIgnoreCase(MediaColumns.IS_PENDING)) { 3083 // Don't include rows that are pending for metadata 3084 final String pendingForMetadata = FileColumns._MODIFIER + "=" 3085 + FileColumns._MODIFIER_CR_PENDING_METADATA; 3086 final String notPending = String.format("(%s=0 AND NOT %s)", column, 3087 pendingForMetadata); 3088 3089 // Include owned pending files from Fuse 3090 final String pendingFromFuse = String.format("(%s=1 AND %s AND %s)", column, 3091 MATCH_PENDING_FROM_FUSE, getWhereForOwnerPackageMatch(mCallingIdentity.get())); 3092 3093 return "(" + notPending + " OR " + pendingFromFuse + ")"; 3094 } 3095 return column + "=0"; 3096 } 3097 3098 /** 3099 * @return where clause to include database rows where 3100 * <ul> 3101 * <li> {@code column} is not set or 3102 * <li> {@code column} is set and calling package has write permission to corresponding db row 3103 * or {@code column} is {@link MediaColumns#IS_PENDING} and is set by FUSE. 3104 * </ul> 3105 * The method is used to match db rows corresponding to writable pending and trashed files. 3106 */ 3107 @Nullable getWhereClauseForMatchableVisibleFromFilePath(@onNull Uri uri, @NonNull String column)3108 private String getWhereClauseForMatchableVisibleFromFilePath(@NonNull Uri uri, 3109 @NonNull String column) { 3110 if (checkCallingPermissionGlobal(uri, /*forWrite*/ true)) { 3111 // No special filtering needed 3112 return null; 3113 } 3114 3115 int uriType = matchUri(uri, isCallingPackageAllowedHidden()); 3116 if (hasAccessToCollection(mCallingIdentity.get(), uriType, /* forWrite */ true)) { 3117 // has direct write access to whole collection, no special filtering needed. 3118 return null; 3119 } 3120 3121 final String writeAccessCheckSql = getWhereForConstrainedAccess(mCallingIdentity.get(), 3122 uriType, /* forWrite */ true, Bundle.EMPTY); 3123 3124 final String matchWritableRowsClause = String.format("%s=0 OR (%s=1 AND (%s OR %s))", 3125 column, column, MATCH_PENDING_FROM_FUSE, writeAccessCheckSql); 3126 3127 return matchWritableRowsClause; 3128 } 3129 3130 /** 3131 * Gets list of files in {@code path} from media provider database. 3132 * 3133 * @param path path of the directory. 3134 * @param uid UID of the calling process. 3135 * @return a list of file names in the given directory path. 3136 * An empty list is returned if no files are visible to the calling app or the given directory 3137 * does not have any files. 3138 * A list with ["/"] is returned if the path is not indexed by MediaProvider database or 3139 * calling package is a legacy app and has appropriate storage permissions for the given path. 3140 * In both scenarios file names should be obtained from lower file system. 3141 * A list with empty string[""] is returned if the calling package doesn't have access to the 3142 * given path. 3143 * 3144 * <p>Directory names are always obtained from lower file system. 3145 * 3146 * Called from JNI in jni/MediaProviderWrapper.cpp 3147 */ 3148 @Keep getFilesInDirectoryForFuse(String path, int uid)3149 public String[] getFilesInDirectoryForFuse(String path, int uid) { 3150 final LocalCallingIdentity token = 3151 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 3152 PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path); 3153 3154 try { 3155 if (isPrivatePackagePathNotAccessibleByCaller(path)) { 3156 return new String[] {""}; 3157 } 3158 3159 if (shouldBypassFuseRestrictions(/*forWrite*/ false, path)) { 3160 return new String[] {"/"}; 3161 } 3162 3163 // Do not allow apps to list Android/data or Android/obb dirs. 3164 // On primary volumes, apps that get special access to these directories get it via 3165 // mount views of lowerfs. On secondary volumes, such apps would return early from 3166 // shouldBypassFuseRestrictions above. 3167 if (isDataOrObbPath(path)) { 3168 return new String[] {""}; 3169 } 3170 3171 // Legacy apps that made is this far don't have the right storage permission and hence 3172 // are not allowed to access anything other than their external app directory 3173 if (isCallingPackageRequestingLegacy()) { 3174 return new String[] {""}; 3175 } 3176 3177 // Get relative path for the contents of given directory. 3178 String relativePath = extractRelativePathWithDisplayName(path); 3179 if (relativePath == null) { 3180 // Path is /storage/emulated/, if relativePath is null, MediaProvider doesn't 3181 // have any details about the given directory. Use lower file system to obtain 3182 // files and directories in the given directory. 3183 return new String[] {"/"}; 3184 } 3185 // Getting UserId from the directory path, as clone user shares the MediaProvider 3186 // of user 0. 3187 int userIdFromPath = FileUtils.extractUserId(path); 3188 // In some cases, like querying public volumes, userId is not available in path. We 3189 // take userId from the user running MediaProvider process (sUserId). 3190 if (userIdFromPath == -1) { 3191 userIdFromPath = sUserId; 3192 } 3193 // For all other paths, get file names from media provider database. 3194 // Return media and non-media files visible to the calling package. 3195 ArrayList<String> fileNamesList = new ArrayList<>(); 3196 3197 // Only FileColumns.DATA contains actual name of the file. 3198 String[] projection = {MediaColumns.DATA}; 3199 3200 Bundle queryArgs = new Bundle(); 3201 queryArgs.putString(QUERY_ARG_SQL_SELECTION, MediaColumns.RELATIVE_PATH + 3202 " =? and " + FileColumns._USER_ID + " =? and mime_type not like 'null'"); 3203 queryArgs.putStringArray(QUERY_ARG_SQL_SELECTION_ARGS, new String[] {relativePath, 3204 String.valueOf(userIdFromPath)}); 3205 // Get database entries for files from MediaProvider database with 3206 // MediaColumns.RELATIVE_PATH as the given path. 3207 try (final Cursor cursor = query(FileUtils.getContentUriForPath(path), projection, 3208 queryArgs, null)) { 3209 while(cursor.moveToNext()) { 3210 fileNamesList.add(extractDisplayName(cursor.getString(0))); 3211 } 3212 } 3213 return fileNamesList.toArray(new String[fileNamesList.size()]); 3214 } finally { 3215 restoreLocalCallingIdentity(token); 3216 } 3217 } 3218 3219 /** 3220 * Scan files during directory renames for the following reasons: 3221 * <ul> 3222 * <li>Because we don't update db rows for directories, we scan the oldPath to discard stale 3223 * directory db rows. This prevents conflicts during subsequent db operations with oldPath. 3224 * <li>We need to scan newPath as well, because the new directory may have become hidden 3225 * or unhidden, in which case we need to update the media types of the contained files 3226 * </ul> 3227 */ scanRenamedDirectoryForFuse(@onNull String oldPath, @NonNull String newPath)3228 private void scanRenamedDirectoryForFuse(@NonNull String oldPath, @NonNull String newPath) { 3229 scanFileAsMediaProvider(new File(oldPath)); 3230 scanFileAsMediaProvider(new File(newPath)); 3231 } 3232 3233 /** 3234 * Checks if given {@code mimeType} is supported in {@code path}. 3235 */ isMimeTypeSupportedInPath(String path, String mimeType)3236 private boolean isMimeTypeSupportedInPath(String path, String mimeType) { 3237 final String supportedPrimaryMimeType; 3238 final int match = matchUri(getContentUriForFile(path, mimeType), true); 3239 switch (match) { 3240 case AUDIO_MEDIA: 3241 supportedPrimaryMimeType = "audio"; 3242 break; 3243 case VIDEO_MEDIA: 3244 supportedPrimaryMimeType = "video"; 3245 break; 3246 case IMAGES_MEDIA: 3247 supportedPrimaryMimeType = "image"; 3248 break; 3249 default: 3250 supportedPrimaryMimeType = ClipDescription.MIMETYPE_UNKNOWN; 3251 } 3252 return (supportedPrimaryMimeType.equalsIgnoreCase(ClipDescription.MIMETYPE_UNKNOWN) || 3253 StringUtils.startsWithIgnoreCase(mimeType, supportedPrimaryMimeType)); 3254 } 3255 3256 /** 3257 * Removes owner package for the renamed path if the calling package doesn't own the db row 3258 * 3259 * When oldPath is renamed to newPath, if newPath exists in the database, and caller is not the 3260 * owner of the file, owner package is set to 'null'. This prevents previous owner of newPath 3261 * from accessing renamed file. 3262 * @return {@code true} if 3263 * <ul> 3264 * <li> there is no corresponding database row for given {@code path} 3265 * <li> shared calling package is the owner of the database row 3266 * <li> owner package name is already set to 'null' 3267 * <li> updating owner package name to 'null' was successful. 3268 * </ul> 3269 * Returns {@code false} otherwise. 3270 */ maybeRemoveOwnerPackageForFuseRename(@onNull DatabaseHelper helper, @NonNull String path)3271 private boolean maybeRemoveOwnerPackageForFuseRename(@NonNull DatabaseHelper helper, 3272 @NonNull String path) { 3273 final Uri uri = FileUtils.getContentUriForPath(path); 3274 final int match = matchUri(uri, isCallingPackageAllowedHidden()); 3275 final String ownerPackageName; 3276 final String selection = MediaColumns.DATA + " =? AND " 3277 + MediaColumns.OWNER_PACKAGE_NAME + " != 'null'"; 3278 final String[] selectionArgs = new String[] {path}; 3279 3280 final SQLiteQueryBuilder qbForQuery = 3281 getQueryBuilder(TYPE_QUERY, match, uri, Bundle.EMPTY, null); 3282 try (Cursor c = qbForQuery.query(helper, new String[] {FileColumns.OWNER_PACKAGE_NAME}, 3283 selection, selectionArgs, null, null, null, null, null)) { 3284 if (!c.moveToFirst()) { 3285 // We don't need to remove owner_package from db row if path doesn't exist in 3286 // database or owner_package is already set to 'null' 3287 return true; 3288 } 3289 ownerPackageName = c.getString(0); 3290 if (isCallingIdentitySharedPackageName(ownerPackageName)) { 3291 // We don't need to remove owner_package from db row if calling package is the owner 3292 // of the database row 3293 return true; 3294 } 3295 } 3296 3297 final SQLiteQueryBuilder qbForUpdate = 3298 getQueryBuilder(TYPE_UPDATE, match, uri, Bundle.EMPTY, null); 3299 ContentValues values = new ContentValues(); 3300 values.put(FileColumns.OWNER_PACKAGE_NAME, "null"); 3301 return qbForUpdate.update(helper, values, selection, selectionArgs) == 1; 3302 } 3303 updateDatabaseForFuseRename(@onNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values)3304 private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper, 3305 @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values) { 3306 return updateDatabaseForFuseRename(helper, oldPath, newPath, values, Bundle.EMPTY); 3307 } 3308 updateDatabaseForFuseRename(@onNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values, @NonNull Bundle qbExtras)3309 private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper, 3310 @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values, 3311 @NonNull Bundle qbExtras) { 3312 return updateDatabaseForFuseRename(helper, oldPath, newPath, values, qbExtras, 3313 FileUtils.getContentUriForPath(oldPath)); 3314 } 3315 3316 /** 3317 * Updates database entry for given {@code path} with {@code values} 3318 */ updateDatabaseForFuseRename(@onNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values, @NonNull Bundle qbExtras, Uri uriOldPath)3319 private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper, 3320 @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values, 3321 @NonNull Bundle qbExtras, Uri uriOldPath) { 3322 boolean allowHidden = isCallingPackageAllowedHidden(); 3323 final SQLiteQueryBuilder qbForUpdate = getQueryBuilder(TYPE_UPDATE, 3324 matchUri(uriOldPath, allowHidden), uriOldPath, qbExtras, null); 3325 3326 // uriOldPath may use Files uri which doesn't allow modifying AudioColumns. Include 3327 // AudioColumns projection map if we are modifying any audio columns while renaming 3328 // database rows. 3329 if (values.containsKey(AudioColumns.IS_RINGTONE)) { 3330 qbForUpdate.setProjectionMap(getProjectionMap(AudioColumns.class, FileColumns.class)); 3331 } 3332 3333 if (values.containsKey(FileColumns._MODIFIER)) { 3334 qbForUpdate.allowColumn(FileColumns._MODIFIER); 3335 } 3336 3337 final String selection = MediaColumns.DATA + " =? "; 3338 int count = 0; 3339 boolean retryUpdateWithReplace = false; 3340 3341 try { 3342 Long parent = values.getAsLong(FileColumns.PARENT); 3343 // Opening a transaction here and ensuring the qbForUpdate happens within 3344 // doesn't open two transactions, but just joins the existing one 3345 count = helper.runWithTransaction((db) -> { 3346 if (parent == null && newPath != null) { 3347 final long parentId = getParent(db, newPath); 3348 values.put(FileColumns.PARENT, parentId); 3349 } 3350 // TODO(b/146777893): System gallery apps can rename a media directory 3351 // containing non-media files. This update doesn't support updating 3352 // non-media files that are not owned by system gallery app. 3353 return qbForUpdate.update(helper, values, selection, new String[]{oldPath}); 3354 }); 3355 } catch (SQLiteConstraintException e) { 3356 Log.w(TAG, "Database update failed while renaming " + oldPath, e); 3357 retryUpdateWithReplace = true; 3358 } 3359 3360 if (retryUpdateWithReplace) { 3361 if (deleteForFuseRename(helper, oldPath, newPath, qbExtras, selection, allowHidden)) { 3362 Log.i(TAG, "Retrying database update after deleting conflicting entry"); 3363 count = qbForUpdate.update(helper, values, selection, new String[]{oldPath}); 3364 } else { 3365 return false; 3366 } 3367 } 3368 return count == 1; 3369 } 3370 deleteForFuseRename(DatabaseHelper helper, String oldPath, String newPath, Bundle qbExtras, String selection, boolean allowHidden)3371 private boolean deleteForFuseRename(DatabaseHelper helper, String oldPath, 3372 String newPath, Bundle qbExtras, String selection, boolean allowHidden) { 3373 // We are replacing file in newPath with file in oldPath. If calling package has 3374 // write permission for newPath, delete existing database entry and retry update. 3375 final Uri uriNewPath = FileUtils.getContentUriForPath(oldPath); 3376 final SQLiteQueryBuilder qbForDelete = getQueryBuilder(TYPE_DELETE, 3377 matchUri(uriNewPath, allowHidden), uriNewPath, qbExtras, null); 3378 if (qbForDelete.delete(helper, selection, new String[] {newPath}) == 1) { 3379 return true; 3380 } 3381 // Check if delete can be done using other URI grants 3382 final String[] projection = new String[] { 3383 FileColumns.MEDIA_TYPE, 3384 FileColumns.DATA, 3385 FileColumns._ID, 3386 FileColumns.IS_DOWNLOAD, 3387 FileColumns.MIME_TYPE, 3388 }; 3389 return 3390 deleteWithOtherUriGrants( 3391 FileUtils.getContentUriForPath(newPath), 3392 helper, projection, selection, new String[] {newPath}, qbExtras) == 1; 3393 } 3394 3395 /** 3396 * Gets {@link ContentValues} for updating database entry to {@code path}. 3397 */ getContentValuesForFuseRename(String path, String newMimeType, boolean wasHidden, boolean isHidden, boolean isSameMimeType)3398 private ContentValues getContentValuesForFuseRename(String path, String newMimeType, 3399 boolean wasHidden, boolean isHidden, boolean isSameMimeType) { 3400 ContentValues values = new ContentValues(); 3401 values.put(MediaColumns.MIME_TYPE, newMimeType); 3402 values.put(MediaColumns.DATA, path); 3403 3404 if (isHidden) { 3405 values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE); 3406 } else { 3407 int mediaType = MimeUtils.resolveMediaType(newMimeType); 3408 values.put(FileColumns.MEDIA_TYPE, mediaType); 3409 } 3410 3411 if ((!isHidden && wasHidden) || !isSameMimeType) { 3412 // Set the modifier as MODIFIER_FUSE so that apps can scan the file to update the 3413 // metadata. Otherwise, scan will skip scanning this file because rename() doesn't 3414 // change lastModifiedTime and scan assumes there is no change in the file. 3415 values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_FUSE); 3416 } 3417 3418 if (MimeUtils.isAudioMimeType(newMimeType) && !values.containsKey(FileColumns._MODIFIER)) { 3419 computeAudioLocalizedValues(values); 3420 computeAudioKeyValues(values); 3421 FileUtils.computeAudioTypeValuesFromData(path, values::put); 3422 } 3423 3424 FileUtils.computeValuesFromData(values, isFuseThread()); 3425 return values; 3426 } 3427 getIncludedDefaultDirectories()3428 private ArrayList<String> getIncludedDefaultDirectories() { 3429 final ArrayList<String> includedDefaultDirs = new ArrayList<>(); 3430 if (mCallingIdentity.get().checkCallingPermissionVideo(/* forWrite */ 3431 true, /* forDataDelivery */ true)) { 3432 includedDefaultDirs.add(Environment.DIRECTORY_DCIM); 3433 includedDefaultDirs.add(Environment.DIRECTORY_PICTURES); 3434 includedDefaultDirs.add(Environment.DIRECTORY_MOVIES); 3435 } else if (mCallingIdentity.get().checkCallingPermissionImages(/* forWrite */ 3436 true, /* forDataDelivery */ true)) { 3437 includedDefaultDirs.add(Environment.DIRECTORY_DCIM); 3438 includedDefaultDirs.add(Environment.DIRECTORY_PICTURES); 3439 } 3440 return includedDefaultDirs; 3441 } 3442 3443 /** 3444 * Gets all files in the given {@code path} and subdirectories of the given {@code path}. 3445 */ getAllFilesForRenameDirectory(String oldPath)3446 private ArrayList<String> getAllFilesForRenameDirectory(String oldPath) { 3447 final String selection = FileColumns.DATA + " LIKE ? ESCAPE '\\'" 3448 + " and mime_type not like 'null'"; 3449 final String[] selectionArgs = new String[] {DatabaseUtils.escapeForLike(oldPath) + "/%"}; 3450 ArrayList<String> fileList = new ArrayList<>(); 3451 3452 final LocalCallingIdentity token = clearLocalCallingIdentity(); 3453 try (final Cursor c = query(FileUtils.getContentUriForPath(oldPath), 3454 new String[] {MediaColumns.DATA}, selection, selectionArgs, null)) { 3455 while (c.moveToNext()) { 3456 String filePath = c.getString(0); 3457 filePath = filePath.replaceFirst(Pattern.quote(oldPath + "/"), ""); 3458 fileList.add(filePath); 3459 } 3460 } finally { 3461 restoreLocalCallingIdentity(token); 3462 } 3463 return fileList; 3464 } 3465 3466 /** 3467 * Gets files in the given {@code path} and subdirectories of the given {@code path} for which 3468 * calling package has write permissions. 3469 * 3470 * This method throws {@code IllegalArgumentException} if the directory has one or more 3471 * files for which calling package doesn't have write permission or if file type is not 3472 * supported in {@code newPath} 3473 */ getWritableFilesForRenameDirectory(String oldPath, String newPath)3474 private ArrayList<String> getWritableFilesForRenameDirectory(String oldPath, String newPath) 3475 throws IllegalArgumentException { 3476 // Try a simple check to see if the caller has full access to the given collections first 3477 // before falling back to performing a query to probe for access. 3478 final String oldRelativePath = extractRelativePathWithDisplayName(oldPath); 3479 final String newRelativePath = extractRelativePathWithDisplayName(newPath); 3480 boolean hasFullAccessToOldPath = false; 3481 boolean hasFullAccessToNewPath = false; 3482 for (String defaultDir : getIncludedDefaultDirectories()) { 3483 if (oldRelativePath.startsWith(defaultDir)) hasFullAccessToOldPath = true; 3484 if (newRelativePath.startsWith(defaultDir)) hasFullAccessToNewPath = true; 3485 } 3486 if (hasFullAccessToNewPath && hasFullAccessToOldPath) { 3487 return getAllFilesForRenameDirectory(oldPath); 3488 } 3489 3490 final int countAllFilesInDirectory; 3491 final String selection = FileColumns.DATA + " LIKE ? ESCAPE '\\'" 3492 + " and mime_type not like 'null'"; 3493 final String[] selectionArgs = new String[] {DatabaseUtils.escapeForLike(oldPath) + "/%"}; 3494 3495 final Uri uriOldPath = FileUtils.getContentUriForPath(oldPath); 3496 3497 final LocalCallingIdentity token = clearLocalCallingIdentity(); 3498 try (final Cursor c = query(uriOldPath, new String[] {MediaColumns._ID}, selection, 3499 selectionArgs, null)) { 3500 // get actual number of files in the given directory. 3501 countAllFilesInDirectory = c.getCount(); 3502 } finally { 3503 restoreLocalCallingIdentity(token); 3504 } 3505 3506 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, 3507 matchUri(uriOldPath, isCallingPackageAllowedHidden()), uriOldPath, Bundle.EMPTY, 3508 null); 3509 final DatabaseHelper helper; 3510 try { 3511 helper = getDatabaseForUri(uriOldPath); 3512 } catch (VolumeNotFoundException e) { 3513 throw new IllegalStateException("Volume not found while querying files for renaming " 3514 + oldPath); 3515 } 3516 3517 ArrayList<String> fileList = new ArrayList<>(); 3518 final String[] projection = {MediaColumns.DATA, MediaColumns.MIME_TYPE}; 3519 try (Cursor c = qb.query(helper, projection, selection, selectionArgs, null, null, null, 3520 null, null)) { 3521 // Check if the calling package has write permission to all files in the given 3522 // directory. If calling package has write permission to all files in the directory, the 3523 // query with update uri should return same number of files as previous query. 3524 if (c.getCount() != countAllFilesInDirectory) { 3525 throw new IllegalArgumentException("Calling package doesn't have write permission " 3526 + " to rename one or more files in " + oldPath); 3527 } 3528 while(c.moveToNext()) { 3529 String filePath = c.getString(0); 3530 filePath = filePath.replaceFirst(Pattern.quote(oldPath + "/"), ""); 3531 3532 final String mimeType = c.getString(1); 3533 if (!isMimeTypeSupportedInPath(newPath + "/" + filePath, mimeType)) { 3534 throw new IllegalArgumentException("Can't rename " + oldPath + "/" + filePath 3535 + ". Mime type " + mimeType + " not supported in " + newPath); 3536 } 3537 fileList.add(filePath); 3538 } 3539 } 3540 return fileList; 3541 } 3542 renameInLowerFs(String oldPath, String newPath)3543 private int renameInLowerFs(String oldPath, String newPath) { 3544 try { 3545 Os.rename(oldPath, newPath); 3546 return 0; 3547 } catch (ErrnoException e) { 3548 final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed."; 3549 Log.e(TAG, errorMessage, e); 3550 return e.errno; 3551 } 3552 } 3553 3554 /** 3555 * Rename directory from {@code oldPath} to {@code newPath}. 3556 * 3557 * Renaming a directory is only allowed if calling package has write permission to all files in 3558 * the given directory tree and all file types in the given directory tree are supported by the 3559 * top level directory of new path. Renaming a directory is split into three steps: 3560 * 1. Check calling package's permissions for all files in the given directory tree. Also check 3561 * file type support for all files in the {@code newPath}. 3562 * 2. Try updating database for all files in the directory. 3563 * 3. Rename the directory in lower file system. If rename in the lower file system is 3564 * successful, commit database update. 3565 * 3566 * @param oldPath path of the directory to be renamed. 3567 * @param newPath new path of directory to be renamed. 3568 * @return 0 on successful rename, appropriate negated errno value if the rename is not allowed. 3569 * <ul> 3570 * <li>{@link OsConstants#EPERM} Renaming a directory with file types not supported by 3571 * {@code newPath} or renaming a directory with files for which calling package doesn't have 3572 * write permission. 3573 * This method can also return errno returned from {@code Os.rename} function. 3574 */ renameDirectoryCheckedForFuse(String oldPath, String newPath)3575 private int renameDirectoryCheckedForFuse(String oldPath, String newPath) { 3576 final ArrayList<String> fileList; 3577 try { 3578 fileList = getWritableFilesForRenameDirectory(oldPath, newPath); 3579 } catch (IllegalArgumentException e) { 3580 final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed. "; 3581 Log.e(TAG, errorMessage, e); 3582 return OsConstants.EPERM; 3583 } 3584 3585 return renameDirectoryUncheckedForFuse(oldPath, newPath, fileList); 3586 } 3587 renameDirectoryUncheckedForFuse(String oldPath, String newPath, ArrayList<String> fileList)3588 private int renameDirectoryUncheckedForFuse(String oldPath, String newPath, 3589 ArrayList<String> fileList) { 3590 final DatabaseHelper helper; 3591 try { 3592 helper = getDatabaseForUri(FileUtils.getContentUriForPath(oldPath)); 3593 } catch (VolumeNotFoundException e) { 3594 throw new IllegalStateException("Volume not found while trying to update database for " 3595 + oldPath, e); 3596 } 3597 3598 helper.beginTransaction(); 3599 try { 3600 final Bundle qbExtras = new Bundle(); 3601 qbExtras.putStringArrayList(INCLUDED_DEFAULT_DIRECTORIES, 3602 getIncludedDefaultDirectories()); 3603 final boolean wasHidden = FileUtils.shouldDirBeHidden(new File(oldPath)); 3604 final boolean isHidden = FileUtils.shouldDirBeHidden(new File(newPath)); 3605 for (String filePath : fileList) { 3606 final String newFilePath = newPath + "/" + filePath; 3607 final String mimeType = MimeUtils.resolveMimeType(new File(newFilePath)); 3608 if(!updateDatabaseForFuseRename(helper, oldPath + "/" + filePath, newFilePath, 3609 getContentValuesForFuseRename(newFilePath, mimeType, wasHidden, isHidden, 3610 /* isSameMimeType */ true), 3611 qbExtras)) { 3612 Log.e(TAG, "Calling package doesn't have write permission to rename file."); 3613 return OsConstants.EPERM; 3614 } 3615 } 3616 3617 // Rename the directory in lower file system. 3618 int errno = renameInLowerFs(oldPath, newPath); 3619 if (errno == 0) { 3620 helper.setTransactionSuccessful(); 3621 } else { 3622 return errno; 3623 } 3624 } finally { 3625 helper.endTransaction(); 3626 } 3627 // Directory movement might have made new/old path hidden. 3628 scanRenamedDirectoryForFuse(oldPath, newPath); 3629 return 0; 3630 } 3631 3632 /** 3633 * Rename a file from {@code oldPath} to {@code newPath}. 3634 * 3635 * Renaming a file is split into three parts: 3636 * 1. Check if {@code newPath} supports new file type. 3637 * 2. Try updating database entry from {@code oldPath} to {@code newPath}. This update may fail 3638 * if calling package doesn't have write permission for {@code oldPath} and {@code newPath}. 3639 * 3. Rename the file in lower file system. If Rename in lower file system succeeds, commit 3640 * database update. 3641 * @param oldPath path of the file to be renamed. 3642 * @param newPath new path of the file to be renamed. 3643 * @return 0 on successful rename, appropriate negated errno value if the rename is not allowed. 3644 * <ul> 3645 * <li>{@link OsConstants#EPERM} Calling package doesn't have write permission for 3646 * {@code oldPath} or {@code newPath}, or file type is not supported by {@code newPath}. 3647 * This method can also return errno returned from {@code Os.rename} function. 3648 */ renameFileCheckedForFuse(String oldPath, String newPath)3649 private int renameFileCheckedForFuse(String oldPath, String newPath) { 3650 // Check if new mime type is supported in new path. 3651 final String newMimeType = MimeUtils.resolveMimeType(new File(newPath)); 3652 if (!isMimeTypeSupportedInPath(newPath, newMimeType)) { 3653 return OsConstants.EPERM; 3654 } 3655 return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ false) ; 3656 } 3657 renameFileUncheckedForFuse(String oldPath, String newPath)3658 private int renameFileUncheckedForFuse(String oldPath, String newPath) { 3659 return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ true) ; 3660 } 3661 renameFileForFuse(String oldPath, String newPath, boolean bypassRestrictions)3662 private int renameFileForFuse(String oldPath, String newPath, boolean bypassRestrictions) { 3663 final DatabaseHelper helper; 3664 try { 3665 helper = getDatabaseForUri(FileUtils.getContentUriForPath(oldPath)); 3666 } catch (VolumeNotFoundException e) { 3667 throw new IllegalStateException("Failed to update database row with " + oldPath, e); 3668 } 3669 3670 final boolean wasHidden = FileUtils.shouldFileBeHidden(new File(oldPath)); 3671 final boolean isHidden = FileUtils.shouldFileBeHidden(new File(newPath)); 3672 helper.beginTransaction(); 3673 try { 3674 final String newMimeType = MimeUtils.resolveMimeType(new File(newPath)); 3675 final String oldMimeType = MimeUtils.resolveMimeType(new File(oldPath)); 3676 final boolean isSameMimeType = newMimeType.equalsIgnoreCase(oldMimeType); 3677 ContentValues contentValues = getContentValuesForFuseRename(newPath, newMimeType, 3678 wasHidden, isHidden, isSameMimeType); 3679 if (!updateDatabaseForFuseRename(helper, oldPath, newPath, contentValues)) { 3680 if (!bypassRestrictions) { 3681 // Check for other URI format grants for oldPath only. Check right before 3682 // returning EPERM, to leave positive case performance unaffected. 3683 if (!renameWithOtherUriGrants(helper, oldPath, newPath, contentValues)) { 3684 Log.e(TAG, "Calling package doesn't have write permission to rename file."); 3685 return OsConstants.EPERM; 3686 } 3687 } else if (!maybeRemoveOwnerPackageForFuseRename(helper, newPath)) { 3688 Log.wtf(TAG, "Couldn't clear owner package name for " + newPath); 3689 return OsConstants.EPERM; 3690 } 3691 } 3692 3693 // Try renaming oldPath to newPath in lower file system. 3694 int errno = renameInLowerFs(oldPath, newPath); 3695 if (errno == 0) { 3696 helper.setTransactionSuccessful(); 3697 } else { 3698 return errno; 3699 } 3700 } finally { 3701 helper.endTransaction(); 3702 } 3703 // The above code should have taken are of the mime/media type of the new file, 3704 // even if it was moved to/from a hidden directory. 3705 // This leaves cases where the source/dest of the move is a .nomedia file itself. Eg: 3706 // 1) /sdcard/foo/.nomedia => /sdcard/foo/bar.mp3 3707 // in this case, the code above has given bar.mp3 the correct mime type, but we should 3708 // still can /sdcard/foo, because it's now no longer hidden 3709 // 2) /sdcard/foo/.nomedia => /sdcard/bar/.nomedia 3710 // in this case, we need to scan both /sdcard/foo and /sdcard/bar/ 3711 // 3) /sdcard/foo/bar.mp3 => /sdcard/foo/.nomedia 3712 // in this case, we need to scan all of /sdcard/foo 3713 if (extractDisplayName(oldPath).equals(".nomedia")) { 3714 scanFileAsMediaProvider(new File(oldPath).getParentFile()); 3715 } 3716 if (extractDisplayName(newPath).equals(".nomedia")) { 3717 scanFileAsMediaProvider(new File(newPath).getParentFile()); 3718 } 3719 3720 return 0; 3721 } 3722 3723 /** 3724 * Rename file by checking for other URI grants on oldPath 3725 * 3726 * We don't support replace scenario by checking for other URI grants on newPath (if it exists). 3727 */ renameWithOtherUriGrants(DatabaseHelper helper, String oldPath, String newPath, ContentValues contentValues)3728 private boolean renameWithOtherUriGrants(DatabaseHelper helper, String oldPath, String newPath, 3729 ContentValues contentValues) { 3730 final Uri oldPathGrantedUri = getOtherUriGrantsForPath(oldPath, /* forWrite */ true); 3731 if (oldPathGrantedUri == null) { 3732 return false; 3733 } 3734 return updateDatabaseForFuseRename(helper, oldPath, newPath, contentValues, Bundle.EMPTY, 3735 oldPathGrantedUri); 3736 } 3737 3738 /** 3739 * Rename file/directory without imposing any restrictions. 3740 * 3741 * We don't impose any rename restrictions for apps that bypass scoped storage restrictions. 3742 * However, we update database entries for renamed files to keep the database consistent. 3743 */ renameUncheckedForFuse(String oldPath, String newPath)3744 private int renameUncheckedForFuse(String oldPath, String newPath) { 3745 if (new File(oldPath).isFile()) { 3746 return renameFileUncheckedForFuse(oldPath, newPath); 3747 } else { 3748 return renameDirectoryUncheckedForFuse(oldPath, newPath, 3749 getAllFilesForRenameDirectory(oldPath)); 3750 } 3751 } 3752 3753 /** 3754 * Rename file or directory from {@code oldPath} to {@code newPath}. 3755 * 3756 * @param oldPath path of the file or directory to be renamed. 3757 * @param newPath new path of the file or directory to be renamed. 3758 * @param uid UID of the calling package. 3759 * @return 0 on successful rename, appropriate errno value if the rename is not allowed. 3760 * <ul> 3761 * <li>{@link OsConstants#ENOENT} Renaming a non-existing file or renaming a file from path that 3762 * is not indexed by MediaProvider database. 3763 * <li>{@link OsConstants#EPERM} Renaming a default directory or renaming a file to a file type 3764 * not supported by new path. 3765 * This method can also return errno returned from {@code Os.rename} function. 3766 * 3767 * Called from JNI in jni/MediaProviderWrapper.cpp 3768 */ 3769 @Keep renameForFuse(String oldPath, String newPath, int uid)3770 public int renameForFuse(String oldPath, String newPath, int uid) { 3771 final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed. "; 3772 final LocalCallingIdentity token = 3773 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 3774 PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), oldPath); 3775 3776 try { 3777 if (isPrivatePackagePathNotAccessibleByCaller(oldPath) 3778 || isPrivatePackagePathNotAccessibleByCaller(newPath)) { 3779 return OsConstants.EACCES; 3780 } 3781 3782 if (!newPath.equals(getAbsoluteSanitizedPath(newPath))) { 3783 Log.e(TAG, "New path name contains invalid characters."); 3784 return OsConstants.EPERM; 3785 } 3786 3787 if (shouldBypassDatabaseAndSetDirtyForFuse(uid, oldPath) 3788 && shouldBypassDatabaseAndSetDirtyForFuse(uid, newPath)) { 3789 return renameInLowerFs(oldPath, newPath); 3790 } 3791 3792 if (shouldBypassFuseRestrictions(/*forWrite*/ true, oldPath) 3793 && shouldBypassFuseRestrictions(/*forWrite*/ true, newPath)) { 3794 return renameUncheckedForFuse(oldPath, newPath); 3795 } 3796 // Legacy apps that made is this far don't have the right storage permission and hence 3797 // are not allowed to access anything other than their external app directory 3798 if (isCallingPackageRequestingLegacy()) { 3799 return OsConstants.EACCES; 3800 } 3801 3802 final String[] oldRelativePath = sanitizePath(extractRelativePath(oldPath)); 3803 final String[] newRelativePath = sanitizePath(extractRelativePath(newPath)); 3804 if (oldRelativePath.length == 0 || newRelativePath.length == 0) { 3805 // Rename not allowed on paths that can't be translated to RELATIVE_PATH. 3806 Log.e(TAG, errorMessage + "Invalid path."); 3807 return OsConstants.EPERM; 3808 } 3809 if (oldRelativePath.length == 1 && TextUtils.isEmpty(oldRelativePath[0])) { 3810 // Allow rename of files/folders other than default directories. 3811 final String displayName = extractDisplayName(oldPath); 3812 for (String defaultFolder : DEFAULT_FOLDER_NAMES) { 3813 if (displayName.equals(defaultFolder)) { 3814 Log.e(TAG, errorMessage + oldPath + " is a default folder." 3815 + " Renaming a default folder is not allowed."); 3816 return OsConstants.EPERM; 3817 } 3818 } 3819 } 3820 if (newRelativePath.length == 1 && TextUtils.isEmpty(newRelativePath[0])) { 3821 Log.e(TAG, errorMessage + newPath + " is in root folder." 3822 + " Renaming a file/directory to root folder is not allowed"); 3823 return OsConstants.EPERM; 3824 } 3825 3826 final File directoryAndroid = new File( 3827 extractVolumePath(oldPath).toLowerCase(Locale.ROOT), 3828 DIRECTORY_ANDROID_LOWER_CASE 3829 ); 3830 final File directoryAndroidMedia = new File(directoryAndroid, DIRECTORY_MEDIA); 3831 String newPathLowerCase = newPath.toLowerCase(Locale.ROOT); 3832 if (directoryAndroidMedia.getAbsolutePath().equalsIgnoreCase(oldPath)) { 3833 // Don't allow renaming 'Android/media' directory. 3834 // Android/[data|obb] are bind mounted and these paths don't go through FUSE. 3835 Log.e(TAG, errorMessage + oldPath + " is a default folder in app external " 3836 + "directory. Renaming a default folder is not allowed."); 3837 return OsConstants.EPERM; 3838 } else if (FileUtils.contains(directoryAndroid, new File(newPathLowerCase))) { 3839 if (newRelativePath.length <= 2) { 3840 // Path is directly under Android, Android/media, Android/data, Android/obb or 3841 // some other directory under Android. Don't allow moving files and directories 3842 // in these paths. Files and directories are only allowed to move to path 3843 // Android/media/<app_specific_directory>/* 3844 Log.e(TAG, errorMessage + newPath + " is in app external directory. " 3845 + "Renaming a file/directory to app external directory is not " 3846 + "allowed."); 3847 return OsConstants.EPERM; 3848 } else if (!FileUtils.contains(directoryAndroidMedia, new File(newPathLowerCase))) { 3849 // New path is not in Android/media/*. Don't allow moving of files or 3850 // directories to app external directory other than media directory. 3851 Log.e(TAG, errorMessage + newPath + " is not in external media directory." 3852 + "File/directory can only be renamed to a path in external media " 3853 + "directory. Renaming file/directory to path in other external " 3854 + "directories is not allowed"); 3855 return OsConstants.EPERM; 3856 } 3857 } 3858 3859 // Continue renaming files/directories if rename of oldPath to newPath is allowed. 3860 if (new File(oldPath).isFile()) { 3861 return renameFileCheckedForFuse(oldPath, newPath); 3862 } else { 3863 return renameDirectoryCheckedForFuse(oldPath, newPath); 3864 } 3865 } finally { 3866 restoreLocalCallingIdentity(token); 3867 } 3868 } 3869 3870 /** 3871 * Check if enable_unicode_check flag is enabled 3872 * Called from JNI in jni/MediaProviderWrapper.cpp 3873 */ 3874 @Keep isUnicodeCheckEnabledForFuse()3875 public boolean isUnicodeCheckEnabledForFuse() { 3876 return Flags.enableUnicodeCheck(); 3877 } 3878 3879 @Override checkUriPermission(@onNull Uri uri, int uid, int modeFlags)3880 public int checkUriPermission(@NonNull Uri uri, int uid, 3881 /* @Intent.AccessUriMode */ int modeFlags) { 3882 final LocalCallingIdentity token = clearLocalCallingIdentity( 3883 LocalCallingIdentity.fromExternal(getContext(), mUserCache, uid)); 3884 3885 if (isRedactedUri(uri)) { 3886 if ((modeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) { 3887 // we don't allow write grants on redacted uris. 3888 return PackageManager.PERMISSION_DENIED; 3889 } 3890 3891 uri = getUriForRedactedUri(uri); 3892 } 3893 3894 if (isPickerUri(uri)) { 3895 if (isCallerPhotoPicker()) { 3896 // Allow PhotoPicker app access to Picker media. 3897 return PERMISSION_GRANTED; 3898 } 3899 // Do not allow implicit access (by the virtue of ownership/permission) to picker uris. 3900 // Picker uris should have explicit permission grants. 3901 // If the calling app A has an explicit grant on picker uri, UriGrantsManagerService 3902 // will check the grant status and allow app A to grant the uri to app B (without 3903 // calling into MediaProvider) 3904 return PackageManager.PERMISSION_DENIED; 3905 } 3906 3907 try { 3908 final boolean allowHidden = isCallingPackageAllowedHidden(); 3909 final int table = matchUri(uri, allowHidden); 3910 3911 final DatabaseHelper helper; 3912 try { 3913 helper = getDatabaseForUri(uri); 3914 } catch (VolumeNotFoundException e) { 3915 return PackageManager.PERMISSION_DENIED; 3916 } 3917 3918 final int type; 3919 if ((modeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) { 3920 type = TYPE_UPDATE; 3921 } else { 3922 type = TYPE_QUERY; 3923 } 3924 3925 final SQLiteQueryBuilder qb = getQueryBuilder(type, table, uri, Bundle.EMPTY, null); 3926 try (Cursor c = qb.query(helper, 3927 new String[] { BaseColumns._ID }, null, null, null, null, null, null, null)) { 3928 if (c.getCount() == 1) { 3929 c.moveToFirst(); 3930 final long cursorId = c.getLong(0); 3931 3932 long uriId = -1; 3933 try { 3934 uriId = ContentUris.parseId(uri); 3935 } catch (NumberFormatException ignored) { 3936 // if the id is not a number, the uri doesn't have a valid ID at the end of 3937 // the uri, (i.e., uri is uri of the table not of the item/row) 3938 } 3939 3940 if (uriId != -1 && cursorId == uriId) { 3941 return PackageManager.PERMISSION_GRANTED; 3942 } 3943 } 3944 } 3945 3946 // For the uri with id cases, if it isn't returned in above query section, the result 3947 // isn't as expected. Don't grant the permission. 3948 switch (table) { 3949 case AUDIO_MEDIA_ID: 3950 case IMAGES_MEDIA_ID: 3951 case VIDEO_MEDIA_ID: 3952 case DOWNLOADS_ID: 3953 case FILES_ID: 3954 case AUDIO_MEDIA_ID_GENRES_ID: 3955 case AUDIO_GENRES_ID: 3956 case AUDIO_PLAYLISTS_ID: 3957 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 3958 case AUDIO_ARTISTS_ID: 3959 case AUDIO_ALBUMS_ID: 3960 return PackageManager.PERMISSION_DENIED; 3961 default: 3962 // continue below 3963 } 3964 3965 // If the uri is a valid content uri and doesn't have a valid ID at the end of the uri, 3966 // (i.e., uri is uri of the table not of the item/row), and app doesn't request prefix 3967 // grant, we are willing to grant this uri permission since this doesn't grant them any 3968 // extra access. This grant will only grant permissions on given uri, it will not grant 3969 // access to db rows of the corresponding table. 3970 if ((modeFlags & Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) == 0) { 3971 return PackageManager.PERMISSION_GRANTED; 3972 } 3973 } finally { 3974 restoreLocalCallingIdentity(token); 3975 } 3976 return PackageManager.PERMISSION_DENIED; 3977 } 3978 3979 @Override query(@onNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)3980 public Cursor query(@NonNull Uri uri, String[] projection, String selection, 3981 String[] selectionArgs, String sortOrder) { 3982 return query(uri, projection, 3983 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, sortOrder), null); 3984 } 3985 3986 @Override query(@onNull Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal)3987 public Cursor query(@NonNull Uri uri, String[] projection, Bundle queryArgs, 3988 CancellationSignal signal) { 3989 return query(uri, projection, queryArgs, signal, /* forSelf */ false); 3990 } 3991 query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal, boolean forSelf)3992 private Cursor query(Uri uri, String[] projection, Bundle queryArgs, 3993 CancellationSignal signal, boolean forSelf) { 3994 Trace.beginSection(safeTraceSectionNameWithUri("query", uri)); 3995 try { 3996 return queryInternal(uri, projection, queryArgs, signal, forSelf); 3997 } catch (FallbackException e) { 3998 return e.translateForQuery(getCallingPackageTargetSdkVersion()); 3999 } finally { 4000 Trace.endSection(); 4001 } 4002 } 4003 queryInternal(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal, boolean forSelf)4004 private Cursor queryInternal(Uri uri, String[] projection, Bundle queryArgs, 4005 CancellationSignal signal, boolean forSelf) throws FallbackException { 4006 if (isPickerUri(uri)) { 4007 return mPickerUriResolver.query(uri, projection, mCallingIdentity.get().pid, 4008 mCallingIdentity.get().uid, mCallingIdentity.get().getPackageName()); 4009 } 4010 4011 final String volumeName = getVolumeName(uri); 4012 PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName); 4013 queryArgs = (queryArgs != null) ? queryArgs : new Bundle(); 4014 4015 // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. 4016 queryArgs.remove(INCLUDED_DEFAULT_DIRECTORIES); 4017 4018 final ArraySet<String> honoredArgs = new ArraySet<>(); 4019 DatabaseUtils.resolveQueryArgs(queryArgs, honoredArgs::add, this::ensureCustomCollator); 4020 4021 // In case of QUERY_ARG_MEDIA_STANDARD_SORT_ORDER 4022 // disregard existing sort order and sort by INFERRED_DATE 4023 if (Flags.inferredMediaDate() && 4024 queryArgs.containsKey(QUERY_ARG_MEDIA_STANDARD_SORT_ORDER)) { 4025 queryArgs.putString(QUERY_ARG_SQL_SORT_ORDER, 4026 MediaColumns.INFERRED_DATE + " DESC"); 4027 } 4028 4029 Uri redactedUri = null; 4030 // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider. 4031 queryArgs.remove(QUERY_ARG_REDACTED_URI); 4032 if (isRedactedUri(uri)) { 4033 redactedUri = uri; 4034 uri = getUriForRedactedUri(uri); 4035 queryArgs.putParcelable(QUERY_ARG_REDACTED_URI, redactedUri); 4036 } 4037 4038 uri = safeUncanonicalize(uri); 4039 4040 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 4041 final boolean allowHidden = isCallingPackageAllowedHidden(); 4042 final int table = mUriMatcher.matchUri(uri, allowHidden, isCallerPhotoPicker()); 4043 4044 if (table == MEDIA_GRANTS) { 4045 return getReadGrantedMediaForPackage(queryArgs); 4046 } 4047 4048 // handle MEDIA_SCANNER before calling getDatabaseForUri() 4049 if (table == MEDIA_SCANNER) { 4050 // create a cursor to return volume currently being scanned by the media scanner 4051 MatrixCursor c = new MatrixCursor(new String[] {MediaStore.MEDIA_SCANNER_VOLUME}); 4052 c.addRow(new String[] {mMediaScannerVolume}); 4053 return c; 4054 } 4055 4056 // Used temporarily (until we have unique media IDs) to get an identifier 4057 // for the current sd card, so that the music app doesn't have to use the 4058 // non-public getFatVolumeId method 4059 if (table == FS_ID) { 4060 MatrixCursor c = new MatrixCursor(new String[] {"fsid"}); 4061 // current FAT volume ID 4062 int volumeId = -1; 4063 c.addRow(new Integer[] {volumeId}); 4064 return c; 4065 } 4066 4067 if (table == VERSION) { 4068 MatrixCursor c = new MatrixCursor(new String[] {"version"}); 4069 c.addRow(new Integer[] {DatabaseHelper.getDatabaseVersion(getContext())}); 4070 return c; 4071 } 4072 4073 if (PickerUriResolver.PICKER_INTERNAL_TABLES.contains(table)) { 4074 return mPickerUriResolver.query(table, queryArgs, mPickerDbFacade.getLocalProvider(), 4075 mPickerSyncController.getCloudProvider(), mPickerDataLayer); 4076 } 4077 if (table == PICKER_INTERNAL_V2) { 4078 return PickerUriResolverV2.query( 4079 getContext().getApplicationContext(), uri, queryArgs, signal); 4080 } 4081 4082 final DatabaseHelper helper = getDatabaseForUri(uri); 4083 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, queryArgs, 4084 honoredArgs::add); 4085 // Allowing hidden column _user_id for this query to support Cloned Profile use case. 4086 if (table == FILES) { 4087 qb.allowColumn(FileColumns._USER_ID); 4088 } 4089 4090 if (targetSdkVersion < Build.VERSION_CODES.R) { 4091 // Some apps are abusing "ORDER BY" clauses to inject "LIMIT" 4092 // clauses; gracefully lift them out. 4093 DatabaseUtils.recoverAbusiveSortOrder(queryArgs); 4094 4095 // Some apps are abusing the Uri query parameters to inject LIMIT 4096 // clauses; gracefully lift them out. 4097 DatabaseUtils.recoverAbusiveLimit(uri, queryArgs); 4098 } 4099 4100 if (targetSdkVersion < Build.VERSION_CODES.Q) { 4101 // Some apps are abusing the "WHERE" clause by injecting "GROUP BY" 4102 // clauses; gracefully lift them out. 4103 DatabaseUtils.recoverAbusiveSelection(queryArgs); 4104 4105 // Some apps are abusing the first column to inject "DISTINCT"; 4106 // gracefully lift them out. 4107 if ((projection != null) && (projection.length > 0) 4108 && projection[0].startsWith("DISTINCT ")) { 4109 projection[0] = projection[0].substring("DISTINCT ".length()); 4110 qb.setDistinct(true); 4111 } 4112 4113 // Some apps are generating thumbnails with getThumbnail(), but then 4114 // ignoring the returned Bitmap and querying the raw table; give 4115 // them a row with enough information to find the original image. 4116 final String selection = queryArgs.getString(QUERY_ARG_SQL_SELECTION); 4117 if ((table == IMAGES_THUMBNAILS || table == VIDEO_THUMBNAILS) 4118 && !TextUtils.isEmpty(selection)) { 4119 final Matcher matcher = PATTERN_SELECTION_ID.matcher(selection); 4120 if (matcher.matches()) { 4121 final long id = Long.parseLong(matcher.group(1)); 4122 4123 final Uri fullUri; 4124 if (table == IMAGES_THUMBNAILS) { 4125 fullUri = ContentUris.withAppendedId( 4126 Images.Media.getContentUri(volumeName), id); 4127 } else if (table == VIDEO_THUMBNAILS) { 4128 fullUri = ContentUris.withAppendedId( 4129 Video.Media.getContentUri(volumeName), id); 4130 } else { 4131 throw new IllegalArgumentException(); 4132 } 4133 4134 final MatrixCursor cursor = new MatrixCursor(projection); 4135 final File file = ContentResolver.encodeToFile( 4136 fullUri.buildUpon().appendPath("thumbnail").build()); 4137 final String data = file.getAbsolutePath(); 4138 cursor.newRow().add(MediaColumns._ID, null) 4139 .add(Images.Thumbnails.IMAGE_ID, id) 4140 .add(Video.Thumbnails.VIDEO_ID, id) 4141 .add(MediaColumns.DATA, data); 4142 return cursor; 4143 } 4144 } 4145 } 4146 4147 // Update locale if necessary. 4148 if (helper.isInternal() && !Locale.getDefault().equals(mLastLocale)) { 4149 Log.i(TAG, "Updating locale within queryInternal"); 4150 onLocaleChanged(false); 4151 } 4152 4153 Cursor c; 4154 4155 if (Flags.enableOemMetadata() 4156 && hasColumnsToFilterInProjection(qb, projection, List.of(OEM_METADATA)) 4157 && !mCallingIdentity.get().checkCallingPermissionOemMetadata()) { 4158 // Filter oem_data column to return as NULL 4159 projection = updateProjectionToFilterColumns(qb, projection, List.of(OEM_METADATA)); 4160 } 4161 4162 // The prev deprecated latitude and longitude columns are being populated again for 4163 // picker search. We prevent any read access to them if they are present in the 4164 // query projection. 4165 if (indexMediaLatitudeLongitude() && hasColumnsToFilterInProjection( 4166 qb, projection, List.of(LATITUDE, LONGITUDE)) && !isCallingPackageSelf()) { 4167 // Filter latitude and longitude to return as NULL 4168 projection = updateProjectionToFilterColumns( 4169 qb, projection, List.of(LATITUDE, LONGITUDE)); 4170 } 4171 4172 if (shouldFilterOwnerPackageNameFlag() 4173 && shouldFilterOwnerPackageNameInProjection(qb, projection)) { 4174 Log.i(TAG, String.format("Filtering owner package name for %s, projection: %s", 4175 mCallingIdentity.get().getPackageName(), Arrays.toString(projection))); 4176 4177 // Get a list of all owner_package_names in the result 4178 final String[] ownerPackageNamesArr = getAllOwnerPackageNames(qb, helper, 4179 queryArgs, signal); 4180 4181 // Get a list of queryable owner_package_names out of all 4182 final Set<String> queryablePackages = getQueryablePackages(ownerPackageNamesArr); 4183 4184 // Substitute owner_package_name column with following: 4185 // CASE WHEN owner_package_name IN ('queryablePackageA','queryablePackageB') 4186 // THEN owner_package_name ELSE NULL END AS owner_package_name 4187 final String[] newProjection = prepareSubstitution(qb, projection, queryablePackages); 4188 c = qb.query(helper, newProjection, queryArgs, signal); 4189 } else { 4190 c = qb.query(helper, projection, queryArgs, signal); 4191 } 4192 4193 if (c != null && !forSelf) { 4194 // As a performance optimization, only configure notifications when 4195 // resulting cursor will leave our process 4196 final boolean callerIsRemote = mCallingIdentity.get().pid != android.os.Process.myPid(); 4197 if (callerIsRemote && !isFuseThread()) { 4198 c.setNotificationUri(getContext().getContentResolver(), uri); 4199 } 4200 4201 final Bundle extras = new Bundle(); 4202 extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, 4203 honoredArgs.toArray(new String[honoredArgs.size()])); 4204 c.setExtras(extras); 4205 } 4206 4207 // Query was on a redacted URI, update the sensitive information such as the _ID, DATA etc. 4208 if (redactedUri != null && c != null) { 4209 try { 4210 return getRedactedUriCursor(redactedUri, c); 4211 } finally { 4212 c.close(); 4213 } 4214 } 4215 4216 return c; 4217 } 4218 hasColumnsToFilterInProjection( SQLiteQueryBuilder qb, String[] projection, List<String> columnsToFilter)4219 private boolean hasColumnsToFilterInProjection( 4220 SQLiteQueryBuilder qb, String[] projection, List<String> columnsToFilter) { 4221 boolean columnsFound = false; 4222 List<String> projectionInLowerCase = new ArrayList<>(); 4223 if (projection != null) { 4224 projectionInLowerCase = Arrays.asList(projection); 4225 projectionInLowerCase.replaceAll(String::toLowerCase); 4226 } 4227 for (String column: columnsToFilter) { 4228 columnsFound = 4229 (!projectionInLowerCase.isEmpty() && projectionInLowerCase.contains(column)) 4230 || (projection == null && qb.getProjectionMap() != null 4231 && qb.getProjectionMap().containsKey(column)); 4232 if (columnsFound) { 4233 return columnsFound; 4234 } 4235 } 4236 return columnsFound; 4237 } 4238 updateProjectionToFilterColumns( SQLiteQueryBuilder qb, String[] projection, List<String> columnsToFilter)4239 private String[] updateProjectionToFilterColumns( 4240 SQLiteQueryBuilder qb, String[] projection, List<String> columnsToFilter) { 4241 projection = maybeReplaceNullProjection(projection, qb); 4242 List<String> projectionList = Arrays.asList(projection); 4243 projectionList.replaceAll(String::toLowerCase); 4244 4245 for (String columnToFilter: columnsToFilter) { 4246 if (projectionList.contains(columnToFilter)) { 4247 int indexOfColumnToBeFiltered = projectionList.indexOf(columnToFilter); 4248 projectionList.set( 4249 indexOfColumnToBeFiltered, 4250 constructNullProjectionForColumn(columnToFilter) 4251 ); 4252 } 4253 } 4254 String[] updatedProjection = new String[projectionList.size()]; 4255 return projectionList.toArray(updatedProjection); 4256 } 4257 constructNullProjectionForColumn(String columnName)4258 private String constructNullProjectionForColumn(String columnName) { 4259 return "NULL AS " + columnName; 4260 } 4261 4262 /** 4263 * Constructs the following projection string: 4264 * CASE WHEN owner_package_name IN ("queryablePackageA","queryablePackageB") 4265 * THEN owner_package_name ELSE NULL END AS owner_package_name 4266 */ constructOwnerPackageNameProjection(Set<String> queryablePackages)4267 private String constructOwnerPackageNameProjection(Set<String> queryablePackages) { 4268 final String packageNames = String.join(",", queryablePackages 4269 .stream() 4270 .map(name -> ("'" + name + "'")) 4271 .collect(Collectors.toList())); 4272 4273 final StringBuilder newProjection = new StringBuilder() 4274 .append("CASE WHEN ") 4275 .append(OWNER_PACKAGE_NAME) 4276 .append(" IN (") 4277 .append(packageNames) 4278 .append(") THEN ") 4279 .append(OWNER_PACKAGE_NAME) 4280 .append(" ELSE NULL END AS ") 4281 .append(OWNER_PACKAGE_NAME); 4282 4283 Log.d(TAG, "Constructed owner_package_name substitution: " + newProjection); 4284 return newProjection.toString(); 4285 } 4286 getAllOwnerPackageNames(SQLiteQueryBuilder qb, DatabaseHelper helper, Bundle queryArgs, CancellationSignal signal)4287 private String[] getAllOwnerPackageNames(SQLiteQueryBuilder qb, DatabaseHelper helper, 4288 Bundle queryArgs, CancellationSignal signal) { 4289 final SQLiteQueryBuilder qbCopy = new SQLiteQueryBuilder(qb); 4290 qbCopy.setDistinct(true); 4291 qbCopy.appendWhereStandalone(OWNER_PACKAGE_NAME + " <> '' AND " 4292 + OWNER_PACKAGE_NAME + " <> 'null' AND " + OWNER_PACKAGE_NAME + " IS NOT NULL"); 4293 final Cursor ownerPackageNames = qbCopy.query(helper, new String[]{OWNER_PACKAGE_NAME}, 4294 queryArgs, signal); 4295 4296 final String[] ownerPackageNamesArr = new String[ownerPackageNames.getCount()]; 4297 int i = 0; 4298 while (ownerPackageNames.moveToNext()) { 4299 ownerPackageNamesArr[i++] = ownerPackageNames.getString(0); 4300 } 4301 return ownerPackageNamesArr; 4302 } 4303 prepareSubstitution(SQLiteQueryBuilder qb, String[] projection, Set<String> queryablePackages)4304 private String[] prepareSubstitution(SQLiteQueryBuilder qb, 4305 String[] projection, Set<String> queryablePackages) { 4306 projection = maybeReplaceNullProjection(projection, qb); 4307 if (qb.getProjectionAllowlist() == null) { 4308 qb.setProjectionAllowlist(new ArrayList<>()); 4309 } 4310 final String[] newProjection = new String[projection.length]; 4311 for (int i = 0; i < projection.length; i++) { 4312 if (!OWNER_PACKAGE_NAME.equalsIgnoreCase(projection[i])) { 4313 newProjection[i] = projection[i]; 4314 } else { 4315 newProjection[i] = constructOwnerPackageNameProjection(queryablePackages); 4316 // Allow constructed owner_package_name column in projection 4317 final String escapedColumnCase = Pattern.quote(newProjection[i]); 4318 qb.getProjectionAllowlist().add(Pattern.compile(escapedColumnCase)); 4319 } 4320 } 4321 return newProjection; 4322 } 4323 maybeReplaceNullProjection(String[] projection, SQLiteQueryBuilder qb)4324 private String[] maybeReplaceNullProjection(String[] projection, SQLiteQueryBuilder qb) { 4325 // List all columns instead of placing "*" in the SQL query 4326 // to be able to substitute some columns 4327 if (projection == null) { 4328 projection = qb.getAllColumnsFromProjectionMap(); 4329 // Allow all columns from the projection map 4330 qb.setStrictColumns(false); 4331 } 4332 return projection; 4333 } 4334 4335 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) getQueryablePackages(String[] packageNames)4336 private Set<String> getQueryablePackages(String[] packageNames) { 4337 final boolean[] canBeQueriedInfo; 4338 try { 4339 canBeQueriedInfo = mPackageManager.canPackageQuery( 4340 mCallingIdentity.get().getPackageName(), packageNames); 4341 } catch (NameNotFoundException e) { 4342 Log.e(TAG, "Invalid package name", e); 4343 // If package manager throws an error, only assume calling package as queryable package 4344 return new HashSet<>(Arrays.asList(mCallingIdentity.get().getPackageName())); 4345 } 4346 4347 final Set<String> queryablePackages = new HashSet<>(); 4348 for (int i = 0; i < packageNames.length; i++) { 4349 if (canBeQueriedInfo[i]) { 4350 queryablePackages.add(packageNames[i]); 4351 } 4352 } 4353 return queryablePackages; 4354 } 4355 4356 @NotNull getReadGrantedMediaForPackage(Bundle extras)4357 private Cursor getReadGrantedMediaForPackage(Bundle extras) { 4358 final int caller = Binder.getCallingUid(); 4359 int userId; 4360 String[] packageNames; 4361 if (!checkPermissionSelf(caller)) { 4362 // All other callers are unauthorized. 4363 throw new SecurityException( 4364 getSecurityExceptionMessage("read media grants")); 4365 } 4366 final PackageManager pm = getContext().getPackageManager(); 4367 final int packageUid = extras.getInt(Intent.EXTRA_UID); 4368 packageNames = pm.getPackagesForUid(packageUid); 4369 // Get the userId from packageUid as the initiator could be a cloned app, which 4370 // accesses Media via MP of its parent user and Binder's callingUid reflects 4371 // the latter. 4372 userId = uidToUserId(packageUid); 4373 String[] mimeTypes = extras.getStringArray(EXTRA_MIME_TYPE_SELECTION); 4374 // Available volumes, to filter out any external storage that may be removed but the grants 4375 // persisted. 4376 String[] availableVolumes = mVolumeCache.getExternalVolumeNames().toArray(new String[0]); 4377 return mMediaGrants.getMediaGrantsForPackages(packageNames, userId, mimeTypes, 4378 availableVolumes); 4379 } 4380 4381 @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) shouldFilterOwnerPackageNameInProjection(SQLiteQueryBuilder qb, String[] projection)4382 private boolean shouldFilterOwnerPackageNameInProjection(SQLiteQueryBuilder qb, 4383 String[] projection) { 4384 return projectionNeedsOwnerPackageFiltering(projection, qb) 4385 && isApplicableForOwnerPackageNameFiltering(); 4386 } 4387 isApplicableForOwnerPackageNameFiltering()4388 private boolean isApplicableForOwnerPackageNameFiltering() { 4389 return SdkLevel.isAtLeastU() 4390 && getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE 4391 && !mCallingIdentity.get().checkCallingPermissionsOwnerPackageName(); 4392 } 4393 projectionNeedsOwnerPackageFiltering(String[] proj, SQLiteQueryBuilder qb)4394 private boolean projectionNeedsOwnerPackageFiltering(String[] proj, SQLiteQueryBuilder qb) { 4395 return (proj != null && Arrays.asList(proj).contains(MediaColumns.OWNER_PACKAGE_NAME)) 4396 || (proj == null && qb.getProjectionMap() != null 4397 && qb.getProjectionMap().containsKey(OWNER_PACKAGE_NAME)); 4398 } 4399 shouldFilterOwnerPackageNameFlag()4400 private boolean shouldFilterOwnerPackageNameFlag() { 4401 return true; 4402 } 4403 isUriSupportedForRedaction(Uri uri)4404 private boolean isUriSupportedForRedaction(Uri uri) { 4405 final int match = matchUri(uri, true); 4406 return REDACTED_URI_SUPPORTED_TYPES.contains(match); 4407 } 4408 getRedactedUriCursor(Uri redactedUri, @NonNull Cursor c)4409 private Cursor getRedactedUriCursor(Uri redactedUri, @NonNull Cursor c) { 4410 final HashSet<String> columnNames = new HashSet<>(Arrays.asList(c.getColumnNames())); 4411 final MatrixCursor redactedUriCursor = new MatrixCursor(c.getColumnNames()); 4412 final String redactedUriId = redactedUri.getLastPathSegment(); 4413 4414 if (!c.moveToFirst()) { 4415 return redactedUriCursor; 4416 } 4417 4418 // NOTE: It is safe to assume that there will only be one entry corresponding to a 4419 // redacted URI as it corresponds to a unique DB entry. 4420 if (c.getCount() != 1) { 4421 throw new AssertionError("Two rows corresponding to " + redactedUri.toString() 4422 + " found, when only one expected"); 4423 } 4424 4425 final MatrixCursor.RowBuilder row = redactedUriCursor.newRow(); 4426 for (String columnName : c.getColumnNames()) { 4427 final int colIndex = c.getColumnIndex(columnName); 4428 if (c.getType(colIndex) == FIELD_TYPE_BLOB) { 4429 row.add(c.getBlob(colIndex)); 4430 } else { 4431 row.add(c.getString(colIndex)); 4432 } 4433 } 4434 4435 String ext = getFileExtensionFromCursor(c, columnNames); 4436 ext = ext == null ? "" : "." + ext; 4437 final String displayName = redactedUriId + ext; 4438 final String data = buildPrimaryVolumeFile(uidToUserId(Binder.getCallingUid()), 4439 getRedactedRelativePath(), displayName).getAbsolutePath(); 4440 4441 updateRow(columnNames, MediaColumns._ID, row, redactedUriId); 4442 updateRow(columnNames, MediaColumns.DISPLAY_NAME, row, displayName); 4443 updateRow(columnNames, MediaColumns.RELATIVE_PATH, row, getRedactedRelativePath()); 4444 updateRow(columnNames, MediaColumns.BUCKET_DISPLAY_NAME, row, getRedactedRelativePath()); 4445 updateRow(columnNames, MediaColumns.DATA, row, data); 4446 updateRow(columnNames, MediaColumns.DOCUMENT_ID, row, null); 4447 updateRow(columnNames, MediaColumns.INSTANCE_ID, row, null); 4448 updateRow(columnNames, MediaColumns.BUCKET_ID, row, null); 4449 4450 return redactedUriCursor; 4451 } 4452 4453 @Nullable getFileExtensionFromCursor(@onNull Cursor c, @NonNull HashSet<String> columnNames)4454 private static String getFileExtensionFromCursor(@NonNull Cursor c, 4455 @NonNull HashSet<String> columnNames) { 4456 if (columnNames.contains(MediaColumns.DATA)) { 4457 return extractFileExtension(c.getString(c.getColumnIndex(MediaColumns.DATA))); 4458 } 4459 if (columnNames.contains(MediaColumns.DISPLAY_NAME)) { 4460 return extractFileExtension(c.getString(c.getColumnIndex(MediaColumns.DISPLAY_NAME))); 4461 } 4462 return null; 4463 } 4464 updateRow(HashSet<String> columnNames, String columnName, MatrixCursor.RowBuilder row, Object val)4465 private void updateRow(HashSet<String> columnNames, String columnName, 4466 MatrixCursor.RowBuilder row, Object val) { 4467 if (columnNames.contains(columnName)) { 4468 row.add(columnName, val); 4469 } 4470 } 4471 getUriForRedactedUri(Uri redactedUri)4472 private Uri getUriForRedactedUri(Uri redactedUri) { 4473 final Uri.Builder builder = redactedUri.buildUpon(); 4474 builder.path(null); 4475 final List<String> segments = redactedUri.getPathSegments(); 4476 for (int i = 0; i < segments.size() - 1; i++) { 4477 builder.appendPath(segments.get(i)); 4478 } 4479 4480 DatabaseHelper helper; 4481 try { 4482 helper = getDatabaseForUri(redactedUri); 4483 } catch (VolumeNotFoundException e) { 4484 throw e.rethrowAsIllegalArgumentException(); 4485 } 4486 4487 try (final Cursor c = helper.runWithoutTransaction( 4488 (db) -> db.query("files", new String[]{MediaColumns._ID}, 4489 FileColumns.REDACTED_URI_ID + "=?", 4490 new String[]{redactedUri.getLastPathSegment()}, null, null, null))) { 4491 if (!c.moveToFirst()) { 4492 throw new IllegalArgumentException( 4493 "Uri: " + redactedUri.toString() + " not found."); 4494 } 4495 4496 builder.appendPath(c.getString(0)); 4497 return builder.build(); 4498 } 4499 } 4500 isRedactedUri(Uri uri)4501 private boolean isRedactedUri(Uri uri) { 4502 String id = uri.getLastPathSegment(); 4503 return id != null && id.startsWith(REDACTED_URI_ID_PREFIX) 4504 && id.length() == REDACTED_URI_ID_SIZE; 4505 } 4506 4507 @Override getType(Uri url)4508 public String getType(Uri url) { 4509 if (isRedactedUri(url)) { 4510 return queryForTypeAsCaller(url); 4511 } 4512 4513 final int match = matchUri(url, true); 4514 switch (match) { 4515 case IMAGES_MEDIA_ID: 4516 case AUDIO_MEDIA_ID: 4517 case AUDIO_PLAYLISTS_ID: 4518 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 4519 case VIDEO_MEDIA_ID: 4520 case DOWNLOADS_ID: 4521 case FILES_ID: 4522 if (SdkLevel.isAtLeastU()) { 4523 // Starting Android 14, there is permission check for 4524 // getting types requiring internal query. 4525 return queryForTypeAsCaller(url); 4526 } else { 4527 return queryForTypeAsSelf(url); 4528 } 4529 4530 case IMAGES_MEDIA: 4531 case IMAGES_THUMBNAILS: 4532 return Images.Media.CONTENT_TYPE; 4533 4534 case AUDIO_ALBUMART_ID: 4535 case AUDIO_ALBUMART_FILE_ID: 4536 case IMAGES_THUMBNAILS_ID: 4537 case VIDEO_THUMBNAILS_ID: 4538 return "image/jpeg"; 4539 4540 case AUDIO_MEDIA: 4541 case AUDIO_GENRES_ID_MEMBERS: 4542 case AUDIO_PLAYLISTS_ID_MEMBERS: 4543 return Audio.Media.CONTENT_TYPE; 4544 4545 case AUDIO_GENRES: 4546 case AUDIO_MEDIA_ID_GENRES: 4547 return Audio.Genres.CONTENT_TYPE; 4548 case AUDIO_GENRES_ID: 4549 case AUDIO_MEDIA_ID_GENRES_ID: 4550 return Audio.Genres.ENTRY_CONTENT_TYPE; 4551 case AUDIO_PLAYLISTS: 4552 return Audio.Playlists.CONTENT_TYPE; 4553 4554 case VIDEO_MEDIA: 4555 return Video.Media.CONTENT_TYPE; 4556 case DOWNLOADS: 4557 return Downloads.CONTENT_TYPE; 4558 4559 case PICKER_ID: 4560 case PICKER_GET_CONTENT_ID: 4561 return mPickerUriResolver.getType(url, Binder.getCallingPid(), 4562 Binder.getCallingUid()); 4563 } 4564 throw new IllegalStateException("Unknown URL : " + url); 4565 } 4566 queryForTypeAsSelf(Uri url)4567 private String queryForTypeAsSelf(Uri url) { 4568 final LocalCallingIdentity token = clearLocalCallingIdentity(); 4569 try { 4570 return queryForTypeAsCaller(url); 4571 } finally { 4572 restoreLocalCallingIdentity(token); 4573 } 4574 } 4575 queryForTypeAsCaller(Uri url)4576 private String queryForTypeAsCaller(Uri url) { 4577 try (Cursor cursor = queryForSingleItem(url, 4578 new String[] { MediaColumns.MIME_TYPE }, null, null, null)) { 4579 return cursor.getString(0); 4580 } catch (FileNotFoundException e) { 4581 throw new IllegalArgumentException(e.getMessage()); 4582 } 4583 } 4584 4585 @VisibleForTesting ensureFileColumns(@onNull Uri uri, @NonNull ContentValues values)4586 void ensureFileColumns(@NonNull Uri uri, @NonNull ContentValues values) 4587 throws VolumeArgumentException, VolumeNotFoundException { 4588 final LocalUriMatcher matcher = new LocalUriMatcher(MediaStore.AUTHORITY); 4589 final int match = matcher.matchUri(uri, true); 4590 ensureNonUniqueFileColumns(match, uri, Bundle.EMPTY, values, null /* currentPath */); 4591 } 4592 ensureUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath)4593 private void ensureUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, 4594 @NonNull ContentValues values, @Nullable String currentPath) 4595 throws VolumeArgumentException, VolumeNotFoundException { 4596 ensureFileColumns(match, uri, extras, values, true, currentPath); 4597 } 4598 ensureNonUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath)4599 private void ensureNonUniqueFileColumns(int match, @NonNull Uri uri, 4600 @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath) 4601 throws VolumeArgumentException, VolumeNotFoundException { 4602 ensureFileColumns(match, uri, extras, values, false, currentPath); 4603 } 4604 4605 /** 4606 * Get the various file-related {@link MediaColumns} in the given 4607 * {@link ContentValues} into a consistent condition. Also validates that defined 4608 * columns are valid for the given {@link Uri}, such as ensuring that only 4609 * {@code image/*} can be inserted into 4610 * {@link android.provider.MediaStore.Images}. 4611 */ ensureFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath)4612 private void ensureFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, 4613 @NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath) 4614 throws VolumeArgumentException, VolumeNotFoundException { 4615 Trace.beginSection("MP.ensureFileColumns"); 4616 4617 Objects.requireNonNull(uri); 4618 Objects.requireNonNull(extras); 4619 Objects.requireNonNull(values); 4620 4621 // Figure out defaults based on Uri being modified 4622 String defaultMimeType = ClipDescription.MIMETYPE_UNKNOWN; 4623 int defaultMediaType = FileColumns.MEDIA_TYPE_NONE; 4624 String defaultPrimary = Environment.DIRECTORY_DOWNLOADS; 4625 String defaultSecondary = null; 4626 List<String> allowedPrimary = Arrays.asList( 4627 Environment.DIRECTORY_DOWNLOADS, 4628 Environment.DIRECTORY_DOCUMENTS); 4629 switch (match) { 4630 case AUDIO_MEDIA: 4631 case AUDIO_MEDIA_ID: 4632 defaultMimeType = "audio/mpeg"; 4633 defaultMediaType = FileColumns.MEDIA_TYPE_AUDIO; 4634 defaultPrimary = Environment.DIRECTORY_MUSIC; 4635 if (SdkLevel.isAtLeastS()) { 4636 allowedPrimary = Arrays.asList( 4637 Environment.DIRECTORY_ALARMS, 4638 Environment.DIRECTORY_AUDIOBOOKS, 4639 Environment.DIRECTORY_MUSIC, 4640 Environment.DIRECTORY_NOTIFICATIONS, 4641 Environment.DIRECTORY_PODCASTS, 4642 Environment.DIRECTORY_RECORDINGS, 4643 Environment.DIRECTORY_RINGTONES); 4644 } else { 4645 allowedPrimary = Arrays.asList( 4646 Environment.DIRECTORY_ALARMS, 4647 Environment.DIRECTORY_AUDIOBOOKS, 4648 Environment.DIRECTORY_MUSIC, 4649 Environment.DIRECTORY_NOTIFICATIONS, 4650 Environment.DIRECTORY_PODCASTS, 4651 FileUtils.DIRECTORY_RECORDINGS, 4652 Environment.DIRECTORY_RINGTONES); 4653 } 4654 break; 4655 case VIDEO_MEDIA: 4656 case VIDEO_MEDIA_ID: 4657 defaultMimeType = "video/mp4"; 4658 defaultMediaType = FileColumns.MEDIA_TYPE_VIDEO; 4659 defaultPrimary = Environment.DIRECTORY_MOVIES; 4660 allowedPrimary = Arrays.asList( 4661 Environment.DIRECTORY_DCIM, 4662 Environment.DIRECTORY_MOVIES, 4663 Environment.DIRECTORY_PICTURES); 4664 break; 4665 case IMAGES_MEDIA: 4666 case IMAGES_MEDIA_ID: 4667 defaultMimeType = "image/jpeg"; 4668 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE; 4669 defaultPrimary = Environment.DIRECTORY_PICTURES; 4670 allowedPrimary = Arrays.asList( 4671 Environment.DIRECTORY_DCIM, 4672 Environment.DIRECTORY_PICTURES); 4673 break; 4674 case AUDIO_ALBUMART: 4675 case AUDIO_ALBUMART_ID: 4676 defaultMimeType = "image/jpeg"; 4677 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE; 4678 defaultPrimary = Environment.DIRECTORY_MUSIC; 4679 allowedPrimary = Collections.singletonList(defaultPrimary); 4680 defaultSecondary = DIRECTORY_THUMBNAILS; 4681 break; 4682 case VIDEO_THUMBNAILS: 4683 case VIDEO_THUMBNAILS_ID: 4684 defaultMimeType = "image/jpeg"; 4685 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE; 4686 defaultPrimary = Environment.DIRECTORY_MOVIES; 4687 allowedPrimary = Collections.singletonList(defaultPrimary); 4688 defaultSecondary = DIRECTORY_THUMBNAILS; 4689 break; 4690 case IMAGES_THUMBNAILS: 4691 case IMAGES_THUMBNAILS_ID: 4692 defaultMimeType = "image/jpeg"; 4693 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE; 4694 defaultPrimary = Environment.DIRECTORY_PICTURES; 4695 allowedPrimary = Collections.singletonList(defaultPrimary); 4696 defaultSecondary = DIRECTORY_THUMBNAILS; 4697 break; 4698 case AUDIO_PLAYLISTS: 4699 case AUDIO_PLAYLISTS_ID: 4700 defaultMimeType = "audio/mpegurl"; 4701 defaultMediaType = FileColumns.MEDIA_TYPE_PLAYLIST; 4702 defaultPrimary = Environment.DIRECTORY_MUSIC; 4703 allowedPrimary = Arrays.asList( 4704 Environment.DIRECTORY_MUSIC, 4705 Environment.DIRECTORY_MOVIES); 4706 break; 4707 case DOWNLOADS: 4708 case DOWNLOADS_ID: 4709 defaultPrimary = Environment.DIRECTORY_DOWNLOADS; 4710 allowedPrimary = Collections.singletonList(defaultPrimary); 4711 break; 4712 case FILES: 4713 case FILES_ID: 4714 // Use defaults above 4715 break; 4716 default: 4717 Log.w(TAG, "Unhandled location " + uri + "; assuming generic files"); 4718 break; 4719 } 4720 4721 final String resolvedVolumeName = resolveVolumeName(uri); 4722 4723 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA)) 4724 && MediaStore.VOLUME_INTERNAL.equals(resolvedVolumeName)) { 4725 // TODO: promote this to top-level check 4726 throw new UnsupportedOperationException( 4727 "Writing to internal storage is not supported."); 4728 } 4729 4730 // Force values when raw path provided 4731 if (!TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) { 4732 FileUtils.computeValuesFromData(values, isFuseThread()); 4733 } 4734 4735 final boolean isTargetSdkROrHigher = 4736 getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R; 4737 final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME); 4738 final String mimeTypeFromExt = TextUtils.isEmpty(displayName) ? null : 4739 MimeUtils.resolveMimeType(new File(displayName)); 4740 4741 if (TextUtils.isEmpty(values.getAsString(MediaColumns.MIME_TYPE))) { 4742 if (isTargetSdkROrHigher) { 4743 // Extract the MIME type from the display name if we couldn't resolve it from the 4744 // raw path 4745 if (mimeTypeFromExt != null) { 4746 values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt); 4747 } else { 4748 // We couldn't resolve mimeType, it means that both display name and MIME type 4749 // were missing in values, so we use defaultMimeType. 4750 values.put(MediaColumns.MIME_TYPE, defaultMimeType); 4751 } 4752 } else if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) { 4753 values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt); 4754 } else { 4755 // We don't use mimeTypeFromExt to preserve legacy behavior. 4756 values.put(MediaColumns.MIME_TYPE, defaultMimeType); 4757 } 4758 } 4759 4760 String mimeType = values.getAsString(MediaColumns.MIME_TYPE); 4761 if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) { 4762 // We allow any mimeType for generic uri with default media type as MEDIA_TYPE_NONE. 4763 } else if (mimeType != null && 4764 MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) == null) { 4765 if (mimeTypeFromExt != null && 4766 defaultMediaType == MimeUtils.resolveMediaType(mimeTypeFromExt)) { 4767 // If mimeType from extension matches the defaultMediaType of uri, we use mimeType 4768 // from file extension as mimeType. This is an effort to guess the mimeType when we 4769 // get unsupported mimeType. 4770 // Note: We can't force defaultMimeType because when we force defaultMimeType, we 4771 // will force the file extension as well. For example, if DISPLAY_NAME=Foo.png and 4772 // mimeType="image/*". If we force mimeType to be "image/jpeg", we append the file 4773 // name with the new file extension i.e., "Foo.png.jpg" where as the expected file 4774 // name was "Foo.png" 4775 values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt); 4776 } else if (isTargetSdkROrHigher) { 4777 // We are here because given mimeType is unsupported also we couldn't guess valid 4778 // mimeType from file extension. 4779 throw new IllegalArgumentException("Unsupported MIME type " + mimeType); 4780 } else { 4781 // We can't throw error for legacy apps, so we try to use defaultMimeType. 4782 values.put(MediaColumns.MIME_TYPE, defaultMimeType); 4783 } 4784 } 4785 4786 // Give ourselves reasonable defaults when missing 4787 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DISPLAY_NAME))) { 4788 values.put(MediaColumns.DISPLAY_NAME, 4789 String.valueOf(System.currentTimeMillis())); 4790 } 4791 final Integer formatObject = values.getAsInteger(FileColumns.FORMAT); 4792 final int format = formatObject == null ? 0 : formatObject; 4793 if (format == MtpConstants.FORMAT_ASSOCIATION) { 4794 values.putNull(MediaColumns.MIME_TYPE); 4795 } 4796 4797 mimeType = values.getAsString(MediaColumns.MIME_TYPE); 4798 // Quick check MIME type against table 4799 if (mimeType != null) { 4800 PulledMetrics.logMimeTypeAccess(getCallingUidOrSelf(), mimeType); 4801 final int actualMediaType = MimeUtils.resolveMediaType(mimeType); 4802 if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) { 4803 // Give callers an opportunity to work with playlists and 4804 // subtitles using the generic files table 4805 switch (actualMediaType) { 4806 case FileColumns.MEDIA_TYPE_PLAYLIST: 4807 defaultMimeType = "audio/mpegurl"; 4808 defaultMediaType = FileColumns.MEDIA_TYPE_PLAYLIST; 4809 defaultPrimary = Environment.DIRECTORY_MUSIC; 4810 allowedPrimary = new ArrayList<>(allowedPrimary); 4811 allowedPrimary.add(Environment.DIRECTORY_MUSIC); 4812 allowedPrimary.add(Environment.DIRECTORY_MOVIES); 4813 break; 4814 case FileColumns.MEDIA_TYPE_SUBTITLE: 4815 defaultMimeType = "application/x-subrip"; 4816 defaultMediaType = FileColumns.MEDIA_TYPE_SUBTITLE; 4817 defaultPrimary = Environment.DIRECTORY_MOVIES; 4818 allowedPrimary = new ArrayList<>(allowedPrimary); 4819 allowedPrimary.add(Environment.DIRECTORY_MUSIC); 4820 allowedPrimary.add(Environment.DIRECTORY_MOVIES); 4821 break; 4822 } 4823 } else if (defaultMediaType != actualMediaType) { 4824 final String[] split = defaultMimeType.split("/"); 4825 throw new IllegalArgumentException( 4826 "MIME type " + mimeType + " cannot be inserted into " + uri 4827 + "; expected MIME type under " + split[0] + "/*"); 4828 } 4829 } 4830 4831 // Use default directories when missing 4832 if (TextUtils.isEmpty(values.getAsString(MediaColumns.RELATIVE_PATH))) { 4833 if (defaultSecondary != null) { 4834 values.put(MediaColumns.RELATIVE_PATH, 4835 defaultPrimary + '/' + defaultSecondary + '/'); 4836 } else { 4837 values.put(MediaColumns.RELATIVE_PATH, 4838 defaultPrimary + '/'); 4839 } 4840 } 4841 4842 // Generate path when undefined 4843 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) { 4844 // Note that just the volume name isn't enough to determine the path, 4845 // since we can manage different volumes with the same name for 4846 // different users. Instead, if we have a current path (which implies 4847 // an already existing file to be renamed), use that to derive the 4848 // user-id of the file, and in turn use that to derive the correct 4849 // volume. Cross-user renames are not supported without a specified 4850 // DATA column. 4851 File volumePath; 4852 UserHandle userHandle = mCallingIdentity.get().getUser(); 4853 Integer userIdFromPathObject = values.getAsInteger(FileColumns._USER_ID); 4854 int userIdFromPath = (userIdFromPathObject == null ? userHandle.getIdentifier() : 4855 userIdFromPathObject); 4856 // In case if the _user_id column is set, and is different from the userHandle 4857 // determined from mCallingIdentity, we prefer the former, as it comes from the original 4858 // path provided to MP process. 4859 // Normally this does not create any issues, but when cloned profile is active, an app 4860 // in root user can try to create an image file in lower file system, by specifying 4861 // the file directory as /storage/emulated/<cloneUserId>/DCIM. For such cases, we 4862 // would want <cloneUserId> to be used to determine path in MP entry. 4863 if (userHandle.getIdentifier() != userIdFromPath 4864 && isAppCloneUserPair(userHandle.getIdentifier(), userIdFromPath)) { 4865 userHandle = UserHandle.of(userIdFromPath); 4866 } 4867 if (currentPath != null) { 4868 int userId = FileUtils.extractUserId(currentPath); 4869 if (userId != -1) { 4870 userHandle = UserHandle.of(userId); 4871 } 4872 } 4873 try { 4874 volumePath = mVolumeCache.getVolumePath(resolvedVolumeName, userHandle); 4875 } catch (FileNotFoundException e) { 4876 throw new IllegalArgumentException(e); 4877 } 4878 4879 FileUtils.sanitizeValues(values, /*rewriteHiddenFileName*/ !isFuseThread()); 4880 FileUtils.computeDataFromValues(values, volumePath, isFuseThread()); 4881 assertFileColumnsConsistent(match, uri, values); 4882 4883 // Create result file 4884 File res = new File(values.getAsString(MediaColumns.DATA)); 4885 try { 4886 if (makeUnique) { 4887 res = FileUtils.buildUniqueFile(res.getParentFile(), 4888 mimeType, res.getName()); 4889 } else { 4890 res = FileUtils.buildNonUniqueFile(res.getParentFile(), 4891 mimeType, res.getName()); 4892 } 4893 } catch (FileNotFoundException e) { 4894 throw new IllegalStateException( 4895 "Failed to build unique file: " + res + " " + values); 4896 } 4897 4898 // Require that content lives under well-defined directories to help 4899 // keep the user's content organized 4900 4901 // Start by saying unchanged directories are valid 4902 final String currentDir = (currentPath != null) 4903 ? new File(currentPath).getParent() : null; 4904 boolean validPath = res.getParent().equals(currentDir); 4905 4906 // Next, consider allowing based on allowed primary directory 4907 final String[] relativePath = values.getAsString(MediaColumns.RELATIVE_PATH).split("/"); 4908 final String primary = extractTopLevelDir(relativePath); 4909 if (!validPath) { 4910 validPath = containsIgnoreCase(allowedPrimary, primary); 4911 } 4912 4913 // Next, consider allowing paths when referencing a related item 4914 final Uri relatedUri = extras.getParcelable(QUERY_ARG_RELATED_URI); 4915 if (!validPath && relatedUri != null) { 4916 try (Cursor c = queryForSingleItem(relatedUri, new String[] { 4917 MediaColumns.MIME_TYPE, 4918 MediaColumns.RELATIVE_PATH, 4919 }, null, null, null)) { 4920 // If top-level MIME type matches, and relative path 4921 // matches, then allow caller to place things here 4922 4923 final String expectedType = MimeUtils.extractPrimaryType( 4924 c.getString(0)); 4925 final String actualType = MimeUtils.extractPrimaryType( 4926 values.getAsString(MediaColumns.MIME_TYPE)); 4927 if (!Objects.equals(expectedType, actualType)) { 4928 throw new IllegalArgumentException("Placement of " + actualType 4929 + " item not allowed in relation to " + expectedType + " item"); 4930 } 4931 4932 final String expectedPath = c.getString(1); 4933 final String actualPath = values.getAsString(MediaColumns.RELATIVE_PATH); 4934 if (!Objects.equals(expectedPath, actualPath)) { 4935 throw new IllegalArgumentException("Placement of " + actualPath 4936 + " item not allowed in relation to " + expectedPath + " item"); 4937 } 4938 4939 // If we didn't see any trouble above, then we'll allow it 4940 validPath = true; 4941 } catch (FileNotFoundException e) { 4942 Log.w(TAG, "Failed to find related item " + relatedUri + ": " + e); 4943 } 4944 } 4945 4946 // Consider allowing external media directory of calling package 4947 if (!validPath) { 4948 final String pathOwnerPackage = extractPathOwnerPackageName(res.getAbsolutePath()); 4949 if (pathOwnerPackage != null) { 4950 validPath = isExternalMediaDirectory(res.getAbsolutePath()) && 4951 isCallingIdentitySharedPackageName(pathOwnerPackage); 4952 } 4953 } 4954 4955 // Allow apps with MANAGE_EXTERNAL_STORAGE to create files anywhere 4956 if (!validPath) { 4957 validPath = isCallingPackageManager(); 4958 } 4959 4960 // Allow system gallery to create image/video files. 4961 if (!validPath) { 4962 // System gallery can create image/video files in any existing directory, it can 4963 // also create subdirectories in any existing top-level directory. However, system 4964 // gallery is not allowed to create non-default top level directory. 4965 final boolean createNonDefaultTopLevelDir = primary != null && 4966 !FileUtils.buildPath(volumePath, primary).exists(); 4967 validPath = !createNonDefaultTopLevelDir && canSystemGalleryAccessTheFile( 4968 res.getAbsolutePath()); 4969 } 4970 4971 // Nothing left to check; caller can't use this path 4972 if (!validPath) { 4973 throw new IllegalArgumentException( 4974 "Primary directory " + primary + " not allowed for " + uri 4975 + "; allowed directories are " + allowedPrimary); 4976 } 4977 4978 boolean isFuseThread = isFuseThread(); 4979 // Check if the following are true: 4980 // 1. Not a FUSE thread 4981 // 2. |res| is a child of a default dir and the default dir is missing 4982 // If true, we want to update the mTime of the volume root, after creating the dir 4983 // on the lower filesystem. This fixes some FileManagers relying on the mTime change 4984 // for UI updates 4985 File defaultDirVolumePath = 4986 isFuseThread ? null : checkDefaultDirMissing(resolvedVolumeName, res); 4987 // Ensure all parent folders of result file exist 4988 res.getParentFile().mkdirs(); 4989 if (!res.getParentFile().exists()) { 4990 throw new IllegalStateException("Failed to create directory: " + res); 4991 } 4992 touchFusePath(defaultDirVolumePath); 4993 4994 values.put(MediaColumns.DATA, res.getAbsolutePath()); 4995 // buildFile may have changed the file name, compute values to extract new DISPLAY_NAME. 4996 // Note: We can't extract displayName from res.getPath() because for pending & trashed 4997 // files DISPLAY_NAME will not be same as file name. 4998 FileUtils.computeValuesFromData(values, isFuseThread); 4999 } else { 5000 assertFileColumnsConsistent(match, uri, values); 5001 } 5002 5003 assertPrivatePathNotInValues(values); 5004 5005 // Drop columns that aren't relevant for special tables 5006 switch (match) { 5007 case AUDIO_ALBUMART: 5008 case VIDEO_THUMBNAILS: 5009 case IMAGES_THUMBNAILS: 5010 final Set<String> valid = getProjectionMap(MediaStore.Images.Thumbnails.class) 5011 .keySet(); 5012 for (String key : new ArraySet<>(values.keySet())) { 5013 if (!valid.contains(key)) { 5014 values.remove(key); 5015 } 5016 } 5017 break; 5018 } 5019 5020 Trace.endSection(); 5021 } 5022 5023 /** 5024 * For apps targetSdk >= S: Check that values does not contain any external private path. 5025 * For all apps: Check that values does not contain any other app's external private paths. 5026 */ assertPrivatePathNotInValues(ContentValues values)5027 private void assertPrivatePathNotInValues(ContentValues values) 5028 throws IllegalArgumentException { 5029 ArrayList<String> relativePaths = new ArrayList<String>(); 5030 relativePaths.add(extractRelativePath(values.getAsString(MediaColumns.DATA))); 5031 relativePaths.add(values.getAsString(MediaColumns.RELATIVE_PATH)); 5032 5033 for (final String relativePath : relativePaths) { 5034 if (!isDataOrObbRelativePath(relativePath)) { 5035 continue; 5036 } 5037 5038 /** 5039 * Don't allow apps to insert/update database row to files in Android/data or 5040 * Android/obb dirs. These are app private directories and files in these private 5041 * directories can't be added to public media collection. 5042 * 5043 * Note: For backwards compatibility we allow apps with targetSdk < S to insert private 5044 * files to MediaProvider 5045 */ 5046 if (CompatChanges.isChangeEnabled(ENABLE_CHECKS_FOR_PRIVATE_FILES, 5047 Binder.getCallingUid())) { 5048 throw new IllegalArgumentException( 5049 "Inserting private file: " + relativePath + " is not allowed."); 5050 } 5051 5052 /** 5053 * Restrict all (legacy and non-legacy) apps from inserting paths in other 5054 * app's private directories. 5055 * Allow legacy apps to insert/update files in app private directories for backward 5056 * compatibility but don't allow them to do so in other app's private directories. 5057 */ 5058 if (!isCallingIdentityAllowedAccessToDataOrObbPath(relativePath)) { 5059 throw new IllegalArgumentException( 5060 "Inserting private file: " + relativePath + " is not allowed."); 5061 } 5062 } 5063 } 5064 5065 /** 5066 * @return the default dir if {@code file} is a child of default dir and it's missing, 5067 * {@code null} otherwise. 5068 */ checkDefaultDirMissing(String volumeName, File file)5069 private File checkDefaultDirMissing(String volumeName, File file) { 5070 String topLevelDir = FileUtils.extractTopLevelDir(file.getPath()); 5071 if (topLevelDir != null && FileUtils.isDefaultDirectoryName(topLevelDir)) { 5072 try { 5073 File volumePath = getVolumePath(volumeName); 5074 if (!new File(volumePath, topLevelDir).exists()) { 5075 return volumePath; 5076 } 5077 } catch (FileNotFoundException e) { 5078 Log.w(TAG, "Failed to checkDefaultDirMissing for " + file, e); 5079 } 5080 } 5081 return null; 5082 } 5083 5084 /** Updates mTime of {@code path} on the FUSE filesystem */ touchFusePath(@ullable File path)5085 private void touchFusePath(@Nullable File path) { 5086 if (path != null) { 5087 // Touch root of volume to update mTime on FUSE filesystem 5088 // This allows FileManagers that may be relying on mTime changes to update their UI 5089 File fusePath = toFuseFile(path); 5090 Log.i(TAG, "Touching FUSE path " + fusePath); 5091 fusePath.setLastModified(System.currentTimeMillis()); 5092 } 5093 } 5094 5095 /** 5096 * Check that any requested {@link MediaColumns#DATA} paths actually 5097 * live on the storage volume being targeted. 5098 */ assertFileColumnsConsistent(int match, Uri uri, ContentValues values)5099 private void assertFileColumnsConsistent(int match, Uri uri, ContentValues values) 5100 throws VolumeArgumentException, VolumeNotFoundException { 5101 if (!values.containsKey(MediaColumns.DATA)) return; 5102 5103 final String volumeName = resolveVolumeName(uri); 5104 try { 5105 // Quick check that the requested path actually lives on volume 5106 final Collection<File> allowed = getAllowedVolumePaths(volumeName); 5107 final File actual = new File(values.getAsString(MediaColumns.DATA)) 5108 .getCanonicalFile(); 5109 if (!FileUtils.contains(allowed, actual)) { 5110 throw new VolumeArgumentException(actual, allowed); 5111 } 5112 } catch (IOException e) { 5113 throw new VolumeNotFoundException(volumeName); 5114 } 5115 } 5116 5117 @Override bulkInsert(Uri uri, ContentValues[] values)5118 public int bulkInsert(Uri uri, ContentValues[] values) { 5119 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 5120 final boolean allowHidden = isCallingPackageAllowedHidden(); 5121 final int match = matchUri(uri, allowHidden); 5122 5123 if (match == VOLUMES) { 5124 return super.bulkInsert(uri, values); 5125 } 5126 5127 if (match == AUDIO_PLAYLISTS_ID || match == AUDIO_PLAYLISTS_ID_MEMBERS) { 5128 final String resolvedVolumeName = resolveVolumeName(uri); 5129 5130 final long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 5131 final Uri playlistUri = ContentUris.withAppendedId( 5132 MediaStore.Audio.Playlists.getContentUri(resolvedVolumeName), playlistId); 5133 5134 final String audioVolumeName = 5135 MediaStore.VOLUME_INTERNAL.equals(resolvedVolumeName) 5136 ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL; 5137 5138 // Require that caller has write access to underlying media 5139 enforceCallingPermission(playlistUri, Bundle.EMPTY, true); 5140 for (ContentValues each : values) { 5141 final long audioId = each.getAsLong(Audio.Playlists.Members.AUDIO_ID); 5142 final Uri audioUri = Audio.Media.getContentUri(audioVolumeName, audioId); 5143 enforceCallingPermission(audioUri, Bundle.EMPTY, false); 5144 } 5145 5146 return bulkInsertPlaylist(playlistUri, values); 5147 } 5148 5149 final DatabaseHelper helper; 5150 try { 5151 helper = getDatabaseForUri(uri); 5152 } catch (VolumeNotFoundException e) { 5153 return e.translateForUpdateDelete(targetSdkVersion); 5154 } 5155 5156 helper.beginTransaction(); 5157 try { 5158 final int result = super.bulkInsert(uri, values); 5159 helper.setTransactionSuccessful(); 5160 return result; 5161 } finally { 5162 helper.endTransaction(); 5163 } 5164 } 5165 bulkInsertPlaylist(@onNull Uri uri, @NonNull ContentValues[] values)5166 private int bulkInsertPlaylist(@NonNull Uri uri, @NonNull ContentValues[] values) { 5167 Trace.beginSection("MP.bulkInsertPlaylist"); 5168 try { 5169 try { 5170 return addPlaylistMembers(uri, values); 5171 } catch (SQLiteConstraintException e) { 5172 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) { 5173 throw e; 5174 } else { 5175 return 0; 5176 } 5177 } 5178 } catch (FallbackException e) { 5179 return e.translateForBulkInsert(getCallingPackageTargetSdkVersion()); 5180 } finally { 5181 Trace.endSection(); 5182 } 5183 } 5184 insertDirectory(@onNull SQLiteDatabase db, @NonNull String path)5185 private long insertDirectory(@NonNull SQLiteDatabase db, @NonNull String path) { 5186 if (LOGV) Log.v(TAG, "inserting directory " + path); 5187 ContentValues values = new ContentValues(); 5188 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION); 5189 values.put(FileColumns.DATA, path); 5190 values.put(FileColumns.PARENT, getParent(db, path)); 5191 values.put(FileColumns.OWNER_PACKAGE_NAME, extractPathOwnerPackageName(path)); 5192 values.put(FileColumns.VOLUME_NAME, extractVolumeName(path)); 5193 values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path)); 5194 values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path)); 5195 values.put(FileColumns.IS_DOWNLOAD, isDownload(path) ? 1 : 0); 5196 5197 // Getting UserId from the directory path, as clone user shares the MediaProvider 5198 // of user 0. 5199 int userIdFromPath = FileUtils.extractUserId(path); 5200 // In some cases, like querying public volumes, userId is not available in path. We 5201 // take userId from the user running MediaProvider process (sUserId). 5202 if (userIdFromPath != -1) { 5203 if (isAppCloneUserForFuse(userIdFromPath)) { 5204 values.put(FileColumns._USER_ID, userIdFromPath); 5205 } else { 5206 values.put(FileColumns._USER_ID, sUserId); 5207 } 5208 } 5209 5210 File file = new File(path); 5211 if (file.exists()) { 5212 values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000); 5213 } 5214 return db.insert("files", FileColumns.DATE_MODIFIED, values); 5215 } 5216 getParent(@onNull SQLiteDatabase db, @NonNull String path)5217 private long getParent(@NonNull SQLiteDatabase db, @NonNull String path) { 5218 final String parentPath = new File(path).getParent(); 5219 if (Objects.equals("/", parentPath)) { 5220 return -1; 5221 } else { 5222 synchronized (mDirectoryCache) { 5223 Long id = mDirectoryCache.get(parentPath); 5224 if (id != null) { 5225 return id; 5226 } 5227 } 5228 5229 final long id; 5230 try (Cursor c = db.query("files", new String[] { FileColumns._ID }, 5231 FileColumns.DATA + "=?", new String[] { parentPath }, null, null, null)) { 5232 if (c.moveToFirst()) { 5233 id = c.getLong(0); 5234 } else { 5235 id = insertDirectory(db, parentPath); 5236 } 5237 } 5238 5239 synchronized (mDirectoryCache) { 5240 mDirectoryCache.put(parentPath, id); 5241 } 5242 return id; 5243 } 5244 } 5245 5246 /** 5247 * @param c the Cursor whose title to retrieve 5248 * @return the result of {@link #getDefaultTitle(String)} if the result is valid; otherwise 5249 * the value of the {@code MediaStore.Audio.Media.TITLE} column 5250 */ getDefaultTitleFromCursor(Cursor c)5251 private String getDefaultTitleFromCursor(Cursor c) { 5252 String title = null; 5253 final int columnIndex = c.getColumnIndex("title_resource_uri"); 5254 // Necessary to check for existence because we may be reading from an old DB version 5255 if (columnIndex > -1) { 5256 final String titleResourceUri = c.getString(columnIndex); 5257 if (titleResourceUri != null) { 5258 try { 5259 title = getDefaultTitle(titleResourceUri); 5260 } catch (Exception e) { 5261 // Best attempt only 5262 } 5263 } 5264 } 5265 if (title == null) { 5266 title = c.getString(c.getColumnIndex(MediaStore.Audio.Media.TITLE)); 5267 } 5268 return title; 5269 } 5270 5271 /** 5272 * @param title_resource_uri The title resource for which to retrieve the default localization 5273 * @return The title localized to {@code Locale.US}, or {@code null} if unlocalizable 5274 * @throws Exception Thrown if the title appears to be localizable, but the localization failed 5275 * for any reason. For example, the application from which the localized title is fetched is not 5276 * installed, or it does not have the resource which needs to be localized 5277 */ getDefaultTitle(String title_resource_uri)5278 private String getDefaultTitle(String title_resource_uri) throws Exception{ 5279 try { 5280 return getTitleFromResourceUri(title_resource_uri, false); 5281 } catch (Exception e) { 5282 Log.e(TAG, "Error getting default title for " + title_resource_uri, e); 5283 throw e; 5284 } 5285 } 5286 5287 /** 5288 * @param title_resource_uri The title resource to localize 5289 * @return The localized title, or {@code null} if unlocalizable 5290 * @throws Exception Thrown if the title appears to be localizable, but the localization failed 5291 * for any reason. For example, the application from which the localized title is fetched is not 5292 * installed, or it does not have the resource which needs to be localized 5293 */ getLocalizedTitle(String title_resource_uri)5294 private String getLocalizedTitle(String title_resource_uri) throws Exception { 5295 try { 5296 return getTitleFromResourceUri(title_resource_uri, true); 5297 } catch (Exception e) { 5298 Log.e(TAG, "Error getting localized title for " + title_resource_uri, e); 5299 throw e; 5300 } 5301 } 5302 5303 /** 5304 * Localizable titles conform to this URI pattern: 5305 * Scheme: {@link ContentResolver.SCHEME_ANDROID_RESOURCE} 5306 * Authority: Package Name of ringtone title provider 5307 * First Path Segment: Type of resource (must be "string") 5308 * Second Path Segment: Resource name of title 5309 * 5310 * @param title_resource_uri The title resource to retrieve 5311 * @param localize Whether or not to localize the title 5312 * @return The title, or {@code null} if unlocalizable 5313 * @throws Exception Thrown if the title appears to be localizable, but the localization failed 5314 * for any reason. For example, the application from which the localized title is fetched is not 5315 * installed, or it does not have the resource which needs to be localized 5316 */ getTitleFromResourceUri(String title_resource_uri, boolean localize)5317 private String getTitleFromResourceUri(String title_resource_uri, boolean localize) 5318 throws Exception { 5319 if (TextUtils.isEmpty(title_resource_uri)) { 5320 return null; 5321 } 5322 final Uri titleUri = Uri.parse(title_resource_uri); 5323 final String scheme = titleUri.getScheme(); 5324 if (!ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) { 5325 return null; 5326 } 5327 final List<String> pathSegments = titleUri.getPathSegments(); 5328 if (pathSegments.size() != 2) { 5329 Log.e(TAG, "Error getting localized title for " + title_resource_uri 5330 + ", must have 2 path segments"); 5331 return null; 5332 } 5333 final String type = pathSegments.get(0); 5334 if (!"string".equals(type)) { 5335 Log.e(TAG, "Error getting localized title for " + title_resource_uri 5336 + ", first path segment must be \"string\""); 5337 return null; 5338 } 5339 final String packageName = titleUri.getAuthority(); 5340 final Resources resources; 5341 if (localize) { 5342 resources = mPackageManager.getResourcesForApplication(packageName); 5343 } else { 5344 final Context packageContext = getContext().createPackageContext(packageName, 0); 5345 final Configuration configuration = packageContext.getResources().getConfiguration(); 5346 configuration.setLocale(Locale.US); 5347 resources = packageContext.createConfigurationContext(configuration).getResources(); 5348 } 5349 final String resourceIdentifier = pathSegments.get(1); 5350 final int id = resources.getIdentifier(resourceIdentifier, type, packageName); 5351 return resources.getString(id); 5352 } 5353 onLocaleChanged()5354 public void onLocaleChanged() { 5355 onLocaleChanged(true); 5356 } 5357 onLocaleChanged(boolean forceUpdate)5358 private void onLocaleChanged(boolean forceUpdate) { 5359 mInternalDatabase.runWithTransaction((db) -> { 5360 if (forceUpdate || !mLastLocale.equals(Locale.getDefault())) { 5361 localizeTitles(db); 5362 mLastLocale = Locale.getDefault(); 5363 } 5364 return null; 5365 }); 5366 } 5367 localizeTitles(@onNull SQLiteDatabase db)5368 private void localizeTitles(@NonNull SQLiteDatabase db) { 5369 try (Cursor c = db.query("files", new String[]{"_id", "title_resource_uri"}, 5370 "title_resource_uri IS NOT NULL", null, null, null, null)) { 5371 while (c.moveToNext()) { 5372 final String id = c.getString(0); 5373 final String titleResourceUri = c.getString(1); 5374 final ContentValues values = new ContentValues(); 5375 try { 5376 values.put(AudioColumns.TITLE_RESOURCE_URI, titleResourceUri); 5377 computeAudioLocalizedValues(values); 5378 computeAudioKeyValues(values); 5379 db.update("files", values, "_id=?", new String[]{id}); 5380 } catch (Exception e) { 5381 Log.e(TAG, "Error updating localized title for " + titleResourceUri 5382 + ", keeping old localization"); 5383 } 5384 } 5385 } 5386 } 5387 insertFile(@onNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper, int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, int mediaType)5388 private Uri insertFile(@NonNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper, 5389 int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, 5390 int mediaType) throws VolumeArgumentException, VolumeNotFoundException { 5391 boolean wasPathEmpty = !values.containsKey(MediaStore.MediaColumns.DATA) 5392 || TextUtils.isEmpty(values.getAsString(MediaStore.MediaColumns.DATA)); 5393 5394 // Make sure all file-related columns are defined 5395 ensureUniqueFileColumns(match, uri, extras, values, null); 5396 5397 switch (mediaType) { 5398 case FileColumns.MEDIA_TYPE_AUDIO: { 5399 computeAudioLocalizedValues(values); 5400 computeAudioKeyValues(values); 5401 break; 5402 } 5403 } 5404 5405 // compute bucket_id and bucket_display_name for all files 5406 String path = values.getAsString(MediaStore.MediaColumns.DATA); 5407 FileUtils.computeValuesFromData(values, isFuseThread()); 5408 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 5409 5410 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 5411 if (title == null && path != null) { 5412 title = extractFileName(path); 5413 } 5414 values.put(FileColumns.TITLE, title); 5415 5416 String mimeType = null; 5417 int format = MtpConstants.FORMAT_ASSOCIATION; 5418 if (path != null && new File(path).isDirectory()) { 5419 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION); 5420 values.putNull(MediaStore.MediaColumns.MIME_TYPE); 5421 } else { 5422 mimeType = values.getAsString(MediaStore.MediaColumns.MIME_TYPE); 5423 final Integer formatObject = values.getAsInteger(FileColumns.FORMAT); 5424 format = (formatObject == null ? 0 : formatObject); 5425 } 5426 5427 if (format == 0) { 5428 format = MimeUtils.resolveFormatCode(mimeType); 5429 } 5430 if (path != null && path.endsWith("/")) { 5431 // TODO: convert to using FallbackException once VERSION_CODES.S is defined 5432 Log.e(TAG, "directory has trailing slash: " + path); 5433 return null; 5434 } 5435 if (format != 0) { 5436 values.put(FileColumns.FORMAT, format); 5437 } 5438 5439 if (mimeType == null && path != null && format != MtpConstants.FORMAT_ASSOCIATION) { 5440 mimeType = MimeUtils.resolveMimeType(new File(path)); 5441 } 5442 5443 if (mimeType != null) { 5444 values.put(FileColumns.MIME_TYPE, mimeType); 5445 if (isCallingPackageSelf() && values.containsKey(FileColumns.MEDIA_TYPE)) { 5446 // Leave FileColumns.MEDIA_TYPE untouched if the caller is ModernMediaScanner and 5447 // FileColumns.MEDIA_TYPE is already populated. 5448 } else if (isFuseThread() && path != null 5449 && FileUtils.shouldFileBeHidden(new File(path))) { 5450 // We should only mark MEDIA_TYPE as MEDIA_TYPE_NONE for Fuse Thread. 5451 // MediaProvider#insert() returns the uri by appending the "rowId" to the given 5452 // uri, hence to ensure the correct working of the returned uri, we shouldn't 5453 // change the MEDIA_TYPE in insert operation and let scan change it for us. 5454 values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE); 5455 } else { 5456 values.put(FileColumns.MEDIA_TYPE, MimeUtils.resolveMediaType(mimeType)); 5457 } 5458 } else { 5459 values.put(FileColumns.MEDIA_TYPE, mediaType); 5460 } 5461 5462 qb.allowColumn(FileColumns._MODIFIER); 5463 if (isCallingPackageSelf() && values.containsKey(FileColumns._MODIFIER)) { 5464 // We can't identify if the call is coming from media scan, hence 5465 // we let ModernMediaScanner send FileColumns._MODIFIER value. 5466 } else if (isFuseThread()) { 5467 values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_FUSE); 5468 } else { 5469 values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_CR); 5470 } 5471 5472 // There is no meaning of an owner in the internal storage. It is shared by all users. 5473 // So we only set the user_id field in the database for external storage. 5474 qb.allowColumn(FileColumns._USER_ID); 5475 int ownerUserId = FileUtils.extractUserId(path); 5476 if (helper.isExternal()) { 5477 if (isAppCloneUserForFuse(ownerUserId)) { 5478 values.put(FileColumns._USER_ID, ownerUserId); 5479 } else { 5480 values.put(FileColumns._USER_ID, sUserId); 5481 } 5482 } 5483 5484 final long rowId; 5485 Uri newUri = uri; 5486 { 5487 if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 5488 String name = values.getAsString(Audio.Playlists.NAME); 5489 if (name == null && path == null) { 5490 // MediaScanner will compute the name from the path if we have one 5491 throw new IllegalArgumentException( 5492 "no name was provided when inserting abstract playlist"); 5493 } 5494 } else { 5495 if (path == null) { 5496 // path might be null for playlists created on the device 5497 // or transfered via MTP 5498 throw new IllegalArgumentException( 5499 "no path was provided when inserting new file"); 5500 } 5501 } 5502 5503 // make sure modification date and size are set 5504 if (path != null) { 5505 File file = new File(path); 5506 if (file.exists()) { 5507 values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000); 5508 if (!values.containsKey(FileColumns.SIZE)) { 5509 values.put(FileColumns.SIZE, file.length()); 5510 } 5511 } 5512 // Checking if the file/directory is hidden can be expensive based on the depth of 5513 // the directory tree. Call shouldFileBeHidden() only when the caller of insert() 5514 // cares about returned uri. 5515 if (!isCallingPackageSelf() && !isFuseThread() 5516 && FileUtils.shouldFileBeHidden(file)) { 5517 newUri = MediaStore.Files.getContentUri(MediaStore.getVolumeName(uri)); 5518 } 5519 } 5520 5521 rowId = insertAllowingUpsert(qb, helper, values, path); 5522 } 5523 if (format == MtpConstants.FORMAT_ASSOCIATION) { 5524 synchronized (mDirectoryCache) { 5525 mDirectoryCache.put(path, rowId); 5526 } 5527 } 5528 5529 return ContentUris.withAppendedId(newUri, rowId); 5530 } 5531 5532 /** 5533 * Inserts a new row in MediaProvider database with {@code values}. Treats insert as upsert for 5534 * double inserts from same package. 5535 */ insertAllowingUpsert(@onNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper, @NonNull ContentValues values, String path)5536 private long insertAllowingUpsert(@NonNull SQLiteQueryBuilder qb, 5537 @NonNull DatabaseHelper helper, @NonNull ContentValues values, String path) 5538 throws SQLiteConstraintException { 5539 return helper.runWithTransaction((db) -> { 5540 Long parent = values.getAsLong(FileColumns.PARENT); 5541 if (parent == null) { 5542 if (path != null) { 5543 final long parentId = getParent(db, path); 5544 values.put(FileColumns.PARENT, parentId); 5545 } 5546 } 5547 5548 try { 5549 return qb.insert(helper, values); 5550 } catch (SQLiteConstraintException e) { 5551 final String packages = getAllowedPackagesForUpsert( 5552 values.getAsString(MediaColumns.OWNER_PACKAGE_NAME)); 5553 SQLiteQueryBuilder qbForUpsert = getQueryBuilderForUpsert(path); 5554 final long rowId = getIdIfPathOwnedByPackages(qbForUpsert, helper, path, packages); 5555 // Apps sometimes create a file via direct path and then insert it into 5556 // MediaStore via ContentResolver. The former should create a database entry, 5557 // so we have to treat the latter as an upsert. 5558 // TODO(b/149917493) Perform all INSERT operations as UPSERT. 5559 if (rowId != -1 && qbForUpsert.update(helper, values, "_id=?", 5560 new String[]{Long.toString(rowId)}) == 1) { 5561 return rowId; 5562 } 5563 // Rethrow SQLiteConstraintException on failed upsert. 5564 throw e; 5565 } 5566 }); 5567 } 5568 5569 /** 5570 * @return row id of the entry with path {@code path} if the owner is one of {@code packages}. 5571 */ 5572 private long getIdIfPathOwnedByPackages(@NonNull SQLiteQueryBuilder qb, 5573 @NonNull DatabaseHelper helper, String path, String packages) { 5574 final String[] projection = new String[] {FileColumns._ID}; 5575 final String ownerPackageMatchClause = DatabaseUtils.bindSelection( 5576 MediaColumns.OWNER_PACKAGE_NAME + " IN " + packages); 5577 final String selection = FileColumns.DATA + " =? AND " + ownerPackageMatchClause; 5578 5579 try (Cursor c = qb.query(helper, projection, selection, new String[] {path}, null, null, 5580 null, null, null)) { 5581 if (c.moveToFirst()) { 5582 return c.getLong(0); 5583 } 5584 } 5585 return -1; 5586 } 5587 5588 /** 5589 * Gets packages that should match to upsert a db row. 5590 * 5591 * A database row can be upserted if 5592 * <ul> 5593 * <li> Calling package or one of the shared packages owns the db row. 5594 * <li> {@code givenOwnerPackage} owns the db row. This is useful when DownloadProvider 5595 * requests upsert on behalf of another app 5596 * </ul> 5597 */ 5598 private String getAllowedPackagesForUpsert(@Nullable String givenOwnerPackage) { 5599 ArrayList<String> packages = new ArrayList<>( 5600 Arrays.asList(mCallingIdentity.get().getSharedPackageNamesArray())); 5601 5602 // If givenOwnerPackage is CallingIdentity, packages list would already have shared package 5603 // names of givenOwnerPackage. If givenOwnerPackage is not CallingIdentity, since 5604 // DownloadProvider can upsert a row on behalf of app, we should include all shared packages 5605 // of givenOwnerPackage. 5606 if (givenOwnerPackage != null && isCallingPackageDelegator() && 5607 !isCallingIdentitySharedPackageName(givenOwnerPackage)) { 5608 // Allow DownloadProvider to Upsert if givenOwnerPackage is owner of the db row. 5609 packages.addAll(Arrays.asList(getSharedPackagesForPackage(givenOwnerPackage))); 5610 } 5611 return bindList((Object[]) packages.toArray()); 5612 } 5613 5614 /** 5615 * @return {@link SQLiteQueryBuilder} for upsert with Files uri. This disables strict columns 5616 * check to allow upsert to update any column with Files uri. 5617 */ 5618 private SQLiteQueryBuilder getQueryBuilderForUpsert(@NonNull String path) { 5619 final boolean allowHidden = isCallingPackageAllowedHidden(); 5620 Bundle extras = new Bundle(); 5621 extras.putInt(QUERY_ARG_MATCH_PENDING, MATCH_INCLUDE); 5622 extras.putInt(QUERY_ARG_MATCH_TRASHED, MATCH_INCLUDE); 5623 5624 // When Fuse inserts a file to database it doesn't set is_download column. When app tries 5625 // insert with Downloads uri, upsert fails because getIdIfPathExistsForCallingPackage can't 5626 // find a row ID with is_download=1. Use Files uri to get queryBuilder & update any existing 5627 // row irrespective of is_download=1. 5628 final Uri uri = FileUtils.getContentUriForPath(path); 5629 SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, matchUri(uri, allowHidden), uri, 5630 extras, null); 5631 5632 // We won't be able to update columns that are not part of projection map of Files table. We 5633 // have already checked strict columns in previous insert operation which failed with 5634 // exception. Any malicious column usage would have got caught in insert operation, hence we 5635 // can safely disable strict column check for upsert. 5636 qb.setStrictColumns(false); 5637 return qb; 5638 } 5639 5640 private void maybePut(@NonNull ContentValues values, @NonNull String key, 5641 @Nullable String value) { 5642 if (value != null) { 5643 values.put(key, value); 5644 } 5645 } 5646 5647 private boolean maybeMarkAsDownload(@NonNull ContentValues values) { 5648 final String path = values.getAsString(MediaColumns.DATA); 5649 if (path != null && isDownload(path)) { 5650 values.put(FileColumns.IS_DOWNLOAD, 1); 5651 return true; 5652 } 5653 return false; 5654 } 5655 5656 @NonNull 5657 private static String resolveVolumeName(@NonNull Uri uri) { 5658 final String volumeName = getVolumeName(uri); 5659 if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) { 5660 return MediaStore.VOLUME_EXTERNAL_PRIMARY; 5661 } else { 5662 return volumeName; 5663 } 5664 } 5665 5666 /** 5667 * @deprecated all operations should be routed through the overload that 5668 * accepts a {@link Bundle} of extras. 5669 */ 5670 @Override 5671 @Deprecated 5672 public Uri insert(Uri uri, ContentValues values) { 5673 return insert(uri, values, null); 5674 } 5675 5676 @Override 5677 @Nullable 5678 public Uri insert(@NonNull Uri uri, @Nullable ContentValues values, 5679 @Nullable Bundle extras) { 5680 Trace.beginSection(safeTraceSectionNameWithUri("insert", uri)); 5681 try { 5682 try { 5683 return insertInternal(uri, values, extras); 5684 } catch (SQLiteConstraintException e) { 5685 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) { 5686 throw e; 5687 } else { 5688 return null; 5689 } 5690 } 5691 } catch (FallbackException e) { 5692 return e.translateForInsert(getCallingPackageTargetSdkVersion()); 5693 } finally { 5694 Trace.endSection(); 5695 } 5696 } 5697 5698 @Nullable 5699 private Uri insertInternal(@NonNull Uri uri, @Nullable ContentValues initialValues, 5700 @Nullable Bundle extras) throws FallbackException { 5701 if (shouldCheckForMaliciousActivity() && !mMaliciousAppDetector.isAppAllowedToCreateFiles( 5702 mCallingIdentity.get().uid)) { 5703 Log.w(TAG, "Cannot be created, app has created files more than threshold limit of " 5704 + mMaliciousAppDetector.getFileCreationThresholdLimit()); 5705 throw new UnsupportedOperationException( 5706 "Cannot be created, app has created files more than threshold limit"); 5707 } 5708 final String originalVolumeName = getVolumeName(uri); 5709 PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), originalVolumeName); 5710 5711 extras = (extras != null) ? extras : new Bundle(); 5712 // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider. 5713 extras.remove(QUERY_ARG_REDACTED_URI); 5714 5715 // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. 5716 extras.remove(INCLUDED_DEFAULT_DIRECTORIES); 5717 5718 final boolean allowHidden = isCallingPackageAllowedHidden(); 5719 final int match = matchUri(uri, allowHidden); 5720 5721 final String resolvedVolumeName = resolveVolumeName(uri); 5722 5723 // handle MEDIA_SCANNER before calling getDatabaseForUri() 5724 if (match == MEDIA_SCANNER) { 5725 mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME); 5726 5727 final DatabaseHelper helper = getDatabaseForUri( 5728 MediaStore.Files.getContentUri(mMediaScannerVolume)); 5729 5730 helper.mScanStartTime = SystemClock.elapsedRealtime(); 5731 return MediaStore.getMediaScannerUri(); 5732 } 5733 5734 if (match == VOLUMES) { 5735 String name = initialValues.getAsString("name"); 5736 MediaVolume volume = null; 5737 try { 5738 volume = getVolume(name); 5739 Uri attachedVolume = attachVolume(volume, /* validate */ true, /* volumeState */ 5740 null); 5741 if (mMediaScannerVolume != null && mMediaScannerVolume.equals(name)) { 5742 final DatabaseHelper helper = getDatabaseForUri( 5743 MediaStore.Files.getContentUri(mMediaScannerVolume)); 5744 helper.mScanStartTime = SystemClock.elapsedRealtime(); 5745 } 5746 return attachedVolume; 5747 } catch (FileNotFoundException e) { 5748 Log.w(TAG, "Couldn't find volume with name " + volume.getName()); 5749 return null; 5750 } 5751 } 5752 5753 final DatabaseHelper helper = getDatabaseForUri(uri); 5754 switch (match) { 5755 case AUDIO_PLAYLISTS_ID: 5756 case AUDIO_PLAYLISTS_ID_MEMBERS: { 5757 final long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 5758 final Uri playlistUri = ContentUris.withAppendedId( 5759 MediaStore.Audio.Playlists.getContentUri(resolvedVolumeName), playlistId); 5760 5761 final long audioId = initialValues 5762 .getAsLong(MediaStore.Audio.Playlists.Members.AUDIO_ID); 5763 final String audioVolumeName = 5764 MediaStore.VOLUME_INTERNAL.equals(resolvedVolumeName) 5765 ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL; 5766 final Uri audioUri = ContentUris.withAppendedId( 5767 MediaStore.Audio.Media.getContentUri(audioVolumeName), audioId); 5768 5769 // Require that caller has write access to underlying media 5770 enforceCallingPermission(playlistUri, Bundle.EMPTY, true); 5771 enforceCallingPermission(audioUri, Bundle.EMPTY, false); 5772 5773 // Playlist contents are always persisted directly into playlist 5774 // files on disk to ensure that we can reliably migrate between 5775 // devices and recover from database corruption 5776 final long id = addPlaylistMembers(playlistUri, initialValues); 5777 acceptWithExpansion(helper::notifyInsert, resolvedVolumeName, playlistId, 5778 FileColumns.MEDIA_TYPE_PLAYLIST, false); 5779 return ContentUris.withAppendedId(MediaStore.Audio.Playlists.Members 5780 .getContentUri(originalVolumeName, playlistId), id); 5781 } 5782 } 5783 5784 String path = null; 5785 String ownerPackageName = null; 5786 if (initialValues != null) { 5787 // IDs are forever; nobody should be editing them 5788 initialValues.remove(MediaColumns._ID); 5789 5790 // Expiration times are hard-coded; let's derive them 5791 FileUtils.computeDateExpires(initialValues); 5792 5793 // Ignore or augment incoming raw filesystem paths 5794 for (String column : sDataColumns.keySet()) { 5795 if (!initialValues.containsKey(column)) continue; 5796 5797 if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) { 5798 // Mutation allowed 5799 } else if (isCallingPackageManager()) { 5800 // Apps with MANAGE_EXTERNAL_STORAGE have all files access, hence they are 5801 // allowed to insert files anywhere. 5802 } else if (getCallingPackageTargetSdkVersion() >= 5803 Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 5804 // Throwing an exception so that it doesn't result in some unexpected 5805 // behavior for apps and make them aware of what is happening. 5806 throw new IllegalArgumentException("Mutation of " + column 5807 + " is not allowed."); 5808 } else { 5809 Log.w(TAG, "Ignoring mutation of " + column + " from " 5810 + getCallingPackageOrSelf()); 5811 initialValues.remove(column); 5812 } 5813 } 5814 5815 path = initialValues.getAsString(MediaStore.MediaColumns.DATA); 5816 5817 if (!isCallingPackageSelf()) { 5818 initialValues.remove(FileColumns.IS_DOWNLOAD); 5819 5820 // We no longer track location metadata 5821 if (initialValues.containsKey(LATITUDE)) { 5822 initialValues.putNull(LATITUDE); 5823 } 5824 if (initialValues.containsKey(LONGITUDE)) { 5825 initialValues.putNull(LONGITUDE); 5826 } 5827 } 5828 5829 if (getCallingPackageTargetSdkVersion() <= Build.VERSION_CODES.Q) { 5830 // These columns are removed in R. 5831 if (initialValues.containsKey("primary_directory")) { 5832 initialValues.remove("primary_directory"); 5833 } 5834 if (initialValues.containsKey("secondary_directory")) { 5835 initialValues.remove("secondary_directory"); 5836 } 5837 } 5838 5839 if (isCallingPackageSelf() || isCallingPackageShell()) { 5840 // When media inserted by ourselves during a scan, or by the 5841 // shell, the best we can do is guess ownership based on path 5842 // when it's not explicitly provided 5843 ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME); 5844 if (TextUtils.isEmpty(ownerPackageName)) { 5845 ownerPackageName = extractPathOwnerPackageName(path); 5846 } 5847 } else if (isCallingPackageDelegator()) { 5848 // When caller is a delegator, we handle ownership as a hybrid 5849 // of the two other cases: we're willing to accept any ownership 5850 // transfer attempted during insert, but we fall back to using 5851 // the Binder identity if they don't request a specific owner 5852 ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME); 5853 if (TextUtils.isEmpty(ownerPackageName)) { 5854 ownerPackageName = getCallingPackageOrSelf(); 5855 } 5856 } else { 5857 // Remote callers have no direct control over owner column; we force 5858 // it be whoever is creating the content. 5859 initialValues.remove(FileColumns.OWNER_PACKAGE_NAME); 5860 ownerPackageName = getCallingPackageOrSelf(); 5861 } 5862 } 5863 5864 5865 // Enforce oem_metadata permission if caller is not MediaProvider 5866 if (Flags.enableOemMetadataUpdate() && initialValues.containsKey(OEM_METADATA)) { 5867 enforcePermissionCheckForOemMetadataUpdate(); 5868 } 5869 5870 long rowId = -1; 5871 Uri newUri = null; 5872 5873 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_INSERT, match, uri, extras, null); 5874 5875 switch (match) { 5876 case IMAGES_MEDIA: { 5877 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 5878 final boolean isDownload = maybeMarkAsDownload(initialValues); 5879 newUri = insertFile(qb, helper, match, uri, extras, initialValues, 5880 FileColumns.MEDIA_TYPE_IMAGE); 5881 break; 5882 } 5883 5884 case IMAGES_THUMBNAILS: { 5885 if (helper.isInternal()) { 5886 throw new UnsupportedOperationException( 5887 "Writing to internal storage is not supported."); 5888 } 5889 5890 // Require that caller has write access to underlying media 5891 final long imageId = initialValues.getAsLong(MediaStore.Images.Thumbnails.IMAGE_ID); 5892 enforceCallingPermission(ContentUris.withAppendedId( 5893 MediaStore.Images.Media.getContentUri(resolvedVolumeName), imageId), 5894 extras, true); 5895 5896 ensureUniqueFileColumns(match, uri, extras, initialValues, null); 5897 5898 rowId = qb.insert(helper, initialValues); 5899 if (rowId > 0) { 5900 newUri = ContentUris.withAppendedId(Images.Thumbnails. 5901 getContentUri(originalVolumeName), rowId); 5902 } 5903 break; 5904 } 5905 5906 case VIDEO_THUMBNAILS: { 5907 if (helper.isInternal()) { 5908 throw new UnsupportedOperationException( 5909 "Writing to internal storage is not supported."); 5910 } 5911 5912 // Require that caller has write access to underlying media 5913 final long videoId = initialValues.getAsLong(MediaStore.Video.Thumbnails.VIDEO_ID); 5914 enforceCallingPermission(ContentUris.withAppendedId( 5915 MediaStore.Video.Media.getContentUri(resolvedVolumeName), videoId), 5916 Bundle.EMPTY, true); 5917 5918 ensureUniqueFileColumns(match, uri, extras, initialValues, null); 5919 5920 rowId = qb.insert(helper, initialValues); 5921 if (rowId > 0) { 5922 newUri = ContentUris.withAppendedId(Video.Thumbnails. 5923 getContentUri(originalVolumeName), rowId); 5924 } 5925 break; 5926 } 5927 5928 case AUDIO_MEDIA: { 5929 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 5930 final boolean isDownload = maybeMarkAsDownload(initialValues); 5931 newUri = insertFile(qb, helper, match, uri, extras, initialValues, 5932 FileColumns.MEDIA_TYPE_AUDIO); 5933 break; 5934 } 5935 5936 case AUDIO_MEDIA_ID_GENRES: { 5937 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R); 5938 } 5939 5940 case AUDIO_GENRES: { 5941 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R); 5942 } 5943 5944 case AUDIO_GENRES_ID_MEMBERS: { 5945 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R); 5946 } 5947 5948 case AUDIO_PLAYLISTS: { 5949 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 5950 final boolean isDownload = maybeMarkAsDownload(initialValues); 5951 ContentValues values = new ContentValues(initialValues); 5952 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000); 5953 // Playlist names are stored as display names, but leave 5954 // values untouched if the caller is ModernMediaScanner 5955 if (!isCallingPackageSelf()) { 5956 if (values.containsKey(Playlists.NAME)) { 5957 values.put(MediaColumns.DISPLAY_NAME, values.getAsString(Playlists.NAME)); 5958 } 5959 if (!values.containsKey(MediaColumns.MIME_TYPE)) { 5960 values.put(MediaColumns.MIME_TYPE, "audio/mpegurl"); 5961 } 5962 } 5963 newUri = insertFile(qb, helper, match, uri, extras, values, 5964 FileColumns.MEDIA_TYPE_PLAYLIST); 5965 if (newUri != null) { 5966 // Touch empty playlist file on disk so its ready for renames 5967 if (Binder.getCallingUid() != android.os.Process.myUid()) { 5968 try (OutputStream out = ContentResolver.wrap(this) 5969 .openOutputStream(newUri)) { 5970 } catch (IOException ignored) { 5971 } 5972 } 5973 } 5974 break; 5975 } 5976 5977 case VIDEO_MEDIA: { 5978 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 5979 final boolean isDownload = maybeMarkAsDownload(initialValues); 5980 newUri = insertFile(qb, helper, match, uri, extras, initialValues, 5981 FileColumns.MEDIA_TYPE_VIDEO); 5982 break; 5983 } 5984 5985 case AUDIO_ALBUMART: { 5986 if (helper.isInternal()) { 5987 throw new UnsupportedOperationException("no internal album art allowed"); 5988 } 5989 5990 ensureUniqueFileColumns(match, uri, extras, initialValues, null); 5991 5992 rowId = qb.insert(helper, initialValues); 5993 if (rowId > 0) { 5994 newUri = ContentUris.withAppendedId(uri, rowId); 5995 } 5996 break; 5997 } 5998 5999 case FILES: { 6000 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 6001 final boolean isDownload = maybeMarkAsDownload(initialValues); 6002 final String mimeType = initialValues.getAsString(MediaColumns.MIME_TYPE); 6003 final int mediaType = MimeUtils.resolveMediaType(mimeType); 6004 newUri = insertFile(qb, helper, match, uri, extras, initialValues, 6005 mediaType); 6006 break; 6007 } 6008 6009 case DOWNLOADS: 6010 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 6011 initialValues.put(FileColumns.IS_DOWNLOAD, 1); 6012 newUri = insertFile(qb, helper, match, uri, extras, initialValues, 6013 FileColumns.MEDIA_TYPE_NONE); 6014 break; 6015 6016 default: 6017 throw new UnsupportedOperationException("Invalid URI " + uri); 6018 } 6019 6020 // Remember that caller is owner of this item, to speed up future 6021 // permission checks for this caller 6022 mCallingIdentity.get().setOwned(rowId, true); 6023 6024 if (path != null && path.toLowerCase(Locale.ROOT).endsWith("/.nomedia")) { 6025 scanFileAsMediaProvider(new File(path).getParentFile()); 6026 } 6027 6028 return newUri; 6029 } 6030 6031 @Override 6032 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 6033 throws OperationApplicationException { 6034 // Open transactions on databases for requested volumes 6035 final Set<DatabaseHelper> transactions = new ArraySet<>(); 6036 try { 6037 for (ContentProviderOperation op : operations) { 6038 final DatabaseHelper helper = getDatabaseForUri(op.getUri()); 6039 if (transactions.contains(helper)) continue; 6040 6041 if (!helper.isTransactionActive()) { 6042 helper.beginTransaction(); 6043 transactions.add(helper); 6044 } else { 6045 // We normally don't allow nested transactions (since we 6046 // don't have a good way to selectively roll them back) but 6047 // if the incoming operation is ignoring exceptions, then we 6048 // don't need to worry about partial rollback and can 6049 // piggyback on the larger active transaction 6050 if (!op.isExceptionAllowed()) { 6051 throw new IllegalStateException("Nested transactions not supported"); 6052 } 6053 } 6054 } 6055 6056 final ContentProviderResult[] result = super.applyBatch(operations); 6057 for (DatabaseHelper helper : transactions) { 6058 helper.setTransactionSuccessful(); 6059 } 6060 return result; 6061 } catch (VolumeNotFoundException e) { 6062 throw e.rethrowAsIllegalArgumentException(); 6063 } finally { 6064 for (DatabaseHelper helper : transactions) { 6065 helper.endTransaction(); 6066 } 6067 } 6068 } 6069 6070 private void appendWhereStandaloneMatch(@NonNull SQLiteQueryBuilder qb, 6071 @NonNull String column, /* @Match */ int match, Uri uri) { 6072 switch (match) { 6073 case MATCH_INCLUDE: 6074 // No special filtering needed 6075 break; 6076 case MATCH_EXCLUDE: 6077 appendWhereStandalone(qb, getWhereClauseForMatchExclude(column)); 6078 break; 6079 case MATCH_ONLY: 6080 appendWhereStandalone(qb, column + "=?", 1); 6081 break; 6082 case MATCH_VISIBLE_FOR_FILEPATH: 6083 final String whereClause = 6084 getWhereClauseForMatchableVisibleFromFilePath(uri, column); 6085 if (whereClause != null) { 6086 appendWhereStandalone(qb, whereClause); 6087 } 6088 break; 6089 default: 6090 throw new IllegalArgumentException(); 6091 } 6092 } 6093 6094 private static void appendWhereStandalone(@NonNull SQLiteQueryBuilder qb, 6095 @Nullable String selection, @Nullable Object... selectionArgs) { 6096 qb.appendWhereStandalone(DatabaseUtils.bindSelection(selection, selectionArgs)); 6097 } 6098 6099 private static void appendWhereStandaloneFilter(@NonNull SQLiteQueryBuilder qb, 6100 @NonNull String[] columns, @Nullable String filter) { 6101 if (TextUtils.isEmpty(filter)) return; 6102 for (String filterWord : filter.split("\\s+")) { 6103 appendWhereStandalone(qb, String.join("||", columns) + " LIKE ? ESCAPE '\\'", 6104 "%" + DatabaseUtils.escapeForLike(Audio.keyFor(filterWord)) + "%"); 6105 } 6106 } 6107 6108 /** 6109 * Gets {@link LocalCallingIdentity} for the calling package 6110 * TODO(b/170465810) Change the method name after refactoring. 6111 */ 6112 LocalCallingIdentity getCachedCallingIdentityForTranscoding(int uid) { 6113 return getCachedCallingIdentityForFuse(uid); 6114 } 6115 6116 /** 6117 * Gets shared packages names for given {@code packageName} 6118 */ 6119 private String[] getSharedPackagesForPackage(String packageName) { 6120 try { 6121 final int packageUid = getContext().getPackageManager() 6122 .getPackageUid(packageName, 0); 6123 return getContext().getPackageManager().getPackagesForUid(packageUid); 6124 } catch (NameNotFoundException ignored) { 6125 return new String[] {packageName}; 6126 } 6127 } 6128 6129 private static final int TYPE_QUERY = 0; 6130 private static final int TYPE_INSERT = 1; 6131 private static final int TYPE_UPDATE = 2; 6132 private static final int TYPE_DELETE = 3; 6133 6134 /** 6135 * Creating a new method for Transcoding to avoid any merge conflicts. 6136 * TODO(b/170465810): Remove this when getQueryBuilder code is refactored. 6137 */ 6138 @NonNull SQLiteQueryBuilder getQueryBuilderForTranscoding(int type, int match, 6139 @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) { 6140 // Force MediaProvider calling identity when accessing the db from transcoding to avoid 6141 // generating 'strict' SQL e.g forcing owner_package_name matches 6142 // We already handle the required permission checks for the app before we get here 6143 final LocalCallingIdentity token = clearLocalCallingIdentity(); 6144 try { 6145 return getQueryBuilder(type, match, uri, extras, honored); 6146 } finally { 6147 restoreLocalCallingIdentity(token); 6148 } 6149 } 6150 6151 /** 6152 * Generate a {@link SQLiteQueryBuilder} that is filtered based on the 6153 * runtime permissions and/or {@link Uri} grants held by the caller. 6154 * <ul> 6155 * <li>If caller holds a {@link Uri} grant, access is allowed according to 6156 * that grant. 6157 * <li>If caller holds the write permission for a collection, they can 6158 * read/write all contents of that collection. 6159 * <li>If caller holds the read permission for a collection, they can read 6160 * all contents of that collection, but writes are limited to content they 6161 * own. 6162 * <li>If caller holds no permissions for a collection, all reads/write are 6163 * limited to content they own. 6164 * </ul> 6165 */ 6166 private @NonNull SQLiteQueryBuilder getQueryBuilder(int type, int match, 6167 @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) { 6168 Trace.beginSection("MP.getQueryBuilder"); 6169 try { 6170 return getQueryBuilderInternal(type, match, uri, extras, honored); 6171 } finally { 6172 Trace.endSection(); 6173 } 6174 } 6175 6176 private @NonNull SQLiteQueryBuilder getQueryBuilderInternal(int type, int match, 6177 @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) { 6178 final boolean forWrite; 6179 switch (type) { 6180 case TYPE_QUERY: forWrite = false; break; 6181 case TYPE_INSERT: forWrite = true; break; 6182 case TYPE_UPDATE: forWrite = true; break; 6183 case TYPE_DELETE: forWrite = true; break; 6184 default: throw new IllegalStateException(); 6185 } 6186 6187 if (forWrite) { 6188 final Uri redactedUri = extras.getParcelable(QUERY_ARG_REDACTED_URI); 6189 if (redactedUri != null) { 6190 throw new UnsupportedOperationException( 6191 "Writes on: " + redactedUri.toString() + " are not supported"); 6192 } 6193 } 6194 6195 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 6196 if (uri.getBooleanQueryParameter("distinct", false)) { 6197 qb.setDistinct(true); 6198 } 6199 qb.setStrict(true); 6200 if (isCallingPackageSelf()) { 6201 // When caller is system, such as the media scanner, we're willing 6202 // to let them access any columns they want 6203 } else { 6204 qb.setTargetSdkVersion(getCallingPackageTargetSdkVersion()); 6205 qb.setStrictColumns(true); 6206 qb.setStrictGrammar(true); 6207 } 6208 6209 // TODO: throw when requesting a currently unmounted volume 6210 final String volumeName = MediaStore.getVolumeName(uri); 6211 final String includeVolumes; 6212 if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) { 6213 includeVolumes = bindList(mVolumeCache.getExternalVolumeNames().toArray()); 6214 } else { 6215 includeVolumes = bindList(volumeName); 6216 } 6217 6218 int matchPending = extras.getInt(QUERY_ARG_MATCH_PENDING, MATCH_DEFAULT); 6219 int matchTrashed = extras.getInt(QUERY_ARG_MATCH_TRASHED, MATCH_DEFAULT); 6220 int matchFavorite = extras.getInt(QUERY_ARG_MATCH_FAVORITE, MATCH_DEFAULT); 6221 6222 6223 // Handle callers using legacy arguments 6224 if (MediaStore.getIncludePending(uri)) matchPending = MATCH_INCLUDE; 6225 6226 // Resolve any remaining default options 6227 final int defaultMatchForPendingAndTrashed; 6228 if (isFuseThread()) { 6229 // Write operations always check for file ownership, we don't need additional write 6230 // permission check for is_pending and is_trashed. 6231 defaultMatchForPendingAndTrashed = 6232 forWrite ? MATCH_INCLUDE : MATCH_VISIBLE_FOR_FILEPATH; 6233 } else { 6234 defaultMatchForPendingAndTrashed = MATCH_EXCLUDE; 6235 } 6236 if (matchPending == MATCH_DEFAULT) matchPending = defaultMatchForPendingAndTrashed; 6237 if (matchTrashed == MATCH_DEFAULT) matchTrashed = defaultMatchForPendingAndTrashed; 6238 if (matchFavorite == MATCH_DEFAULT) matchFavorite = MATCH_INCLUDE; 6239 6240 // Handle callers using legacy filtering 6241 final String filter = uri.getQueryParameter("filter"); 6242 6243 // Only accept ALL_VOLUMES parameter up until R, because we're not convinced we want 6244 // to commit to this as an API. 6245 final boolean includeAllVolumes = shouldIncludeRecentlyUnmountedVolumes(uri, extras); 6246 6247 appendAccessCheckQuery(qb, forWrite, uri, match, extras, volumeName); 6248 6249 switch (match) { 6250 case IMAGES_MEDIA_ID: 6251 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 6252 matchPending = MATCH_INCLUDE; 6253 matchTrashed = MATCH_INCLUDE; 6254 // fall-through 6255 case IMAGES_MEDIA: { 6256 if (type == TYPE_QUERY) { 6257 qb.setTables("images"); 6258 qb.setProjectionMap( 6259 getProjectionMap(Images.Media.class)); 6260 } else { 6261 qb.setTables("files"); 6262 qb.setProjectionMap( 6263 getProjectionMap(Images.Media.class, Files.FileColumns.class)); 6264 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 6265 FileColumns.MEDIA_TYPE_IMAGE); 6266 } 6267 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); 6268 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); 6269 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 6270 if (honored != null) { 6271 honored.accept(QUERY_ARG_MATCH_PENDING); 6272 honored.accept(QUERY_ARG_MATCH_TRASHED); 6273 honored.accept(QUERY_ARG_MATCH_FAVORITE); 6274 } 6275 if (!includeAllVolumes) { 6276 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 6277 } 6278 break; 6279 } 6280 case IMAGES_THUMBNAILS_ID: 6281 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 6282 // fall-through 6283 case IMAGES_THUMBNAILS: { 6284 qb.setTables("thumbnails"); 6285 6286 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 6287 getProjectionMap(Images.Thumbnails.class)); 6288 projectionMap.put(Images.Thumbnails.THUMB_DATA, 6289 "NULL AS " + Images.Thumbnails.THUMB_DATA); 6290 qb.setProjectionMap(projectionMap); 6291 6292 break; 6293 } 6294 case AUDIO_MEDIA_ID: 6295 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 6296 matchPending = MATCH_INCLUDE; 6297 matchTrashed = MATCH_INCLUDE; 6298 // fall-through 6299 case AUDIO_MEDIA: { 6300 if (type == TYPE_QUERY) { 6301 qb.setTables("audio"); 6302 qb.setProjectionMap( 6303 getProjectionMap(Audio.Media.class)); 6304 } else { 6305 qb.setTables("files"); 6306 qb.setProjectionMap( 6307 getProjectionMap(Audio.Media.class, Files.FileColumns.class)); 6308 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 6309 FileColumns.MEDIA_TYPE_AUDIO); 6310 } 6311 appendWhereStandaloneFilter(qb, new String[] { 6312 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY 6313 }, filter); 6314 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); 6315 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); 6316 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 6317 if (honored != null) { 6318 honored.accept(QUERY_ARG_MATCH_PENDING); 6319 honored.accept(QUERY_ARG_MATCH_TRASHED); 6320 honored.accept(QUERY_ARG_MATCH_FAVORITE); 6321 } 6322 if (!includeAllVolumes) { 6323 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 6324 } 6325 break; 6326 } 6327 case AUDIO_MEDIA_ID_GENRES_ID: 6328 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(5)); 6329 // fall-through 6330 case AUDIO_MEDIA_ID_GENRES: { 6331 if (type == TYPE_QUERY) { 6332 qb.setTables("audio_genres"); 6333 qb.setProjectionMap(getProjectionMap(Audio.Genres.class)); 6334 } else { 6335 throw new UnsupportedOperationException("Genres cannot be directly modified"); 6336 } 6337 appendWhereStandalone(qb, "_id IN (SELECT genre_id FROM " + 6338 "audio WHERE _id=?)", uri.getPathSegments().get(3)); 6339 break; 6340 } 6341 case AUDIO_GENRES_ID: 6342 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 6343 // fall-through 6344 case AUDIO_GENRES: { 6345 qb.setTables("audio_genres"); 6346 qb.setProjectionMap(getProjectionMap(Audio.Genres.class)); 6347 break; 6348 } 6349 case AUDIO_GENRES_ID_MEMBERS: 6350 appendWhereStandalone(qb, "genre_id=?", uri.getPathSegments().get(3)); 6351 // fall-through 6352 case AUDIO_GENRES_ALL_MEMBERS: { 6353 if (type == TYPE_QUERY) { 6354 qb.setTables("audio"); 6355 6356 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 6357 getProjectionMap(Audio.Genres.Members.class)); 6358 projectionMap.put(Audio.Genres.Members.AUDIO_ID, 6359 "_id AS " + Audio.Genres.Members.AUDIO_ID); 6360 qb.setProjectionMap(projectionMap); 6361 } else { 6362 throw new UnsupportedOperationException("Genres cannot be directly modified"); 6363 } 6364 appendWhereStandaloneFilter(qb, new String[] { 6365 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY 6366 }, filter); 6367 // In order to be consistent with other audio views like audio_artist, audio_albums, 6368 // and audio_genres, exclude pending and trashed item 6369 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, MATCH_EXCLUDE, uri); 6370 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, MATCH_EXCLUDE, uri); 6371 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 6372 if (honored != null) { 6373 honored.accept(QUERY_ARG_MATCH_FAVORITE); 6374 } 6375 if (!includeAllVolumes) { 6376 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 6377 } 6378 break; 6379 } 6380 case AUDIO_PLAYLISTS_ID: 6381 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 6382 matchPending = MATCH_INCLUDE; 6383 matchTrashed = MATCH_INCLUDE; 6384 // fall-through 6385 case AUDIO_PLAYLISTS: { 6386 if (type == TYPE_QUERY) { 6387 qb.setTables("audio_playlists"); 6388 qb.setProjectionMap( 6389 getProjectionMap(Audio.Playlists.class)); 6390 } else { 6391 qb.setTables("files"); 6392 qb.setProjectionMap( 6393 getProjectionMap(Audio.Playlists.class, Files.FileColumns.class)); 6394 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 6395 FileColumns.MEDIA_TYPE_PLAYLIST); 6396 } 6397 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); 6398 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); 6399 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 6400 if (honored != null) { 6401 honored.accept(QUERY_ARG_MATCH_PENDING); 6402 honored.accept(QUERY_ARG_MATCH_TRASHED); 6403 honored.accept(QUERY_ARG_MATCH_FAVORITE); 6404 } 6405 if (!includeAllVolumes) { 6406 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 6407 } 6408 break; 6409 } 6410 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 6411 appendWhereStandalone(qb, "audio_playlists_map._id=?", 6412 uri.getPathSegments().get(5)); 6413 // fall-through 6414 case AUDIO_PLAYLISTS_ID_MEMBERS: { 6415 appendWhereStandalone(qb, "playlist_id=?", uri.getPathSegments().get(3)); 6416 if (type == TYPE_QUERY) { 6417 qb.setTables("audio_playlists_map, audio"); 6418 6419 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 6420 getProjectionMap(Audio.Playlists.Members.class)); 6421 projectionMap.put(Audio.Playlists.Members._ID, 6422 "audio_playlists_map._id AS " + Audio.Playlists.Members._ID); 6423 qb.setProjectionMap(projectionMap); 6424 6425 appendWhereStandalone(qb, "audio._id = audio_id"); 6426 // Since we use audio table along with audio_playlists_map 6427 // for querying, we should only include database rows of 6428 // the attached volumes. 6429 if (!includeAllVolumes) { 6430 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " 6431 + includeVolumes); 6432 } 6433 } else { 6434 qb.setTables("audio_playlists_map"); 6435 qb.setProjectionMap(getProjectionMap(Audio.Playlists.Members.class)); 6436 } 6437 appendWhereStandaloneFilter(qb, new String[] { 6438 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY 6439 }, filter); 6440 break; 6441 } 6442 case AUDIO_ALBUMART_ID: 6443 appendWhereStandalone(qb, "album_id=?", uri.getPathSegments().get(3)); 6444 // fall-through 6445 case AUDIO_ALBUMART: { 6446 qb.setTables("album_art"); 6447 6448 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 6449 getProjectionMap(Audio.Thumbnails.class)); 6450 projectionMap.put(Audio.Thumbnails._ID, 6451 "album_id AS " + Audio.Thumbnails._ID); 6452 qb.setProjectionMap(projectionMap); 6453 6454 break; 6455 } 6456 case AUDIO_ARTISTS_ID_ALBUMS: { 6457 if (type == TYPE_QUERY) { 6458 qb.setTables("audio_artists_albums"); 6459 qb.setProjectionMap(getProjectionMap(Audio.Artists.Albums.class)); 6460 6461 final String artistId = uri.getPathSegments().get(3); 6462 appendWhereStandalone(qb, "artist_id=?", artistId); 6463 } else { 6464 throw new UnsupportedOperationException("Albums cannot be directly modified"); 6465 } 6466 appendWhereStandaloneFilter(qb, new String[] { 6467 AudioColumns.ALBUM_KEY 6468 }, filter); 6469 break; 6470 } 6471 case AUDIO_ARTISTS_ID: 6472 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 6473 // fall-through 6474 case AUDIO_ARTISTS: { 6475 if (type == TYPE_QUERY) { 6476 qb.setTables("audio_artists"); 6477 qb.setProjectionMap(getProjectionMap(Audio.Artists.class)); 6478 } else { 6479 throw new UnsupportedOperationException("Artists cannot be directly modified"); 6480 } 6481 appendWhereStandaloneFilter(qb, new String[] { 6482 AudioColumns.ARTIST_KEY 6483 }, filter); 6484 break; 6485 } 6486 case AUDIO_ALBUMS_ID: 6487 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 6488 // fall-through 6489 case AUDIO_ALBUMS: { 6490 if (type == TYPE_QUERY) { 6491 qb.setTables("audio_albums"); 6492 qb.setProjectionMap(getProjectionMap(Audio.Albums.class)); 6493 } else { 6494 throw new UnsupportedOperationException("Albums cannot be directly modified"); 6495 } 6496 appendWhereStandaloneFilter(qb, new String[] { 6497 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY 6498 }, filter); 6499 break; 6500 } 6501 case VIDEO_MEDIA_ID: 6502 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 6503 matchPending = MATCH_INCLUDE; 6504 matchTrashed = MATCH_INCLUDE; 6505 // fall-through 6506 case VIDEO_MEDIA: { 6507 if (type == TYPE_QUERY) { 6508 qb.setTables("video"); 6509 qb.setProjectionMap( 6510 getProjectionMap(Video.Media.class)); 6511 } else { 6512 qb.setTables("files"); 6513 qb.setProjectionMap( 6514 getProjectionMap(Video.Media.class, Files.FileColumns.class)); 6515 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 6516 FileColumns.MEDIA_TYPE_VIDEO); 6517 } 6518 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); 6519 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); 6520 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 6521 if (honored != null) { 6522 honored.accept(QUERY_ARG_MATCH_PENDING); 6523 honored.accept(QUERY_ARG_MATCH_TRASHED); 6524 honored.accept(QUERY_ARG_MATCH_FAVORITE); 6525 } 6526 if (!includeAllVolumes) { 6527 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 6528 } 6529 break; 6530 } 6531 case VIDEO_THUMBNAILS_ID: 6532 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 6533 // fall-through 6534 case VIDEO_THUMBNAILS: { 6535 qb.setTables("videothumbnails"); 6536 qb.setProjectionMap(getProjectionMap(Video.Thumbnails.class)); 6537 break; 6538 } 6539 case FILES_ID: 6540 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2)); 6541 matchPending = MATCH_INCLUDE; 6542 matchTrashed = MATCH_INCLUDE; 6543 // fall-through 6544 case FILES: { 6545 qb.setTables("files"); 6546 qb.setProjectionMap(getProjectionMap(Files.FileColumns.class)); 6547 6548 appendWhereStandaloneFilter(qb, new String[] { 6549 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY 6550 }, filter); 6551 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); 6552 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); 6553 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 6554 if (honored != null) { 6555 honored.accept(QUERY_ARG_MATCH_PENDING); 6556 honored.accept(QUERY_ARG_MATCH_TRASHED); 6557 honored.accept(QUERY_ARG_MATCH_FAVORITE); 6558 } 6559 if (!includeAllVolumes) { 6560 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 6561 } 6562 break; 6563 } 6564 case DOWNLOADS_ID: 6565 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2)); 6566 matchPending = MATCH_INCLUDE; 6567 matchTrashed = MATCH_INCLUDE; 6568 // fall-through 6569 case DOWNLOADS: { 6570 if (type == TYPE_QUERY) { 6571 qb.setTables("downloads"); 6572 qb.setProjectionMap( 6573 getProjectionMap(Downloads.class)); 6574 } else { 6575 qb.setTables("files"); 6576 qb.setProjectionMap( 6577 getProjectionMap(Downloads.class, Files.FileColumns.class)); 6578 appendWhereStandalone(qb, FileColumns.IS_DOWNLOAD + "=1"); 6579 } 6580 6581 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); 6582 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); 6583 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 6584 if (honored != null) { 6585 honored.accept(QUERY_ARG_MATCH_PENDING); 6586 honored.accept(QUERY_ARG_MATCH_TRASHED); 6587 honored.accept(QUERY_ARG_MATCH_FAVORITE); 6588 } 6589 if (!includeAllVolumes) { 6590 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 6591 } 6592 break; 6593 } 6594 default: 6595 throw new UnsupportedOperationException( 6596 "Unknown or unsupported URL: " + uri.toString()); 6597 } 6598 6599 // To ensure we're enforcing our security model, all operations must 6600 // have a projection map configured 6601 if (qb.getProjectionMap() == null) { 6602 throw new IllegalStateException("All queries must have a projection map"); 6603 } 6604 6605 // If caller is an older app, we're willing to let through a 6606 // allowlist of technically invalid columns 6607 if (getCallingPackageTargetSdkVersion() < Build.VERSION_CODES.Q) { 6608 qb.setProjectionAllowlist(sAllowlist); 6609 } 6610 6611 // Starting U, if owner package name is used in query arguments, 6612 // we are restricting result set to only self-owned packages. 6613 if (shouldFilterOwnerPackageNameFlag() 6614 && shouldFilterOwnerPackageNameInSelection(extras, type)) { 6615 Log.d(TAG, "Restricting result set to only packages owned by calling package: " 6616 + mCallingIdentity.get().getSharedPackagesAsString()); 6617 final String ownerPackageMatchClause = getWhereForOwnerPackageMatch( 6618 mCallingIdentity.get()); 6619 appendWhereStandalone(qb, ownerPackageMatchClause); 6620 } 6621 6622 // Prevent a query from returning results if the selection clauses query on latitude and 6623 // longitude. Only return results if these columns are present in the sort clause to avoid 6624 // breaking any existing usage but return them in any arbitrary fashion instead of actually 6625 // sorting them. 6626 List<String> filterClauses = getClausesForFilteringGeolocationData(extras, type); 6627 if (indexMediaLatitudeLongitude() && !isCallingPackageSelf() && !filterClauses.isEmpty()) { 6628 if (filterClauses.contains(QUERY_ARG_SQL_SORT_ORDER)) { 6629 String sortArgs = extras.getString(QUERY_ARG_SQL_SORT_ORDER); 6630 if (sortArgs != null) { 6631 if (sortArgs.contains(LATITUDE)) { 6632 sortArgs = sortArgs.replace(LATITUDE, /* replacement */ "NULL"); 6633 } 6634 if (sortArgs.contains(LONGITUDE)) { 6635 sortArgs = sortArgs.replace(LONGITUDE, /* replacement */ "NULL"); 6636 } 6637 extras.putString(QUERY_ARG_SQL_SORT_ORDER, sortArgs); 6638 } 6639 } else { 6640 final String geolocationClause = "FALSE"; 6641 appendWhereStandalone(qb, geolocationClause); 6642 } 6643 } 6644 return qb; 6645 } 6646 6647 private List<String> getClausesForFilteringGeolocationData( 6648 Bundle queryArgs, int type) { 6649 if (type == TYPE_QUERY) { 6650 return getClausesForFilteringGeolocationData(queryArgs); 6651 } 6652 return List.of(); 6653 } 6654 6655 private List<String> getClausesForFilteringGeolocationData(Bundle queryArgs) { 6656 final String selection = queryArgs.getString(QUERY_ARG_SQL_SELECTION, "") 6657 .toLowerCase(Locale.ROOT); 6658 final String groupBy = queryArgs.getString(QUERY_ARG_SQL_GROUP_BY, "") 6659 .toLowerCase(Locale.ROOT); 6660 final String sort = queryArgs.getString(QUERY_ARG_SQL_SORT_ORDER, "") 6661 .toLowerCase(Locale.ROOT); 6662 final String having = queryArgs.getString(QUERY_ARG_SQL_HAVING, "") 6663 .toLowerCase(Locale.ROOT); 6664 6665 List<String> filteringClauses = new ArrayList<>(); 6666 if (sort.contains(LATITUDE) || sort.contains(LONGITUDE)) { 6667 filteringClauses.add(QUERY_ARG_SQL_SORT_ORDER); 6668 } 6669 if (selection.contains(LATITUDE) || selection.contains(LONGITUDE) 6670 || groupBy.contains(LATITUDE) || groupBy.contains(LONGITUDE) 6671 || having.contains(LATITUDE) || having.contains(LONGITUDE)) { 6672 filteringClauses.add(QUERY_ARG_SQL_SELECTION); 6673 filteringClauses.add(QUERY_ARG_SQL_GROUP_BY); 6674 filteringClauses.add(QUERY_ARG_SQL_HAVING); 6675 } 6676 return filteringClauses; 6677 } 6678 6679 private boolean shouldFilterOwnerPackageNameInSelection(Bundle queryArgs, int type) { 6680 return type == TYPE_QUERY && containsOwnerPackageName(queryArgs) 6681 && isApplicableForOwnerPackageNameFiltering(); 6682 } 6683 6684 private boolean containsOwnerPackageName(Bundle queryArgs) { 6685 final String selection = queryArgs.getString(QUERY_ARG_SQL_SELECTION, "") 6686 .toLowerCase(Locale.ROOT); 6687 final String groupBy = queryArgs.getString(QUERY_ARG_SQL_GROUP_BY, "") 6688 .toLowerCase(Locale.ROOT); 6689 final String sort = queryArgs.getString(QUERY_ARG_SQL_SORT_ORDER, "") 6690 .toLowerCase(Locale.ROOT); 6691 final String having = queryArgs.getString(QUERY_ARG_SQL_HAVING, "") 6692 .toLowerCase(Locale.ROOT); 6693 6694 return selection.contains(OWNER_PACKAGE_NAME) || groupBy.contains(OWNER_PACKAGE_NAME) 6695 || sort.contains(OWNER_PACKAGE_NAME) || having.contains(OWNER_PACKAGE_NAME); 6696 } 6697 6698 private void appendAccessCheckQuery(@NonNull SQLiteQueryBuilder qb, boolean forWrite, 6699 @NonNull Uri uri, int uriType, @NonNull Bundle extras, @NonNull String volumeName) { 6700 Objects.requireNonNull(extras); 6701 final Uri redactedUri = extras.getParcelable(QUERY_ARG_REDACTED_URI); 6702 6703 final boolean allowGlobal; 6704 if (redactedUri != null) { 6705 allowGlobal = checkCallingPermissionGlobal(redactedUri, false); 6706 } else { 6707 allowGlobal = checkCallingPermissionGlobal(uri, forWrite); 6708 } 6709 6710 if (allowGlobal) { 6711 return; 6712 } 6713 6714 if (hasAccessToCollection(mCallingIdentity.get(), uriType, forWrite)) { 6715 // has direct access to whole collection, no special filtering needed. 6716 return; 6717 } 6718 6719 final ArrayList<String> options = new ArrayList<>(); 6720 boolean isLatestSelectionOnlyRequired = extras.getBoolean(QUERY_ARG_LATEST_SELECTION_ONLY, 6721 false); 6722 if (!MediaStore.VOLUME_INTERNAL.equals(volumeName) 6723 && hasUserSelectedAccess(mCallingIdentity.get(), uriType, forWrite)) { 6724 // If app has READ_MEDIA_VISUAL_USER_SELECTED permission, allow access on files granted 6725 // via PhotoPicker launched for Permission. These grants are defined in media_grants 6726 // table. 6727 // We exclude volume internal from the query because media_grants are not supported. 6728 if (isLatestSelectionOnlyRequired) { 6729 // If the query arg to include only recent selection has been received then include 6730 // this as filter while doing the access check for grants from the media_grants 6731 // table. This reduces the clauses needed in the query and makes it more efficient. 6732 Log.d(TAG, "In user_select mode, recent selection only is required."); 6733 options.add(getWhereForLatestSelection(mCallingIdentity.get(), uriType)); 6734 } else { 6735 Log.d(TAG, "In user_select mode, recent selection only is not required."); 6736 options.add(getWhereForUserSelectedAccess(mCallingIdentity.get(), uriType)); 6737 // Allow access to files which are owned by the caller. Or allow access to files 6738 // based on legacy or any other special access permissions. 6739 options.add(getWhereForConstrainedAccess(mCallingIdentity.get(), uriType, forWrite, 6740 extras)); 6741 } 6742 } else { 6743 if (isLatestSelectionOnlyRequired) { 6744 Log.w(TAG, "Latest selection request cannot be honored in the current" 6745 + " access mode."); 6746 } 6747 // Allow access to files which are owned by the caller. Or allow access to files 6748 // based on legacy or any other special access permissions. 6749 options.add(getWhereForConstrainedAccess(mCallingIdentity.get(), uriType, forWrite, 6750 extras)); 6751 } 6752 6753 appendWhereStandalone(qb, TextUtils.join(" OR ", options)); 6754 } 6755 6756 /** 6757 * @return {@code true} if app requests to include database rows from 6758 * recently unmounted volume. 6759 * {@code false} otherwise. 6760 */ 6761 private boolean shouldIncludeRecentlyUnmountedVolumes(Uri uri, Bundle extras) { 6762 if (isFuseThread()) { 6763 // File path requests don't require to query from unmounted volumes. 6764 return false; 6765 } 6766 6767 boolean isIncludeVolumesChangeEnabled = SdkLevel.isAtLeastS() && 6768 CompatChanges.isChangeEnabled(ENABLE_INCLUDE_ALL_VOLUMES, Binder.getCallingUid()); 6769 if ("1".equals(uri.getQueryParameter(ALL_VOLUMES))) { 6770 // Support uri parameter only in R OS and below. Apps should use 6771 // MediaStore#QUERY_ARG_RECENTLY_UNMOUNTED_VOLUMES on S OS onwards. 6772 if (!isIncludeVolumesChangeEnabled) { 6773 return true; 6774 } 6775 throw new IllegalArgumentException("Unsupported uri parameter \"all_volumes\""); 6776 } 6777 if (isIncludeVolumesChangeEnabled) { 6778 // MediaStore#QUERY_ARG_INCLUDE_RECENTLY_UNMOUNTED_VOLUMES is only supported on S OS and 6779 // for app targeting targetSdk>=S. 6780 return extras.getBoolean(MediaStore.QUERY_ARG_INCLUDE_RECENTLY_UNMOUNTED_VOLUMES, 6781 false); 6782 } 6783 return false; 6784 } 6785 6786 /** 6787 * Determine if given {@link Uri} has a 6788 * {@link MediaColumns#OWNER_PACKAGE_NAME} column. 6789 */ 6790 private boolean hasOwnerPackageName(Uri uri) { 6791 // It's easier to maintain this as an inverted list 6792 final int table = matchUri(uri, true); 6793 switch (table) { 6794 case IMAGES_THUMBNAILS_ID: 6795 case IMAGES_THUMBNAILS: 6796 case VIDEO_THUMBNAILS_ID: 6797 case VIDEO_THUMBNAILS: 6798 case AUDIO_ALBUMART: 6799 case AUDIO_ALBUMART_ID: 6800 case AUDIO_ALBUMART_FILE_ID: 6801 return false; 6802 default: 6803 return true; 6804 } 6805 } 6806 6807 /** 6808 * @deprecated all operations should be routed through the overload that 6809 * accepts a {@link Bundle} of extras. 6810 */ 6811 @Override 6812 @Deprecated 6813 public int delete(Uri uri, String selection, String[] selectionArgs) { 6814 return delete(uri, 6815 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null)); 6816 } 6817 6818 @Override 6819 public int delete(@NonNull Uri uri, @Nullable Bundle extras) { 6820 Trace.beginSection(safeTraceSectionNameWithUri("delete", uri)); 6821 try { 6822 return deleteInternal(uri, extras); 6823 } catch (FallbackException e) { 6824 return e.translateForUpdateDelete(getCallingPackageTargetSdkVersion()); 6825 } finally { 6826 Trace.endSection(); 6827 } 6828 } 6829 6830 private int deleteInternal(@NonNull Uri uri, @Nullable Bundle extras) 6831 throws FallbackException { 6832 final String volumeName = getVolumeName(uri); 6833 PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName); 6834 6835 extras = (extras != null) ? extras : new Bundle(); 6836 // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider. 6837 extras.remove(QUERY_ARG_REDACTED_URI); 6838 6839 if (isRedactedUri(uri)) { 6840 // we don't support deletion on redacted uris. 6841 return 0; 6842 } 6843 6844 // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. 6845 extras.remove(INCLUDED_DEFAULT_DIRECTORIES); 6846 6847 uri = safeUncanonicalize(uri); 6848 final boolean allowHidden = isCallingPackageAllowedHidden(); 6849 final int match = matchUri(uri, allowHidden); 6850 6851 switch (match) { 6852 case AUDIO_MEDIA_ID: 6853 case AUDIO_PLAYLISTS_ID: 6854 case VIDEO_MEDIA_ID: 6855 case IMAGES_MEDIA_ID: 6856 case DOWNLOADS_ID: 6857 case FILES_ID: { 6858 if (!isFuseThread() && getCachedCallingIdentityForFuse(Binder.getCallingUid()). 6859 removeDeletedRowId(Long.parseLong(uri.getLastPathSegment()))) { 6860 // Apps sometimes delete the file via filePath and then try to delete the db row 6861 // using MediaProvider#delete. Since we would have already deleted the db row 6862 // during the filePath operation, the latter will result in a security 6863 // exception. Apps which don't expect an exception will break here. Since we 6864 // have already deleted the db row, silently return zero as deleted count. 6865 return 0; 6866 } 6867 } 6868 break; 6869 default: 6870 // For other match types, given uri will not correspond to a valid file. 6871 break; 6872 } 6873 6874 final String userWhere = extras.getString(QUERY_ARG_SQL_SELECTION); 6875 final String[] userWhereArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS); 6876 6877 int count = 0; 6878 6879 // handle MEDIA_SCANNER before calling getDatabaseForUri() 6880 if (match == MEDIA_SCANNER) { 6881 if (mMediaScannerVolume == null) { 6882 return 0; 6883 } 6884 6885 final DatabaseHelper helper = getDatabaseForUri( 6886 MediaStore.Files.getContentUri(mMediaScannerVolume)); 6887 6888 helper.mScanStopTime = SystemClock.elapsedRealtime(); 6889 6890 mMediaScannerVolume = null; 6891 return 1; 6892 } 6893 6894 if (match == VOLUMES_ID) { 6895 detachVolume(uri); 6896 count = 1; 6897 } 6898 6899 final DatabaseHelper helper = getDatabaseForUri(uri); 6900 switch (match) { 6901 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 6902 extras.putString(QUERY_ARG_SQL_SELECTION, 6903 BaseColumns._ID + "=" + uri.getPathSegments().get(5)); 6904 // fall-through 6905 case AUDIO_PLAYLISTS_ID_MEMBERS: { 6906 final long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 6907 final Uri playlistUri = ContentUris.withAppendedId( 6908 MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId); 6909 6910 // Playlist contents are always persisted directly into playlist 6911 // files on disk to ensure that we can reliably migrate between 6912 // devices and recover from database corruption 6913 int numOfRemovedPlaylistMembers = removePlaylistMembers(playlistUri, extras); 6914 if (numOfRemovedPlaylistMembers > 0) { 6915 acceptWithExpansion(helper::notifyDelete, volumeName, playlistId, 6916 FileColumns.MEDIA_TYPE_PLAYLIST, false); 6917 } 6918 return numOfRemovedPlaylistMembers; 6919 } 6920 } 6921 6922 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, match, uri, extras, null); 6923 6924 { 6925 // Give callers interacting with a specific media item a chance to 6926 // escalate access if they don't already have it 6927 switch (match) { 6928 case AUDIO_MEDIA_ID: 6929 case VIDEO_MEDIA_ID: 6930 case IMAGES_MEDIA_ID: 6931 enforceCallingPermission(uri, extras, true); 6932 } 6933 6934 final String[] projection = new String[] { 6935 FileColumns.MEDIA_TYPE, 6936 FileColumns.DATA, 6937 FileColumns._ID, 6938 FileColumns.IS_DOWNLOAD, 6939 FileColumns.MIME_TYPE, 6940 }; 6941 final boolean isFilesTable = qb.getTables().equals("files"); 6942 final LongSparseArray<String> deletedDownloadIds = new LongSparseArray<>(); 6943 final int[] countPerMediaType = new int[FileColumns.MEDIA_TYPE_COUNT]; 6944 if (isFilesTable) { 6945 String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA); 6946 6947 // if calling package is not self and its target SDK version is greater than U, 6948 // ignore the deleteparam and do not allow use by apps 6949 if (!isCallingPackageSelf() && getCallingPackageTargetSdkVersion() 6950 > Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 6951 deleteparam = null; 6952 Log.w(TAG, "Ignoring param:deletedata post U for external apps"); 6953 } 6954 6955 if (deleteparam == null || ! deleteparam.equals("false")) { 6956 Cursor c = qb.query(helper, projection, userWhere, userWhereArgs, 6957 null, null, null, null, null); 6958 try { 6959 while (c.moveToNext()) { 6960 final int mediaType = c.getInt(0); 6961 final String data = c.getString(1); 6962 final long id = c.getLong(2); 6963 final int isDownload = c.getInt(3); 6964 final String mimeType = c.getString(4); 6965 6966 // TODO(b/188782594) Consider logging mime type access on delete too. 6967 6968 // Forget that caller is owner of this item 6969 mCallingIdentity.get().setOwned(id, false); 6970 6971 deleteIfAllowed(uri, extras, data); 6972 int res = qb.delete(helper, BaseColumns._ID + "=" + id, null); 6973 count += res; 6974 // Avoid ArrayIndexOutOfBounds if more mediaTypes are added, 6975 // but mediaTypeSize is not updated 6976 if (res > 0 && mediaType < countPerMediaType.length) { 6977 countPerMediaType[mediaType] += res; 6978 } 6979 6980 if (isDownload == 1) { 6981 deletedDownloadIds.put(id, mimeType); 6982 } 6983 } 6984 } finally { 6985 FileUtils.closeQuietly(c); 6986 } 6987 // Do not allow deletion if the file/object is referenced as parent 6988 // by some other entries. It could cause database corruption. 6989 appendWhereStandalone(qb, ID_NOT_PARENT_CLAUSE); 6990 } 6991 } 6992 6993 switch (match) { 6994 case AUDIO_GENRES_ID_MEMBERS: 6995 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R); 6996 6997 case IMAGES_THUMBNAILS_ID: 6998 case IMAGES_THUMBNAILS: 6999 case VIDEO_THUMBNAILS_ID: 7000 case VIDEO_THUMBNAILS: 7001 // Delete the referenced files first. 7002 Cursor c = qb.query(helper, sDataOnlyColumn, userWhere, userWhereArgs, null, 7003 null, null, null, null); 7004 if (c != null) { 7005 try { 7006 while (c.moveToNext()) { 7007 deleteIfAllowed(uri, extras, c.getString(0)); 7008 } 7009 } finally { 7010 FileUtils.closeQuietly(c); 7011 } 7012 } 7013 count += deleteRecursive(qb, helper, userWhere, userWhereArgs); 7014 break; 7015 7016 default: 7017 count += deleteRecursive(qb, helper, userWhere, userWhereArgs); 7018 break; 7019 } 7020 7021 if (deletedDownloadIds.size() > 0) { 7022 notifyDownloadManagerOnDelete(helper, deletedDownloadIds); 7023 } 7024 7025 // Check for other URI format grants for File API call only. Check right before 7026 // returning count = 0, to leave positive cases performance unaffected. 7027 if (count == 0 && isFuseThread()) { 7028 count += deleteWithOtherUriGrants(uri, helper, projection, userWhere, userWhereArgs, 7029 extras); 7030 } 7031 7032 if (isFilesTable && !isCallingPackageSelf()) { 7033 Metrics.logDeletion(volumeName, mCallingIdentity.get().uid, 7034 getCallingPackageOrSelf(), count, countPerMediaType); 7035 } 7036 } 7037 7038 return count; 7039 } 7040 7041 private int deleteWithOtherUriGrants(@NonNull Uri uri, DatabaseHelper helper, 7042 String[] projection, String userWhere, String[] userWhereArgs, 7043 @Nullable Bundle extras) { 7044 try (Cursor c = queryForSingleItemAsMediaProvider(uri, projection, userWhere, userWhereArgs, 7045 null)) { 7046 final int mediaType = c.getInt(0); 7047 final String data = c.getString(1); 7048 final long id = c.getLong(2); 7049 final int isDownload = c.getInt(3); 7050 final String mimeType = c.getString(4); 7051 7052 final Uri uriGranted = getOtherUriGrantsForPath(data, mediaType, Long.toString(id), 7053 /* forWrite */ true); 7054 if (uriGranted != null) { 7055 // 1. delete file 7056 deleteIfAllowed(uriGranted, extras, data); 7057 // 2. delete file row from the db 7058 final boolean allowHidden = isCallingPackageAllowedHidden(); 7059 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, 7060 matchUri(uriGranted, allowHidden), uriGranted, extras, null); 7061 int count = qb.delete(helper, BaseColumns._ID + "=" + id, null); 7062 7063 if (isDownload == 1) { 7064 final LongSparseArray<String> deletedDownloadIds = new LongSparseArray<>(); 7065 deletedDownloadIds.put(id, mimeType); 7066 notifyDownloadManagerOnDelete(helper, deletedDownloadIds); 7067 } 7068 return count; 7069 } 7070 } catch (FileNotFoundException ignored) { 7071 // Do nothing. Returns 0 files deleted. 7072 } 7073 return 0; 7074 } 7075 7076 private void notifyDownloadManagerOnDelete(DatabaseHelper helper, 7077 LongSparseArray<String> deletedDownloadIds) { 7078 // Do this on a background thread, since we don't want to make binder 7079 // calls as part of a FUSE call. 7080 helper.postBackground(() -> { 7081 DownloadManager dm = getContext().getSystemService(DownloadManager.class); 7082 if (dm != null) { 7083 dm.onMediaStoreDownloadsDeleted(deletedDownloadIds); 7084 } 7085 }); 7086 } 7087 7088 /** 7089 * Executes identical delete repeatedly within a single transaction until 7090 * stability is reached. Combined with {@link #ID_NOT_PARENT_CLAUSE}, this 7091 * can be used to recursively delete all matching entries, since it only 7092 * deletes parents when no references remaining. 7093 */ 7094 private int deleteRecursive(SQLiteQueryBuilder qb, DatabaseHelper helper, String userWhere, 7095 String[] userWhereArgs) { 7096 return helper.runWithTransaction((db) -> { 7097 synchronized (mDirectoryCache) { 7098 mDirectoryCache.clear(); 7099 } 7100 7101 int n = 0; 7102 int total = 0; 7103 do { 7104 n = qb.delete(helper, userWhere, userWhereArgs); 7105 total += n; 7106 } while (n > 0); 7107 return total; 7108 }); 7109 } 7110 7111 @Nullable 7112 @VisibleForTesting 7113 Uri getRedactedUri(@NonNull Uri uri) { 7114 if (!isUriSupportedForRedaction(uri)) { 7115 return null; 7116 } 7117 7118 DatabaseHelper helper; 7119 try { 7120 helper = getDatabaseForUri(uri); 7121 } catch (VolumeNotFoundException e) { 7122 throw e.rethrowAsIllegalArgumentException(); 7123 } 7124 7125 try (final Cursor c = helper.runWithoutTransaction( 7126 (db) -> db.query("files", 7127 new String[]{FileColumns.REDACTED_URI_ID}, FileColumns._ID + "=?", 7128 new String[]{uri.getLastPathSegment()}, null, null, null))) { 7129 // Database entry for uri not found. 7130 if (!c.moveToFirst()) return null; 7131 7132 String redactedUriID = c.getString(c.getColumnIndex(FileColumns.REDACTED_URI_ID)); 7133 if (redactedUriID == null) { 7134 // No redacted has even been created for this uri. Create a new redacted URI ID for 7135 // the uri and store it in the DB. 7136 redactedUriID = REDACTED_URI_ID_PREFIX + UUID.randomUUID().toString().replace("-", 7137 ""); 7138 7139 ContentValues cv = new ContentValues(); 7140 cv.put(FileColumns.REDACTED_URI_ID, redactedUriID); 7141 int rowsAffected = helper.runWithTransaction( 7142 (db) -> db.update("files", cv, FileColumns._ID + "=?", 7143 new String[]{uri.getLastPathSegment()})); 7144 if (rowsAffected == 0) { 7145 // this shouldn't happen ideally, only reason this might happen is if the db 7146 // entry got deleted in b/w in which case we should return null. 7147 return null; 7148 } 7149 } 7150 7151 // Create and return a uri with ID = redactedUriID. 7152 final Uri.Builder builder = ContentUris.removeId(uri).buildUpon(); 7153 builder.appendPath(redactedUriID); 7154 7155 return builder.build(); 7156 } 7157 } 7158 7159 @NonNull 7160 @VisibleForTesting 7161 List<Uri> getRedactedUri(@NonNull List<Uri> uris) { 7162 ArrayList<Uri> redactedUris = new ArrayList<>(); 7163 for (Uri uri : uris) { 7164 redactedUris.add(getRedactedUri(uri)); 7165 } 7166 7167 return redactedUris; 7168 } 7169 7170 @Override 7171 public Bundle call(String method, String arg, Bundle extras) { 7172 Trace.beginSection("MP.call [" + method + ']'); 7173 try { 7174 return callInternal(method, arg, extras); 7175 } finally { 7176 Trace.endSection(); 7177 } 7178 } 7179 7180 private Bundle callInternal(String method, String arg, Bundle extras) { 7181 switch (method) { 7182 case MediaStore.RESOLVE_PLAYLIST_MEMBERS_CALL: { 7183 return getResultForResolvePlaylistMembers(extras); 7184 } 7185 case MediaStore.SET_STABLE_URIS_FLAG: { 7186 return getResultForSetStableUrisFlag(arg, extras); 7187 } 7188 case MediaStore.RUN_IDLE_MAINTENANCE_CALL: { 7189 return getResultForRunIdleMaintenance(); 7190 } 7191 case MediaStore.WAIT_FOR_IDLE_CALL: { 7192 return getResultForWaitForIdle(); 7193 } 7194 case MediaStore.SCAN_FILE_CALL: { 7195 return getResultForScanFile(arg); 7196 } 7197 case MediaStore.SCAN_VOLUME_CALL: { 7198 return getResultForScanVolume(arg); 7199 } 7200 case MediaStore.GET_VERSION_CALL: { 7201 return getResultForGetVersion(extras); 7202 } 7203 case MediaStore.GET_GENERATION_CALL: { 7204 return getResultForGetGeneration(extras); 7205 } 7206 case MediaStore.GET_DOCUMENT_URI_CALL: { 7207 return getResultForGetDocumentUri(method, extras); 7208 } 7209 case MediaStore.GET_MEDIA_URI_CALL: { 7210 return getResultForGetMediaUri(method, extras); 7211 } 7212 case MediaStore.GET_REDACTED_MEDIA_URI_CALL: { 7213 return getResultForGetRedactedMediaUri(extras); 7214 } 7215 case MediaStore.GET_REDACTED_MEDIA_URI_LIST_CALL: { 7216 return getResultForGetRedactedMediaUriList(extras); 7217 } 7218 case MediaStore.GRANT_MEDIA_READ_FOR_PACKAGE_CALL: { 7219 return getResultForGrantMediaReadForPackage(extras); 7220 } 7221 case MediaStore.REVOKE_READ_GRANT_FOR_PACKAGE_CALL: { 7222 return getResultForRevokeReadGrantForPackage(extras); 7223 } 7224 case MediaStore.CREATE_WRITE_REQUEST_CALL: 7225 case MediaStore.CREATE_FAVORITE_REQUEST_CALL: 7226 case MediaStore.CREATE_TRASH_REQUEST_CALL: 7227 case MediaStore.CREATE_DELETE_REQUEST_CALL: { 7228 return getResultForCreateOperationsRequest(method, extras); 7229 } 7230 case MediaStore.MARK_MEDIA_AS_FAVORITE: { 7231 return markMediaAsFavorite(extras); 7232 } 7233 case MediaStore.CREATE_CANCELLATION_SIGNAL_CALL: { 7234 return getResultForCreateCancellationSignal(); 7235 } 7236 case MediaStore.OPEN_FILE_CALL: { 7237 return getResultForOpenFile(extras); 7238 } 7239 case MediaStore.OPEN_ASSET_FILE_CALL: { 7240 return getResultForOpenAssetFile(extras); 7241 } 7242 case MediaStore.IS_SYSTEM_GALLERY_CALL: 7243 return getResultForIsSystemGallery(arg, extras); 7244 case MediaStore.PICKER_MEDIA_INIT_CALL: { 7245 return getResultForPickerMediaInit(extras); 7246 } 7247 case MediaStore.PICKER_MEDIA_IN_MEDIA_SET_INIT_CALL: { 7248 initMediaInMediaSet(extras); 7249 return new Bundle(); 7250 } 7251 case MediaStore.PICKER_INTERNAL_SEARCH_MEDIA_INIT_CALL: { 7252 return getResultForPickerSearchMediaInit(extras); 7253 } 7254 case MediaStore.PICKER_MEDIA_SETS_INIT_CALL: { 7255 initMediaSets(extras); 7256 return new Bundle(); 7257 } 7258 case MediaStore.PICKER_GET_SEARCH_PROVIDERS_CALL: { 7259 return getPickerSearchProviders(); 7260 } 7261 case MediaStore.PICKER_TRANSCODE_CALL: { 7262 return getResultForPickerTranscode(extras); 7263 } 7264 case MediaStore.GET_CLOUD_PROVIDER_CALL: { 7265 return getResultForGetCloudProvider(); 7266 } 7267 case MediaStore.GET_CLOUD_PROVIDER_LABEL_CALL: { 7268 return getResultForGetCloudProviderLabel(); 7269 } 7270 case MediaStore.GET_CLOUD_PROVIDER_DETAILS: { 7271 if (isCallerPhotoPicker()) { 7272 return PickerDataLayerV2.getCloudProviderDetails(extras); 7273 } else { 7274 throw new SecurityException( 7275 getSecurityExceptionMessage("GET_CLOUD_PROVIDER_DETAILS")); 7276 } 7277 } 7278 case MediaStore.ENSURE_PROVIDERS_CALL: { 7279 if (isCallerPhotoPicker()) { 7280 PickerDataLayerV2.ensureProviders(); 7281 return new Bundle(); 7282 } else { 7283 throw new SecurityException( 7284 getSecurityExceptionMessage("ENSURE_PROVIDERS_CALL")); 7285 } 7286 } 7287 case MediaStore.SET_CLOUD_PROVIDER_CALL: { 7288 return getResultForSetCloudProvider(extras); 7289 } 7290 case MediaStore.SYNC_PROVIDERS_CALL: { 7291 return getResultForSyncProviders(); 7292 } 7293 case MediaStore.IS_SUPPORTED_CLOUD_PROVIDER_CALL: { 7294 return getResultForIsSupportedCloudProvider(arg); 7295 } 7296 case MediaStore.IS_CURRENT_CLOUD_PROVIDER_CALL: { 7297 return getResultForIsCurrentCloudProviderCall(arg); 7298 } 7299 case MediaStore.NOTIFY_CLOUD_MEDIA_CHANGED_EVENT_CALL: { 7300 return getResultForNotifyCloudMediaChangedEvent(arg, extras); 7301 } 7302 case MediaStore.USES_FUSE_PASSTHROUGH: { 7303 return getResultForUsesFusePassThrough(arg); 7304 } 7305 case MediaStore.RUN_IDLE_MAINTENANCE_FOR_STABLE_URIS: { 7306 return getResultForIdleMaintenanceForStableUris(); 7307 } 7308 case READ_BACKUP: { 7309 return getResultForReadBackup(arg, extras); 7310 } 7311 case GET_OWNER_PACKAGE_NAME: { 7312 return getResultForGetOwnerPackageName(arg); 7313 } 7314 case MediaStore.DELETE_BACKED_UP_FILE_PATHS: { 7315 return getResultForDeleteBackedUpFilePaths(arg); 7316 } 7317 case MediaStore.GET_BACKUP_FILES: { 7318 return getResultForGetBackupFiles(); 7319 } 7320 case MediaStore.GET_RECOVERY_DATA: { 7321 return getResultForGetRecoveryData(); 7322 } 7323 case MediaStore.REMOVE_RECOVERY_DATA: { 7324 removeRecoveryData(); 7325 return new Bundle(); 7326 } 7327 case MediaStore.BULK_UPDATE_OEM_METADATA_CALL: { 7328 callForBulkUpdateOemMetadataColumn(); 7329 return new Bundle(); 7330 } 7331 default: 7332 throw new UnsupportedOperationException("Unsupported call: " + method); 7333 } 7334 } 7335 7336 private void callForBulkUpdateOemMetadataColumn() { 7337 if (!Flags.enableOemMetadataUpdate()) { 7338 return; 7339 } 7340 7341 enforcePermissionCheckForOemMetadataUpdate(); 7342 Set<String> oemSupportedMimeTypes = mMediaScanner.getOemSupportedMimeTypes(); 7343 if (oemSupportedMimeTypes == null || oemSupportedMimeTypes.isEmpty()) { 7344 // Nothing to update 7345 return; 7346 } 7347 7348 // Get media types to update rows based on media type 7349 Set<Integer> mediaTypesToBeUpdated = new HashSet<>(); 7350 for (String mimeType : oemSupportedMimeTypes) { 7351 // Convert to media type to avoid using like clause on mime types to protect against 7352 // SQL injection 7353 mediaTypesToBeUpdated.add(MimeUtils.resolveMediaType(mimeType)); 7354 } 7355 7356 if (mediaTypesToBeUpdated.isEmpty()) { 7357 // For invalid mime types, do not bother 7358 return; 7359 } 7360 7361 final LocalCallingIdentity token = clearLocalCallingIdentity(); 7362 try { 7363 ContentValues values = new ContentValues(); 7364 values.putNull(OEM_METADATA); 7365 // Mark _modifier as _MODIFIER_CR to allow metadata update on next scan. This 7366 // is explicitly required when calling update with MediaProvider identity 7367 values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_CR); 7368 Bundle extras = new Bundle(); 7369 extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, 7370 appendMediaTypeClause(mediaTypesToBeUpdated)); 7371 Log.v(TAG, "Trigger bulk update of OEM metadata"); 7372 update(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), values, extras); 7373 } finally { 7374 restoreLocalCallingIdentity(token); 7375 } 7376 } 7377 7378 private String appendMediaTypeClause(Set<Integer> mediaTypesToBeUpdated) { 7379 List<String> whereMediaTypesCondition = new ArrayList<String>(); 7380 for (Integer mediaType : mediaTypesToBeUpdated) { 7381 whereMediaTypesCondition.add( 7382 String.format(Locale.ROOT, "%s=%d", MEDIA_TYPE, mediaType)); 7383 } 7384 7385 StringBuilder sb = new StringBuilder(); 7386 sb.append("("); 7387 sb.append(TextUtils.join(" OR ", whereMediaTypesCondition)); 7388 sb.append(")"); 7389 return sb.toString(); 7390 } 7391 7392 @VisibleForTesting 7393 protected void enforcePermissionCheckForOemMetadataUpdate() { 7394 if (!isCallingPackageSelf() 7395 && !mCallingIdentity.get().checkCallingPermissionToUpdateOemMetadata()) { 7396 throw new SecurityException( 7397 "Calling package does not have permission to update OEM metadata"); 7398 } 7399 } 7400 7401 @Nullable 7402 private Bundle getResultForRevokeReadGrantForPackage(Bundle extras) { 7403 final int caller = Binder.getCallingUid(); 7404 final Boolean isCallForRevokeAll = extras.getBoolean( 7405 REVOKED_ALL_READ_GRANTS_FOR_PACKAGE_CALL); 7406 int userId; 7407 List<Uri> uris = null; 7408 String[] packageNames; 7409 int packageUid; 7410 if (checkPermissionShell(caller)) { 7411 // If the caller is the shell, the accepted parameter is EXTRA_PACKAGE_NAME 7412 // (as string). 7413 if (!extras.containsKey(Intent.EXTRA_PACKAGE_NAME)) { 7414 throw new IllegalArgumentException( 7415 "Missing required extras arguments: EXTRA_URI or" 7416 + " EXTRA_PACKAGE_NAME"); 7417 } 7418 String packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME); 7419 packageNames = new String[]{packageName}; 7420 try { 7421 packageUid = mPackageManager.getPackageUid(packageName, 0); 7422 } catch (NameNotFoundException e) { 7423 Log.e(TAG, "No packageUid found for packageName " + packageName, e); 7424 throw new RuntimeException(e); 7425 } 7426 // Uris are not a requirement for revoke all call 7427 if (!isCallForRevokeAll) { 7428 uris = List.of(Uri.parse(extras.getString(MediaStore.EXTRA_URI))); 7429 } 7430 // Caller is always shell which may not have the desired userId. Hence, use 7431 // UserId from the MediaProvider process itself. 7432 userId = UserHandle.myUserId(); 7433 } else if (checkPermissionSelf(caller) || isCallerPhotoPicker()) { 7434 final PackageManager pm = getContext().getPackageManager(); 7435 packageUid = extras.getInt(Intent.EXTRA_UID); 7436 packageNames = pm.getPackagesForUid(packageUid); 7437 // Get the userId from packageUid as the initiator could be a cloned app, which 7438 // accesses Media via MP of its parent user and Binder's callingUid reflects 7439 // the latter. 7440 userId = uidToUserId(packageUid); 7441 // Uris are not a requirement for revoke all call 7442 if (!isCallForRevokeAll) { 7443 uris = extras.getParcelableArrayList(EXTRA_URI_LIST); 7444 } 7445 } else { 7446 // All other callers are unauthorized. 7447 throw new SecurityException( 7448 getSecurityExceptionMessage("revoke media grants")); 7449 } 7450 7451 if (isCallForRevokeAll && !isOwnedPhotosEnabled(packageUid)) { 7452 mMediaGrants.removeAllMediaGrantsForPackages(packageNames, "user de-selections", 7453 userId); 7454 } else if (uris != null) { 7455 mMediaGrants.removeMediaGrantsForPackage(packageNames, uris, userId); 7456 if (isOwnedPhotosEnabled(packageUid)) { 7457 mFilesOwnershipUtils.removeOwnerPackageNameForUris(packageNames, uris, 7458 userId); 7459 } 7460 } 7461 return null; 7462 } 7463 7464 @Nullable 7465 private Bundle getResultForResolvePlaylistMembers(Bundle extras) { 7466 final LocalCallingIdentity token = clearLocalCallingIdentity(); 7467 final CallingIdentity providerToken = clearCallingIdentity(); 7468 try { 7469 final Uri playlistUri = extras.getParcelable(MediaStore.EXTRA_URI); 7470 resolvePlaylistMembers(playlistUri); 7471 } finally { 7472 restoreCallingIdentity(providerToken); 7473 restoreLocalCallingIdentity(token); 7474 } 7475 return null; 7476 } 7477 7478 @Nullable 7479 private Bundle getResultForSetStableUrisFlag(String volumeName, Bundle extras) { 7480 // WRITE_MEDIA_STORAGE is a privileged permission which only MediaProvider and some other 7481 // system apps have. 7482 getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE, 7483 "Permission missing to call SET_STABLE_URIS by uid:" + Binder.getCallingUid()); 7484 final LocalCallingIdentity token = clearLocalCallingIdentity(); 7485 final CallingIdentity providerToken = clearCallingIdentity(); 7486 try { 7487 final boolean isEnabled = extras.getBoolean(EXTRA_IS_STABLE_URIS_ENABLED); 7488 mDatabaseBackupAndRecovery.setStableUrisGlobalFlag(volumeName, isEnabled); 7489 } finally { 7490 restoreCallingIdentity(providerToken); 7491 restoreLocalCallingIdentity(token); 7492 } 7493 return null; 7494 } 7495 7496 @Nullable 7497 private Bundle getResultForRunIdleMaintenance() { 7498 // Protect ourselves from random apps by requiring a generic 7499 // permission held by common debugging components, such as shell 7500 getContext().enforceCallingOrSelfPermission( 7501 Manifest.permission.DUMP, TAG); 7502 final LocalCallingIdentity token = clearLocalCallingIdentity(); 7503 final CallingIdentity providerToken = clearCallingIdentity(); 7504 try { 7505 onIdleMaintenance(new CancellationSignal()); 7506 } finally { 7507 restoreCallingIdentity(providerToken); 7508 restoreLocalCallingIdentity(token); 7509 } 7510 return null; 7511 } 7512 7513 /** 7514 * Triggers backup for MediaProvider. 7515 */ 7516 public void triggerBackup() { 7517 mExternalPrimaryBackupExecutor.doBackup(null); 7518 } 7519 7520 @Nullable 7521 private Bundle getResultForWaitForIdle() { 7522 // TODO(b/195009139): Remove after overriding wait for idle in test to sync picker 7523 // Syncing the picker while waiting for idle fixes tests with the picker db 7524 // flag enabled because the picker db is in a consistent state with the external 7525 // db after the sync 7526 syncAllMedia(); 7527 ForegroundThread.waitForIdle(); 7528 final CountDownLatch latch = new CountDownLatch(1); 7529 BackgroundThread.getExecutor().execute(latch::countDown); 7530 try { 7531 latch.await(30, TimeUnit.SECONDS); 7532 } catch (InterruptedException e) { 7533 throw new IllegalStateException(e); 7534 } 7535 return null; 7536 } 7537 7538 @NotNull 7539 private Bundle getResultForScanFile(String arg) { 7540 final LocalCallingIdentity token = clearLocalCallingIdentity(); 7541 final CallingIdentity providerToken = clearCallingIdentity(); 7542 7543 final String filePath = arg; 7544 final Uri uri; 7545 try { 7546 File file; 7547 try { 7548 file = FileUtils.getCanonicalFile(filePath); 7549 } catch (IOException e) { 7550 file = null; 7551 } 7552 7553 uri = file != null ? scanFile(file, REASON_DEMAND) : null; 7554 } finally { 7555 restoreCallingIdentity(providerToken); 7556 restoreLocalCallingIdentity(token); 7557 } 7558 7559 // TODO(b/262244882): maybe enforceCallingPermissionInternal(uri, ...) 7560 7561 final Bundle res = new Bundle(); 7562 res.putParcelable(Intent.EXTRA_STREAM, uri); 7563 return res; 7564 } 7565 7566 private Bundle getResultForScanVolume(String arg) { 7567 final int userId = uidToUserId(Binder.getCallingUid()); 7568 final LocalCallingIdentity token = clearLocalCallingIdentity(); 7569 final CallingIdentity providerToken = clearCallingIdentity(); 7570 7571 final String volumeName = arg; 7572 try { 7573 final MediaVolume volume = mVolumeCache.findVolume(volumeName, 7574 UserHandle.of(userId)); 7575 MediaService.onScanVolume(getContext(), volume, REASON_DEMAND); 7576 } catch (FileNotFoundException e) { 7577 Log.w(TAG, "Failed to find volume " + volumeName, e); 7578 } catch (IOException e) { 7579 throw new RuntimeException(e); 7580 } finally { 7581 restoreCallingIdentity(providerToken); 7582 restoreLocalCallingIdentity(token); 7583 } 7584 return Bundle.EMPTY; 7585 } 7586 7587 @NotNull 7588 private Bundle getResultForGetVersion(Bundle extras) { 7589 final String volumeName = extras.getString(Intent.EXTRA_TEXT); 7590 7591 final DatabaseHelper helper; 7592 try { 7593 helper = getDatabaseForUri(Files.getContentUri(volumeName)); 7594 } catch (VolumeNotFoundException e) { 7595 throw e.rethrowAsIllegalArgumentException(); 7596 } 7597 7598 final String version = 7599 helper.runWithoutTransaction( 7600 (db) -> { 7601 final String dbUuid = DatabaseHelper.getOrCreateUuid(db); 7602 if (shouldLockdownMediaStoreVersion()) { 7603 final String input = dbUuid + mCallingIdentity.get().uid; 7604 final HashCode uuidHashCode = 7605 Hashing.farmHashFingerprint64() 7606 .hashString(input, StandardCharsets.UTF_8); 7607 return uuidHashCode.toString(); 7608 } else { 7609 return db.getVersion() + ":" + dbUuid; 7610 } 7611 }); 7612 final Bundle res = new Bundle(); 7613 res.putString(Intent.EXTRA_TEXT, version); 7614 return res; 7615 } 7616 7617 @VisibleForTesting 7618 boolean shouldLockdownMediaStoreVersion() { 7619 return versionLockdown() && CompatChanges.isChangeEnabled( 7620 LOCKDOWN_MEDIASTORE_VERSION, mCallingIdentity.get().uid); 7621 } 7622 7623 @NotNull 7624 private Bundle getResultForGetGeneration(Bundle extras) { 7625 final String volumeName = extras.getString(Intent.EXTRA_TEXT); 7626 7627 final DatabaseHelper helper; 7628 try { 7629 helper = getDatabaseForUri(Files.getContentUri(volumeName)); 7630 } catch (VolumeNotFoundException e) { 7631 throw e.rethrowAsIllegalArgumentException(); 7632 } 7633 7634 final long generation = helper.runWithoutTransaction(DatabaseHelper::getGeneration); 7635 7636 final Bundle res = new Bundle(); 7637 res.putLong(Intent.EXTRA_INDEX, generation); 7638 return res; 7639 } 7640 7641 private Bundle getResultForGetDocumentUri(String method, Bundle extras) { 7642 final Uri mediaUri = extras.getParcelable(MediaStore.EXTRA_URI); 7643 enforceCallingPermission(mediaUri, extras, false); 7644 7645 final Uri fileUri; 7646 final LocalCallingIdentity token = clearLocalCallingIdentity(); 7647 try { 7648 fileUri = Uri.fromFile(queryForDataFile(mediaUri, null)); 7649 } catch (FileNotFoundException e) { 7650 throw new IllegalArgumentException(e); 7651 } finally { 7652 restoreLocalCallingIdentity(token); 7653 } 7654 7655 try (ContentProviderClient client = getContext().getContentResolver() 7656 .acquireUnstableContentProviderClient( 7657 getExternalStorageProviderAuthority())) { 7658 extras.putParcelable(MediaStore.EXTRA_URI, fileUri); 7659 return client.call(method, null, extras); 7660 } catch (RemoteException e) { 7661 throw new IllegalStateException(e); 7662 } 7663 } 7664 7665 @NotNull 7666 private Bundle getResultForGetMediaUri(String method, Bundle extras) { 7667 final Uri documentUri = extras.getParcelable(MediaStore.EXTRA_URI); 7668 getContext().enforceCallingUriPermission(documentUri, 7669 Intent.FLAG_GRANT_READ_URI_PERMISSION, TAG); 7670 7671 final int callingPid = mCallingIdentity.get().pid; 7672 final int callingUid = mCallingIdentity.get().uid; 7673 final String callingPackage = getCallingPackage(); 7674 final CallingIdentity token = clearCallingIdentity(); 7675 final String authority = documentUri.getAuthority(); 7676 7677 if (!authority.equals(MediaDocumentsProvider.AUTHORITY) 7678 && !authority.equals(DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) { 7679 throw new IllegalArgumentException("Provider for this Uri is not supported."); 7680 } 7681 7682 try (ContentProviderClient client = getContext().getContentResolver() 7683 .acquireUnstableContentProviderClient(authority)) { 7684 final Bundle clientRes = client.call(method, null, extras); 7685 final Uri fileUri = clientRes.getParcelable(MediaStore.EXTRA_URI); 7686 final Bundle res = new Bundle(); 7687 final Uri mediaStoreUri = fileUri.getAuthority().equals(MediaStore.AUTHORITY) 7688 ? fileUri : queryForMediaUri(new File(fileUri.getPath()), null); 7689 copyUriPermissionGrants(documentUri, mediaStoreUri, callingPid, 7690 callingUid, callingPackage); 7691 res.putParcelable(MediaStore.EXTRA_URI, mediaStoreUri); 7692 return res; 7693 } catch (FileNotFoundException e) { 7694 throw new IllegalArgumentException(e); 7695 } catch (RemoteException e) { 7696 throw new IllegalStateException(e); 7697 } finally { 7698 restoreCallingIdentity(token); 7699 } 7700 } 7701 7702 @NotNull 7703 private Bundle getResultForGetRedactedMediaUri(Bundle extras) { 7704 final Uri uri = extras.getParcelable(MediaStore.EXTRA_URI); 7705 // NOTE: It is ok to update the DB and return a redacted URI for the cases when 7706 // the user code only has read access, hence we don't check for write permission. 7707 enforceCallingPermission(uri, Bundle.EMPTY, false); 7708 final LocalCallingIdentity token = clearLocalCallingIdentity(); 7709 try { 7710 final Bundle res = new Bundle(); 7711 res.putParcelable(MediaStore.EXTRA_URI, getRedactedUri(uri)); 7712 return res; 7713 } finally { 7714 restoreLocalCallingIdentity(token); 7715 } 7716 } 7717 7718 @NotNull 7719 private Bundle getResultForGetRedactedMediaUriList(Bundle extras) { 7720 final List<Uri> uris = extras.getParcelableArrayList(EXTRA_URI_LIST); 7721 // NOTE: It is ok to update the DB and return a redacted URI for the cases when 7722 // the user code only has read access, hence we don't check for write permission. 7723 enforceCallingPermission(uris, false); 7724 final LocalCallingIdentity token = clearLocalCallingIdentity(); 7725 try { 7726 final Bundle res = new Bundle(); 7727 res.putParcelableArrayList(EXTRA_URI_LIST, 7728 (ArrayList<? extends Parcelable>) getRedactedUri(uris)); 7729 return res; 7730 } finally { 7731 restoreLocalCallingIdentity(token); 7732 } 7733 } 7734 7735 @Nullable 7736 private Bundle getResultForGrantMediaReadForPackage(Bundle extras) { 7737 final int caller = Binder.getCallingUid(); 7738 int userId; 7739 final List<Uri> uris; 7740 String packageName; 7741 if (checkPermissionShell(caller)) { 7742 // If the caller is the shell, the accepted parameters are EXTRA_URI (as string) 7743 // and EXTRA_PACKAGE_NAME (as string). 7744 if (!extras.containsKey(MediaStore.EXTRA_URI) 7745 && !extras.containsKey(Intent.EXTRA_PACKAGE_NAME)) { 7746 throw new IllegalArgumentException( 7747 "Missing required extras arguments: EXTRA_URI or" + " EXTRA_PACKAGE_NAME"); 7748 } 7749 packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME); 7750 uris = List.of(Uri.parse(extras.getString(MediaStore.EXTRA_URI))); 7751 // Caller is always shell which may not have the desired userId. Hence, use 7752 // UserId from the MediaProvider process itself. 7753 userId = UserHandle.myUserId(); 7754 7755 } else if (checkPermissionSelf(caller) || isCallerPhotoPicker()) { 7756 // If the caller is MediaProvider the accepted parameters are EXTRA_URI_LIST 7757 // and EXTRA_UID. 7758 if (!extras.containsKey(EXTRA_URI_LIST) 7759 && !extras.containsKey(Intent.EXTRA_UID)) { 7760 throw new IllegalArgumentException( 7761 "Missing required extras arguments: EXTRA_URI_LIST or" + " EXTRA_UID"); 7762 } 7763 uris = extras.getParcelableArrayList(EXTRA_URI_LIST); 7764 final PackageManager pm = getContext().getPackageManager(); 7765 final int packageUid = extras.getInt(Intent.EXTRA_UID); 7766 final String[] packages = pm.getPackagesForUid(packageUid); 7767 if (packages == null || packages.length == 0) { 7768 throw new IllegalArgumentException( 7769 String.format( 7770 "Could not find packages for media_grants with uid: %d", 7771 packageUid)); 7772 } 7773 // Use the first package in the returned list for grants. In the case this 7774 // uid has multiple shared packages, the eventual queries to check for file 7775 // access will use all of the packages in this list, so just one is needed 7776 // to create the grants. 7777 packageName = packages[0]; 7778 // Get the userId from packageUid as the initiator could be a cloned app, which 7779 // accesses Media via MP of its parent user and Binder's callingUid reflects 7780 // the latter. 7781 userId = uidToUserId(packageUid); 7782 } else { 7783 // All other callers are unauthorized. 7784 7785 throw new SecurityException(getSecurityExceptionMessage("Create media grants")); 7786 } 7787 7788 mMediaGrants.addMediaGrantsForPackage(packageName, uris, userId); 7789 return null; 7790 } 7791 7792 @NotNull 7793 private Bundle getResultForCreateOperationsRequest(String method, Bundle extras) { 7794 final PendingIntent pi = createRequest(method, extras); 7795 final Bundle res = new Bundle(); 7796 res.putParcelable(MediaStore.EXTRA_RESULT, pi); 7797 return res; 7798 } 7799 7800 private Bundle markMediaAsFavorite(Bundle extras) { 7801 final ContentValues values = extras.getParcelable(MediaStore.EXTRA_CONTENT_VALUES); 7802 final ClipData clipData = extras.getParcelable(MediaStore.EXTRA_CLIP_DATA); 7803 final List<Uri> uris = collectUris(clipData); 7804 7805 if (!isCallingPackageManager()) { 7806 for (Uri uri : uris) { 7807 if (!AccessChecker.hasAccessToCollection(mCallingIdentity.get(), 7808 matchUri(uri, isCallingPackageAllowedHidden()), /* forWrite= */false)) { 7809 throw new UnsupportedOperationException("Uri " + uri 7810 + " does not have required permission to mark media as favorite"); 7811 } 7812 } 7813 } 7814 7815 final LocalCallingIdentity token = clearLocalCallingIdentity(); 7816 try { 7817 for (Uri uri : uris) { 7818 update(uri, values, null); 7819 } 7820 } finally { 7821 restoreLocalCallingIdentity(token); 7822 } 7823 return null; 7824 } 7825 7826 @NotNull 7827 private Bundle getResultForCreateCancellationSignal() { 7828 final Bundle res = new Bundle(); 7829 res.putBinder(MediaStore.CREATE_CANCELLATION_SIGNAL_RESULT, 7830 (new MPCancellationSignal()).asBinder()); 7831 return res; 7832 } 7833 7834 @NotNull 7835 private Bundle getResultForOpenFile(Bundle extras) { 7836 OpenFileRequest request = extras.getParcelable(EXTRA_OPEN_FILE_REQUEST); 7837 if (!isPickerUri(request.getUri())) { 7838 throw new IllegalArgumentException("Given Uri " + request.getUri() 7839 + " should be a picker URI"); 7840 } 7841 mAsyncPickerFileOpener.scheduleOpenFileAsync(request, mCallingIdentity.get()); 7842 return new Bundle(); 7843 } 7844 7845 @NotNull 7846 private Bundle getResultForOpenAssetFile(Bundle extras) { 7847 OpenAssetFileRequest request = extras.getParcelable(EXTRA_OPEN_ASSET_FILE_REQUEST); 7848 if (!isPickerUri(request.getUri())) { 7849 throw new IllegalArgumentException("Given Uri " + request.getUri() 7850 + " should be a picker URI"); 7851 } 7852 mAsyncPickerFileOpener.scheduleOpenAssetFileAsync(request, mCallingIdentity.get()); 7853 return new Bundle(); 7854 } 7855 7856 @NotNull 7857 private Bundle getResultForIsSystemGallery(String arg, Bundle extras) { 7858 final LocalCallingIdentity token = clearLocalCallingIdentity(); 7859 try { 7860 String packageName = arg; 7861 int uid = extras.getInt(MediaStore.EXTRA_IS_SYSTEM_GALLERY_UID); 7862 boolean isSystemGallery = PermissionUtils.checkWriteImagesOrVideoAppOps( 7863 getContext(), uid, packageName, getContext().getAttributionTag(), 7864 /*forDataDelivery*/ false); 7865 Bundle res = new Bundle(); 7866 res.putBoolean(MediaStore.EXTRA_IS_SYSTEM_GALLERY_RESPONSE, isSystemGallery); 7867 return res; 7868 } finally { 7869 restoreLocalCallingIdentity(token); 7870 } 7871 } 7872 7873 @Nullable 7874 private Bundle getResultForPickerMediaInit(Bundle extras) { 7875 Log.i(TAG, "Received media init query for extras: " + extras); 7876 if (!checkPermissionShell(Binder.getCallingUid()) 7877 && !checkPermissionSelf(Binder.getCallingUid()) 7878 && !isCallerPhotoPicker()) { 7879 throw new SecurityException( 7880 getSecurityExceptionMessage("Picker media init")); 7881 } 7882 mPickerDataLayer.initMediaData(PickerSyncRequestExtras.fromBundle(extras)); 7883 return null; 7884 } 7885 7886 private void initMediaInMediaSet(@NonNull Bundle extras) { 7887 Objects.requireNonNull(extras); 7888 Log.i(TAG, "Extras received for media in media set init: " + extras); 7889 if (!checkPermissionSelf(Binder.getCallingUid()) && !isCallerPhotoPicker()) { 7890 throw new SecurityException( 7891 getSecurityExceptionMessage("Picker media in media set init")); 7892 } 7893 PickerDataLayerV2.triggerMediaSyncForMediaSet(extras, getContext()); 7894 } 7895 7896 @Nullable 7897 private Bundle getPickerSearchProviders() { 7898 Log.i(TAG, "Received picker internal call to get available search providers."); 7899 if (!checkPermissionShell(Binder.getCallingUid()) 7900 && !checkPermissionSelf(Binder.getCallingUid()) 7901 && !isCallerPhotoPicker()) { 7902 throw new SecurityException( 7903 getSecurityExceptionMessage("Picker get search providers")); 7904 } 7905 return PickerDataLayerV2.getSearchProviders(getContext()); 7906 } 7907 7908 /** 7909 * Checks if the caller has the permission to handle picker search media init. If not, 7910 * this method throws a security exception. 7911 */ 7912 @NonNull 7913 private Bundle getResultForPickerSearchMediaInit(@NonNull Bundle extras) { 7914 Log.i(TAG, "Received search media init query for extras: " + extras); 7915 if (!checkPermissionSelf(Binder.getCallingUid()) 7916 && !isCallerPhotoPicker()) { 7917 throw new SecurityException( 7918 getSecurityExceptionMessage("Picker search media init")); 7919 } 7920 return PickerDataLayerV2.handleSearchResultsInit(getContext(), extras); 7921 } 7922 7923 private void initMediaSets(@NonNull Bundle extras) { 7924 Objects.requireNonNull(extras); 7925 Log.i(TAG, "Extras received for media sets init: " + extras); 7926 if (!checkPermissionSelf(Binder.getCallingUid()) && !isCallerPhotoPicker()) { 7927 throw new SecurityException( 7928 getSecurityExceptionMessage("Picker media sets init")); 7929 } 7930 PickerDataLayerV2.triggerMediaSetsSync(extras, getContext()); 7931 } 7932 7933 @NotNull 7934 private Bundle getResultForPickerTranscode(@NonNull Bundle extras) { 7935 Log.i(TAG, "Received media transcode request for extras: " + extras); 7936 7937 // Check the caller. 7938 if (!checkPermissionShell(Binder.getCallingUid()) 7939 && !checkPermissionSelf(Binder.getCallingUid()) 7940 && !isCallerPhotoPicker()) { 7941 throw new SecurityException(getSecurityExceptionMessage("Picker media transcode")); 7942 } 7943 7944 // Transcode the media. 7945 final Uri uri = Objects.requireNonNull(extras).getParcelable(MediaStore.EXTRA_URI); 7946 if (uri == null) { 7947 throw new IllegalArgumentException("Extras does not contains a URI for transcoding."); 7948 } 7949 boolean transcodeResult = mPhotoPickerTranscodeHelper.transcode(getContext(), uri); 7950 7951 // Return the result. 7952 final Bundle bundle = new Bundle(); 7953 bundle.putBoolean(MediaStore.PICKER_TRANSCODE_RESULT, transcodeResult); 7954 return bundle; 7955 } 7956 7957 @NotNull 7958 private Bundle getResultForGetCloudProvider() { 7959 if (!checkPermissionShell(Binder.getCallingUid()) 7960 && !checkPermissionSelf(Binder.getCallingUid())) { 7961 throw new SecurityException( 7962 getSecurityExceptionMessage("Get cloud provider")); 7963 } 7964 final Bundle bundle = new Bundle(); 7965 bundle.putString(MediaStore.GET_CLOUD_PROVIDER_RESULT, 7966 mPickerSyncController.getCloudProvider()); 7967 return bundle; 7968 } 7969 7970 @NotNull 7971 private Bundle getResultForGetCloudProviderLabel() { 7972 if (!checkPermissionSystem(Binder.getCallingUid())) { 7973 throw new SecurityException(getSecurityExceptionMessage("Get cloud provider label")); 7974 } 7975 final Bundle res = new Bundle(); 7976 String cloudProviderLabel = null; 7977 try { 7978 cloudProviderLabel = mPickerSyncController.getCurrentCloudProviderLocalizedLabel(); 7979 } catch (UnableToAcquireLockException e) { 7980 Log.d(TAG, "Timed out while attempting to acquire the cloud provider lock when getting " 7981 + "the cloud provider label.", e); 7982 } 7983 res.putString(META_DATA_PREFERENCE_SUMMARY, cloudProviderLabel); 7984 return res; 7985 } 7986 7987 @NotNull 7988 private Bundle getResultForSetCloudProvider(Bundle extras) { 7989 final String cloudProvider = extras.getString(MediaStore.EXTRA_CLOUD_PROVIDER); 7990 Log.i(TAG, "Request received to set cloud provider to " + cloudProvider); 7991 boolean isUpdateSuccessful = false; 7992 if (checkPermissionSelf(Binder.getCallingUid())) { 7993 isUpdateSuccessful = mPickerSyncController.setCloudProvider(cloudProvider); 7994 } else if (checkPermissionShell(Binder.getCallingUid())) { 7995 isUpdateSuccessful = 7996 mPickerSyncController.forceSetCloudProvider(cloudProvider); 7997 } else { 7998 throw new SecurityException( 7999 getSecurityExceptionMessage("Set cloud provider")); 8000 } 8001 8002 if (isUpdateSuccessful) { 8003 Log.i(TAG, "Completed request to set cloud provider to " + cloudProvider); 8004 } 8005 final Bundle bundle = new Bundle(); 8006 bundle.putBoolean(MediaStore.SET_CLOUD_PROVIDER_RESULT, isUpdateSuccessful); 8007 return bundle; 8008 } 8009 8010 @NotNull 8011 private Bundle getResultForSyncProviders() { 8012 syncAllMedia(); 8013 return new Bundle(); 8014 } 8015 8016 @NotNull 8017 private Bundle getResultForIsSupportedCloudProvider(String arg) { 8018 final boolean isSupported = mPickerSyncController.isProviderSupported(arg, 8019 Binder.getCallingUid()); 8020 8021 Bundle bundle = new Bundle(); 8022 bundle.putBoolean(MediaStore.EXTRA_CLOUD_PROVIDER_RESULT, isSupported); 8023 return bundle; 8024 } 8025 8026 @NotNull 8027 private Bundle getResultForIsCurrentCloudProviderCall(String arg) { 8028 Bundle bundle = new Bundle(); 8029 boolean isEnabled = false; 8030 8031 if (mConfigStore.isCloudMediaInPhotoPickerEnabled()) { 8032 isEnabled = 8033 mPickerSyncController.isProviderEnabled( 8034 arg, Binder.getCallingUid()); 8035 } 8036 8037 bundle.putBoolean(MediaStore.EXTRA_CLOUD_PROVIDER_RESULT, isEnabled); 8038 return bundle; 8039 } 8040 8041 @NotNull 8042 private Bundle getResultForNotifyCloudMediaChangedEvent(String arg, Bundle extras) { 8043 final boolean notifyCloudEventResult; 8044 if (mPickerSyncController.isProviderEnabled(arg, Binder.getCallingUid())) { 8045 mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ false, arg, extras); 8046 notifyCloudEventResult = true; 8047 } else { 8048 notifyCloudEventResult = false; 8049 } 8050 8051 Bundle bundle = new Bundle(); 8052 bundle.putBoolean(MediaStore.EXTRA_CLOUD_PROVIDER_RESULT, 8053 notifyCloudEventResult); 8054 return bundle; 8055 } 8056 8057 @NotNull 8058 private Bundle getResultForUsesFusePassThrough(String arg) { 8059 boolean isEnabled = false; 8060 try { 8061 FuseDaemon daemon = getFuseDaemonForFile(new File(arg), mVolumeCache); 8062 if (daemon != null) { 8063 isEnabled = daemon.usesFusePassthrough(); 8064 } 8065 } catch (FileNotFoundException e) { 8066 } 8067 8068 Bundle bundle = new Bundle(); 8069 bundle.putBoolean(MediaStore.USES_FUSE_PASSTHROUGH_RESULT, isEnabled); 8070 return bundle; 8071 } 8072 8073 @NotNull 8074 private Bundle getResultForIdleMaintenanceForStableUris() { 8075 backupDatabases(null); 8076 return new Bundle(); 8077 } 8078 8079 @NotNull 8080 private Bundle getResultForReadBackup(String arg, Bundle extras) { 8081 getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE, 8082 "Permission missing to call READ_BACKUP by uid:" + Binder.getCallingUid()); 8083 Bundle bundle = new Bundle(); 8084 Optional<BackupIdRow> backupIdRowOptional = 8085 mDatabaseBackupAndRecovery.readDataFromBackup(arg, extras.getString( 8086 FileColumns.DATA)); 8087 String data = null; 8088 try { 8089 data = backupIdRowOptional.isPresent() ? BackupIdRow.serialize( 8090 backupIdRowOptional.get()) : null; 8091 } catch (IOException e) { 8092 throw new RuntimeException(e); 8093 } 8094 bundle.putString(READ_BACKUP, data); 8095 return bundle; 8096 } 8097 8098 @NotNull 8099 private Bundle getResultForGetOwnerPackageName(String arg) { 8100 getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE, 8101 "Permission missing to call GET_OWNER_PACKAGE_NAME by " 8102 + "uid:" + Binder.getCallingUid()); 8103 try { 8104 String ownerPackageName = mDatabaseBackupAndRecovery.readOwnerPackageName(arg); 8105 Bundle result = new Bundle(); 8106 result.putString(GET_OWNER_PACKAGE_NAME, ownerPackageName); 8107 return result; 8108 } catch (IOException e) { 8109 throw new RuntimeException(e); 8110 } 8111 } 8112 8113 @NotNull 8114 private Bundle getResultForDeleteBackedUpFilePaths(String arg) { 8115 getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE, 8116 "Permission missing to call DELETE_BACKED_UP_FILE_PATHS by " 8117 + "uid:" + Binder.getCallingUid()); 8118 mDatabaseBackupAndRecovery.deleteBackupForVolume(arg); 8119 return new Bundle(); 8120 } 8121 8122 @NotNull 8123 private Bundle getResultForGetBackupFiles() { 8124 getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE, 8125 "Permission missing to call GET_BACKUP_FILES by " 8126 + "uid:" + Binder.getCallingUid()); 8127 List<File> backupFiles = mDatabaseBackupAndRecovery.getBackupFiles(); 8128 List<String> fileNames = new ArrayList<>(); 8129 for (File file : backupFiles) { 8130 fileNames.add(file.getName()); 8131 } 8132 Bundle bundle = new Bundle(); 8133 Object[] values = fileNames.toArray(); 8134 String[] resultArray = Arrays.copyOf(values, values.length, String[].class); 8135 bundle.putStringArray(GET_BACKUP_FILES, resultArray); 8136 return bundle; 8137 } 8138 8139 @NotNull 8140 private Bundle getResultForGetRecoveryData() { 8141 getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE, 8142 "Permission missing to call GET_RECOVERY_DATA by " 8143 + "uid:" + Binder.getCallingUid()); 8144 8145 String[] xattrs = null; 8146 try { 8147 xattrs = Os.listxattr("/data/media/0"); 8148 } catch (ErrnoException e) { 8149 Log.w(TAG, "Error in getting xattr list ", e); 8150 } 8151 8152 Bundle bundle = new Bundle(); 8153 bundle.putStringArray(MediaStore.GET_RECOVERY_DATA, xattrs); 8154 return bundle; 8155 } 8156 8157 private void removeRecoveryData() { 8158 getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE, 8159 "Permission missing to call REMOVE_RECOVERY_DATA by " 8160 + "uid:" + Binder.getCallingUid()); 8161 8162 List<String> validUsers = mUserManager.getUserHandles(/* excludeDying */ true).stream() 8163 .map(userHandle -> String.valueOf(userHandle.getIdentifier())).collect( 8164 Collectors.toList()); 8165 Log.i(TAG, "Active user ids are:" + validUsers); 8166 mDatabaseBackupAndRecovery.removeRecoveryDataExceptValidUsers(validUsers); 8167 } 8168 8169 private String getSecurityExceptionMessage(String method) { 8170 int callingUid = Binder.getCallingUid(); 8171 return String.format("%s not allowed. Calling app ID: %d, Calling UID %d. " 8172 + "Media Provider app ID: %d, Media Provider UID: %d.", 8173 method, 8174 UserHandle.getAppId(callingUid), 8175 callingUid, 8176 UserHandle.getAppId(MY_UID), 8177 MY_UID); 8178 } 8179 8180 public void backupDatabases(CancellationSignal signal) { 8181 mDatabaseBackupAndRecovery.backupDatabases(mInternalDatabase, mExternalDatabase, signal); 8182 } 8183 8184 public void recoverPublicVolume(MediaVolume volume) { 8185 if (volume.isPublicVolume() 8186 && mDatabaseBackupAndRecovery.isStableUrisEnabled(volume.getName())) { 8187 Log.d(TAG, "Querying external_primary to make sure it's available"); 8188 try (Cursor cursor = getContext().getContentResolver() 8189 .query(MediaStore.Images.Media.getContentUri(VOLUME_EXTERNAL_PRIMARY), 8190 new String[]{FileColumns._ID}, null, null)) { 8191 } catch (Exception e) { 8192 Log.e(TAG, "Can't restore public volume because EXTERNAL_PRIMARY is not available"); 8193 return; 8194 } 8195 8196 try { 8197 mExternalDatabase.tryRecoverPublicVolume(volume.getName()); 8198 } catch (Exception e) { 8199 Log.e(TAG, "Exception in public volume recovery for " + volume.getName(), e); 8200 } 8201 } 8202 } 8203 8204 private void syncAllMedia() { 8205 // Clear the binder calling identity so that we can sync the unexported 8206 // local_provider while running as MediaProvider 8207 final long t = Binder.clearCallingIdentity(); 8208 try { 8209 Log.v(TAG, "Test initiated cloud provider sync"); 8210 mPickerSyncController.syncAllMedia(); 8211 } finally { 8212 Binder.restoreCallingIdentity(t); 8213 } 8214 } 8215 8216 private AssetFileDescriptor getOriginalMediaFormatFileDescriptor(Bundle extras) 8217 throws FileNotFoundException { 8218 try (ParcelFileDescriptor inputPfd = 8219 extras.getParcelable(MediaStore.EXTRA_FILE_DESCRIPTOR)) { 8220 File file = getFileFromFileDescriptor(inputPfd); 8221 // Convert from FUSE file to lower fs file because the supportsTranscode() check below 8222 // expects a lower fs file format 8223 file = fromFuseFile(file); 8224 if (!mTranscodeHelper.supportsTranscode(file.getPath())) { 8225 // Note that we should be checking if a file is a modern format and not just 8226 // that it supports transcoding, unfortunately, checking modern format 8227 // requires either a db query or media scan which can lead to ANRs if apps 8228 // or the system implicitly call this method as part of a 8229 // MediaPlayer#setDataSource. 8230 throw new FileNotFoundException("Input file descriptor is already original"); 8231 } 8232 8233 FuseDaemon fuseDaemon = getFuseDaemonForFile(file, mVolumeCache); 8234 int uid = Binder.getCallingUid(); 8235 8236 FdAccessResult result = fuseDaemon.checkFdAccess(inputPfd, uid); 8237 if (!result.isSuccess()) { 8238 throw new FileNotFoundException("Invalid path for original media format file"); 8239 } 8240 8241 String outputPath = result.filePath; 8242 boolean shouldRedact = result.shouldRedact; 8243 8244 int posixMode = Os.fcntlInt(inputPfd.getFileDescriptor(), F_GETFL, 8245 0 /* args */); 8246 int modeBits = FileUtils.translateModePosixToPfd(posixMode); 8247 8248 ParcelFileDescriptor pfd = openWithFuse(outputPath, uid, 0 /* mediaCapabilitiesUid */, 8249 modeBits, shouldRedact, false /* shouldTranscode */, 8250 0 /* transcodeReason */); 8251 return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH); 8252 } catch (IOException e) { 8253 throw new FileNotFoundException("Failed to fetch original file descriptor"); 8254 } catch (ErrnoException e) { 8255 Log.w(TAG, "Failed to fetch access mode for file descriptor", e); 8256 throw new FileNotFoundException("Failed to fetch access mode for file descriptor"); 8257 } 8258 } 8259 8260 /** 8261 * Grant similar read/write access for mediaStoreUri as the caller has for documentsUri. 8262 * 8263 * Note: This function assumes that read permission check for documentsUri is already enforced. 8264 * Note: This function currently does not check/grant for persisted Uris. Support for this can 8265 * be added eventually, but the calling application will have to call 8266 * ContentResolver#takePersistableUriPermission(Uri, int) for the mediaStoreUri to persist. 8267 * 8268 * @param documentsUri DocumentsProvider format content Uri 8269 * @param mediaStoreUri MediaStore format content Uri 8270 * @param callingPid pid of the caller 8271 * @param callingUid uid of the caller 8272 * @param callingPackage package name of the caller 8273 */ 8274 private void copyUriPermissionGrants(Uri documentsUri, Uri mediaStoreUri, 8275 int callingPid, int callingUid, String callingPackage) { 8276 // No need to check for read permission, as we enforce it already. 8277 int modeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION; 8278 if (getContext().checkUriPermission(documentsUri, callingPid, callingUid, 8279 Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == PERMISSION_GRANTED) { 8280 modeFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; 8281 } 8282 getContext().grantUriPermission(callingPackage, mediaStoreUri, modeFlags); 8283 } 8284 8285 static List<Uri> collectUris(ClipData clipData) { 8286 final ArrayList<Uri> res = new ArrayList<>(); 8287 for (int i = 0; i < clipData.getItemCount(); i++) { 8288 res.add(clipData.getItemAt(i).getUri()); 8289 } 8290 return res; 8291 } 8292 8293 /** 8294 * Return the filesystem path of the real file on disk that is represented 8295 * by the given {@link ParcelFileDescriptor}. 8296 * 8297 * Note that the file may be a FUSE or lower fs file and depending on the purpose might need 8298 * to be converted with {@link FileUtils#toFuseFile} or {@link FileUtils#fromFuseFile}. 8299 * 8300 * Copied from {@link ParcelFileDescriptor#getFile} 8301 */ 8302 private static File getFileFromFileDescriptor(ParcelFileDescriptor fileDescriptor) 8303 throws IOException { 8304 try { 8305 final String path = Os.readlink("/proc/self/fd/" + fileDescriptor.getFd()); 8306 if (OsConstants.S_ISREG(Os.stat(path).st_mode)) { 8307 return new File(path); 8308 } else { 8309 throw new IOException("Not a regular file: " + path); 8310 } 8311 } catch (ErrnoException e) { 8312 throw e.rethrowAsIOException(); 8313 } 8314 } 8315 8316 /** 8317 * Generate the {@link PendingIntent} for the given grant request. This 8318 * method also checks the incoming arguments for security purposes 8319 * before creating the privileged {@link PendingIntent}. 8320 */ 8321 @NonNull 8322 private PendingIntent createRequest(@NonNull String method, @NonNull Bundle extras) { 8323 final ClipData clipData = extras.getParcelable(MediaStore.EXTRA_CLIP_DATA); 8324 final List<Uri> uris = collectUris(clipData); 8325 8326 if (getCallingPackageTargetSdkVersion() > Build.VERSION_CODES.VANILLA_ICE_CREAM 8327 && CompatChanges.isChangeEnabled(LIMIT_CREATE_REQUEST_URIS) && uris.size() > 2000) { 8328 throw new IllegalArgumentException("URI list restricted to 2000 per request"); 8329 } 8330 8331 for (Uri uri : uris) { 8332 final int match = matchUri(uri, false); 8333 switch (match) { 8334 case IMAGES_MEDIA_ID: 8335 case AUDIO_MEDIA_ID: 8336 case VIDEO_MEDIA_ID: 8337 case AUDIO_PLAYLISTS_ID: 8338 // Caller is requesting a specific media item by its ID, 8339 // which means it's valid for requests 8340 break; 8341 case FILES_ID: 8342 // Allow only subtitle files 8343 if (!isSubtitleFile(uri)) { 8344 throw new IllegalArgumentException( 8345 "All requested items must be Media items"); 8346 } 8347 break; 8348 default: 8349 throw new IllegalArgumentException( 8350 "All requested items must be referenced by specific ID"); 8351 } 8352 } 8353 8354 // Enforce that limited set of columns can be mutated 8355 final ContentValues values = extras.getParcelable(MediaStore.EXTRA_CONTENT_VALUES); 8356 final List<String> allowedColumns; 8357 switch (method) { 8358 case MediaStore.CREATE_FAVORITE_REQUEST_CALL: 8359 allowedColumns = Collections.singletonList( 8360 MediaColumns.IS_FAVORITE); 8361 break; 8362 case MediaStore.CREATE_TRASH_REQUEST_CALL: 8363 allowedColumns = Collections.singletonList( 8364 MediaColumns.IS_TRASHED); 8365 break; 8366 default: 8367 allowedColumns = Collections.emptyList(); 8368 break; 8369 } 8370 if (values != null) { 8371 for (String key : values.keySet()) { 8372 if (!allowedColumns.contains(key)) { 8373 throw new IllegalArgumentException("Invalid column " + key); 8374 } 8375 } 8376 } 8377 8378 final Context context = getContext(); 8379 final Intent intent = new Intent(method, null, context, PermissionActivity.class); 8380 intent.putExtras(extras); 8381 final ActivityOptions options = ActivityOptions.makeBasic(); 8382 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 8383 options.setPendingIntentCreatorBackgroundActivityStartMode( 8384 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); 8385 } 8386 return PendingIntent.getActivity(context, PermissionActivity.REQUEST_CODE, intent, 8387 FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE, options.toBundle()); 8388 } 8389 8390 /** 8391 * @return true if the given Files uri has media_type=MEDIA_TYPE_SUBTITLE 8392 */ 8393 private boolean isSubtitleFile(Uri uri) { 8394 final LocalCallingIdentity tokenInner = clearLocalCallingIdentity(); 8395 try (Cursor cursor = queryForSingleItem(uri, new String[]{FileColumns.MEDIA_TYPE}, null, 8396 null, null)) { 8397 return cursor.getInt(0) == FileColumns.MEDIA_TYPE_SUBTITLE; 8398 } catch (FileNotFoundException e) { 8399 Log.e(TAG, "Couldn't find database row for requested uri " + uri, e); 8400 } finally { 8401 restoreLocalCallingIdentity(tokenInner); 8402 } 8403 return false; 8404 } 8405 8406 /** 8407 * Ensure that all local databases have a custom collator registered for the 8408 * given {@link ULocale} locale. 8409 * 8410 * @return the corresponding custom collation name to be used in 8411 * {@code ORDER BY} clauses. 8412 */ 8413 @NonNull 8414 private String ensureCustomCollator(@NonNull String locale) { 8415 // Quick check that requested locale looks reasonable 8416 new ULocale(locale); 8417 8418 final String collationName = "custom_" + locale.replaceAll("[^a-zA-Z]", ""); 8419 synchronized (mCustomCollators) { 8420 if (!mCustomCollators.contains(collationName)) { 8421 for (DatabaseHelper helper : new DatabaseHelper[] { 8422 mInternalDatabase, 8423 mExternalDatabase 8424 }) { 8425 helper.runWithoutTransaction((db) -> { 8426 db.execPerConnectionSQL("SELECT icu_load_collation(?, ?);", 8427 new String[] { locale, collationName }); 8428 return null; 8429 }); 8430 } 8431 mCustomCollators.add(collationName); 8432 } 8433 } 8434 return collationName; 8435 } 8436 8437 private int pruneThumbnails(@NonNull SQLiteDatabase db, @NonNull CancellationSignal signal) { 8438 int prunedCount = 0; 8439 8440 // Determine all known media items 8441 final LongArray knownIds = new LongArray(); 8442 try (Cursor c = db.query(true, "files", new String[] { BaseColumns._ID }, 8443 null, null, null, null, null, null, signal)) { 8444 while (c.moveToNext()) { 8445 knownIds.add(c.getLong(0)); 8446 } 8447 } 8448 8449 final long[] knownIdsRaw = knownIds.toArray(); 8450 Arrays.sort(knownIdsRaw); 8451 8452 for (MediaVolume volume : mVolumeCache.getExternalVolumes()) { 8453 final List<File> thumbDirs; 8454 try { 8455 thumbDirs = getThumbnailDirectories(volume); 8456 } catch (FileNotFoundException e) { 8457 Log.w(TAG, "Failed to resolve volume " + volume.getName(), e); 8458 continue; 8459 } 8460 8461 // Reconcile all thumbnails, deleting stale items 8462 for (File thumbDir : thumbDirs) { 8463 // Possibly bail before digging into each directory 8464 signal.throwIfCanceled(); 8465 8466 final File[] files = thumbDir.listFiles(); 8467 for (File thumbFile : (files != null) ? files : new File[0]) { 8468 if (Objects.equals(thumbFile.getName(), FILE_DATABASE_UUID)) continue; 8469 if (Objects.equals(thumbFile.getName(), MEDIA_IGNORE_FILENAME)) continue; 8470 final String name = FileUtils.extractFileName(thumbFile.getName()); 8471 try { 8472 final long id = Long.parseLong(name); 8473 if (Arrays.binarySearch(knownIdsRaw, id) >= 0) { 8474 // Thumbnail belongs to known media, keep it 8475 continue; 8476 } 8477 } catch (NumberFormatException e) { 8478 } 8479 8480 Log.v(TAG, "Deleting stale thumbnail " + thumbFile); 8481 deleteAndInvalidate(thumbFile); 8482 prunedCount++; 8483 } 8484 } 8485 } 8486 8487 // Also delete stale items from legacy tables 8488 db.execSQL("delete from thumbnails " 8489 + "where image_id not in (select _id from images)"); 8490 db.execSQL("delete from videothumbnails " 8491 + "where video_id not in (select _id from video)"); 8492 8493 return prunedCount; 8494 } 8495 8496 abstract class Thumbnailer { 8497 final String directoryName; 8498 8499 public Thumbnailer(String directoryName) { 8500 this.directoryName = directoryName; 8501 } 8502 8503 private File getThumbnailFile(Uri uri) throws IOException { 8504 // Always save generated thumbnails to primary storage 8505 final File volumePath = getVolumePath(MediaStore.VOLUME_EXTERNAL_PRIMARY); 8506 return FileUtils.buildPath(volumePath, directoryName, 8507 DIRECTORY_THUMBNAILS, ContentUris.parseId(uri) + ".jpg"); 8508 } 8509 8510 public abstract Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) 8511 throws IOException; 8512 8513 public ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal) 8514 throws IOException { 8515 // First attempt to fast-path by opening the thumbnail; if it 8516 // doesn't exist we fall through to create it below 8517 final File thumbFile = getThumbnailFile(uri); 8518 try { 8519 return FileUtils.openSafely(thumbFile, 8520 ParcelFileDescriptor.MODE_READ_ONLY); 8521 } catch (FileNotFoundException ignored) { 8522 } 8523 8524 final File thumbDir = thumbFile.getParentFile(); 8525 thumbDir.mkdirs(); 8526 8527 // When multiple threads race for the same thumbnail, the second 8528 // thread could return a file with a thumbnail still in 8529 // progress. We could add heavy per-ID locking to mitigate this 8530 // rare race condition, but it's simpler to have both threads 8531 // generate the same thumbnail using temporary files and rename 8532 // them into place once finished. 8533 final File thumbTempFile = File.createTempFile("thumb", null, thumbDir); 8534 8535 ParcelFileDescriptor thumbWrite = null; 8536 ParcelFileDescriptor thumbRead = null; 8537 try { 8538 // Open our temporary file twice: once for local writing, and 8539 // once for remote reading. Both FDs point at the same 8540 // underlying inode on disk, so they're stable across renames 8541 // to avoid race conditions between threads. 8542 thumbWrite = FileUtils.openSafely(thumbTempFile, 8543 ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_CREATE); 8544 thumbRead = FileUtils.openSafely(thumbTempFile, 8545 ParcelFileDescriptor.MODE_READ_ONLY); 8546 8547 final Bitmap thumbnail = getThumbnailBitmap(uri, signal); 8548 thumbnail.compress(Bitmap.CompressFormat.JPEG, 90, 8549 new FileOutputStream(thumbWrite.getFileDescriptor())); 8550 8551 try { 8552 // Use direct syscall for better failure logs 8553 Os.rename(thumbTempFile.getAbsolutePath(), thumbFile.getAbsolutePath()); 8554 } catch (ErrnoException e) { 8555 e.rethrowAsIOException(); 8556 } 8557 8558 // Everything above went peachy, so return a duplicate of our 8559 // already-opened read FD to keep our finally logic below simple 8560 return thumbRead.dup(); 8561 8562 } finally { 8563 // Regardless of success or failure, try cleaning up any 8564 // remaining temporary file and close all our local FDs 8565 FileUtils.closeQuietly(thumbWrite); 8566 FileUtils.closeQuietly(thumbRead); 8567 deleteAndInvalidate(thumbTempFile); 8568 } 8569 } 8570 8571 public void invalidateThumbnail(Uri uri) throws IOException { 8572 deleteAndInvalidate(getThumbnailFile(uri)); 8573 } 8574 } 8575 8576 private final Thumbnailer mAudioThumbnailer = new Thumbnailer(Environment.DIRECTORY_MUSIC) { 8577 @Override 8578 public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException { 8579 return ThumbnailUtils.createAudioThumbnail(queryForDataFile(uri, signal), 8580 mThumbSize, signal); 8581 } 8582 }; 8583 8584 private final Thumbnailer mVideoThumbnailer = new Thumbnailer(Environment.DIRECTORY_MOVIES) { 8585 @Override 8586 public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException { 8587 return ThumbnailUtils.createVideoThumbnail(queryForDataFile(uri, signal), 8588 mThumbSize, signal); 8589 } 8590 }; 8591 8592 private final Thumbnailer mImageThumbnailer = new Thumbnailer(Environment.DIRECTORY_PICTURES) { 8593 @Override 8594 public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException { 8595 return ThumbnailUtils.createImageThumbnail(queryForDataFile(uri, signal), 8596 mThumbSize, signal); 8597 } 8598 }; 8599 8600 private List<File> getThumbnailDirectories(MediaVolume volume) throws FileNotFoundException { 8601 final File volumePath = volume.getPath(); 8602 return Arrays.asList( 8603 FileUtils.buildPath(volumePath, Environment.DIRECTORY_MUSIC, DIRECTORY_THUMBNAILS), 8604 FileUtils.buildPath(volumePath, Environment.DIRECTORY_MOVIES, DIRECTORY_THUMBNAILS), 8605 FileUtils.buildPath(volumePath, Environment.DIRECTORY_PICTURES, 8606 DIRECTORY_THUMBNAILS)); 8607 } 8608 8609 private void invalidateThumbnails(Uri uri) { 8610 Trace.beginSection("MP.invalidateThumbnails"); 8611 try { 8612 invalidateThumbnailsInternal(uri); 8613 } finally { 8614 Trace.endSection(); 8615 } 8616 } 8617 8618 private void invalidateThumbnailsInternal(Uri uri) { 8619 final long id = ContentUris.parseId(uri); 8620 try { 8621 mAudioThumbnailer.invalidateThumbnail(uri); 8622 mVideoThumbnailer.invalidateThumbnail(uri); 8623 mImageThumbnailer.invalidateThumbnail(uri); 8624 } catch (IOException ignored) { 8625 } 8626 8627 final DatabaseHelper helper; 8628 try { 8629 helper = getDatabaseForUri(uri); 8630 } catch (VolumeNotFoundException e) { 8631 Log.w(TAG, e); 8632 return; 8633 } 8634 8635 helper.runWithTransaction((db) -> { 8636 final String idString = Long.toString(id); 8637 try (Cursor c = db.rawQuery("select _data from thumbnails where image_id=?" 8638 + " union all select _data from videothumbnails where video_id=?", 8639 new String[] { idString, idString })) { 8640 while (c.moveToNext()) { 8641 String path = c.getString(0); 8642 deleteIfAllowed(uri, Bundle.EMPTY, path); 8643 } 8644 } 8645 8646 db.execSQL("delete from thumbnails where image_id=?", new String[] { idString }); 8647 db.execSQL("delete from videothumbnails where video_id=?", new String[] { idString }); 8648 return null; 8649 }); 8650 } 8651 8652 /** 8653 * @deprecated all operations should be routed through the overload that 8654 * accepts a {@link Bundle} of extras. 8655 */ 8656 @Override 8657 @Deprecated 8658 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 8659 return update(uri, values, 8660 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null)); 8661 } 8662 8663 @Override 8664 public int update(@NonNull Uri uri, @Nullable ContentValues values, 8665 @Nullable Bundle extras) { 8666 Trace.beginSection(safeTraceSectionNameWithUri("update", uri)); 8667 try { 8668 return updateInternal(uri, values, extras); 8669 } catch (FallbackException e) { 8670 return e.translateForUpdateDelete(getCallingPackageTargetSdkVersion()); 8671 } finally { 8672 Trace.endSection(); 8673 } 8674 } 8675 8676 private int updateInternal(@NonNull Uri uri, @Nullable ContentValues initialValues, 8677 @Nullable Bundle extras) throws FallbackException { 8678 final String volumeName = getVolumeName(uri); 8679 PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName); 8680 8681 extras = (extras != null) ? extras : new Bundle(); 8682 // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider. 8683 extras.remove(QUERY_ARG_REDACTED_URI); 8684 8685 if (isRedactedUri(uri)) { 8686 // we don't support update on redacted uris. 8687 return 0; 8688 } 8689 8690 // Related items are only considered for new media creation, and they 8691 // can't be leveraged to move existing content into blocked locations 8692 extras.remove(QUERY_ARG_RELATED_URI); 8693 // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. 8694 extras.remove(INCLUDED_DEFAULT_DIRECTORIES); 8695 8696 final String userWhere = extras.getString(QUERY_ARG_SQL_SELECTION); 8697 final String[] userWhereArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS); 8698 8699 // Limit the hacky workaround to camera targeting Q and below, to allow newer versions 8700 // of camera that does the right thing to work correctly. 8701 if ("com.google.android.GoogleCamera".equals(getCallingPackageOrSelf()) 8702 && getCallingPackageTargetSdkVersion() <= Build.VERSION_CODES.Q) { 8703 if (matchUri(uri, false) == IMAGES_MEDIA_ID) { 8704 Log.w(TAG, "Working around app bug in b/111966296"); 8705 uri = MediaStore.Files.getContentUri("external", ContentUris.parseId(uri)); 8706 } else if (matchUri(uri, false) == VIDEO_MEDIA_ID) { 8707 Log.w(TAG, "Working around app bug in b/112246630"); 8708 uri = MediaStore.Files.getContentUri("external", ContentUris.parseId(uri)); 8709 } 8710 } 8711 8712 uri = safeUncanonicalize(uri); 8713 8714 int count; 8715 8716 final boolean allowHidden = isCallingPackageAllowedHidden(); 8717 final int match = matchUri(uri, allowHidden); 8718 final DatabaseHelper helper = getDatabaseForUri(uri); 8719 8720 switch (match) { 8721 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 8722 extras.putString(QUERY_ARG_SQL_SELECTION, 8723 BaseColumns._ID + "=" + uri.getPathSegments().get(5)); 8724 // fall-through 8725 case AUDIO_PLAYLISTS_ID_MEMBERS: { 8726 final long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 8727 final Uri playlistUri = ContentUris.withAppendedId( 8728 MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId); 8729 if (uri.getBooleanQueryParameter("move", false)) { 8730 // Convert explicit request into query; sigh, moveItem() 8731 // uses zero-based indexing instead of one-based indexing 8732 final int from = Integer.parseInt(uri.getPathSegments().get(5)) + 1; 8733 final int to = initialValues.getAsInteger(Playlists.Members.PLAY_ORDER) + 1; 8734 extras.putString(QUERY_ARG_SQL_SELECTION, 8735 Playlists.Members.PLAY_ORDER + "=" + from); 8736 initialValues.put(Playlists.Members.PLAY_ORDER, to); 8737 } 8738 8739 // Playlist contents are always persisted directly into playlist 8740 // files on disk to ensure that we can reliably migrate between 8741 // devices and recover from database corruption 8742 final int index; 8743 if (initialValues.containsKey(Playlists.Members.PLAY_ORDER)) { 8744 index = movePlaylistMembers(playlistUri, initialValues, extras); 8745 } else { 8746 index = resolvePlaylistIndex(playlistUri, extras); 8747 } 8748 if (initialValues.containsKey(Playlists.Members.AUDIO_ID)) { 8749 final Bundle queryArgs = new Bundle(); 8750 queryArgs.putString(QUERY_ARG_SQL_SELECTION, 8751 Playlists.Members.PLAY_ORDER + "=" + (index + 1)); 8752 removePlaylistMembers(playlistUri, queryArgs); 8753 8754 final ContentValues values = new ContentValues(); 8755 values.put(Playlists.Members.AUDIO_ID, 8756 initialValues.getAsString(Playlists.Members.AUDIO_ID)); 8757 values.put(Playlists.Members.PLAY_ORDER, (index + 1)); 8758 addPlaylistMembers(playlistUri, values); 8759 } 8760 8761 acceptWithExpansion(helper::notifyUpdate, volumeName, playlistId, 8762 FileColumns.MEDIA_TYPE_PLAYLIST, false); 8763 return 1; 8764 } 8765 } 8766 8767 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, match, uri, extras, null); 8768 8769 // Give callers interacting with a specific media item a chance to 8770 // escalate access if they don't already have it 8771 switch (match) { 8772 case AUDIO_MEDIA_ID: 8773 case VIDEO_MEDIA_ID: 8774 case IMAGES_MEDIA_ID: 8775 enforceCallingPermission(uri, extras, true); 8776 } 8777 8778 boolean triggerInvalidate = false; 8779 boolean triggerScan = false; 8780 boolean isUriPublished = false; 8781 if (initialValues != null) { 8782 // IDs are forever; nobody should be editing them 8783 initialValues.remove(MediaColumns._ID); 8784 8785 // Expiration times are hard-coded; let's derive them 8786 FileUtils.computeDateExpires(initialValues); 8787 8788 // Ignore or augment incoming raw filesystem paths 8789 for (String column : sDataColumns.keySet()) { 8790 if (!initialValues.containsKey(column)) continue; 8791 8792 if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) { 8793 // Mutation allowed 8794 } else { 8795 Log.w(TAG, "Ignoring mutation of " + column + " from " 8796 + getCallingPackageOrSelf()); 8797 initialValues.remove(column); 8798 } 8799 } 8800 8801 // Enforce allowed ownership transfers 8802 if (initialValues.containsKey(MediaColumns.OWNER_PACKAGE_NAME)) { 8803 if (isCallingPackageSelf() || isCallingPackageShell()) { 8804 // When the caller is the media scanner or the shell, we let 8805 // them change ownership however they see fit; nothing to do 8806 } else if (isCallingPackageDelegator()) { 8807 // When the caller is a delegator, allow them to shift 8808 // ownership only when current owner, or when ownerless 8809 final String currentOwner; 8810 final String proposedOwner = initialValues 8811 .getAsString(MediaColumns.OWNER_PACKAGE_NAME); 8812 final Uri genericUri = MediaStore.Files.getContentUri(volumeName, 8813 ContentUris.parseId(uri)); 8814 try (Cursor c = queryForSingleItem(genericUri, 8815 new String[] { MediaColumns.OWNER_PACKAGE_NAME }, null, null, null)) { 8816 currentOwner = c.getString(0); 8817 } catch (FileNotFoundException e) { 8818 throw new IllegalStateException(e); 8819 } 8820 final boolean transferAllowed = (currentOwner == null) 8821 || Arrays.asList(getSharedPackagesForPackage(getCallingPackageOrSelf())) 8822 .contains(currentOwner); 8823 if (transferAllowed) { 8824 Log.v(TAG, "Ownership transfer from " + currentOwner + " to " 8825 + proposedOwner + " allowed"); 8826 } else { 8827 Log.w(TAG, "Ownership transfer from " + currentOwner + " to " 8828 + proposedOwner + " blocked"); 8829 initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME); 8830 } 8831 } else { 8832 // Otherwise no ownership changes are allowed 8833 initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME); 8834 } 8835 } 8836 8837 if (initialValues.containsKey(FileColumns.GENERATION_MODIFIED) 8838 && !isCallingPackageSelf()) { 8839 // We only allow MediaScanner to send updates for generation modified 8840 initialValues.remove(FileColumns.GENERATION_MODIFIED); 8841 } 8842 8843 // Enforce oem_metadata permission if caller is not MediaProvider 8844 if (Flags.enableOemMetadataUpdate() && initialValues.containsKey(OEM_METADATA)) { 8845 enforcePermissionCheckForOemMetadataUpdate(); 8846 } 8847 8848 if (!isCallingPackageSelf()) { 8849 Trace.beginSection("MP.filter"); 8850 8851 // We default to filtering mutable columns, except when we know 8852 // the single item being updated is pending; when it's finally 8853 // published we'll overwrite these values. 8854 final Uri finalUri = uri; 8855 final Supplier<Boolean> isPending = new CachedSupplier<>(() -> { 8856 return isPending(finalUri); 8857 }); 8858 8859 // Column values controlled by media scanner aren't writable by 8860 // apps, since any edits here don't reflect the metadata on 8861 // disk, and they'd be overwritten during a rescan. 8862 for (String column : new ArraySet<>(initialValues.keySet())) { 8863 if (sMutableColumns.contains(column)) { 8864 // Mutation normally allowed 8865 } else if (isPending.get()) { 8866 // Mutation relaxed while pending 8867 } else { 8868 Log.w(TAG, "Ignoring mutation of " + column + " from " 8869 + getCallingPackageOrSelf()); 8870 initialValues.remove(column); 8871 triggerScan = true; 8872 } 8873 8874 // If we're publishing this item, perform a blocking scan to 8875 // make sure metadata is updated 8876 if (MediaColumns.IS_PENDING.equals(column)) { 8877 triggerScan = true; 8878 isUriPublished = true; 8879 // Explicitly clear columns used to ignore no-op scans, 8880 // since we need to force a scan on publish 8881 initialValues.putNull(MediaColumns.DATE_MODIFIED); 8882 initialValues.putNull(MediaColumns.SIZE); 8883 } 8884 } 8885 8886 Trace.endSection(); 8887 } 8888 8889 if ("files".equals(qb.getTables())) { 8890 maybeMarkAsDownload(initialValues); 8891 } 8892 8893 // We no longer track location metadata 8894 if (!isCallingPackageSelf()) { 8895 if (initialValues.containsKey(LATITUDE)) { 8896 initialValues.putNull(LATITUDE); 8897 } 8898 if (initialValues.containsKey(LONGITUDE)) { 8899 initialValues.putNull(LONGITUDE); 8900 } 8901 } 8902 8903 if (getCallingPackageTargetSdkVersion() <= Build.VERSION_CODES.Q) { 8904 // These columns are removed in R. 8905 if (initialValues.containsKey("primary_directory")) { 8906 initialValues.remove("primary_directory"); 8907 } 8908 if (initialValues.containsKey("secondary_directory")) { 8909 initialValues.remove("secondary_directory"); 8910 } 8911 } 8912 } 8913 8914 // If we're not updating anything, then we can skip 8915 if (initialValues.isEmpty()) return 0; 8916 8917 final boolean isThumbnail; 8918 switch (match) { 8919 case IMAGES_THUMBNAILS: 8920 case IMAGES_THUMBNAILS_ID: 8921 case VIDEO_THUMBNAILS: 8922 case VIDEO_THUMBNAILS_ID: 8923 case AUDIO_ALBUMART: 8924 case AUDIO_ALBUMART_ID: 8925 isThumbnail = true; 8926 break; 8927 default: 8928 isThumbnail = false; 8929 break; 8930 } 8931 8932 switch (match) { 8933 case AUDIO_PLAYLISTS: 8934 case AUDIO_PLAYLISTS_ID: 8935 // Playlist names are stored as display names, but leave 8936 // values untouched if the caller is ModernMediaScanner 8937 if (!isCallingPackageSelf()) { 8938 if (initialValues.containsKey(Playlists.NAME)) { 8939 initialValues.put(MediaColumns.DISPLAY_NAME, 8940 initialValues.getAsString(Playlists.NAME)); 8941 } 8942 if (!initialValues.containsKey(MediaColumns.MIME_TYPE)) { 8943 initialValues.put(MediaColumns.MIME_TYPE, "audio/mpegurl"); 8944 } 8945 } 8946 break; 8947 } 8948 8949 // If we're touching columns that would change placement of a file, 8950 // blend in current values and recalculate path 8951 final boolean allowMovement = extras.getBoolean(MediaStore.QUERY_ARG_ALLOW_MOVEMENT, 8952 !isCallingPackageSelf()); 8953 if (containsAny(initialValues.keySet(), sPlacementColumns) 8954 && !initialValues.containsKey(MediaColumns.DATA) 8955 && !isThumbnail 8956 && allowMovement) { 8957 Trace.beginSection("MP.movement"); 8958 8959 // We only support movement under well-defined collections 8960 switch (match) { 8961 case AUDIO_MEDIA_ID: 8962 case AUDIO_PLAYLISTS_ID: 8963 case VIDEO_MEDIA_ID: 8964 case IMAGES_MEDIA_ID: 8965 case DOWNLOADS_ID: 8966 case FILES_ID: 8967 // Check if the caller has the required permissions to do placement 8968 enforceCallingPermission(uri, extras, true); 8969 break; 8970 default: 8971 throw new IllegalArgumentException("Movement of " + uri 8972 + " which isn't part of well-defined collection not allowed"); 8973 } 8974 8975 final LocalCallingIdentity token = clearLocalCallingIdentity(); 8976 final Uri genericUri = MediaStore.Files.getContentUri(volumeName, 8977 ContentUris.parseId(uri)); 8978 try (Cursor c = queryForSingleItem(genericUri, 8979 sPlacementColumns.toArray(new String[0]), userWhere, userWhereArgs, null)) { 8980 for (int i = 0; i < c.getColumnCount(); i++) { 8981 final String column = c.getColumnName(i); 8982 if (!initialValues.containsKey(column)) { 8983 initialValues.put(column, c.getString(i)); 8984 } 8985 } 8986 } catch (FileNotFoundException e) { 8987 throw new IllegalStateException(e); 8988 } finally { 8989 restoreLocalCallingIdentity(token); 8990 } 8991 8992 // Regenerate path using blended values; this will throw if caller 8993 // is attempting to place file into invalid location 8994 final String beforePath = initialValues.getAsString(MediaColumns.DATA); 8995 final String beforeVolume = extractVolumeName(beforePath); 8996 final String beforeOwner = extractPathOwnerPackageName(beforePath); 8997 8998 if (beforeVolume != null && MediaStore.VOLUME_EXTERNAL.equals(volumeName)) { 8999 // Replace "external" with the volumeName 9000 uri = replaceExternalUriWithVolumeName(uri, beforeVolume); 9001 } 9002 9003 initialValues.remove(MediaColumns.DATA); 9004 ensureNonUniqueFileColumns(match, uri, extras, initialValues, beforePath); 9005 9006 final String probePath = initialValues.getAsString(MediaColumns.DATA); 9007 final String probeVolume = extractVolumeName(probePath); 9008 final String probeOwner = extractPathOwnerPackageName(probePath); 9009 if (StringUtils.equalIgnoreCase(beforePath, probePath)) { 9010 Log.d(TAG, "Identical paths " + beforePath + "; not moving"); 9011 } else if (!Objects.equals(beforeVolume, probeVolume)) { 9012 throw new IllegalArgumentException("Changing volume from " + beforePath + " to " 9013 + probePath + " not allowed"); 9014 } else if (!isUpdateAllowedForOwnedPath(beforeOwner, probeOwner, beforePath, 9015 probePath)) { 9016 throw new IllegalArgumentException("Changing ownership from " + beforePath + " to " 9017 + probePath + " not allowed"); 9018 } else { 9019 // Now that we've confirmed an actual movement is taking place, 9020 // ensure we have a unique destination 9021 initialValues.remove(MediaColumns.DATA); 9022 ensureUniqueFileColumns(match, uri, extras, initialValues, beforePath); 9023 9024 String afterPath = initialValues.getAsString(MediaColumns.DATA); 9025 9026 if (isCrossUserEnabled()) { 9027 String afterVolume = extractVolumeName(afterPath); 9028 String afterVolumePath = extractVolumePath(afterPath); 9029 String beforeVolumePath = extractVolumePath(beforePath); 9030 9031 if (MediaStore.VOLUME_EXTERNAL_PRIMARY.equals(beforeVolume) 9032 && beforeVolume.equals(afterVolume) 9033 && !beforeVolumePath.equals(afterVolumePath)) { 9034 // On cross-user enabled devices, it can happen that a rename intended as 9035 // /storage/emulated/999/foo -> /storage/emulated/999/foo can end up as 9036 // /storage/emulated/999/foo -> /storage/emulated/0/foo. We now fix-up 9037 afterPath = afterPath.replaceFirst(afterVolumePath, beforeVolumePath); 9038 } 9039 } 9040 9041 Log.d(TAG, "Moving " + beforePath + " to " + afterPath); 9042 try { 9043 Os.rename(beforePath, afterPath); 9044 invalidateFuseDentry(beforePath); 9045 invalidateFuseDentry(afterPath); 9046 } catch (ErrnoException e) { 9047 if (e.errno == OsConstants.ENOENT) { 9048 Log.d(TAG, "Missing file at " + beforePath + "; continuing anyway"); 9049 } else { 9050 throw new IllegalStateException(e); 9051 } 9052 } 9053 initialValues.put(MediaColumns.DATA, afterPath); 9054 9055 // Some indexed metadata may have been derived from the path on 9056 // disk, so scan this item again to update it 9057 triggerScan = true; 9058 } 9059 9060 Trace.endSection(); 9061 } 9062 9063 assertPrivatePathNotInValues(initialValues); 9064 9065 // Make sure any updated paths look consistent 9066 assertFileColumnsConsistent(match, uri, initialValues); 9067 9068 if (initialValues.containsKey(FileColumns.DATA)) { 9069 // If we're changing paths, invalidate any thumbnails 9070 triggerInvalidate = true; 9071 9072 // If the new file exists, trigger a scan to adjust any metadata 9073 // that might be derived from the path 9074 final String data = initialValues.getAsString(FileColumns.DATA); 9075 if (!TextUtils.isEmpty(data) && new File(data).exists()) { 9076 triggerScan = true; 9077 } 9078 } 9079 9080 // If we're already doing this update from an internal scan, no need to 9081 // kick off another no-op scan 9082 if (isCallingPackageSelf()) { 9083 triggerScan = false; 9084 } 9085 9086 // Since the update mutation may prevent us from matching items after 9087 // it's applied, we need to snapshot affected IDs here 9088 final LongArray updatedIds = new LongArray(); 9089 if (triggerInvalidate || triggerScan) { 9090 Trace.beginSection("MP.snapshot"); 9091 final LocalCallingIdentity token = clearLocalCallingIdentity(); 9092 try (Cursor c = qb.query(helper, new String[] { FileColumns._ID }, 9093 userWhere, userWhereArgs, null, null, null, null, null)) { 9094 while (c.moveToNext()) { 9095 updatedIds.add(c.getLong(0)); 9096 } 9097 } finally { 9098 restoreLocalCallingIdentity(token); 9099 Trace.endSection(); 9100 } 9101 } 9102 9103 final ContentValues values = new ContentValues(initialValues); 9104 switch (match) { 9105 case AUDIO_MEDIA_ID: 9106 case AUDIO_PLAYLISTS_ID: 9107 case VIDEO_MEDIA_ID: 9108 case IMAGES_MEDIA_ID: 9109 case FILES_ID: 9110 case DOWNLOADS_ID: { 9111 FileUtils.computeValuesFromData(values, isFuseThread()); 9112 break; 9113 } 9114 } 9115 9116 if (initialValues.containsKey(FileColumns.MEDIA_TYPE)) { 9117 final int mediaType = initialValues.getAsInteger(FileColumns.MEDIA_TYPE); 9118 switch (mediaType) { 9119 case FileColumns.MEDIA_TYPE_AUDIO: { 9120 computeAudioLocalizedValues(values); 9121 computeAudioKeyValues(values); 9122 break; 9123 } 9124 } 9125 } 9126 9127 boolean deferScan = false; 9128 if (triggerScan) { 9129 if (SdkLevel.isAtLeastS() && 9130 CompatChanges.isChangeEnabled(ENABLE_DEFERRED_SCAN, Binder.getCallingUid())) { 9131 if (extras.containsKey(QUERY_ARG_DO_ASYNC_SCAN)) { 9132 throw new IllegalArgumentException("Unsupported argument " + 9133 QUERY_ARG_DO_ASYNC_SCAN + " used in extras"); 9134 } 9135 deferScan = extras.getBoolean(QUERY_ARG_DEFER_SCAN, false); 9136 if (deferScan && initialValues.containsKey(MediaColumns.IS_PENDING) && 9137 (initialValues.getAsInteger(MediaColumns.IS_PENDING) == 1)) { 9138 // if the scan runs in async, ensure that the database row is excluded in 9139 // default query until the metadata is updated by deferred scan. 9140 // Apps will still be able to see this database row when queried with 9141 // QUERY_ARG_MATCH_PENDING=MATCH_INCLUDE 9142 values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_CR_PENDING_METADATA); 9143 qb.allowColumn(FileColumns._MODIFIER); 9144 } 9145 } else { 9146 // Allow apps to use QUERY_ARG_DO_ASYNC_SCAN if the device is R or app is targeting 9147 // targetSDK<=R. 9148 deferScan = extras.getBoolean(QUERY_ARG_DO_ASYNC_SCAN, false); 9149 } 9150 } 9151 9152 count = updateAllowingReplace(qb, helper, values, userWhere, userWhereArgs); 9153 9154 // If the caller tried (and failed) to update metadata, the file on disk 9155 // might have changed, to scan it to collect the latest metadata. 9156 if (triggerInvalidate || triggerScan) { 9157 Trace.beginSection("MP.invalidate"); 9158 final LocalCallingIdentity token = clearLocalCallingIdentity(); 9159 try { 9160 for (int i = 0; i < updatedIds.size(); i++) { 9161 final long updatedId = updatedIds.get(i); 9162 final Uri updatedUri = Files.getContentUri(volumeName, updatedId); 9163 helper.postBackground(() -> { 9164 invalidateThumbnails(updatedUri); 9165 }); 9166 9167 if (triggerScan) { 9168 try (Cursor c = queryForSingleItem(updatedUri, 9169 new String[] { FileColumns.DATA }, null, null, null)) { 9170 final File file = new File(c.getString(0)); 9171 final boolean notifyTranscodeHelper = isUriPublished; 9172 if (deferScan) { 9173 helper.postBackground(() -> { 9174 scanFileAsMediaProvider(file); 9175 if (notifyTranscodeHelper) { 9176 notifyTranscodeHelperOnUriPublished(updatedUri, file); 9177 } 9178 }); 9179 } else { 9180 helper.postBlocking(() -> { 9181 scanFileAsMediaProvider(file); 9182 if (notifyTranscodeHelper) { 9183 notifyTranscodeHelperOnUriPublished(updatedUri, file); 9184 } 9185 }); 9186 } 9187 } catch (Exception e) { 9188 Log.w(TAG, "Failed to update metadata for " + updatedUri, e); 9189 } 9190 } 9191 } 9192 } finally { 9193 restoreLocalCallingIdentity(token); 9194 Trace.endSection(); 9195 } 9196 } 9197 9198 return count; 9199 } 9200 9201 private boolean isUpdateAllowedForOwnedPath(@Nullable String srcOwner, 9202 @Nullable String destOwner, @NonNull String srcPath, @NonNull String destPath) { 9203 // 1. Allow if the update is within owned path 9204 // update() from /sdcard/Android/media/com.foo/ABC/image.jpeg to 9205 // /sdcard/Android/media/com.foo/XYZ/image.jpeg - Allowed 9206 if(Objects.equals(srcOwner, destOwner)) { 9207 return true; 9208 } 9209 9210 // 2. Check if the calling package is a special app which has global access 9211 if (isCallingPackageManager() || (canSystemGalleryAccessTheFile(srcPath) && 9212 (canSystemGalleryAccessTheFile(destPath)))) { 9213 return true; 9214 } 9215 9216 // 3. Allow update from srcPath if the source is not a owned path or calling package is the 9217 // owner of the source path or calling package shares the UID with the owner of the source 9218 // path 9219 // update() from /sdcard/DCIM/Foo.jpeg - Allowed 9220 // update() from /sdcard/Android/media/com.foo/image.jpeg - Allowed for 9221 // callingPackage=com.foo, not allowed for callingPackage=com.bar 9222 final boolean isSrcUpdateAllowed = srcOwner == null 9223 || isCallingIdentitySharedPackageName(srcOwner); 9224 9225 // 4. Allow update to dstPath if the destination is not a owned path or calling package is 9226 // the owner of the destination path or calling package shares the UID with the owner of the 9227 // destination path 9228 // update() to /sdcard/Pictures/image.jpeg - Allowed 9229 // update() to /sdcard/Android/media/com.foo/image.jpeg - Allowed for 9230 // callingPackage=com.foo, not allowed for callingPackage=com.bar 9231 final boolean isDestUpdateAllowed = destOwner == null 9232 || isCallingIdentitySharedPackageName(destOwner); 9233 9234 return isSrcUpdateAllowed && isDestUpdateAllowed; 9235 } 9236 9237 private void notifyTranscodeHelperOnUriPublished(Uri uri, File file) { 9238 if (!mTranscodeHelper.supportsTranscode(file.getPath())) { 9239 return; 9240 } 9241 9242 BackgroundThread.getExecutor().execute(() -> { 9243 final LocalCallingIdentity token = clearLocalCallingIdentity(); 9244 try { 9245 mTranscodeHelper.onUriPublished(uri); 9246 } finally { 9247 restoreLocalCallingIdentity(token); 9248 } 9249 }); 9250 } 9251 9252 private void notifyTranscodeHelperOnFileOpen(String path, String ioPath, int uid, 9253 int transformsReason) { 9254 if (!mTranscodeHelper.supportsTranscode(path)) { 9255 return; 9256 } 9257 9258 BackgroundThread.getExecutor().execute(() -> { 9259 final LocalCallingIdentity token = clearLocalCallingIdentity(); 9260 try { 9261 mTranscodeHelper.onFileOpen(path, ioPath, uid, transformsReason); 9262 } finally { 9263 restoreLocalCallingIdentity(token); 9264 } 9265 }); 9266 } 9267 9268 /** 9269 * Update row(s) that match {@code userWhere} in MediaProvider database with {@code values}. 9270 * Treats update as replace for updates with conflicts. 9271 */ 9272 private int updateAllowingReplace(@NonNull SQLiteQueryBuilder qb, 9273 @NonNull DatabaseHelper helper, @NonNull ContentValues values, String userWhere, 9274 String[] userWhereArgs) throws SQLiteConstraintException { 9275 return helper.runWithTransaction((db) -> { 9276 try { 9277 return qb.update(helper, values, userWhere, userWhereArgs); 9278 } catch (SQLiteConstraintException e) { 9279 // b/155320967 Apps sometimes create a file via file path and then update another 9280 // explicitly inserted db row to this file. We have to resolve this update with a 9281 // replace. 9282 9283 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) { 9284 // We don't support replace for non-legacy apps. Non legacy apps should have 9285 // clearer interactions with MediaProvider. 9286 throw e; 9287 } 9288 9289 final String path = values.getAsString(FileColumns.DATA); 9290 9291 // We will only handle UNIQUE constraint error for FileColumns.DATA. We will not try 9292 // update and replace if no file exists for conflicting db row. 9293 if (path == null || !new File(path).exists()) { 9294 throw e; 9295 } 9296 9297 final Uri uri = FileUtils.getContentUriForPath(path); 9298 final boolean allowHidden = isCallingPackageAllowedHidden(); 9299 // The db row which caused UNIQUE constraint error may not match all column values 9300 // of the given queryBuilder, hence using a generic queryBuilder with Files uri. 9301 Bundle extras = new Bundle(); 9302 extras.putInt(QUERY_ARG_MATCH_PENDING, MATCH_INCLUDE); 9303 extras.putInt(QUERY_ARG_MATCH_TRASHED, MATCH_INCLUDE); 9304 final SQLiteQueryBuilder qbForReplace = getQueryBuilder(TYPE_DELETE, 9305 matchUri(uri, allowHidden), uri, extras, null); 9306 final long rowId = getIdIfPathOwnedByPackages(qbForReplace, helper, path, 9307 mCallingIdentity.get().getSharedPackagesAsString()); 9308 9309 if (rowId != -1 && qbForReplace.delete(helper, "_id=?", 9310 new String[] {Long.toString(rowId)}) == 1) { 9311 Log.i(TAG, "Retrying database update after deleting conflicting entry"); 9312 return qb.update(helper, values, userWhere, userWhereArgs); 9313 } 9314 // Rethrow SQLiteConstraintException if app doesn't own the conflicting db row. 9315 throw e; 9316 } 9317 }); 9318 } 9319 9320 /** 9321 * Update the internal table of {@link MediaStore.Audio.Playlists.Members} 9322 * by parsing the playlist file on disk and resolving it against scanned 9323 * audio items. 9324 * <p> 9325 * When a playlist references a missing audio item, the associated 9326 * {@link Playlists.Members#PLAY_ORDER} is skipped, leaving a gap to ensure 9327 * that the playlist entry is retained to avoid user data loss. 9328 */ 9329 private void resolvePlaylistMembers(@NonNull Uri playlistUri) { 9330 Trace.beginSection("MP.resolvePlaylistMembers"); 9331 try { 9332 final DatabaseHelper helper; 9333 try { 9334 helper = getDatabaseForUri(playlistUri); 9335 } catch (VolumeNotFoundException e) { 9336 throw e.rethrowAsIllegalArgumentException(); 9337 } 9338 9339 helper.runWithTransaction((db) -> { 9340 resolvePlaylistMembersInternal(playlistUri, db); 9341 return null; 9342 }); 9343 } finally { 9344 Trace.endSection(); 9345 } 9346 } 9347 9348 private void resolvePlaylistMembersInternal(@NonNull Uri playlistUri, 9349 @NonNull SQLiteDatabase db) { 9350 try { 9351 // Refresh playlist members based on what we parse from disk 9352 final long playlistId = ContentUris.parseId(playlistUri); 9353 final Map<String, Long> membersMap = getAllPlaylistMembers(playlistId); 9354 db.delete("audio_playlists_map", "playlist_id=" + playlistId, null); 9355 9356 final Path playlistPath = queryForDataFile(playlistUri, null).toPath(); 9357 final Playlist playlist = new Playlist(); 9358 playlist.read(playlistPath.toFile()); 9359 9360 final List<Path> members = playlist.asList(); 9361 for (int i = 0; i < members.size(); i++) { 9362 try { 9363 final Path audioPath = playlistPath.getParent().resolve(members.get(i)); 9364 final long audioId = queryForPlaylistMember(audioPath, membersMap); 9365 9366 final ContentValues values = new ContentValues(); 9367 values.put(Playlists.Members.PLAY_ORDER, i + 1); 9368 values.put(Playlists.Members.PLAYLIST_ID, playlistId); 9369 values.put(Playlists.Members.AUDIO_ID, audioId); 9370 db.insert("audio_playlists_map", null, values); 9371 } catch (IOException e) { 9372 Log.w(TAG, "Failed to resolve playlist member", e); 9373 } 9374 } 9375 } catch (IOException e) { 9376 Log.w(TAG, "Failed to refresh playlist", e); 9377 } 9378 } 9379 9380 private Map<String, Long> getAllPlaylistMembers(long playlistId) { 9381 final Map<String, Long> membersMap = new ArrayMap<>(); 9382 9383 final Uri uri = Playlists.Members.getContentUri(MediaStore.VOLUME_EXTERNAL, playlistId); 9384 final String[] projection = new String[] { 9385 Playlists.Members.DATA, 9386 Playlists.Members.AUDIO_ID 9387 }; 9388 try (Cursor c = query(uri, projection, null, null)) { 9389 if (c == null) { 9390 Log.e(TAG, "Cursor is null, failed to create cached playlist member info."); 9391 return membersMap; 9392 } 9393 while (c.moveToNext()) { 9394 membersMap.put(c.getString(0), c.getLong(1)); 9395 } 9396 } 9397 return membersMap; 9398 } 9399 9400 /** 9401 * Make two attempts to query this playlist member: first based on the exact 9402 * path, and if that fails, fall back to picking a single item matching the 9403 * display name. When there are multiple items with the same display name, 9404 * we can't resolve between them, and leave this member unresolved. 9405 */ 9406 private long queryForPlaylistMember(@NonNull Path path, @NonNull Map<String, Long> membersMap) 9407 throws IOException { 9408 final String data = path.toFile().getCanonicalPath(); 9409 if (membersMap.containsKey(data)) { 9410 return membersMap.get(data); 9411 } 9412 final Uri audioUri = Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL); 9413 try (Cursor c = queryForSingleItem(audioUri, 9414 new String[] { BaseColumns._ID }, MediaColumns.DATA + "=?", 9415 new String[] { data }, null)) { 9416 return c.getLong(0); 9417 } catch (FileNotFoundException ignored) { 9418 } 9419 try (Cursor c = queryForSingleItem(audioUri, 9420 new String[] { BaseColumns._ID }, MediaColumns.DISPLAY_NAME + "=?", 9421 new String[] { path.toFile().getName() }, null)) { 9422 return c.getLong(0); 9423 } catch (FileNotFoundException ignored) { 9424 } 9425 throw new FileNotFoundException(); 9426 } 9427 9428 /** 9429 * Add the given audio item to the given playlist. Defaults to adding at the 9430 * end of the playlist when no {@link Playlists.Members#PLAY_ORDER} is 9431 * defined. 9432 */ 9433 private long addPlaylistMembers(@NonNull Uri playlistUri, @NonNull ContentValues values) 9434 throws FallbackException { 9435 final long audioId = values.getAsLong(Audio.Playlists.Members.AUDIO_ID); 9436 final String volumeName = MediaStore.VOLUME_INTERNAL.equals(getVolumeName(playlistUri)) 9437 ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL; 9438 final Uri audioUri = Audio.Media.getContentUri(volumeName, audioId); 9439 9440 Integer playOrder = values.getAsInteger(Playlists.Members.PLAY_ORDER); 9441 playOrder = (playOrder != null) ? (playOrder - 1) : Integer.MAX_VALUE; 9442 9443 try { 9444 final File playlistFile = queryForDataFile(playlistUri, null); 9445 final File audioFile = queryForDataFile(audioUri, null); 9446 9447 final Playlist playlist = new Playlist(); 9448 playlist.read(playlistFile); 9449 playOrder = playlist.add(playOrder, 9450 playlistFile.toPath().getParent().relativize(audioFile.toPath())); 9451 playlist.write(playlistFile); 9452 invalidateFuseDentry(playlistFile); 9453 9454 resolvePlaylistMembers(playlistUri); 9455 9456 // Callers are interested in the actual ID we generated 9457 final Uri membersUri = Playlists.Members.getContentUri(volumeName, 9458 ContentUris.parseId(playlistUri)); 9459 try (Cursor c = query(membersUri, new String[] { BaseColumns._ID }, 9460 Playlists.Members.PLAY_ORDER + "=" + (playOrder + 1), null, null)) { 9461 c.moveToFirst(); 9462 return c.getLong(0); 9463 } 9464 } catch (IOException e) { 9465 throw new FallbackException("Failed to update playlist", e, 9466 android.os.Build.VERSION_CODES.R); 9467 } 9468 } 9469 9470 private int addPlaylistMembers(@NonNull Uri playlistUri, @NonNull ContentValues[] initialValues) 9471 throws FallbackException { 9472 final String volumeName = getVolumeName(playlistUri); 9473 final String audioVolumeName = 9474 MediaStore.VOLUME_INTERNAL.equals(volumeName) 9475 ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL; 9476 9477 try { 9478 final File playlistFile = queryForDataFile(playlistUri, null); 9479 final Playlist playlist = new Playlist(); 9480 playlist.read(playlistFile); 9481 9482 for (ContentValues values : initialValues) { 9483 final long audioId = values.getAsLong(Audio.Playlists.Members.AUDIO_ID); 9484 final Uri audioUri = Audio.Media.getContentUri(audioVolumeName, audioId); 9485 final File audioFile = queryForDataFile(audioUri, null); 9486 9487 Integer playOrder = values.getAsInteger(Playlists.Members.PLAY_ORDER); 9488 playOrder = (playOrder != null) ? (playOrder - 1) : Integer.MAX_VALUE; 9489 playlist.add(playOrder, 9490 playlistFile.toPath().getParent().relativize(audioFile.toPath())); 9491 } 9492 playlist.write(playlistFile); 9493 9494 resolvePlaylistMembers(playlistUri); 9495 } catch (IOException e) { 9496 throw new FallbackException("Failed to update playlist", e, 9497 android.os.Build.VERSION_CODES.R); 9498 } 9499 9500 return initialValues.length; 9501 } 9502 9503 /** 9504 * Move an audio item within the given playlist. 9505 */ 9506 private int movePlaylistMembers(@NonNull Uri playlistUri, @NonNull ContentValues values, 9507 @NonNull Bundle queryArgs) throws FallbackException { 9508 final int fromIndex = resolvePlaylistIndex(playlistUri, queryArgs); 9509 final int toIndex = values.getAsInteger(Playlists.Members.PLAY_ORDER) - 1; 9510 if (fromIndex == -1) { 9511 throw new FallbackException("Failed to resolve playlist member " + queryArgs, 9512 android.os.Build.VERSION_CODES.R); 9513 } 9514 try { 9515 final File playlistFile = queryForDataFile(playlistUri, null); 9516 9517 final Playlist playlist = new Playlist(); 9518 playlist.read(playlistFile); 9519 final int finalIndex = playlist.move(fromIndex, toIndex); 9520 playlist.write(playlistFile); 9521 invalidateFuseDentry(playlistFile); 9522 9523 resolvePlaylistMembers(playlistUri); 9524 return finalIndex; 9525 } catch (IOException e) { 9526 throw new FallbackException("Failed to update playlist", e, 9527 android.os.Build.VERSION_CODES.R); 9528 } 9529 } 9530 9531 /** 9532 * Removes an audio item or multiple audio items(if targetSDK<R) from the given playlist. 9533 */ 9534 private int removePlaylistMembers(@NonNull Uri playlistUri, @NonNull Bundle queryArgs) 9535 throws FallbackException { 9536 final int[] indexes = resolvePlaylistIndexes(playlistUri, queryArgs); 9537 try { 9538 final File playlistFile = queryForDataFile(playlistUri, null); 9539 9540 final Playlist playlist = new Playlist(); 9541 playlist.read(playlistFile); 9542 final int count; 9543 if (indexes.length == 0) { 9544 // This means either no playlist members match the query or VolumeNotFoundException 9545 // was thrown. So we don't have anything to delete. 9546 count = 0; 9547 } else { 9548 count = playlist.removeMultiple(indexes); 9549 } 9550 playlist.write(playlistFile); 9551 invalidateFuseDentry(playlistFile); 9552 9553 resolvePlaylistMembers(playlistUri); 9554 return count; 9555 } catch (IOException e) { 9556 throw new FallbackException("Failed to update playlist", e, 9557 android.os.Build.VERSION_CODES.R); 9558 } 9559 } 9560 9561 /** 9562 * Remove an audio item from the given playlist since the playlist file or the audio file is 9563 * already removed. 9564 */ 9565 private void removePlaylistMembers(int mediaType, long id) { 9566 final DatabaseHelper helper; 9567 try { 9568 helper = getDatabaseForUri(Audio.Media.EXTERNAL_CONTENT_URI); 9569 } catch (VolumeNotFoundException e) { 9570 Log.w(TAG, e); 9571 return; 9572 } 9573 9574 helper.runWithTransaction((db) -> { 9575 final String where; 9576 if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 9577 where = "playlist_id=?"; 9578 } else { 9579 where = "audio_id=?"; 9580 } 9581 db.delete("audio_playlists_map", where, new String[] { "" + id }); 9582 return null; 9583 }); 9584 } 9585 9586 /** 9587 * Resolve query arguments that are designed to select specific playlist 9588 * items using the playlist's {@link Playlists.Members#PLAY_ORDER}. 9589 * 9590 * @return an array of the indexes that match the query. 9591 */ 9592 private int[] resolvePlaylistIndexes(@NonNull Uri playlistUri, @NonNull Bundle queryArgs) { 9593 final Uri membersUri = Playlists.Members.getContentUri( 9594 getVolumeName(playlistUri), ContentUris.parseId(playlistUri)); 9595 9596 final DatabaseHelper helper; 9597 final SQLiteQueryBuilder qb; 9598 try { 9599 helper = getDatabaseForUri(membersUri); 9600 qb = getQueryBuilder(TYPE_DELETE, AUDIO_PLAYLISTS_ID_MEMBERS, 9601 membersUri, queryArgs, null); 9602 } catch (VolumeNotFoundException ignored) { 9603 return new int[0]; 9604 } 9605 9606 try (Cursor c = qb.query(helper, 9607 new String[] { Playlists.Members.PLAY_ORDER }, queryArgs, null)) { 9608 if ((c.getCount() >= 1) && c.moveToFirst()) { 9609 int size = c.getCount(); 9610 int[] res = new int[size]; 9611 for (int i = 0; i < size; ++i) { 9612 res[i] = c.getInt(0) - 1; 9613 c.moveToNext(); 9614 } 9615 return res; 9616 } else { 9617 // Cursor size is 0 9618 return new int[0]; 9619 } 9620 } 9621 } 9622 9623 /** 9624 * Resolve query arguments that are designed to select a specific playlist 9625 * item using its {@link Playlists.Members#PLAY_ORDER}. 9626 * 9627 * @return if there's only 1 item that matches the query, returns its index. Returns -1 9628 * otherwise. 9629 */ 9630 private int resolvePlaylistIndex(@NonNull Uri playlistUri, @NonNull Bundle queryArgs) { 9631 int[] indexes = resolvePlaylistIndexes(playlistUri, queryArgs); 9632 if (indexes.length == 1) { 9633 return indexes[0]; 9634 } 9635 return -1; 9636 } 9637 9638 private boolean isPickerUri(Uri uri) { 9639 final int match = matchUri(uri, /* allowHidden */ isCallingPackageAllowedHidden()); 9640 return match == PICKER_ID || match == PICKER_GET_CONTENT_ID 9641 || match == PICKER_TRANSCODED_ID; 9642 } 9643 9644 @Override 9645 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 9646 return openFileCommon(uri, mode, /*signal*/ null, /*opts*/ null); 9647 } 9648 9649 @Override 9650 public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) 9651 throws FileNotFoundException { 9652 return openFileCommon(uri, mode, signal, /*opts*/ null); 9653 } 9654 9655 private ParcelFileDescriptor openFileCommon(Uri uri, String mode, CancellationSignal signal, 9656 @Nullable Bundle opts) 9657 throws FileNotFoundException { 9658 opts = opts == null ? new Bundle() : opts; 9659 // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider. 9660 opts.remove(QUERY_ARG_REDACTED_URI); 9661 if (isRedactedUri(uri)) { 9662 opts.putParcelable(QUERY_ARG_REDACTED_URI, uri); 9663 uri = getUriForRedactedUri(uri); 9664 } 9665 uri = safeUncanonicalize(uri); 9666 9667 if (isPickerUri(uri)) { 9668 int tid = Process.myTid(); 9669 synchronized (mPendingOpenInfo) { 9670 mPendingOpenInfo.put(tid, new PendingOpenInfo( 9671 Binder.getCallingUid(), /* mediaCapabilitiesUid */ 0, /* shouldRedact */ 9672 false, /* transcodeReason */ 0)); 9673 } 9674 9675 try { 9676 return mPickerUriResolver.openFile(uri, mode, signal, mCallingIdentity.get()); 9677 } finally { 9678 synchronized (mPendingOpenInfo) { 9679 mPendingOpenInfo.remove(tid); 9680 } 9681 } 9682 } 9683 9684 final boolean allowHidden = isCallingPackageAllowedHidden(); 9685 final int match = matchUri(uri, allowHidden); 9686 final String volumeName = getVolumeName(uri); 9687 9688 // Handle some legacy cases where we need to redirect thumbnails 9689 try { 9690 switch (match) { 9691 case AUDIO_ALBUMART_ID: { 9692 final long albumId = Long.parseLong(uri.getPathSegments().get(3)); 9693 final Uri targetUri = ContentUris 9694 .withAppendedId(Audio.Albums.getContentUri(volumeName), albumId); 9695 return ensureThumbnail(targetUri, signal); 9696 } 9697 case AUDIO_ALBUMART_FILE_ID: { 9698 final long audioId = Long.parseLong(uri.getPathSegments().get(3)); 9699 final Uri targetUri = ContentUris 9700 .withAppendedId(Audio.Media.getContentUri(volumeName), audioId); 9701 return ensureThumbnail(targetUri, signal); 9702 } 9703 case VIDEO_MEDIA_ID_THUMBNAIL: { 9704 final long videoId = Long.parseLong(uri.getPathSegments().get(3)); 9705 final Uri targetUri = ContentUris 9706 .withAppendedId(Video.Media.getContentUri(volumeName), videoId); 9707 return ensureThumbnail(targetUri, signal); 9708 } 9709 case IMAGES_MEDIA_ID_THUMBNAIL: { 9710 final long imageId = Long.parseLong(uri.getPathSegments().get(3)); 9711 final Uri targetUri = ContentUris 9712 .withAppendedId(Images.Media.getContentUri(volumeName), imageId); 9713 return ensureThumbnail(targetUri, signal); 9714 } 9715 case CLI: { 9716 // Command Line Interface "file" - content://media/cli - may be opened only by 9717 // Shell (or Root). 9718 if (!isCallingPackageShell()) { 9719 throw new SecurityException("Only shell (or root) is allowed to open " 9720 + "MediaProvider's CLI file (" + uri + ')'); 9721 } 9722 9723 // We expect the uri's query to hold a single parameter - "cmd" - which contains 9724 // the command name followed by the arguments (if any), all joined with '+' 9725 // symbols: 9726 // ?cmd=command[+arg1+[arg2+[arg3...]]] 9727 // 9728 // For example: 9729 // (1) ?cmd=version 9730 // (2) ?cmd=set-cloud-provider=com.my.cloud.provider.authority 9731 // 9732 // We retrieve the command name and the argument (if any) with 9733 // uri.getQueryParameter("cmd") call, which will replace all '+' delimiters with 9734 // spaces. 9735 9736 final String[] cmdAndArgs = uri.getQueryParameter("cmd").split("\\s+"); 9737 Log.d(TAG, "MediaProvider CLI command: " + Arrays.toString(cmdAndArgs)); 9738 try { 9739 // Create a UNIX pipe. 9740 final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); 9741 // Pass the write end - pipe[1] - to our shell command. 9742 final var cmd = new MediaProviderShellCommand(getContext(), 9743 mConfigStore, 9744 mPickerSyncController, 9745 /* out */ pipe[1]); 9746 cmd.exec(cmdAndArgs); 9747 // Return the read end - pipe[0] - to the caller. 9748 return pipe[0]; 9749 } catch (IOException e) { 9750 Log.e(TAG, "Could not create a pipe", e); 9751 return null; 9752 } 9753 } 9754 } 9755 } finally { 9756 // We have to log separately here because openFileAndEnforcePathPermissionsHelper calls 9757 // a public MediaProvider API and so logs the access there. 9758 PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName); 9759 } 9760 9761 return openFileAndEnforcePathPermissionsHelper(uri, match, mode, signal, opts); 9762 } 9763 9764 @Override 9765 public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) 9766 throws FileNotFoundException { 9767 return openTypedAssetFileCommon(uri, mimeTypeFilter, opts, null); 9768 } 9769 9770 @Override 9771 public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts, 9772 CancellationSignal signal) throws FileNotFoundException { 9773 return openTypedAssetFileCommon(uri, mimeTypeFilter, opts, signal); 9774 } 9775 9776 private AssetFileDescriptor openTypedAssetFileCommon(Uri uri, String mimeTypeFilter, 9777 Bundle opts, CancellationSignal signal) throws FileNotFoundException { 9778 final boolean wantsThumb = (opts != null) && opts.containsKey(ContentResolver.EXTRA_SIZE) 9779 && StringUtils.startsWithIgnoreCase(mimeTypeFilter, "image/"); 9780 String mode = "r"; 9781 9782 // If request is not for thumbnail and arising from MediaProvider, then check for EXTRA_MODE 9783 if (opts != null && !wantsThumb && isCallingPackageSelf()) { 9784 mode = opts.getString(MediaStore.EXTRA_MODE, "r"); 9785 } else if (opts != null) { 9786 opts.remove(MediaStore.EXTRA_MODE); 9787 } 9788 9789 if (opts != null && opts.containsKey(MediaStore.EXTRA_FILE_DESCRIPTOR)) { 9790 // This is called as part of MediaStore#getOriginalMediaFormatFileDescriptor 9791 // We don't need to use the |uri| because the input fd already identifies the file and 9792 // we actually don't have a valid URI, we are going to identify the file via the fd. 9793 // While identifying the file, we also perform the following security checks. 9794 // 1. Find the FUSE file with the associated inode 9795 // 2. Verify that the binder caller opened it 9796 // 3. Verify the access level the fd is opened with (r/w) 9797 // 4. Open the original (non-transcoded) file *with* redaction enabled and the access 9798 // level from #3 9799 // 5. Return the fd from #4 to the app or throw an exception if any of the conditions 9800 // are not met 9801 try { 9802 return getOriginalMediaFormatFileDescriptor(opts); 9803 } finally { 9804 // Clearing the Bundle closes the underlying Parcel, ensuring that the input fd 9805 // owned by the Parcel is closed immediately and not at the next GC. 9806 // This works around a change in behavior introduced by: 9807 // aosp/Icfe8880cad00c3cd2afcbe4b92400ad4579e680e 9808 opts.clear(); 9809 } 9810 } 9811 9812 // This is needed for thumbnail resolution as it doesn't go through openFileCommon 9813 if (isPickerUri(uri)) { 9814 int tid = Process.myTid(); 9815 synchronized (mPendingOpenInfo) { 9816 mPendingOpenInfo.put(tid, new PendingOpenInfo( 9817 Binder.getCallingUid(), /* mediaCapabilitiesUid */ 0, /* shouldRedact */ 9818 false, /* transcodeReason */ 0)); 9819 } 9820 9821 try { 9822 return mPickerUriResolver.openTypedAssetFile(uri, mimeTypeFilter, opts, signal, 9823 mCallingIdentity.get(), wantsThumb); 9824 } finally { 9825 synchronized (mPendingOpenInfo) { 9826 mPendingOpenInfo.remove(tid); 9827 } 9828 } 9829 } 9830 9831 // Offer thumbnail of media, when requested 9832 if (wantsThumb) { 9833 final ParcelFileDescriptor pfd = ensureThumbnail(uri, signal); 9834 return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH); 9835 } 9836 9837 // Worst case, return the underlying file 9838 return new AssetFileDescriptor(openFileCommon(uri, mode, signal, opts), 0, 9839 AssetFileDescriptor.UNKNOWN_LENGTH); 9840 } 9841 9842 private ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal) 9843 throws FileNotFoundException { 9844 final boolean allowHidden = isCallingPackageAllowedHidden(); 9845 final int match = matchUri(uri, allowHidden); 9846 9847 Trace.beginSection("MP.ensureThumbnail"); 9848 checkAccessForThumbnail(uri, match, signal); 9849 final LocalCallingIdentity token = clearLocalCallingIdentity(); 9850 try { 9851 switch (match) { 9852 case AUDIO_ALBUMS_ID: { 9853 final String volumeName = MediaStore.getVolumeName(uri); 9854 final Uri baseUri = MediaStore.Audio.Media.getContentUri(volumeName); 9855 final long albumId = ContentUris.parseId(uri); 9856 try (Cursor c = query(baseUri, new String[] { MediaStore.Audio.Media._ID }, 9857 MediaStore.Audio.Media.ALBUM_ID + "=" + albumId, null, null, signal)) { 9858 if (c.moveToFirst()) { 9859 final long audioId = c.getLong(0); 9860 final Uri targetUri = ContentUris.withAppendedId(baseUri, audioId); 9861 return mAudioThumbnailer.ensureThumbnail(targetUri, signal); 9862 } else { 9863 throw new FileNotFoundException("No media for album " + uri); 9864 } 9865 } 9866 } 9867 case AUDIO_MEDIA_ID: 9868 return mAudioThumbnailer.ensureThumbnail(uri, signal); 9869 case VIDEO_MEDIA_ID: 9870 return mVideoThumbnailer.ensureThumbnail(uri, signal); 9871 case IMAGES_MEDIA_ID: 9872 return mImageThumbnailer.ensureThumbnail(uri, signal); 9873 case FILES_ID: 9874 case DOWNLOADS_ID: { 9875 // When item is referenced in a generic way, resolve to actual type 9876 final int mediaType = MimeUtils.resolveMediaType(getType(uri)); 9877 switch (mediaType) { 9878 case FileColumns.MEDIA_TYPE_AUDIO: 9879 return mAudioThumbnailer.ensureThumbnail(uri, signal); 9880 case FileColumns.MEDIA_TYPE_VIDEO: 9881 return mVideoThumbnailer.ensureThumbnail(uri, signal); 9882 case FileColumns.MEDIA_TYPE_IMAGE: 9883 return mImageThumbnailer.ensureThumbnail(uri, signal); 9884 default: 9885 throw new FileNotFoundException(); 9886 } 9887 } 9888 default: 9889 throw new FileNotFoundException(); 9890 } 9891 } catch (IOException e) { 9892 Log.w(TAG, e); 9893 throw new FileNotFoundException(e.getMessage()); 9894 } finally { 9895 restoreLocalCallingIdentity(token); 9896 Trace.endSection(); 9897 } 9898 } 9899 9900 private void checkAccessForThumbnail(Uri uri, int match, CancellationSignal signal) 9901 throws FileNotFoundException { 9902 int mediaType = -1; 9903 if (match == DOWNLOADS_ID || match == FILES_ID) { 9904 mediaType = MimeUtils.resolveMediaType(queryForTypeAsSelf(uri)); 9905 } 9906 9907 // check access only for image and video thumbnails 9908 // audio thumbnails have many legacy paths that we could break by checking for access 9909 // and it doesn't reveal much of data that could be a risk 9910 if (match == IMAGES_MEDIA_ID || match == VIDEO_MEDIA_ID 9911 || mediaType == MEDIA_TYPE_IMAGE || mediaType == MEDIA_TYPE_VIDEO) { 9912 9913 // First check existence of the file 9914 final String[] projection = new String[] { MediaColumns.DATA }; 9915 final File file; 9916 try (Cursor c = queryForSingleItemAsMediaProvider( 9917 uri, projection, null, null, signal)) { 9918 final String data = c.getString(0); 9919 if (TextUtils.isEmpty(data)) { 9920 throw new FileNotFoundException("Missing path for " + uri); 9921 } else { 9922 file = new File(data).getCanonicalFile(); 9923 } 9924 } catch (IOException e) { 9925 throw new FileNotFoundException(e.toString()); 9926 } 9927 9928 // Then check if the caller has access to the file 9929 checkAccess(uri, Bundle.EMPTY, file, false); 9930 } 9931 } 9932 9933 /** 9934 * Update the metadata columns for the image residing at given {@link Uri} 9935 * by reading data from the underlying image. 9936 */ 9937 private void updateImageMetadata(ContentValues values, File file) { 9938 final BitmapFactory.Options bitmapOpts = new BitmapFactory.Options(); 9939 bitmapOpts.inJustDecodeBounds = true; 9940 BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOpts); 9941 9942 values.put(MediaColumns.WIDTH, bitmapOpts.outWidth); 9943 values.put(MediaColumns.HEIGHT, bitmapOpts.outHeight); 9944 } 9945 9946 private void handleInsertedRowForFuse(long rowId) { 9947 if (isFuseThread()) { 9948 // Removes restored row ID saved list. 9949 mCallingIdentity.get().removeDeletedRowId(rowId); 9950 } 9951 } 9952 9953 private void handleUpdatedRowForFuse(@NonNull String oldPath, @NonNull String ownerPackage, 9954 long oldRowId, long newRowId) { 9955 if (oldRowId == newRowId) { 9956 // Update didn't delete or add row ID. We don't need to save row ID or remove saved 9957 // deleted ID. 9958 return; 9959 } 9960 9961 handleDeletedRowForFuse(oldPath, ownerPackage, oldRowId); 9962 handleInsertedRowForFuse(newRowId); 9963 } 9964 9965 private void handleDeletedRowForFuse(@NonNull String path, @NonNull String ownerPackage, 9966 long rowId) { 9967 if (!isFuseThread()) { 9968 return; 9969 } 9970 9971 // Invalidate saved owned ID's of the previous owner of the deleted path, this prevents old 9972 // owner from gaining access to newly created file with restored row ID. 9973 if (!ownerPackage.equals("null") && !ownerPackage.equals(getCallingPackageOrSelf())) { 9974 invalidateLocalCallingIdentityCache(ownerPackage, "owned_database_row_deleted:" 9975 + path); 9976 } 9977 // Saves row ID corresponding to deleted path. Saved row ID will be restored on subsequent 9978 // create or rename. 9979 mCallingIdentity.get().addDeletedRowId(path, rowId); 9980 } 9981 9982 private void handleOwnerPackageNameChange(@NonNull String oldPath, 9983 @NonNull String oldOwnerPackage, @NonNull String newOwnerPackage) { 9984 if (Objects.equals(oldOwnerPackage, newOwnerPackage)) { 9985 return; 9986 } 9987 // Invalidate saved owned ID's of the previous owner of the renamed path, this prevents old 9988 // owner from gaining access to replaced file. 9989 invalidateLocalCallingIdentityCache(oldOwnerPackage, "owner_package_changed:" + oldPath); 9990 } 9991 9992 /** 9993 * Return the {@link MediaColumns#DATA} field for the given {@code Uri}. 9994 */ 9995 File queryForDataFile(Uri uri, CancellationSignal signal) 9996 throws FileNotFoundException { 9997 return queryForDataFile(uri, null, null, signal); 9998 } 9999 10000 /** 10001 * Return the {@link MediaColumns#DATA} field for the given {@code Uri}. 10002 */ 10003 File queryForDataFile(Uri uri, String selection, String[] selectionArgs, 10004 CancellationSignal signal) throws FileNotFoundException { 10005 try (Cursor cursor = queryForSingleItem(uri, new String[] { MediaColumns.DATA }, 10006 selection, selectionArgs, signal)) { 10007 final String data = cursor.getString(0); 10008 if (TextUtils.isEmpty(data)) { 10009 throw new FileNotFoundException("Missing path for " + uri); 10010 } else { 10011 return new File(data); 10012 } 10013 } 10014 } 10015 10016 /** 10017 * Return the {@link Uri} for the given {@code File}. 10018 */ 10019 Uri queryForMediaUri(File file, CancellationSignal signal) throws FileNotFoundException { 10020 final String volumeName = FileUtils.getVolumeName(getContext(), file); 10021 final Uri uri = Files.getContentUri(volumeName); 10022 try (Cursor cursor = queryForSingleItem(uri, new String[] { MediaColumns._ID }, 10023 MediaColumns.DATA + "=?", new String[] { file.getAbsolutePath() }, signal)) { 10024 return ContentUris.withAppendedId(uri, cursor.getLong(0)); 10025 } 10026 } 10027 10028 /** 10029 * Query the given {@link Uri} as MediaProvider, expecting only a single item to be found. 10030 * 10031 * @throws FileNotFoundException if no items were found, or multiple items 10032 * were found, or there was trouble reading the data. 10033 */ 10034 Cursor queryForSingleItemAsMediaProvider(Uri uri, String[] projection, String selection, 10035 String[] selectionArgs, CancellationSignal signal) 10036 throws FileNotFoundException { 10037 final LocalCallingIdentity tokenInner = clearLocalCallingIdentity(); 10038 try { 10039 return queryForSingleItem(uri, projection, selection, selectionArgs, signal); 10040 } finally { 10041 restoreLocalCallingIdentity(tokenInner); 10042 } 10043 } 10044 10045 /** 10046 * Query the given {@link Uri}, expecting only a single item to be found. 10047 * 10048 * @throws FileNotFoundException if no items were found, or multiple items 10049 * were found, or there was trouble reading the data. 10050 */ 10051 Cursor queryForSingleItem(Uri uri, String[] projection, String selection, 10052 String[] selectionArgs, CancellationSignal signal) throws FileNotFoundException { 10053 Cursor c = null; 10054 try { 10055 c = query(uri, projection, 10056 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null), 10057 signal, true); 10058 } catch (IllegalArgumentException e) { 10059 throw new FileNotFoundException("Volume not found for " + uri); 10060 } 10061 if (c == null) { 10062 throw new FileNotFoundException("Missing cursor for " + uri); 10063 } else if (c.getCount() < 1) { 10064 FileUtils.closeQuietly(c); 10065 throw new FileNotFoundException("No item at " + uri); 10066 } else if (c.getCount() > 1) { 10067 FileUtils.closeQuietly(c); 10068 throw new FileNotFoundException("Multiple items at " + uri); 10069 } 10070 10071 if (c.moveToFirst()) { 10072 return c; 10073 } else { 10074 FileUtils.closeQuietly(c); 10075 throw new FileNotFoundException("Failed to read row from " + uri); 10076 } 10077 } 10078 10079 /** 10080 * Compares {@code itemOwner} with package name of {@link LocalCallingIdentity} and throws 10081 * {@link IllegalStateException} if it doesn't match. 10082 * Make sure to set calling identity properly before calling. 10083 */ 10084 private void requireOwnershipForItem(@Nullable String itemOwner, Uri item) { 10085 final boolean hasOwner = (itemOwner != null); 10086 final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), itemOwner); 10087 if (hasOwner && !callerIsOwner) { 10088 throw new IllegalStateException( 10089 "Only owner is able to interact with pending/trashed item " + item); 10090 } 10091 } 10092 10093 private ParcelFileDescriptor openWithFuse(String filePath, int uid, int mediaCapabilitiesUid, 10094 int modeBits, boolean shouldRedact, boolean shouldTranscode, int transcodeReason) 10095 throws FileNotFoundException { 10096 Log.d(TAG, "Open with FUSE. FilePath: " + filePath 10097 + ". Uid: " + uid 10098 + ". Media Capabilities Uid: " + mediaCapabilitiesUid 10099 + ". ShouldRedact: " + shouldRedact 10100 + ". ShouldTranscode: " + shouldTranscode); 10101 10102 int tid = android.os.Process.myTid(); 10103 synchronized (mPendingOpenInfo) { 10104 mPendingOpenInfo.put(tid, 10105 new PendingOpenInfo(uid, mediaCapabilitiesUid, shouldRedact, transcodeReason)); 10106 } 10107 10108 try { 10109 return FileUtils.openSafely(toFuseFile(new File(filePath)), modeBits); 10110 } finally { 10111 synchronized (mPendingOpenInfo) { 10112 mPendingOpenInfo.remove(tid); 10113 } 10114 } 10115 } 10116 10117 /** 10118 * @return {@link FuseDaemon} corresponding to a given file 10119 */ 10120 @NonNull 10121 public static FuseDaemon getFuseDaemonForFile(@NonNull File file, VolumeCache volumeCache) 10122 throws FileNotFoundException { 10123 final FuseDaemon daemon = ExternalStorageServiceImpl.getFuseDaemon( 10124 volumeCache.getVolumeId(file)); 10125 if (daemon == null) { 10126 throw new FileNotFoundException("Missing FUSE daemon for " + file); 10127 } else { 10128 return daemon; 10129 } 10130 } 10131 10132 @NonNull 10133 public static FuseDaemon getFuseDaemonForFileWithWait(@NonNull File file, 10134 VolumeCache volumeCache, long waitTimeInMilliseconds) throws FileNotFoundException { 10135 FuseDaemon fuseDaemon = null; 10136 long time = 0; 10137 while (time < waitTimeInMilliseconds) { 10138 fuseDaemon = ExternalStorageServiceImpl.getFuseDaemon( 10139 volumeCache.getVolumeId(file)); 10140 if (fuseDaemon != null) { 10141 break; 10142 } 10143 SystemClock.sleep(POLLING_TIME_IN_MILLIS); 10144 time += POLLING_TIME_IN_MILLIS; 10145 } 10146 10147 if (fuseDaemon == null) { 10148 throw new FileNotFoundException("Missing FUSE daemon for " + file); 10149 } else { 10150 return fuseDaemon; 10151 } 10152 } 10153 10154 /** 10155 * Invalidate fuse dentry cache for filepath 10156 */ 10157 public void invalidateFuseDentry(@NonNull File file) { 10158 invalidateFuseDentry(file.getAbsolutePath()); 10159 } 10160 10161 private void invalidateFuseDentry(@NonNull String path) { 10162 try { 10163 final FuseDaemon daemon = getFuseDaemonForFile(new File(path), mVolumeCache); 10164 if (isFuseThread()) { 10165 // If we are on a FUSE thread, we don't need to invalidate, 10166 // (and *must* not, otherwise we'd crash) because the invalidation 10167 // is already reflected in the lower filesystem 10168 return; 10169 } else { 10170 daemon.invalidateFuseDentryCache(path); 10171 } 10172 } catch (FileNotFoundException e) { 10173 Log.w(TAG, "Failed to invalidate FUSE dentry", e); 10174 } 10175 } 10176 10177 /** 10178 * Replacement for {@link #openFileHelper(Uri, String)} which enforces any 10179 * permissions applicable to the path before returning. 10180 * 10181 * <p>This function should never be called from the fuse thread since it tries to open 10182 * a "/mnt/user" path. 10183 */ 10184 private ParcelFileDescriptor openFileAndEnforcePathPermissionsHelper(Uri uri, int match, 10185 String mode, CancellationSignal signal, @NonNull Bundle opts) 10186 throws FileNotFoundException { 10187 int modeBits = ParcelFileDescriptor.parseMode(mode); 10188 boolean forWrite = (modeBits & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0; 10189 final Uri redactedUri = opts.getParcelable(QUERY_ARG_REDACTED_URI); 10190 if (forWrite) { 10191 if (redactedUri != null) { 10192 throw new UnsupportedOperationException( 10193 "Write is not supported on " + redactedUri.toString()); 10194 } 10195 // Upgrade 'w' only to 'rw'. This allows us acquire a WR_LOCK when calling 10196 // #shouldOpenWithFuse 10197 modeBits |= ParcelFileDescriptor.MODE_READ_WRITE; 10198 } 10199 10200 final boolean hasOwnerPackageName = hasOwnerPackageName(uri); 10201 final String[] projection = new String[] { 10202 MediaColumns.DATA, 10203 hasOwnerPackageName ? MediaColumns.OWNER_PACKAGE_NAME : "NULL", 10204 hasOwnerPackageName ? MediaColumns.IS_PENDING : "0", 10205 }; 10206 10207 final File file; 10208 final String ownerPackageName; 10209 final boolean isPending; 10210 final LocalCallingIdentity token = clearLocalCallingIdentity(); 10211 try (Cursor c = queryForSingleItem(uri, projection, null, null, signal)) { 10212 final String data = c.getString(0); 10213 if (TextUtils.isEmpty(data)) { 10214 throw new FileNotFoundException("Missing path for " + uri); 10215 } else { 10216 file = new File(data).getCanonicalFile(); 10217 } 10218 ownerPackageName = c.getString(1); 10219 isPending = c.getInt(2) != 0; 10220 } catch (IOException e) { 10221 throw new FileNotFoundException(e.toString()); 10222 } finally { 10223 restoreLocalCallingIdentity(token); 10224 } 10225 10226 if (redactedUri == null) { 10227 checkAccess(uri, Bundle.EMPTY, file, forWrite); 10228 } else { 10229 checkAccess(redactedUri, Bundle.EMPTY, file, false); 10230 } 10231 10232 // We don't check ownership for files with IS_PENDING set by FUSE 10233 if (isPending && !isPendingFromFuse(file)) { 10234 requireOwnershipForItem(ownerPackageName, uri); 10235 } 10236 10237 // Figure out if we need to redact contents 10238 final boolean redactionNeeded = isRedactionNeededForOpenViaContentResolver(redactedUri, 10239 ownerPackageName, file); 10240 long[] redactionRanges; 10241 try { 10242 redactionRanges = redactionNeeded ? RedactionUtils.getRedactionRanges(file) 10243 : new long[0]; 10244 } catch (IOException e) { 10245 throw new IllegalStateException(e); 10246 } 10247 10248 // Yell if caller requires original, since we can't give it to them 10249 // unless they have access granted above 10250 if (redactionNeeded && MediaStore.getRequireOriginal(uri)) { 10251 throw new UnsupportedOperationException( 10252 "Caller must hold ACCESS_MEDIA_LOCATION permission to access original"); 10253 } 10254 10255 // Kick off metadata update when writing is finished 10256 final OnCloseListener listener = (e) -> { 10257 // We always update metadata to reflect the state on disk, even when 10258 // the remote writer tried claiming an exception 10259 invalidateThumbnails(uri); 10260 10261 // Invalidate so subsequent stat(2) on the upper fs is eventually consistent 10262 invalidateFuseDentry(file); 10263 try { 10264 switch (match) { 10265 case IMAGES_THUMBNAILS_ID: 10266 case VIDEO_THUMBNAILS_ID: 10267 final ContentValues values = new ContentValues(); 10268 updateImageMetadata(values, file); 10269 update(uri, values, null, null); 10270 break; 10271 default: 10272 scanFileAsMediaProvider(file); 10273 break; 10274 } 10275 } catch (Exception e2) { 10276 Log.w(TAG, "Failed to update metadata for " + uri, e2); 10277 } 10278 }; 10279 10280 try { 10281 // First, handle any redaction that is needed for caller 10282 final ParcelFileDescriptor pfd; 10283 final String filePath = file.getPath(); 10284 final int uid = Binder.getCallingUid(); 10285 final int transcodeReason = mTranscodeHelper.shouldTranscode(filePath, uid, opts); 10286 final boolean shouldTranscode = transcodeReason > 0; 10287 int mediaCapabilitiesUid = opts.getInt(MediaStore.EXTRA_MEDIA_CAPABILITIES_UID); 10288 if (!shouldTranscode || mediaCapabilitiesUid < Process.FIRST_APPLICATION_UID) { 10289 // Although 0 is a valid UID, it's not a valid app uid. 10290 // So, we use it to signify that mediaCapabilitiesUid is not set. 10291 mediaCapabilitiesUid = 0; 10292 } 10293 if (redactionRanges.length > 0) { 10294 // If fuse is enabled, we can provide an fd that points to the fuse 10295 // file system and handle redaction in the fuse handler when the caller reads. 10296 pfd = openWithFuse(filePath, uid, mediaCapabilitiesUid, modeBits, 10297 true /* shouldRedact */, shouldTranscode, transcodeReason); 10298 } else if (shouldTranscode) { 10299 pfd = openWithFuse(filePath, uid, mediaCapabilitiesUid, modeBits, 10300 false /* shouldRedact */, shouldTranscode, transcodeReason); 10301 } else { 10302 FuseDaemon daemon = null; 10303 try { 10304 daemon = getFuseDaemonForFile(file, mVolumeCache); 10305 } catch (FileNotFoundException ignored) { 10306 } 10307 ParcelFileDescriptor lowerFsFd = FileUtils.openSafely(file, modeBits); 10308 // Always acquire a readLock. This allows us make multiple opens via lower 10309 // filesystem 10310 boolean shouldOpenWithFuse = daemon != null 10311 && daemon.shouldOpenWithFuse(filePath, true /* forRead */, 10312 lowerFsFd.getFd()); 10313 10314 if (shouldOpenWithFuse) { 10315 // If the file is already opened on the FUSE mount with VFS caching enabled 10316 // we return an upper filesystem fd (via FUSE) to avoid file corruption 10317 // resulting from cache inconsistencies between the upper and lower 10318 // filesystem caches 10319 pfd = openWithFuse(filePath, uid, mediaCapabilitiesUid, modeBits, 10320 false /* shouldRedact */, shouldTranscode, transcodeReason); 10321 try { 10322 lowerFsFd.close(); 10323 } catch (IOException e) { 10324 Log.w(TAG, "Failed to close lower filesystem fd " + file.getPath(), e); 10325 } 10326 } else { 10327 Log.i(TAG, "Open with lower FS for " + filePath + ". Uid: " + uid); 10328 if (forWrite) { 10329 // When opening for write on the lower filesystem, invalidate the VFS dentry 10330 // so subsequent open/getattr calls will return correctly. 10331 // 10332 // A 'dirty' dentry with write back cache enabled can cause the kernel to 10333 // ignore file attributes or even see stale page cache data when the lower 10334 // filesystem has been modified outside of the FUSE driver 10335 invalidateFuseDentry(file); 10336 } 10337 10338 pfd = lowerFsFd; 10339 } 10340 } 10341 10342 // Second, wrap in any listener that we've requested 10343 if (!isPending && forWrite) { 10344 return ParcelFileDescriptor.wrap(pfd, BackgroundThread.getHandler(), listener); 10345 } else { 10346 return pfd; 10347 } 10348 } catch (IOException e) { 10349 if (e instanceof FileNotFoundException) { 10350 throw (FileNotFoundException) e; 10351 } else { 10352 throw new IllegalStateException(e); 10353 } 10354 } 10355 } 10356 10357 private boolean isRedactionNeededForOpenViaContentResolver(Uri redactedUri, 10358 String ownerPackageName, File file) { 10359 // Redacted Uris should always redact information 10360 if (redactedUri != null) { 10361 return true; 10362 } 10363 10364 final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), ownerPackageName); 10365 if (callerIsOwner) { 10366 return false; 10367 } 10368 10369 // To be consistent with FUSE redaction checks we allow similar access for File Manager 10370 // and System Gallery apps. 10371 if (isCallingPackageManager() || canSystemGalleryAccessTheFile(file.getPath())) { 10372 return false; 10373 } 10374 10375 return isRedactionNeeded(); 10376 } 10377 10378 private void deleteAndInvalidate(@NonNull Path path) { 10379 if (path == null) { 10380 return; 10381 } 10382 10383 String fileName = path.getFileName().toString(); 10384 // Delete and invalidate all files except .nomedia and .database_uuid 10385 if (!fileName.equalsIgnoreCase(MEDIA_IGNORE_FILENAME) 10386 && !fileName.equalsIgnoreCase(FILE_DATABASE_UUID)) { 10387 deleteAndInvalidate(path.toFile()); 10388 } 10389 } 10390 10391 private void deleteAndInvalidate(@NonNull File file) { 10392 file.delete(); 10393 invalidateFuseDentry(file); 10394 } 10395 10396 private void deleteIfAllowed(Uri uri, Bundle extras, String path) { 10397 try { 10398 final File file = new File(path).getCanonicalFile(); 10399 checkAccess(uri, extras, file, true); 10400 deleteAndInvalidate(file); 10401 } catch (Exception e) { 10402 Log.e(TAG, "Couldn't delete " + path, e); 10403 } 10404 } 10405 10406 @Deprecated 10407 private boolean isPending(Uri uri) { 10408 final int match = matchUri(uri, true); 10409 switch (match) { 10410 case AUDIO_MEDIA_ID: 10411 case VIDEO_MEDIA_ID: 10412 case IMAGES_MEDIA_ID: 10413 try (Cursor c = queryForSingleItem(uri, 10414 new String[] { MediaColumns.IS_PENDING }, null, null, null)) { 10415 return (c.getInt(0) != 0); 10416 } catch (FileNotFoundException e) { 10417 throw new IllegalStateException(e); 10418 } 10419 default: 10420 return false; 10421 } 10422 } 10423 10424 @Deprecated 10425 private boolean isRedactionNeeded(Uri uri) { 10426 return mCallingIdentity.get().hasPermission(PERMISSION_IS_REDACTION_NEEDED); 10427 } 10428 10429 private boolean isRedactionNeeded() { 10430 return mCallingIdentity.get().hasPermission(PERMISSION_IS_REDACTION_NEEDED); 10431 } 10432 10433 private boolean isCallingPackageRequestingLegacy() { 10434 return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_GRANTED); 10435 } 10436 10437 private boolean shouldBypassDatabase(int uid) { 10438 if (uid != android.os.Process.SHELL_UID && isCallingPackageManager()) { 10439 return mCallingIdentity.get().shouldBypassDatabase(false /*isSystemGallery*/); 10440 } else if (isCallingPackageSystemGallery()) { 10441 if (isCallingPackageLegacyWrite()) { 10442 // We bypass db operations for legacy system galleries with W_E_S (see b/167307393). 10443 // Tracking a longer term solution in b/168784136. 10444 return true; 10445 } else if (!SdkLevel.isAtLeastS()) { 10446 // We don't parse manifest flags for SdkLevel<=R yet. Hence, we don't bypass 10447 // database updates for SystemGallery targeting R or above on R OS. 10448 return false; 10449 } 10450 return mCallingIdentity.get().shouldBypassDatabase(true /*isSystemGallery*/); 10451 } 10452 return false; 10453 } 10454 10455 private static int getFileMediaType(String path) { 10456 final File file = new File(path); 10457 final String mimeType = MimeUtils.resolveMimeType(file); 10458 return MimeUtils.resolveMediaType(mimeType); 10459 } 10460 10461 private boolean canSystemGalleryAccessTheFile(String filePath) { 10462 10463 if (!isCallingPackageSystemGallery()) { 10464 return false; 10465 } 10466 10467 final int mediaType = getFileMediaType(filePath); 10468 10469 return mediaType == FileColumns.MEDIA_TYPE_IMAGE || 10470 mediaType == FileColumns.MEDIA_TYPE_VIDEO; 10471 } 10472 10473 /** 10474 * Returns true if: 10475 * <ul> 10476 * <li>the calling identity is an app targeting Q or older versions AND is requesting legacy 10477 * storage and has the corresponding legacy access (read/write) permissions 10478 * <li>the calling identity holds {@code MANAGE_EXTERNAL_STORAGE} 10479 * <li>the calling identity owns or has access to the filePath (eg /Android/data/com.foo) 10480 * <li>the calling identity has permission to write images and the given file is an image file 10481 * <li>the calling identity has permission to write video and the given file is an video file 10482 * </ul> 10483 */ 10484 private boolean shouldBypassFuseRestrictions(boolean forWrite, String filePath) { 10485 boolean isRequestingLegacyStorage = forWrite ? isCallingPackageLegacyWrite() 10486 : isCallingPackageLegacyRead(); 10487 if (isRequestingLegacyStorage) { 10488 return true; 10489 } 10490 10491 if (isCallingPackageManager()) { 10492 return true; 10493 } 10494 10495 // Check if the caller has access to private app directories. Checks for Android/data, 10496 // Android/media and Android/obb 10497 boolean isUidAllowedAccessToDataOrObbPath = 10498 isUidAllowedAccessToDataOrObbPathForFuse(mCallingIdentity.get().uid, filePath); 10499 10500 /* 10501 * If owned photos is enabled, then image or video stored in app's private directory may 10502 * not have access to it (i.e, have owner_package_name as null). So, only checking path is 10503 * not enough to bypass fuse restrictions. We will have to additionally check if calling 10504 * app has read permission. 10505 */ 10506 if (isUidAllowedAccessToDataOrObbPath) { 10507 if (!isExternalMediaDirectory(filePath)) { 10508 return true; 10509 } 10510 10511 if (!isOwnedPhotosEnabled(mCallingIdentity.get().uid)) { 10512 return true; 10513 } 10514 10515 int mediaType = getFileMediaType(filePath); 10516 10517 if (MEDIA_TYPE_IMAGE == mediaType) { 10518 boolean hasReadImagesPermission = 10519 mCallingIdentity.get().hasPermission(PERMISSION_READ_IMAGES); 10520 Log.v(TAG, "calling app has PERMISSION_READ_IMAGES? " 10521 + hasReadImagesPermission); 10522 return hasReadImagesPermission; 10523 } 10524 10525 if (MEDIA_TYPE_VIDEO == mediaType) { 10526 boolean hasReadVideoPermission = 10527 mCallingIdentity.get().hasPermission(PERMISSION_READ_VIDEO); 10528 Log.v(TAG, "calling app has PERMISSION_READ_VIDEO? " 10529 + hasReadVideoPermission); 10530 return hasReadVideoPermission; 10531 } 10532 10533 return true; 10534 } 10535 10536 // Apps with write access to images and/or videos can bypass our restrictions if all of the 10537 // the files they're accessing are of the compatible media type. 10538 return canSystemGalleryAccessTheFile(filePath); 10539 } 10540 10541 /** 10542 * Returns true if the passed in path is an application-private data directory 10543 * (such as Android/data/com.foo or Android/obb/com.foo) that does not belong to the caller and 10544 * the caller does not have special access. 10545 */ 10546 private boolean isPrivatePackagePathNotAccessibleByCaller(String path) { 10547 // Files under the apps own private directory 10548 final String appSpecificDir = extractPathOwnerPackageName(path); 10549 10550 if (appSpecificDir == null) { 10551 return false; 10552 } 10553 10554 // Android/media is not considered private, because it contains media that is explicitly 10555 // scanned and shared by other apps 10556 if (isExternalMediaDirectory(path)) { 10557 return false; 10558 } 10559 return !isUidAllowedAccessToDataOrObbPathForFuse(mCallingIdentity.get().uid, path); 10560 } 10561 10562 private boolean shouldBypassDatabaseAndSetDirtyForFuse(int uid, String path) { 10563 if (shouldBypassDatabase(uid)) { 10564 synchronized (mNonHiddenPaths) { 10565 File file = new File(path); 10566 String key = file.getParent(); 10567 boolean maybeHidden = !mNonHiddenPaths.containsKey(key); 10568 10569 if (maybeHidden) { 10570 File topNoMediaDir = FileUtils.getTopLevelNoMedia(new File(path)); 10571 if (topNoMediaDir == null) { 10572 mNonHiddenPaths.put(key, 0); 10573 } else { 10574 mMediaScanner.onDirectoryDirty(topNoMediaDir); 10575 invalidateFuseDentry(topNoMediaDir); 10576 } 10577 } 10578 } 10579 return true; 10580 } 10581 return false; 10582 } 10583 10584 private static class LRUCache<K, V> extends LinkedHashMap<K, V> { 10585 private final int mMaxSize; 10586 10587 public LRUCache(int maxSize) { 10588 this.mMaxSize = maxSize; 10589 } 10590 10591 @Override 10592 protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { 10593 return size() > mMaxSize; 10594 } 10595 } 10596 10597 private static final class PendingOpenInfo { 10598 public final int uid; 10599 public final int mediaCapabilitiesUid; 10600 public final boolean shouldRedact; 10601 public final int transcodeReason; 10602 10603 public PendingOpenInfo(int uid, int mediaCapabilitiesUid, boolean shouldRedact, 10604 int transcodeReason) { 10605 this.uid = uid; 10606 this.mediaCapabilitiesUid = mediaCapabilitiesUid; 10607 this.shouldRedact = shouldRedact; 10608 this.transcodeReason = transcodeReason; 10609 } 10610 } 10611 10612 /** 10613 * Calculates the ranges that need to be redacted for the given file and user that wants to 10614 * access the file. 10615 * Note: This method assumes that the caller of this function has already done permission checks 10616 * for the uid to access this path. 10617 * 10618 * @param uid UID of the package wanting to access the file 10619 * @param path File path 10620 * @param tid thread id making IO on the FUSE filesystem 10621 * @return Ranges that should be redacted. 10622 * 10623 * @throws IOException if an error occurs while calculating the redaction ranges 10624 */ 10625 @NonNull 10626 private long[] getRedactionRangesForFuse(String path, String ioPath, int original_uid, int uid, 10627 int tid, boolean forceRedaction) throws IOException { 10628 // |ioPath| might refer to a transcoded file path (which is not indexed in the db) 10629 // |path| will always refer to a valid _data column 10630 // We use |ioPath| for the filesystem access because in the case of transcoding, 10631 // we want to get redaction ranges from the transcoded file and *not* the original file 10632 final File file = new File(ioPath); 10633 10634 if (forceRedaction) { 10635 return RedactionUtils.getRedactionRanges(file); 10636 } 10637 10638 // When calculating redaction ranges initiated from MediaProvider, the redaction policy 10639 // is slightly different from the FUSE initiated opens redaction policy. targetSdk=29 from 10640 // MediaProvider requires redaction, but targetSdk=29 apps from FUSE don't require redaction 10641 // Hence, we check the mPendingOpenInfo object (populated when opens are initiated from 10642 // MediaProvider) if there's a pending open from MediaProvider with matching tid and uid and 10643 // use the shouldRedact decision there if there's one. 10644 PendingOpenInfo info; 10645 synchronized (mPendingOpenInfo) { 10646 info = mPendingOpenInfo.get(tid); 10647 } 10648 10649 if (info != null && info.uid == original_uid) { 10650 boolean shouldRedact = info.shouldRedact; 10651 if (shouldRedact) { 10652 return RedactionUtils.getRedactionRanges(file); 10653 } else { 10654 return new long[0]; 10655 } 10656 } 10657 10658 final LocalCallingIdentity token = 10659 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 10660 try { 10661 if (!isRedactionNeeded() 10662 || shouldBypassFuseRestrictions(/* forWrite */ false, path)) { 10663 return new long[0]; 10664 } 10665 10666 final Uri contentUri = FileUtils.getContentUriForPath(path); 10667 final String[] projection = new String[]{ 10668 MediaColumns.OWNER_PACKAGE_NAME, MediaColumns._ID , FileColumns.MEDIA_TYPE}; 10669 final String selection = MediaColumns.DATA + "=?"; 10670 final String[] selectionArgs = new String[]{path}; 10671 final String ownerPackageName; 10672 final int id; 10673 final int mediaType; 10674 // Query as MediaProvider as non-RES apps will result in FileNotFoundException. 10675 // Note: The caller uid already has passed permission checks to access this file. 10676 try (final Cursor c = queryForSingleItemAsMediaProvider(contentUri, projection, 10677 selection, selectionArgs, null)) { 10678 c.moveToFirst(); 10679 ownerPackageName = c.getString(0); 10680 id = c.getInt(1); 10681 mediaType = c.getInt(2); 10682 } catch (FileNotFoundException e) { 10683 // Ideally, this shouldn't happen unless the file was deleted after we checked its 10684 // existence and before we get to the redaction logic here. In this case we throw 10685 // and fail the operation and FuseDaemon should handle this and fail the whole open 10686 // operation gracefully. 10687 throw new FileNotFoundException( 10688 path + " not found while calculating redaction ranges: " + e.getMessage()); 10689 } 10690 10691 final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), 10692 ownerPackageName); 10693 10694 // Do not redact if the caller is the owner 10695 if (callerIsOwner) { 10696 return new long[0]; 10697 } 10698 10699 // Do not redact if the caller has write uri permission granted on the file. 10700 final Uri fileUri = ContentUris.withAppendedId(contentUri, id); 10701 boolean callerHasWriteUriPermission = getContext().checkUriPermission( 10702 fileUri, mCallingIdentity.get().pid, mCallingIdentity.get().uid, 10703 Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == PERMISSION_GRANTED; 10704 if (callerHasWriteUriPermission) { 10705 return new long[0]; 10706 } 10707 // Check if the caller has write access to other uri formats for the same file. 10708 callerHasWriteUriPermission = getOtherUriGrantsForPath(path, mediaType, 10709 Long.toString(id), /* forWrite */ true) != null; 10710 if (callerHasWriteUriPermission) { 10711 return new long[0]; 10712 } 10713 10714 return RedactionUtils.getRedactionRanges(file); 10715 } finally { 10716 restoreLocalCallingIdentity(token); 10717 } 10718 } 10719 10720 /** 10721 * @return {@code true} if {@code file} is pending from FUSE, {@code false} otherwise. 10722 * Files pending from FUSE will not have pending file pattern. 10723 */ 10724 private static boolean isPendingFromFuse(@NonNull File file) { 10725 final Matcher matcher = 10726 FileUtils.PATTERN_EXPIRES_FILE.matcher(extractDisplayName(file.getName())); 10727 return !matcher.matches(); 10728 } 10729 10730 private FileAccessAttributes queryForFileAttributes(final String path) 10731 throws FileNotFoundException { 10732 Trace.beginSection("MP.queryFileAttr"); 10733 final Uri contentUri = FileUtils.getContentUriForPath(path); 10734 final String[] projection = new String[]{ 10735 MediaColumns._ID, 10736 MediaColumns.OWNER_PACKAGE_NAME, 10737 MediaColumns.IS_PENDING, 10738 FileColumns.MEDIA_TYPE, 10739 MediaColumns.IS_TRASHED 10740 }; 10741 final String selection = MediaColumns.DATA + "=?"; 10742 final String[] selectionArgs = new String[]{path}; 10743 FileAccessAttributes fileAccessAttributes; 10744 try (final Cursor c = queryForSingleItemAsMediaProvider(contentUri, projection, 10745 selection, 10746 selectionArgs, null)) { 10747 fileAccessAttributes = FileAccessAttributes.fromCursor(c); 10748 } 10749 Trace.endSection(); 10750 return fileAccessAttributes; 10751 } 10752 10753 private void checkIfFileOpenIsPermitted(String path, 10754 FileAccessAttributes fileAccessAttributes, String redactedUriId, 10755 boolean forWrite) throws FileNotFoundException { 10756 final File file = new File(path); 10757 Uri fileUri = MediaStore.Files.getContentUri(extractVolumeName(path), 10758 fileAccessAttributes.getId()); 10759 // We don't check ownership for files with IS_PENDING set by FUSE 10760 // Please note that even if ownerPackageName is null, the check below will throw an 10761 // IllegalStateException 10762 if (fileAccessAttributes.isTrashed() || (fileAccessAttributes.isPending() 10763 && !isPendingFromFuse(new File(path)))) { 10764 requireOwnershipForItem(fileAccessAttributes.getOwnerPackageName(), fileUri); 10765 } 10766 10767 // Check that path looks consistent before uri checks 10768 if (!FileUtils.contains(Environment.getStorageDirectory(), file)) { 10769 checkWorldReadAccess(file.getAbsolutePath()); 10770 } 10771 10772 try { 10773 // checkAccess throws FileNotFoundException only from checkWorldReadAccess(), 10774 // which we already check above. Hence, handling only SecurityException. 10775 if (redactedUriId != null) { 10776 fileUri = ContentUris.removeId(fileUri).buildUpon().appendPath( 10777 redactedUriId).build(); 10778 } 10779 checkAccess(fileUri, Bundle.EMPTY, file, forWrite); 10780 } catch (SecurityException e) { 10781 // Check for other Uri formats only when the single uri check flow fails. 10782 // Throw the previous exception if the multi-uri checks failed. 10783 final String uriId = redactedUriId == null 10784 ? Long.toString(fileAccessAttributes.getId()) : redactedUriId; 10785 if (getOtherUriGrantsForPath(path, fileAccessAttributes.getMediaType(), 10786 uriId, forWrite) == null) { 10787 throw e; 10788 } 10789 } 10790 } 10791 10792 10793 /** 10794 * Checks if the app identified by the given UID is allowed to open the given file for the given 10795 * access mode. 10796 * 10797 * @param path the path of the file to be opened 10798 * @param uid UID of the app requesting to open the file 10799 * @param forWrite specifies if the file is to be opened for write 10800 * @return {@link FileOpenResult} with {@code status} {@code 0} upon success and 10801 * {@link FileOpenResult} with {@code status} {@link OsConstants#EACCES} if the operation is 10802 * illegal or not permitted for the given {@code uid} or if the calling package is a legacy app 10803 * that doesn't have right storage permission. 10804 * 10805 * Called from JNI in jni/MediaProviderWrapper.cpp 10806 */ 10807 @Keep 10808 public FileOpenResult onFileOpenForFuse(String path, String ioPath, int uid, int tid, 10809 int transformsReason, boolean forWrite, boolean redact, boolean logTransformsMetrics) { 10810 final LocalCallingIdentity token = 10811 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 10812 10813 PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path); 10814 10815 boolean isSuccess = false; 10816 10817 final int originalUid = getBinderUidForFuse(uid, tid); 10818 // Use MediaProvider's own ID here since the caller may be cross profile. 10819 final int userId = UserHandle.myUserId(); 10820 int mediaCapabilitiesUid = 0; 10821 final PendingOpenInfo pendingOpenInfo; 10822 synchronized (mPendingOpenInfo) { 10823 pendingOpenInfo = mPendingOpenInfo.get(tid); 10824 } 10825 10826 if (pendingOpenInfo != null && pendingOpenInfo.uid == originalUid) { 10827 mediaCapabilitiesUid = pendingOpenInfo.mediaCapabilitiesUid; 10828 } 10829 10830 try { 10831 boolean forceRedaction = false; 10832 String redactedUriId = null; 10833 if (isSyntheticPath(path, userId)) { 10834 if (forWrite) { 10835 // Synthetic URIs are not allowed to update EXIF headers. 10836 return new FileOpenResult(OsConstants.EACCES /* status */, originalUid, 10837 mediaCapabilitiesUid, new long[0]); 10838 } 10839 10840 if (isRedactedPath(path, userId)) { 10841 redactedUriId = extractFileName(path); 10842 10843 // If path is redacted Uris' path, ioPath must be the real path, ioPath must 10844 // haven been updated to the real path during onFileLookupForFuse. 10845 path = ioPath; 10846 10847 // Irrespective of the permissions we want to redact in this case. 10848 redact = true; 10849 forceRedaction = true; 10850 } else if (isPickerPath(path, userId)) { 10851 return handlePickerFileOpen(path, originalUid); 10852 } else { 10853 // we don't support any other transformations under .transforms/synthetic dir 10854 return new FileOpenResult(OsConstants.ENOENT /* status */, originalUid, 10855 mediaCapabilitiesUid, new long[0]); 10856 } 10857 } 10858 10859 if (isPrivatePackagePathNotAccessibleByCaller(path)) { 10860 Log.e(TAG, "Can't open a file in another app's external directory!"); 10861 return new FileOpenResult(OsConstants.ENOENT, originalUid, mediaCapabilitiesUid, 10862 new long[0]); 10863 } 10864 10865 if (shouldBypassFuseRestrictions(forWrite, path)) { 10866 isSuccess = true; 10867 return new FileOpenResult(0 /* status */, originalUid, mediaCapabilitiesUid, 10868 redact ? getRedactionRangesForFuse(path, ioPath, originalUid, uid, tid, 10869 forceRedaction) : new long[0]); 10870 } 10871 // Legacy apps that made is this far don't have the right storage permission and hence 10872 // are not allowed to access anything other than their external app directory 10873 if (isCallingPackageRequestingLegacy()) { 10874 return new FileOpenResult(OsConstants.EACCES /* status */, originalUid, 10875 mediaCapabilitiesUid, new long[0]); 10876 } 10877 10878 checkIfFileOpenIsPermitted(path, queryForFileAttributes(path), redactedUriId, forWrite); 10879 isSuccess = true; 10880 return new FileOpenResult(0 /* status */, originalUid, mediaCapabilitiesUid, 10881 redact ? getRedactionRangesForFuse(path, ioPath, originalUid, uid, tid, 10882 forceRedaction) : new long[0]); 10883 } catch (IOException e) { 10884 // We are here because 10885 // * There is no db row corresponding to the requested path, which is more unlikely. 10886 // * getRedactionRangesForFuse couldn't fetch the redaction info correctly 10887 // In all of these cases, it means that app doesn't have access permission to the file. 10888 Log.e(TAG, "Couldn't find file: " + path, e); 10889 return new FileOpenResult(OsConstants.EACCES /* status */, originalUid, 10890 mediaCapabilitiesUid, new long[0]); 10891 } catch (IllegalStateException | SecurityException e) { 10892 Log.e(TAG, "Permission to access file: " + path + " is denied"); 10893 return new FileOpenResult(OsConstants.EACCES /* status */, originalUid, 10894 mediaCapabilitiesUid, new long[0]); 10895 } finally { 10896 if (isSuccess && logTransformsMetrics) { 10897 notifyTranscodeHelperOnFileOpen(path, ioPath, originalUid, transformsReason); 10898 } 10899 restoreLocalCallingIdentity(token); 10900 } 10901 } 10902 10903 @Nullable 10904 private Uri getOtherUriGrantsForPath(String path, boolean forWrite) { 10905 final Uri contentUri = FileUtils.getContentUriForPath(path); 10906 final String[] projection = new String[]{ 10907 MediaColumns._ID, 10908 FileColumns.MEDIA_TYPE}; 10909 final String selection = MediaColumns.DATA + "=?"; 10910 final String[] selectionArgs = new String[]{path}; 10911 final String id; 10912 final int mediaType; 10913 try (final Cursor c = queryForSingleItemAsMediaProvider(contentUri, projection, selection, 10914 selectionArgs, null)) { 10915 id = c.getString(0); 10916 mediaType = c.getInt(1); 10917 return getOtherUriGrantsForPath(path, mediaType, id, forWrite); 10918 } catch (FileNotFoundException ignored) { 10919 } 10920 return null; 10921 } 10922 10923 @Nullable 10924 private Uri getOtherUriGrantsForPath(String path, int mediaType, String id, boolean forWrite) { 10925 List<Uri> otherUris = new ArrayList<>(); 10926 final Uri mediaUri = getMediaUriForFuse(extractVolumeName(path), mediaType, id); 10927 otherUris.add(mediaUri); 10928 final Uri externalMediaUri = getMediaUriForFuse(MediaStore.VOLUME_EXTERNAL, mediaType, id); 10929 otherUris.add(externalMediaUri); 10930 return getPermissionGrantedUri(otherUris, forWrite); 10931 } 10932 10933 @NonNull 10934 private Uri getMediaUriForFuse(@NonNull String volumeName, int mediaType, String id) { 10935 Uri uri = MediaStore.Files.getContentUri(volumeName); 10936 switch (mediaType) { 10937 case FileColumns.MEDIA_TYPE_IMAGE: 10938 uri = MediaStore.Images.Media.getContentUri(volumeName); 10939 break; 10940 case FileColumns.MEDIA_TYPE_VIDEO: 10941 uri = MediaStore.Video.Media.getContentUri(volumeName); 10942 break; 10943 case FileColumns.MEDIA_TYPE_AUDIO: 10944 uri = MediaStore.Audio.Media.getContentUri(volumeName); 10945 break; 10946 case FileColumns.MEDIA_TYPE_PLAYLIST: 10947 uri = MediaStore.Audio.Playlists.getContentUri(volumeName); 10948 break; 10949 } 10950 10951 return uri.buildUpon().appendPath(id).build(); 10952 } 10953 10954 /** 10955 * Returns {@code true} if {@link #mCallingIdentity#getSharedPackageNamesList(String)} contains 10956 * the given package name, {@code false} otherwise. 10957 * <p> Assumes that {@code mCallingIdentity} has been properly set to reflect the calling 10958 * package. 10959 */ 10960 private boolean isCallingIdentitySharedPackageName(@NonNull String packageName) { 10961 for (String sharedPkgName : mCallingIdentity.get().getSharedPackageNamesArray()) { 10962 if (packageName.toLowerCase(Locale.ROOT) 10963 .equals(sharedPkgName.toLowerCase(Locale.ROOT))) { 10964 return true; 10965 } 10966 } 10967 return false; 10968 } 10969 10970 /** 10971 * @throws IllegalStateException if path is invalid or doesn't match a volume. 10972 */ 10973 @NonNull 10974 private Uri getContentUriForFile(@NonNull String filePath, @NonNull String mimeType) { 10975 final String volName; 10976 try { 10977 volName = FileUtils.getVolumeName(getContext(), new File(filePath)); 10978 } catch (FileNotFoundException e) { 10979 throw new IllegalStateException("Couldn't get volume name for " + filePath); 10980 } 10981 Uri uri = Files.getContentUri(volName); 10982 String topLevelDir = extractTopLevelDir(filePath); 10983 if (topLevelDir == null) { 10984 // If the file path doesn't match the external storage directory, we use the files URI 10985 // as default and let #insert enforce the restrictions 10986 return uri; 10987 } 10988 topLevelDir = topLevelDir.toLowerCase(Locale.ROOT); 10989 10990 switch (topLevelDir) { 10991 case DIRECTORY_PODCASTS_LOWER_CASE: 10992 case DIRECTORY_RINGTONES_LOWER_CASE: 10993 case DIRECTORY_ALARMS_LOWER_CASE: 10994 case DIRECTORY_NOTIFICATIONS_LOWER_CASE: 10995 case DIRECTORY_AUDIOBOOKS_LOWER_CASE: 10996 case DIRECTORY_RECORDINGS_LOWER_CASE: 10997 uri = Audio.Media.getContentUri(volName); 10998 break; 10999 case DIRECTORY_MUSIC_LOWER_CASE: 11000 if (MimeUtils.isPlaylistMimeType(mimeType)) { 11001 uri = Audio.Playlists.getContentUri(volName); 11002 } else if (!MimeUtils.isSubtitleMimeType(mimeType)) { 11003 // Send Files uri for media type subtitle 11004 uri = Audio.Media.getContentUri(volName); 11005 } 11006 break; 11007 case DIRECTORY_MOVIES_LOWER_CASE: 11008 if (MimeUtils.isPlaylistMimeType(mimeType)) { 11009 uri = Audio.Playlists.getContentUri(volName); 11010 } else if (!MimeUtils.isSubtitleMimeType(mimeType)) { 11011 // Send Files uri for media type subtitle 11012 uri = Video.Media.getContentUri(volName); 11013 } 11014 break; 11015 case DIRECTORY_DCIM_LOWER_CASE: 11016 case DIRECTORY_PICTURES_LOWER_CASE: 11017 if (MimeUtils.isImageMimeType(mimeType)) { 11018 uri = Images.Media.getContentUri(volName); 11019 } else { 11020 uri = Video.Media.getContentUri(volName); 11021 } 11022 break; 11023 case DIRECTORY_DOWNLOADS_LOWER_CASE: 11024 case DIRECTORY_DOCUMENTS_LOWER_CASE: 11025 break; 11026 default: 11027 Log.w(TAG, "Forgot to handle a top level directory in getContentUriForFile?"); 11028 } 11029 return uri; 11030 } 11031 11032 private boolean containsIgnoreCase(@Nullable List<String> stringsList, @Nullable String item) { 11033 if (item == null || stringsList == null) return false; 11034 11035 for (String current : stringsList) { 11036 if (item.equalsIgnoreCase(current)) return true; 11037 } 11038 return false; 11039 } 11040 11041 private boolean fileExists(@NonNull String absolutePath) { 11042 // We don't care about specific columns in the match, 11043 // we just want to check IF there's a match 11044 final String[] projection = {}; 11045 final String selection = FileColumns.DATA + " = ?"; 11046 final String[] selectionArgs = {absolutePath}; 11047 final Uri uri = FileUtils.getContentUriForPath(absolutePath); 11048 11049 final LocalCallingIdentity token = clearLocalCallingIdentity(); 11050 try { 11051 try (final Cursor c = query(uri, projection, selection, selectionArgs, null)) { 11052 // Shouldn't return null 11053 return c.getCount() > 0; 11054 } 11055 } finally { 11056 clearLocalCallingIdentity(token); 11057 } 11058 } 11059 11060 private Uri insertFileForFuse(@NonNull String path, @NonNull Uri uri, @NonNull String mimeType, 11061 boolean useData) { 11062 ContentValues values = new ContentValues(); 11063 values.put(FileColumns.OWNER_PACKAGE_NAME, getCallingPackageOrSelf()); 11064 values.put(MediaColumns.MIME_TYPE, mimeType); 11065 values.put(FileColumns.IS_PENDING, 1); 11066 11067 int userIdFromPath = FileUtils.extractUserId(path); 11068 11069 if (useData) { 11070 values.put(FileColumns.DATA, path); 11071 } else { 11072 values.put(FileColumns.VOLUME_NAME, extractVolumeName(path)); 11073 values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path)); 11074 values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path)); 11075 // In some cases when clone profile is active, this userId can be used to determine 11076 // the path to be saved in MP database. 11077 // We do this only if the path contains a valid user-id and any such value set is 11078 // only a hint, the actual userId set will be determined later. 11079 if (userIdFromPath != -1) { 11080 values.put(FileColumns._USER_ID, userIdFromPath); 11081 } 11082 } 11083 return insert(uri, values, Bundle.EMPTY); 11084 } 11085 11086 /** 11087 * Enforces file creation restrictions (see return values) for the given file on behalf of the 11088 * app with the given {@code uid}. If the file is added to the shared storage, creates a 11089 * database entry for it. 11090 * <p> Does NOT create file. 11091 * 11092 * @param path the path of the file 11093 * @param uid UID of the app requesting to create the file 11094 * @return In case of success, 0. If the operation is illegal or not permitted, returns the 11095 * appropriate {@code errno} value: 11096 * <ul> 11097 * <li>{@link OsConstants#ENOENT} if the app tries to create file in other app's external dir 11098 * <li>{@link OsConstants#EEXIST} if the file already exists 11099 * <li>{@link OsConstants#EPERM} if the file type doesn't match the relative path, or if the 11100 * calling package is a legacy app that doesn't have WRITE_EXTERNAL_STORAGE permission. 11101 * <li>{@link OsConstants#EIO} in case of any other I/O exception 11102 * </ul> 11103 * 11104 * @throws IllegalStateException if given path is invalid. 11105 * 11106 * Called from JNI in jni/MediaProviderWrapper.cpp 11107 */ 11108 @Keep 11109 public int insertFileIfNecessaryForFuse(@NonNull String path, int uid) { 11110 final LocalCallingIdentity token = 11111 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 11112 PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path); 11113 11114 try { 11115 if (isPrivatePackagePathNotAccessibleByCaller(path)) { 11116 Log.e(TAG, "Can't create a file in another app's external directory"); 11117 return OsConstants.ENOENT; 11118 } 11119 11120 if (!path.equals(getAbsoluteSanitizedPath(path))) { 11121 Log.e(TAG, "File name contains invalid characters"); 11122 return OsConstants.EPERM; 11123 } 11124 11125 if (shouldBypassDatabaseAndSetDirtyForFuse(uid, path)) { 11126 if (path.endsWith("/.nomedia")) { 11127 File parent = new File(path).getParentFile(); 11128 synchronized (mNonHiddenPaths) { 11129 mNonHiddenPaths.keySet().removeIf( 11130 k -> FileUtils.contains(parent, new File(k))); 11131 } 11132 } 11133 return 0; 11134 } 11135 11136 final String mimeType = MimeUtils.resolveMimeType(new File(path)); 11137 11138 if (shouldBypassFuseRestrictions(/* forWrite */ true, path)) { 11139 final boolean callerRequestingLegacy = isCallingPackageRequestingLegacy(); 11140 if (!fileExists(path)) { 11141 // If app has already inserted the db row, inserting the row again might set 11142 // IS_PENDING=1. We shouldn't overwrite existing entry as part of FUSE 11143 // operation, hence, insert the db row only when it doesn't exist. 11144 try { 11145 insertFileForFuse(path, FileUtils.getContentUriForPath(path), 11146 mimeType, /* useData */ callerRequestingLegacy); 11147 } catch (Exception ignored) { 11148 } 11149 } else { 11150 // Upon creating a file via FUSE, if a row matching the path already exists 11151 // but a file doesn't exist on the filesystem, we transfer ownership to the 11152 // app attempting to create the file. If we don't update ownership, then the 11153 // app that inserted the original row may be able to observe the contents of 11154 // written file even though they don't hold the right permissions to do so. 11155 if (callerRequestingLegacy) { 11156 final String owner = getCallingPackageOrSelf(); 11157 if (owner != null && !updateOwnerForPath(path, owner)) { 11158 return OsConstants.EPERM; 11159 } 11160 } 11161 } 11162 11163 return 0; 11164 } 11165 11166 // Legacy apps that made is this far don't have the right storage permission and hence 11167 // are not allowed to access anything other than their external app directory 11168 if (isCallingPackageRequestingLegacy()) { 11169 return OsConstants.EPERM; 11170 } 11171 11172 if (fileExists(path)) { 11173 // If the file already exists in the db, we shouldn't allow the file creation. 11174 return OsConstants.EEXIST; 11175 } 11176 11177 final Uri contentUri = getContentUriForFile(path, mimeType); 11178 final Uri item = insertFileForFuse(path, contentUri, mimeType, /* useData */ false); 11179 if (item == null) { 11180 return OsConstants.EPERM; 11181 } 11182 return 0; 11183 } catch (IllegalArgumentException e) { 11184 Log.e(TAG, "insertFileIfNecessary failed", e); 11185 return OsConstants.EPERM; 11186 } finally { 11187 restoreLocalCallingIdentity(token); 11188 } 11189 } 11190 11191 private boolean updateOwnerForPath(@NonNull String path, @NonNull String newOwner) { 11192 final DatabaseHelper helper; 11193 try { 11194 helper = getDatabaseForUri(FileUtils.getContentUriForPath(path)); 11195 } catch (VolumeNotFoundException e) { 11196 // Cannot happen, as this is a path that we already resolved. 11197 throw new AssertionError("Path must already be resolved", e); 11198 } 11199 11200 ContentValues values = new ContentValues(1); 11201 values.put(FileColumns.OWNER_PACKAGE_NAME, newOwner); 11202 11203 return helper.runWithoutTransaction( 11204 (db) -> db.update("files", values, "_data=?", new String[] { path })) == 1; 11205 } 11206 11207 private int deleteFileUnchecked(@NonNull String path, 11208 LocalCallingIdentity localCallingIdentity) { 11209 final File toDelete = new File(path); 11210 if (toDelete.delete()) { 11211 final int mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(toDelete)); 11212 localCallingIdentity.incrementDeletedFileCountBypassingDatabase(mediaType); 11213 return 0; 11214 } else { 11215 return OsConstants.ENOENT; 11216 } 11217 } 11218 11219 /** 11220 * Deletes file with the given {@code path} on behalf of the app with the given {@code uid}. 11221 * <p>Before deleting, checks if app has permissions to delete this file. 11222 * 11223 * @param path the path of the file 11224 * @param uid UID of the app requesting to delete the file 11225 * @return 0 upon success. 11226 * In case of error, return the appropriate negated {@code errno} value: 11227 * <ul> 11228 * <li>{@link OsConstants#ENOENT} if the file does not exist or if the app tries to delete file 11229 * in another app's external dir 11230 * <li>{@link OsConstants#EPERM} a security exception was thrown by {@link #delete}, or if the 11231 * calling package is a legacy app that doesn't have WRITE_EXTERNAL_STORAGE permission. 11232 * </ul> 11233 * 11234 * Called from JNI in jni/MediaProviderWrapper.cpp 11235 */ 11236 @Keep 11237 public int deleteFileForFuse(@NonNull String path, int uid) throws IOException { 11238 final LocalCallingIdentity localCallingIdentity = getCachedCallingIdentityForFuse(uid); 11239 final LocalCallingIdentity token = clearLocalCallingIdentity(localCallingIdentity); 11240 PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path); 11241 11242 try { 11243 if (isPrivatePackagePathNotAccessibleByCaller(path)) { 11244 Log.e(TAG, "Can't delete a file in another app's external directory!"); 11245 return OsConstants.ENOENT; 11246 } 11247 11248 if (shouldBypassDatabaseAndSetDirtyForFuse(uid, path)) { 11249 return deleteFileUnchecked(path, localCallingIdentity); 11250 } 11251 11252 final boolean shouldBypass = shouldBypassFuseRestrictions(/*forWrite*/ true, path); 11253 11254 // Legacy apps that made is this far don't have the right storage permission and hence 11255 // are not allowed to access anything other than their external app directory 11256 if (!shouldBypass && isCallingPackageRequestingLegacy()) { 11257 return OsConstants.EPERM; 11258 } 11259 11260 final Uri contentUri = FileUtils.getContentUriForPath(path); 11261 final String where = FileColumns.DATA + " = ?"; 11262 final String[] whereArgs = {path}; 11263 11264 if (delete(contentUri, where, whereArgs) == 0) { 11265 if (shouldBypass) { 11266 return deleteFileUnchecked(path, localCallingIdentity); 11267 } 11268 return OsConstants.ENOENT; 11269 } else { 11270 // success - 1 file was deleted 11271 return 0; 11272 } 11273 11274 } catch (SecurityException e) { 11275 Log.e(TAG, "File deletion not allowed", e); 11276 return OsConstants.EPERM; 11277 } finally { 11278 restoreLocalCallingIdentity(token); 11279 } 11280 } 11281 11282 // These need to stay in sync with MediaProviderWrapper.cpp's DirectoryAccessRequestType enum 11283 @IntDef(flag = true, prefix = { "DIRECTORY_ACCESS_FOR_" }, value = { 11284 DIRECTORY_ACCESS_FOR_READ, 11285 DIRECTORY_ACCESS_FOR_WRITE, 11286 DIRECTORY_ACCESS_FOR_CREATE, 11287 DIRECTORY_ACCESS_FOR_DELETE, 11288 }) 11289 @Retention(RetentionPolicy.SOURCE) 11290 @VisibleForTesting 11291 @interface DirectoryAccessType {} 11292 11293 @VisibleForTesting 11294 static final int DIRECTORY_ACCESS_FOR_READ = 1; 11295 11296 @VisibleForTesting 11297 static final int DIRECTORY_ACCESS_FOR_WRITE = 2; 11298 11299 @VisibleForTesting 11300 static final int DIRECTORY_ACCESS_FOR_CREATE = 3; 11301 11302 @VisibleForTesting 11303 static final int DIRECTORY_ACCESS_FOR_DELETE = 4; 11304 11305 /** 11306 * Checks whether the app with the given UID is allowed to access the directory denoted by the 11307 * given path. 11308 * 11309 * @param path directory's path 11310 * @param uid UID of the requesting app 11311 * @param accessType type of access being requested - eg {@link 11312 * MediaProvider#DIRECTORY_ACCESS_FOR_READ} 11313 * @return 0 if it's allowed to access the directory, {@link OsConstants#ENOENT} for attempts 11314 * to access a private package path in Android/data or Android/obb the caller doesn't have 11315 * access to, and otherwise {@link OsConstants#EACCES} if the calling package is a legacy app 11316 * that doesn't have READ_EXTERNAL_STORAGE permission or for other invalid attempts to access 11317 * Android/data or Android/obb dirs. 11318 * 11319 * Called from JNI in jni/MediaProviderWrapper.cpp 11320 */ 11321 @Keep 11322 public int isDirAccessAllowedForFuse(@NonNull String path, int uid, 11323 @DirectoryAccessType int accessType) { 11324 Preconditions.checkArgumentInRange(accessType, 1, DIRECTORY_ACCESS_FOR_DELETE, 11325 "accessType"); 11326 11327 final boolean forRead = accessType == DIRECTORY_ACCESS_FOR_READ; 11328 final LocalCallingIdentity token = 11329 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 11330 PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path); 11331 try { 11332 if ("/storage/emulated".equals(path)) { 11333 return OsConstants.EPERM; 11334 } 11335 if (isPrivatePackagePathNotAccessibleByCaller(path)) { 11336 Log.e(TAG, "Can't access another app's external directory!"); 11337 return OsConstants.ENOENT; 11338 } 11339 11340 if (shouldBypassFuseRestrictions(/* forWrite= */ !forRead, path)) { 11341 return 0; 11342 } 11343 11344 // Do not allow apps that reach this point to access Android/data or Android/obb dirs. 11345 // Creation should be via getContext().getExternalFilesDir() etc methods. 11346 // Reads and writes on primary volumes should be via mount views of lowerfs for apps 11347 // that get special access to these directories. 11348 // Reads and writes on secondary volumes would be provided via an early return from 11349 // shouldBypassFuseRestrictions above (again just for apps with special access). 11350 if (isDataOrObbPath(path)) { 11351 return OsConstants.EACCES; 11352 } 11353 11354 // Legacy apps that made is this far don't have the right storage permission and hence 11355 // are not allowed to access anything other than their external app directory 11356 if (isCallingPackageRequestingLegacy()) { 11357 return OsConstants.EACCES; 11358 } 11359 // This is a non-legacy app. Rest of the directories are generally writable 11360 // except for non-default top-level directories. 11361 if (!forRead) { 11362 final String[] relativePath = sanitizePath(extractRelativePath(path)); 11363 if (relativePath.length == 0) { 11364 Log.e(TAG, 11365 "Directory update not allowed on invalid relative path for " + path); 11366 return OsConstants.EPERM; 11367 } 11368 final boolean isTopLevelDir = 11369 relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]); 11370 if (isTopLevelDir) { 11371 // We don't allow deletion of any top-level folders 11372 if (accessType == DIRECTORY_ACCESS_FOR_DELETE) { 11373 Log.e(TAG, "Deleting top level directories are not allowed!"); 11374 return OsConstants.EACCES; 11375 } 11376 11377 // We allow creating or writing to default top-level folders, but we don't 11378 // allow creation or writing to non-default top-level folders. 11379 if ((accessType == DIRECTORY_ACCESS_FOR_CREATE 11380 || accessType == DIRECTORY_ACCESS_FOR_WRITE) 11381 && FileUtils.isDefaultDirectoryName(extractDisplayName(path))) { 11382 return 0; 11383 } 11384 11385 Log.e(TAG, 11386 "Creating or writing to a non-default top level directory is not " 11387 + "allowed!"); 11388 return OsConstants.EACCES; 11389 } 11390 } 11391 11392 return 0; 11393 } finally { 11394 restoreLocalCallingIdentity(token); 11395 } 11396 } 11397 11398 @Keep 11399 public boolean isUidAllowedAccessToDataOrObbPathForFuse(int uid, String path) { 11400 final LocalCallingIdentity token = 11401 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 11402 try { 11403 return isCallingIdentityAllowedAccessToDataOrObbPath( 11404 extractRelativePathWithDisplayName(path)); 11405 } finally { 11406 restoreLocalCallingIdentity(token); 11407 } 11408 } 11409 11410 private boolean isCallingIdentityAllowedAccessToDataOrObbPath(String relativePath) { 11411 // Files under the apps own private directory 11412 final String appSpecificDir = extractOwnerPackageNameFromRelativePath(relativePath); 11413 11414 if (appSpecificDir != null && isCallingIdentitySharedPackageName(appSpecificDir)) { 11415 return true; 11416 } 11417 // This is a private-package relativePath; return true if accessible by the caller 11418 return isCallingIdentityAllowedSpecialPrivatePathAccess(relativePath); 11419 } 11420 11421 /** 11422 * @return true iff the caller has installer privileges which gives write access to obb dirs. 11423 * 11424 * @deprecated This method should only be called for Android R. For Android S+, please use 11425 * {@link StorageManager#getExternalStorageMountMode} to check if the caller has 11426 * {@link StorageManager#MOUNT_MODE_EXTERNAL_INSTALLER} access. 11427 * 11428 * Note: WRITE_EXTERNAL_STORAGE permission should ideally not be requested by non-legacy apps. 11429 * But to be consistent with {@link StorageManager} check for Installer apps access for primary 11430 * volumes in Android R, we do not add non-legacy apps check here as well. 11431 */ 11432 @Deprecated 11433 private boolean isCallingIdentityAllowedInstallerAccess() { 11434 final boolean hasWrite = mCallingIdentity.get(). 11435 hasPermission(PERMISSION_WRITE_EXTERNAL_STORAGE); 11436 11437 if (!hasWrite) { 11438 return false; 11439 } 11440 11441 // We're only willing to give out installer access if they also hold 11442 // runtime permission; this is a firm CDD requirement 11443 final boolean hasInstall = mCallingIdentity.get(). 11444 hasPermission(PERMISSION_INSTALL_PACKAGES); 11445 11446 if (hasInstall) { 11447 return true; 11448 } 11449 // OPSTR_REQUEST_INSTALL_PACKAGES is granted/denied per package but vold can't 11450 // update mountpoints of a specific package. So, check the appop for all packages 11451 // sharing the uid and allow same level of storage access for all packages even if 11452 // one of the packages has the appop granted. 11453 // To maintain consistency of access in primary volume and secondary volumes use the same 11454 // logic as we do for Zygote.MOUNT_EXTERNAL_INSTALLER view. 11455 return mCallingIdentity.get().hasPermission(APPOP_REQUEST_INSTALL_PACKAGES_FOR_SHARED_UID); 11456 } 11457 11458 private String getExternalStorageProviderAuthority() { 11459 if (SdkLevel.isAtLeastS()) { 11460 return getExternalStorageProviderAuthorityFromDocumentsContract(); 11461 } 11462 return MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY; 11463 } 11464 11465 @RequiresApi(Build.VERSION_CODES.S) 11466 private String getExternalStorageProviderAuthorityFromDocumentsContract() { 11467 return DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY; 11468 } 11469 11470 private String getDownloadsProviderAuthority() { 11471 if (SdkLevel.isAtLeastS()) { 11472 return getDownloadsProviderAuthorityFromDocumentsContract(); 11473 } 11474 return DOWNLOADS_PROVIDER_AUTHORITY; 11475 } 11476 11477 @RequiresApi(Build.VERSION_CODES.S) 11478 private String getDownloadsProviderAuthorityFromDocumentsContract() { 11479 return DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY; 11480 } 11481 11482 private boolean isCallingIdentityDownloadProvider() { 11483 return UserHandle.getAppId(getCallingUidOrSelf()) == mDownloadsAuthorityAppId; 11484 } 11485 11486 private boolean isCallingIdentityExternalStorageProvider() { 11487 return UserHandle.getAppId(getCallingUidOrSelf()) == mExternalStorageAuthorityAppId; 11488 } 11489 11490 private boolean isCallingIdentityMtp() { 11491 return mCallingIdentity.get().hasPermission(PERMISSION_ACCESS_MTP); 11492 } 11493 11494 /** 11495 * The following apps have access to all private-app directories on secondary volumes: 11496 * * ExternalStorageProvider 11497 * * DownloadProvider 11498 * * Signature apps with ACCESS_MTP permission granted 11499 * (Note: For Android R we also allow privileged apps with ACCESS_MTP to access all 11500 * private-app directories, this additional access is removed for Android S+). 11501 * 11502 * Installer apps can only access private-app directories on Android/obb. 11503 * 11504 * @param relativePath the relative path of the file to access 11505 */ 11506 private boolean isCallingIdentityAllowedSpecialPrivatePathAccess(String relativePath) { 11507 if (SdkLevel.isAtLeastS()) { 11508 return isMountModeAllowedPrivatePathAccess(getCallingUidOrSelf(), getCallingPackage(), 11509 relativePath); 11510 } else { 11511 if (isCallingIdentityDownloadProvider() || 11512 isCallingIdentityExternalStorageProvider() || isCallingIdentityMtp()) { 11513 return true; 11514 } 11515 return (isObbOrChildRelativePath(relativePath) && 11516 isCallingIdentityAllowedInstallerAccess()); 11517 } 11518 } 11519 11520 @RequiresApi(Build.VERSION_CODES.S) 11521 private boolean isMountModeAllowedPrivatePathAccess(int uid, String packageName, 11522 String relativePath) { 11523 // This is required as only MediaProvider (package with WRITE_MEDIA_STORAGE) can access 11524 // mount modes. 11525 final CallingIdentity token = clearCallingIdentity(); 11526 try { 11527 final int mountMode = mStorageManager.getExternalStorageMountMode(uid, packageName); 11528 switch (mountMode) { 11529 case StorageManager.MOUNT_MODE_EXTERNAL_ANDROID_WRITABLE: 11530 case StorageManager.MOUNT_MODE_EXTERNAL_PASS_THROUGH: 11531 return true; 11532 case StorageManager.MOUNT_MODE_EXTERNAL_INSTALLER: 11533 return isObbOrChildRelativePath(relativePath); 11534 } 11535 } catch (Exception e) { 11536 Log.w(TAG, "Caller does not have the permissions to access mount modes: ", e); 11537 } finally { 11538 restoreCallingIdentity(token); 11539 } 11540 return false; 11541 } 11542 11543 private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) { 11544 // System internals can work with all media 11545 if (isCallingPackageSelf() || isCallingPackageShell()) { 11546 return true; 11547 } 11548 11549 // Apps that have permission to manage external storage can work with all files 11550 if (isCallingPackageManager()) { 11551 return true; 11552 } 11553 11554 // Check if caller is known to be owner of this item, to speed up 11555 // performance of our permission checks 11556 final int table = matchUri(uri, true); 11557 switch (table) { 11558 case AUDIO_MEDIA_ID: 11559 case VIDEO_MEDIA_ID: 11560 case IMAGES_MEDIA_ID: 11561 case FILES_ID: 11562 case DOWNLOADS_ID: 11563 final long id = ContentUris.parseId(uri); 11564 if (mCallingIdentity.get().isOwned(id)) { 11565 return true; 11566 } 11567 break; 11568 default: 11569 // continue below 11570 } 11571 11572 // Check whether the uri is a specific table or not. Don't allow the global access to these 11573 // table uris 11574 switch (table) { 11575 case AUDIO_MEDIA: 11576 case IMAGES_MEDIA: 11577 case VIDEO_MEDIA: 11578 case DOWNLOADS: 11579 case FILES: 11580 case AUDIO_ALBUMS: 11581 case AUDIO_ARTISTS: 11582 case AUDIO_GENRES: 11583 case AUDIO_PLAYLISTS: 11584 return false; 11585 default: 11586 // continue below 11587 } 11588 11589 // Outstanding grant means they get access 11590 return isUriPermissionGranted(uri, forWrite); 11591 } 11592 11593 /** 11594 * Returns any uri that is granted from the set of Uris passed. 11595 */ 11596 @Nullable 11597 private Uri getPermissionGrantedUri(@NonNull List<Uri> uris, boolean forWrite) { 11598 for (Uri uri : uris) { 11599 if (isUriPermissionGranted(uri, forWrite)) { 11600 return uri; 11601 } 11602 } 11603 return null; 11604 } 11605 11606 private boolean isUriPermissionGranted(Uri uri, boolean forWrite) { 11607 final int modeFlags = forWrite 11608 ? Intent.FLAG_GRANT_WRITE_URI_PERMISSION 11609 : Intent.FLAG_GRANT_READ_URI_PERMISSION; 11610 int uriPermission = getContext().checkUriPermission(uri, mCallingIdentity.get().pid, 11611 mCallingIdentity.get().uid, modeFlags); 11612 return uriPermission == PERMISSION_GRANTED; 11613 } 11614 11615 @VisibleForTesting 11616 public boolean isFuseThread() { 11617 return FuseDaemon.native_is_fuse_thread(); 11618 } 11619 11620 11621 /** 11622 * Enforce that caller has access to the given {@link Uri}. 11623 * 11624 * @throws SecurityException if access isn't allowed. 11625 */ 11626 @VisibleForTesting 11627 protected void enforceCallingPermission(@NonNull Uri uri, @NonNull Bundle extras, 11628 boolean forWrite) { 11629 Trace.beginSection("MP.enforceCallingPermission"); 11630 try { 11631 enforceCallingPermissionInternal(uri, extras, forWrite); 11632 } finally { 11633 Trace.endSection(); 11634 } 11635 } 11636 11637 private void enforceCallingPermission(@NonNull Collection<Uri> uris, boolean forWrite) { 11638 for (Uri uri : uris) { 11639 enforceCallingPermission(uri, Bundle.EMPTY, forWrite); 11640 } 11641 } 11642 11643 private void enforceCallingPermissionInternal(@NonNull Uri uri, @NonNull Bundle extras, 11644 boolean forWrite) { 11645 Objects.requireNonNull(uri); 11646 Objects.requireNonNull(extras); 11647 11648 // Try a simple global check first before falling back to performing a 11649 // simple query to probe for access. 11650 if (checkCallingPermissionGlobal(uri, forWrite)) { 11651 // Access allowed, yay! 11652 return; 11653 } 11654 11655 // For redacted URI proceed with its corresponding URI as query builder doesn't support 11656 // redacted URIs for fetching a database row 11657 // NOTE: The grants (if any) must have been on redacted URI hence global check requires 11658 // redacted URI 11659 Uri redactedUri = null; 11660 if (isRedactedUri(uri)) { 11661 redactedUri = uri; 11662 uri = getUriForRedactedUri(uri); 11663 } 11664 11665 final DatabaseHelper helper; 11666 try { 11667 helper = getDatabaseForUri(uri); 11668 } catch (VolumeNotFoundException e) { 11669 throw e.rethrowAsIllegalArgumentException(); 11670 } 11671 11672 final boolean allowHidden = isCallingPackageAllowedHidden(); 11673 final int table = matchUri(uri, allowHidden); 11674 11675 final String selection = extras.getString(QUERY_ARG_SQL_SELECTION); 11676 final String[] selectionArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS); 11677 11678 // First, check to see if caller has direct write access 11679 if (forWrite) { 11680 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, table, uri, extras, null); 11681 qb.allowColumn(SQLiteQueryBuilder.ROWID_COLUMN); 11682 try (Cursor c = qb.query(helper, new String[] { SQLiteQueryBuilder.ROWID_COLUMN }, 11683 selection, selectionArgs, null, null, null, null, null)) { 11684 if (c.moveToFirst()) { 11685 // Direct write access granted, yay! 11686 return; 11687 } 11688 } 11689 } 11690 11691 // We only allow the user to grant access to specific media items in 11692 // strongly typed collections; never to broad collections 11693 boolean allowUserGrant = false; 11694 final int matchUri = matchUri(uri, true); 11695 switch (matchUri) { 11696 case IMAGES_MEDIA_ID: 11697 case AUDIO_MEDIA_ID: 11698 case VIDEO_MEDIA_ID: 11699 allowUserGrant = true; 11700 break; 11701 } 11702 11703 // Second, check to see if caller has direct read access 11704 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, extras, null); 11705 qb.allowColumn(SQLiteQueryBuilder.ROWID_COLUMN); 11706 try (Cursor c = qb.query(helper, new String[] { SQLiteQueryBuilder.ROWID_COLUMN }, 11707 selection, selectionArgs, null, null, null, null, null)) { 11708 if (c.moveToFirst()) { 11709 if (!forWrite) { 11710 // Direct read access granted, yay! 11711 return; 11712 } else if (allowUserGrant) { 11713 // Caller has read access, but they wanted to write, and 11714 // they'll need to get the user to grant that access 11715 final Context context = getContext(); 11716 final Collection<Uri> uris = Collections.singletonList(uri); 11717 final PendingIntent intent = MediaStore 11718 .createWriteRequest(ContentResolver.wrap(this), uris); 11719 11720 final Icon icon = getCollectionIcon(uri); 11721 final RemoteAction action = new RemoteAction(icon, 11722 context.getText(R.string.permission_required_action), 11723 context.getText(R.string.permission_required_action), 11724 intent); 11725 11726 throw new RecoverableSecurityException(new SecurityException( 11727 getCallingPackageOrSelf() + " has no access to " + uri), 11728 context.getText(R.string.permission_required), action); 11729 } 11730 } 11731 } 11732 11733 if (redactedUri != null) uri = redactedUri; 11734 throw new SecurityException(getCallingPackageOrSelf() + " has no access to " + uri); 11735 } 11736 11737 private Icon getCollectionIcon(Uri uri) { 11738 final PackageManager pm = getContext().getPackageManager(); 11739 final String type = uri.getPathSegments().get(1); 11740 final String groupName; 11741 switch (type) { 11742 default: groupName = android.Manifest.permission_group.STORAGE; break; 11743 } 11744 try { 11745 final PermissionGroupInfo perm = pm.getPermissionGroupInfo(groupName, 0); 11746 return Icon.createWithResource(perm.packageName, perm.icon); 11747 } catch (NameNotFoundException e) { 11748 throw new RuntimeException(e); 11749 } 11750 } 11751 11752 private void checkAccess(@NonNull Uri uri, @NonNull Bundle extras, @NonNull File file, 11753 boolean isWrite) throws FileNotFoundException { 11754 // First, does caller have the needed row-level access? 11755 enforceCallingPermission(uri, extras, isWrite); 11756 11757 // Second, does the path look consistent? 11758 if (!FileUtils.contains(Environment.getStorageDirectory(), file)) { 11759 checkWorldReadAccess(file.getAbsolutePath()); 11760 } 11761 } 11762 11763 /** 11764 * Check whether the path is a world-readable file 11765 */ 11766 @VisibleForTesting 11767 public static void checkWorldReadAccess(String path) throws FileNotFoundException { 11768 // Path has already been canonicalized, and we relax the check to look 11769 // at groups to support runtime storage permissions. 11770 final int accessBits = path.startsWith("/storage/") ? OsConstants.S_IRGRP 11771 : OsConstants.S_IROTH; 11772 try { 11773 StructStat stat = Os.stat(path); 11774 if (OsConstants.S_ISREG(stat.st_mode) && 11775 ((stat.st_mode & accessBits) == accessBits)) { 11776 checkLeadingPathComponentsWorldExecutable(path); 11777 return; 11778 } 11779 } catch (ErrnoException e) { 11780 // couldn't stat the file, either it doesn't exist or isn't 11781 // accessible to us 11782 } 11783 11784 throw new FileNotFoundException("Can't access " + path); 11785 } 11786 11787 private static void checkLeadingPathComponentsWorldExecutable(String filePath) 11788 throws FileNotFoundException { 11789 File parent = new File(filePath).getParentFile(); 11790 11791 // Path has already been canonicalized, and we relax the check to look 11792 // at groups to support runtime storage permissions. 11793 final int accessBits = filePath.startsWith("/storage/") ? OsConstants.S_IXGRP 11794 : OsConstants.S_IXOTH; 11795 11796 while (parent != null) { 11797 if (! parent.exists()) { 11798 // parent dir doesn't exist, give up 11799 throw new FileNotFoundException("access denied"); 11800 } 11801 try { 11802 StructStat stat = Os.stat(parent.getPath()); 11803 if ((stat.st_mode & accessBits) != accessBits) { 11804 // the parent dir doesn't have the appropriate access 11805 throw new FileNotFoundException("Can't access " + filePath); 11806 } 11807 } catch (ErrnoException e1) { 11808 // couldn't stat() parent 11809 throw new FileNotFoundException("Can't access " + filePath); 11810 } 11811 parent = parent.getParentFile(); 11812 } 11813 } 11814 11815 @VisibleForTesting 11816 static class FallbackException extends Exception { 11817 private final int mThrowSdkVersion; 11818 11819 public FallbackException(String message, int throwSdkVersion) { 11820 super(message); 11821 mThrowSdkVersion = throwSdkVersion; 11822 } 11823 11824 public FallbackException(String message, Throwable cause, int throwSdkVersion) { 11825 super(message, cause); 11826 mThrowSdkVersion = throwSdkVersion; 11827 } 11828 11829 @Override 11830 public String getMessage() { 11831 if (getCause() != null) { 11832 return super.getMessage() + ": " + getCause().getMessage(); 11833 } else { 11834 return super.getMessage(); 11835 } 11836 } 11837 11838 public IllegalArgumentException rethrowAsIllegalArgumentException() { 11839 throw new IllegalArgumentException(getMessage()); 11840 } 11841 11842 public Cursor translateForQuery(int targetSdkVersion) { 11843 if (targetSdkVersion >= mThrowSdkVersion) { 11844 throw new IllegalArgumentException(getMessage()); 11845 } else { 11846 Log.w(TAG, getMessage()); 11847 return null; 11848 } 11849 } 11850 11851 public Uri translateForInsert(int targetSdkVersion) { 11852 if (targetSdkVersion >= mThrowSdkVersion) { 11853 throw new IllegalArgumentException(getMessage()); 11854 } else { 11855 Log.w(TAG, getMessage()); 11856 return null; 11857 } 11858 } 11859 11860 public int translateForBulkInsert(int targetSdkVersion) { 11861 if (targetSdkVersion >= mThrowSdkVersion) { 11862 throw new IllegalArgumentException(getMessage()); 11863 } else { 11864 Log.w(TAG, getMessage()); 11865 return 0; 11866 } 11867 } 11868 11869 public int translateForUpdateDelete(int targetSdkVersion) { 11870 if (targetSdkVersion >= mThrowSdkVersion) { 11871 throw new IllegalArgumentException(getMessage()); 11872 } else { 11873 Log.w(TAG, getMessage()); 11874 return 0; 11875 } 11876 } 11877 } 11878 11879 @VisibleForTesting 11880 static class VolumeNotFoundException extends FallbackException { 11881 public VolumeNotFoundException(String volumeName) { 11882 super("Volume " + volumeName + " not found", Build.VERSION_CODES.Q); 11883 } 11884 } 11885 11886 @VisibleForTesting 11887 static class VolumeArgumentException extends FallbackException { 11888 public VolumeArgumentException(File actual, Collection<File> allowed) { 11889 super("Requested path " + actual + " doesn't appear under " + allowed, 11890 Build.VERSION_CODES.Q); 11891 } 11892 } 11893 11894 public List<String> getSupportedTranscodingRelativePaths() { 11895 return mTranscodeHelper.getSupportedRelativePaths(); 11896 } 11897 11898 public List<String> getSupportedUncachedRelativePaths() { 11899 return StringUtils.verifySupportedUncachedRelativePaths( 11900 StringUtils.getStringArrayConfig(getContext(), 11901 R.array.config_supported_uncached_relative_paths)); 11902 } 11903 11904 /** 11905 * Creating a new method for Transcoding to avoid any merge conflicts. 11906 * TODO(b/170465810): Remove this when the code is refactored. 11907 */ 11908 @NonNull DatabaseHelper getDatabaseForUriForTranscoding(Uri uri) 11909 throws VolumeNotFoundException { 11910 return getDatabaseForUri(uri); 11911 } 11912 11913 @NonNull 11914 private DatabaseHelper getDatabaseForUri(Uri uri) throws VolumeNotFoundException { 11915 final String volumeName = resolveVolumeName(uri); 11916 synchronized (mAttachedVolumes) { 11917 boolean volumeAttached = false; 11918 UserHandle user = mCallingIdentity.get().getUser(); 11919 for (MediaVolume vol : mAttachedVolumes) { 11920 if (vol.getName().equals(volumeName) 11921 && (vol.isVisibleToUser(user) || vol.isPublicVolume()) ) { 11922 volumeAttached = true; 11923 break; 11924 } 11925 } 11926 if (!volumeAttached) { 11927 // Dump some more debug info 11928 Log.e(TAG, "Volume " + volumeName + " not found, calling identity: " 11929 + user + ", attached volumes: " + mAttachedVolumes); 11930 throw new VolumeNotFoundException(volumeName); 11931 } 11932 } 11933 if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) { 11934 return mInternalDatabase; 11935 } else { 11936 return mExternalDatabase; 11937 } 11938 } 11939 11940 static boolean isMediaDatabaseName(String name) { 11941 if (INTERNAL_DATABASE_NAME.equals(name)) { 11942 return true; 11943 } 11944 if (EXTERNAL_DATABASE_NAME.equals(name)) { 11945 return true; 11946 } 11947 return name.startsWith("external-") && name.endsWith(".db"); 11948 } 11949 11950 @NonNull 11951 private Uri getBaseContentUri(@NonNull String volumeName) { 11952 return MediaStore.AUTHORITY_URI.buildUpon().appendPath(volumeName).build(); 11953 } 11954 11955 public Uri attachVolume(MediaVolume volume, boolean validate, String volumeState) { 11956 Log.v(TAG, "attachVolume() called for " + volume.getName() + " with state:" + volumeState); 11957 if (mCallingIdentity.get().pid != android.os.Process.myPid()) { 11958 throw new SecurityException( 11959 "Opening and closing databases not allowed."); 11960 } 11961 11962 final String volumeName = volume.getName(); 11963 11964 // Quick check for shady volume names 11965 MediaStore.checkArgumentVolumeName(volumeName); 11966 11967 // Quick check that volume actually exists 11968 if (!MediaStore.VOLUME_INTERNAL.equals(volumeName) && validate) { 11969 try { 11970 getVolumePath(volumeName); 11971 } catch (IOException e) { 11972 throw new IllegalArgumentException( 11973 "Volume " + volume + " currently unavailable", e); 11974 } 11975 } 11976 11977 synchronized (mAttachedVolumes) { 11978 mAttachedVolumes.add(volume); 11979 } 11980 11981 final ContentResolver resolver = getContext().getContentResolver(); 11982 final Uri uri = getBaseContentUri(volumeName); 11983 // TODO(b/182396009) we probably also want to notify clone profile (and vice versa) 11984 ForegroundThread.getExecutor().execute(() -> { 11985 resolver.notifyChange(getBaseContentUri(volumeName), null); 11986 }); 11987 11988 if (LOGV) Log.v(TAG, "Attached volume: " + volume); 11989 if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)) { 11990 ForegroundThread.getExecutor().execute(() -> { 11991 // Also notify on synthetic view of all devices 11992 resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null); 11993 mExternalDatabase.runWithTransaction((db) -> { 11994 ensureNecessaryFolders(volume, db); 11995 return null; 11996 }); 11997 11998 // We just finished the database operation above, we know that 11999 // it's ready to answer queries, so notify our DocumentProvider 12000 // so it can answer queries without risking ANR 12001 MediaDocumentsProvider.onMediaStoreReady(getContext()); 12002 }); 12003 } 12004 12005 if (Environment.MEDIA_MOUNTED.equalsIgnoreCase(volumeState)) { 12006 mDatabaseBackupAndRecovery.setupVolumeDbBackupAndRecovery(volume.getName()); 12007 } 12008 12009 return uri; 12010 } 12011 12012 private void detachVolume(Uri uri) { 12013 final String volumeName = MediaStore.getVolumeName(uri); 12014 try { 12015 detachVolume(getVolume(volumeName)); 12016 } catch (FileNotFoundException e) { 12017 Log.e(TAG, "Couldn't find volume for URI " + uri, e) ; 12018 } 12019 } 12020 12021 public boolean isVolumeAttached(MediaVolume volume) { 12022 synchronized (mAttachedVolumes) { 12023 return mAttachedVolumes.contains(volume); 12024 } 12025 } 12026 12027 public void detachVolume(MediaVolume volume) { 12028 Log.v(TAG, "detachVolume() received for " + volume.getName()); 12029 if (mCallingIdentity.get().pid != android.os.Process.myPid()) { 12030 throw new SecurityException( 12031 "Opening and closing databases not allowed."); 12032 } 12033 12034 final String volumeName = volume.getName(); 12035 12036 // Quick check for shady volume names 12037 MediaStore.checkArgumentVolumeName(volumeName); 12038 12039 if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) { 12040 throw new UnsupportedOperationException( 12041 "Deleting the internal volume is not allowed"); 12042 } 12043 12044 mDatabaseBackupAndRecovery.onDetachVolume(volumeName); 12045 // Signal any scanning to shut down 12046 mMediaScanner.onDetachVolume(volume); 12047 12048 synchronized (mAttachedVolumes) { 12049 mAttachedVolumes.remove(volume); 12050 } 12051 12052 final ContentResolver resolver = getContext().getContentResolver(); 12053 ForegroundThread.getExecutor().execute(() -> { 12054 resolver.notifyChange(getBaseContentUri(volumeName), null); 12055 }); 12056 12057 if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)) { 12058 ForegroundThread.getExecutor().execute(() -> { 12059 // Also notify on synthetic view of all devices 12060 resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null); 12061 }); 12062 } 12063 12064 if (LOGV) Log.v(TAG, "Detached volume: " + volumeName); 12065 } 12066 12067 private void ensureNecessaryFolders(MediaVolume volume, SQLiteDatabase db) { 12068 ensureDefaultFolders(volume, db); 12069 ensureThumbnailsValid(volume, db); 12070 12071 // Create redacted directories 12072 if (MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volume.getName())) { 12073 // Create dir for redacted and picker URI paths. 12074 File redactedRelativePath = buildPrimaryVolumeFile(uidToUserId(MY_UID), 12075 getRedactedRelativePath()); 12076 if (!redactedRelativePath.exists() && !redactedRelativePath.mkdirs()) { 12077 // We should always be able to create these directories from MediaProvider 12078 Log.wtf(TAG, "Couldn't create redacted path for " + UserHandle.myUserId()); 12079 } 12080 } 12081 } 12082 12083 @GuardedBy("mAttachedVolumes") 12084 private final ArraySet<MediaVolume> mAttachedVolumes = new ArraySet<>(); 12085 @GuardedBy("mCustomCollators") 12086 private final ArraySet<String> mCustomCollators = new ArraySet<>(); 12087 12088 private MediaScanner mMediaScanner; 12089 12090 private ProjectionHelper mProjectionHelper; 12091 private DatabaseHelper mInternalDatabase; 12092 private DatabaseHelper mExternalDatabase; 12093 private PickerDbFacade mPickerDbFacade; 12094 private ExternalDbFacade mExternalDbFacade; 12095 private PickerDataLayer mPickerDataLayer; 12096 private ConfigStore mConfigStore; 12097 private PickerSyncController mPickerSyncController; 12098 private TranscodeHelper mTranscodeHelper; 12099 private PhotoPickerTranscodeHelper mPhotoPickerTranscodeHelper; 12100 private MediaGrants mMediaGrants; 12101 private FilesOwnershipUtils mFilesOwnershipUtils; 12102 private DatabaseBackupAndRecovery mDatabaseBackupAndRecovery; 12103 12104 private BackupExecutor mExternalPrimaryBackupExecutor; 12105 12106 // name of the volume currently being scanned by the media scanner (or null) 12107 private String mMediaScannerVolume; 12108 12109 12110 private static final HashSet<Integer> REDACTED_URI_SUPPORTED_TYPES = new HashSet<>( 12111 Arrays.asList(AUDIO_MEDIA_ID, IMAGES_MEDIA_ID, VIDEO_MEDIA_ID, FILES_ID, DOWNLOADS_ID)); 12112 12113 private LocalUriMatcher mUriMatcher; 12114 12115 private int matchUri(Uri uri, boolean allowHidden) { 12116 return mUriMatcher.matchUri(uri, allowHidden); 12117 } 12118 12119 12120 12121 /** 12122 * Set of columns that can be safely mutated by external callers; all other 12123 * columns are treated as read-only, since they reflect what the media 12124 * scanner found on disk, and any mutations would be overwritten the next 12125 * time the media was scanned. 12126 */ 12127 private static final ArraySet<String> sMutableColumns = new ArraySet<>(); 12128 12129 static { 12130 sMutableColumns.add(MediaStore.MediaColumns.DATA); 12131 sMutableColumns.add(MediaStore.MediaColumns.RELATIVE_PATH); 12132 sMutableColumns.add(MediaStore.MediaColumns.DISPLAY_NAME); 12133 sMutableColumns.add(MediaStore.MediaColumns.IS_PENDING); 12134 sMutableColumns.add(MediaStore.MediaColumns.IS_TRASHED); 12135 sMutableColumns.add(MediaStore.MediaColumns.IS_FAVORITE); 12136 sMutableColumns.add(MediaStore.MediaColumns.OWNER_PACKAGE_NAME); 12137 12138 sMutableColumns.add(MediaStore.Audio.AudioColumns.BOOKMARK); 12139 12140 sMutableColumns.add(MediaStore.Video.VideoColumns.TAGS); 12141 sMutableColumns.add(MediaStore.Video.VideoColumns.CATEGORY); 12142 sMutableColumns.add(MediaStore.Video.VideoColumns.BOOKMARK); 12143 12144 sMutableColumns.add(MediaStore.Audio.Playlists.NAME); 12145 sMutableColumns.add(MediaStore.Audio.Playlists.Members.AUDIO_ID); 12146 sMutableColumns.add(MediaStore.Audio.Playlists.Members.PLAY_ORDER); 12147 12148 sMutableColumns.add(MediaStore.DownloadColumns.DOWNLOAD_URI); 12149 sMutableColumns.add(MediaStore.DownloadColumns.REFERER_URI); 12150 12151 sMutableColumns.add(MediaStore.Files.FileColumns.MIME_TYPE); 12152 sMutableColumns.add(MediaStore.Files.FileColumns.MEDIA_TYPE); 12153 12154 sMutableColumns.add(MediaStore.Files.FileColumns.OEM_METADATA); 12155 } 12156 12157 /** 12158 * Set of columns that affect placement of files on disk. 12159 */ 12160 private static final ArraySet<String> sPlacementColumns = new ArraySet<>(); 12161 12162 static { 12163 sPlacementColumns.add(MediaStore.MediaColumns.DATA); 12164 sPlacementColumns.add(MediaStore.MediaColumns.RELATIVE_PATH); 12165 sPlacementColumns.add(MediaStore.MediaColumns.DISPLAY_NAME); 12166 sPlacementColumns.add(MediaStore.MediaColumns.MIME_TYPE); 12167 sPlacementColumns.add(MediaStore.MediaColumns.IS_PENDING); 12168 sPlacementColumns.add(MediaStore.MediaColumns.IS_TRASHED); 12169 sPlacementColumns.add(MediaStore.MediaColumns.DATE_EXPIRES); 12170 } 12171 12172 /** 12173 * List of abusive custom columns that we're willing to allow via 12174 * {@link SQLiteQueryBuilder#setProjectionAllowlist(Collection)}. 12175 */ 12176 static final ArrayList<Pattern> sAllowlist = new ArrayList<>(); 12177 12178 private static void addAllowlistPattern(String pattern) { 12179 sAllowlist.add(Pattern.compile(" *" + pattern + " *")); 12180 } 12181 12182 static { 12183 final String maybeAs = "( (as )?[_a-z0-9]+)?"; 12184 addAllowlistPattern("(?i)[_a-z0-9]+" + maybeAs); 12185 addAllowlistPattern("audio\\._id AS _id"); 12186 addAllowlistPattern( 12187 "(?i)(min|max|sum|avg|total|count|cast)\\(([_a-z0-9]+" 12188 + maybeAs 12189 + "|\\*)\\)" 12190 + maybeAs); 12191 addAllowlistPattern( 12192 "case when case when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added" 12193 + " \\* \\d+ when \\(date_added >= \\d+ and date_added < \\d+\\) then" 12194 + " date_added when \\(date_added >= \\d+ and date_added < \\d+\\) then" 12195 + " date_added / \\d+ else \\d+ end > case when \\(date_modified >= \\d+ and" 12196 + " date_modified < \\d+\\) then date_modified \\* \\d+ when \\(date_modified" 12197 + " >= \\d+ and date_modified < \\d+\\) then date_modified when" 12198 + " \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified /" 12199 + " \\d+ else \\d+ end then case when \\(date_added >= \\d+ and date_added <" 12200 + " \\d+\\) then date_added \\* \\d+ when \\(date_added >= \\d+ and date_added" 12201 + " < \\d+\\) then date_added when \\(date_added >= \\d+ and date_added <" 12202 + " \\d+\\) then date_added / \\d+ else \\d+ end else case when" 12203 + " \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified \\*" 12204 + " \\d+ when \\(date_modified >= \\d+ and date_modified < \\d+\\) then" 12205 + " date_modified when \\(date_modified >= \\d+ and date_modified < \\d+\\)" 12206 + " then date_modified / \\d+ else \\d+ end end as corrected_added_modified"); 12207 addAllowlistPattern( 12208 "MAX\\(case when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken \\*" 12209 + " \\d+ when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken when" 12210 + " \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken / \\d+ else" 12211 + " \\d+ end\\)"); 12212 addAllowlistPattern( 12213 "MAX\\(case when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added \\*" 12214 + " \\d+ when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added" 12215 + " when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added / \\d+" 12216 + " else \\d+ end\\)"); 12217 addAllowlistPattern( 12218 "MAX\\(case when \\(date_modified >= \\d+ and date_modified < \\d+\\) then" 12219 + " date_modified \\* \\d+ when \\(date_modified >= \\d+ and date_modified <" 12220 + " \\d+\\) then date_modified when \\(date_modified >= \\d+ and date_modified" 12221 + " < \\d+\\) then date_modified / \\d+ else \\d+ end\\)"); 12222 addAllowlistPattern("\"content://media/[a-z]+/audio/media\""); 12223 addAllowlistPattern( 12224 "substr\\(_data, length\\(_data\\)-length\\(_display_name\\), 1\\) as" 12225 + " filename_prevchar"); 12226 addAllowlistPattern("\\*" + maybeAs); 12227 addAllowlistPattern( 12228 "case when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken \\* \\d+" 12229 + " when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken when" 12230 + " \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken / \\d+ else" 12231 + " \\d+ end"); 12232 } 12233 12234 public ArrayMap<String, String> getProjectionMap(Class<?>... clazzes) { 12235 return mProjectionHelper.getProjectionMap(clazzes); 12236 } 12237 12238 static <T> boolean containsAny(Set<T> a, Set<T> b) { 12239 for (T i : b) { 12240 if (a.contains(i)) { 12241 return true; 12242 } 12243 } 12244 return false; 12245 } 12246 12247 @VisibleForTesting 12248 @Nullable 12249 static Uri computeCommonPrefix(@NonNull List<Uri> uris) { 12250 if (uris.isEmpty()) return null; 12251 12252 final Uri base = uris.get(0); 12253 final List<String> basePath = new ArrayList<>(base.getPathSegments()); 12254 for (int i = 1; i < uris.size(); i++) { 12255 final List<String> probePath = uris.get(i).getPathSegments(); 12256 for (int j = 0; j < basePath.size() && j < probePath.size(); j++) { 12257 if (!Objects.equals(basePath.get(j), probePath.get(j))) { 12258 // Trim away all remaining common elements 12259 while (basePath.size() > j) { 12260 basePath.remove(j); 12261 } 12262 } 12263 } 12264 12265 final int probeSize = probePath.size(); 12266 while (basePath.size() > probeSize) { 12267 basePath.remove(probeSize); 12268 } 12269 } 12270 12271 final Uri.Builder builder = base.buildUpon().path(null); 12272 for (String s : basePath) { 12273 builder.appendPath(s); 12274 } 12275 return builder.build(); 12276 } 12277 12278 public ExternalDbFacade getExternalDbFacade() { 12279 return mExternalDbFacade; 12280 } 12281 12282 public PickerSyncController getPickerSyncController() { 12283 return mPickerSyncController; 12284 } 12285 12286 private boolean isCallingPackageSystemGallery() { 12287 if (mCallingIdentity.get().hasPermission(PERMISSION_IS_SYSTEM_GALLERY)) { 12288 if (isCallingPackageRequestingLegacy()) { 12289 return isCallingPackageLegacyWrite(); 12290 } 12291 return true; 12292 } 12293 return false; 12294 } 12295 12296 private int getCallingUidOrSelf() { 12297 return mCallingIdentity.get().uid; 12298 } 12299 12300 private boolean isCallerPhotoPicker() { 12301 try { 12302 return PermissionUtils.checkManageCloudMediaProvidersPermission( 12303 getContext(), 12304 mCallingIdentity.get().pid, 12305 mCallingIdentity.get().uid 12306 ); 12307 } catch (RuntimeException e) { 12308 Log.e(TAG, "Could not check MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION permission", e); 12309 return false; 12310 } 12311 } 12312 12313 @Deprecated 12314 private String getCallingPackageOrSelf() { 12315 return mCallingIdentity.get().getPackageName(); 12316 } 12317 12318 @Deprecated 12319 @VisibleForTesting 12320 public int getCallingPackageTargetSdkVersion() { 12321 return mCallingIdentity.get().getTargetSdkVersion(); 12322 } 12323 12324 @Deprecated 12325 private boolean isCallingPackageAllowedHidden() { 12326 return isCallingPackageSelf(); 12327 } 12328 12329 @Deprecated 12330 private boolean isCallingPackageSelf() { 12331 return mCallingIdentity.get().hasPermission(PERMISSION_IS_SELF); 12332 } 12333 12334 @Deprecated 12335 private boolean isCallingPackageShell() { 12336 return mCallingIdentity.get().hasPermission(PERMISSION_IS_SHELL); 12337 } 12338 12339 @Deprecated 12340 private boolean isCallingPackageManager() { 12341 return mCallingIdentity.get().hasPermission(PERMISSION_IS_MANAGER); 12342 } 12343 12344 @Deprecated 12345 private boolean isCallingPackageDelegator() { 12346 return mCallingIdentity.get().hasPermission(PERMISSION_IS_DELEGATOR); 12347 } 12348 12349 @Deprecated 12350 private boolean isCallingPackageLegacyRead() { 12351 return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_READ); 12352 } 12353 12354 @Deprecated 12355 private boolean isCallingPackageLegacyWrite() { 12356 return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_WRITE); 12357 } 12358 12359 @Override 12360 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 12361 writer.println("mThumbSize=" + mThumbSize); 12362 synchronized (mAttachedVolumes) { 12363 writer.println("mAttachedVolumes=" + mAttachedVolumes); 12364 } 12365 writer.println(); 12366 12367 mVolumeCache.dump(writer); 12368 writer.println(); 12369 12370 mUserCache.dump(writer); 12371 writer.println(); 12372 12373 mTranscodeHelper.dump(writer); 12374 writer.println(); 12375 12376 mConfigStore.dump(writer); 12377 writer.println(); 12378 12379 mPickerDbFacade.dump(writer); 12380 writer.println(); 12381 12382 mPickerSyncController.dump(writer); 12383 writer.println(); 12384 12385 dumpAccessLogs(writer); 12386 writer.println(); 12387 12388 Logging.dumpPersistent(writer); 12389 } 12390 12391 private void dumpAccessLogs(PrintWriter writer) { 12392 synchronized (mCachedCallingIdentityForFuse) { 12393 for (int i = 0; i < mCachedCallingIdentityForFuse.size(); i++) { 12394 mCachedCallingIdentityForFuse.valueAt(i).dump(writer); 12395 } 12396 } 12397 } 12398 12399 /** 12400 * Replaces "external" in the URI path with the specified volumeName. 12401 * Example: 12402 * Input: content://media/external/images/media/1232 12403 * Output: content://media/{volumeName}/images/media/1232 12404 */ 12405 private Uri replaceExternalUriWithVolumeName(Uri uri, String volumeName) { 12406 List<String> pathSegments = uri.getPathSegments(); 12407 if (!pathSegments.isEmpty() && pathSegments.get(0).equalsIgnoreCase( 12408 MediaStore.VOLUME_EXTERNAL)) { 12409 List<String> updatedSegments = new ArrayList<>(pathSegments); 12410 updatedSegments.set(0, volumeName); 12411 return uri.buildUpon() 12412 .path(TextUtils.join("/", updatedSegments)) 12413 .build(); 12414 } 12415 return uri; 12416 } 12417 12418 /** 12419 * Called once - from {@link #onCreate()}. 12420 */ 12421 @NonNull 12422 private ConfigStore createConfigStore() { 12423 // Tests may want override provideConfigStore() in order to inject a mock object. 12424 ConfigStore configStore = provideConfigStore(); 12425 if (configStore == null) { 12426 // Tests did not provide an alternative implementation: create our regular "production" 12427 // ConfigStore. 12428 configStore = MediaApplication.getConfigStore(); 12429 } 12430 return configStore; 12431 } 12432 12433 /** 12434 * Initializes the MimeTypeFixHandler for Android 15, running only for Android 15 12435 * This method loads the mime types from the res/raw directory 12436 * This is necessary to ensure that MediaProvider can handle mime types correctly on Android 15 12437 */ 12438 private void initializeMimeTypeFixHandlerForAndroid15(Context context) { 12439 if (Build.VERSION.SDK_INT != Build.VERSION_CODES.VANILLA_ICE_CREAM) { 12440 return; 12441 } 12442 12443 if (!Flags.enableMimeTypeFixForAndroid15()) { 12444 return; 12445 } 12446 12447 // Load all the MIME types from various files in the background to reduce the latency 12448 // caused when this method is called from onCreate 12449 BackgroundThread.getExecutor().execute(() -> { 12450 try { 12451 MimeTypeFixHandler.loadMimeTypes(context); 12452 } catch (Exception e) { 12453 Log.e(TAG, "Failed to initialize MimeTypeFixHandler: ", e); 12454 } 12455 }); 12456 } 12457 12458 /** 12459 * <b>FOT TESTING PURPOSES ONLY</b> 12460 * <p> 12461 * Allows injecting alternative {@link ConfigStore} implementation. 12462 */ 12463 @VisibleForTesting 12464 @Nullable 12465 protected ConfigStore provideConfigStore() { 12466 return null; 12467 } 12468 12469 protected VolumeCache getVolumeCache() { 12470 return mVolumeCache; 12471 } 12472 12473 protected DatabaseBackupAndRecovery createDatabaseBackupAndRecovery() { 12474 return new DatabaseBackupAndRecovery(mConfigStore, mVolumeCache); 12475 } 12476 12477 protected MaliciousAppDetector createMaliciousAppDetector() { 12478 return new MaliciousAppDetector(getContext()); 12479 } 12480 12481 protected boolean shouldCheckForMaliciousActivity() { 12482 // Check for malicious activity if not a system gallery app, not the media provider itself, 12483 // and the malicious app detector flag is enabled 12484 if (!SdkLevel.isAtLeastS()) { 12485 return false; 12486 } 12487 return Flags.enableMaliciousAppDetector() && !isCallingPackageSystemGallery() 12488 && !isCallingPackageSelf(); 12489 } 12490 } 12491