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.Manifest.permission.QUERY_ALL_PACKAGES; 21 import static android.app.AppOpsManager.permissionToOp; 22 import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; 23 import static android.app.PendingIntent.FLAG_IMMUTABLE; 24 import static android.app.PendingIntent.FLAG_ONE_SHOT; 25 import static android.content.ContentResolver.QUERY_ARG_SQL_GROUP_BY; 26 import static android.content.ContentResolver.QUERY_ARG_SQL_HAVING; 27 import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION; 28 import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS; 29 import static android.content.ContentResolver.QUERY_ARG_SQL_SORT_ORDER; 30 import static android.content.pm.PackageManager.PERMISSION_DENIED; 31 import static android.content.pm.PackageManager.PERMISSION_GRANTED; 32 import static android.database.Cursor.FIELD_TYPE_BLOB; 33 import static android.provider.CloudMediaProviderContract.EXTRA_ASYNC_CONTENT_PROVIDER; 34 import static android.provider.CloudMediaProviderContract.METHOD_GET_ASYNC_CONTENT_PROVIDER; 35 import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE; 36 import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE; 37 import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT; 38 import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_NONE; 39 import static android.provider.MediaStore.GET_BACKUP_FILES; 40 import static android.provider.MediaStore.MATCH_DEFAULT; 41 import static android.provider.MediaStore.MATCH_EXCLUDE; 42 import static android.provider.MediaStore.MATCH_INCLUDE; 43 import static android.provider.MediaStore.MATCH_ONLY; 44 import static android.provider.MediaStore.MEDIA_IGNORE_FILENAME; 45 import static android.provider.MediaStore.MY_UID; 46 import static android.provider.MediaStore.MediaColumns.OWNER_PACKAGE_NAME; 47 import static android.provider.MediaStore.PER_USER_RANGE; 48 import static android.provider.MediaStore.QUERY_ARG_DEFER_SCAN; 49 import static android.provider.MediaStore.QUERY_ARG_MATCH_FAVORITE; 50 import static android.provider.MediaStore.QUERY_ARG_MATCH_PENDING; 51 import static android.provider.MediaStore.QUERY_ARG_MATCH_TRASHED; 52 import static android.provider.MediaStore.QUERY_ARG_REDACTED_URI; 53 import static android.provider.MediaStore.QUERY_ARG_RELATED_URI; 54 import static android.provider.MediaStore.READ_BACKED_UP_FILE_PATHS; 55 import static android.provider.MediaStore.getVolumeName; 56 import static android.system.OsConstants.F_GETFL; 57 58 import static com.android.providers.media.AccessChecker.getWhereForConstrainedAccess; 59 import static com.android.providers.media.AccessChecker.getWhereForOwnerPackageMatch; 60 import static com.android.providers.media.AccessChecker.getWhereForUserSelectedAccess; 61 import static com.android.providers.media.AccessChecker.hasAccessToCollection; 62 import static com.android.providers.media.AccessChecker.hasUserSelectedAccess; 63 import static com.android.providers.media.DatabaseBackupAndRecovery.LEVEL_DB_READ_LIMIT; 64 import static com.android.providers.media.DatabaseHelper.EXTERNAL_DATABASE_NAME; 65 import static com.android.providers.media.DatabaseHelper.INTERNAL_DATABASE_NAME; 66 import static com.android.providers.media.LocalCallingIdentity.APPOP_REQUEST_INSTALL_PACKAGES_FOR_SHARED_UID; 67 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_ACCESS_MTP; 68 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_INSTALL_PACKAGES; 69 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_DELEGATOR; 70 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_GRANTED; 71 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_READ; 72 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE; 73 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_MANAGER; 74 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED; 75 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SELF; 76 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SHELL; 77 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SYSTEM_GALLERY; 78 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_EXTERNAL_STORAGE; 79 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMART; 80 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMART_FILE_ID; 81 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMART_ID; 82 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMS; 83 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMS_ID; 84 import static com.android.providers.media.LocalUriMatcher.AUDIO_ARTISTS; 85 import static com.android.providers.media.LocalUriMatcher.AUDIO_ARTISTS_ID; 86 import static com.android.providers.media.LocalUriMatcher.AUDIO_ARTISTS_ID_ALBUMS; 87 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES; 88 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES_ALL_MEMBERS; 89 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES_ID; 90 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES_ID_MEMBERS; 91 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA; 92 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA_ID; 93 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA_ID_GENRES; 94 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA_ID_GENRES_ID; 95 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS; 96 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS_ID; 97 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS_ID_MEMBERS; 98 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS_ID_MEMBERS_ID; 99 import static com.android.providers.media.LocalUriMatcher.CLI; 100 import static com.android.providers.media.LocalUriMatcher.DOWNLOADS; 101 import static com.android.providers.media.LocalUriMatcher.DOWNLOADS_ID; 102 import static com.android.providers.media.LocalUriMatcher.FILES; 103 import static com.android.providers.media.LocalUriMatcher.FILES_ID; 104 import static com.android.providers.media.LocalUriMatcher.FS_ID; 105 import static com.android.providers.media.LocalUriMatcher.IMAGES_MEDIA; 106 import static com.android.providers.media.LocalUriMatcher.IMAGES_MEDIA_ID; 107 import static com.android.providers.media.LocalUriMatcher.IMAGES_MEDIA_ID_THUMBNAIL; 108 import static com.android.providers.media.LocalUriMatcher.IMAGES_THUMBNAILS; 109 import static com.android.providers.media.LocalUriMatcher.IMAGES_THUMBNAILS_ID; 110 import static com.android.providers.media.LocalUriMatcher.MEDIA_SCANNER; 111 import static com.android.providers.media.LocalUriMatcher.PICKER_ID; 112 import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_ALBUMS_ALL; 113 import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_ALBUMS_LOCAL; 114 import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_MEDIA_ALL; 115 import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_MEDIA_LOCAL; 116 import static com.android.providers.media.LocalUriMatcher.VERSION; 117 import static com.android.providers.media.LocalUriMatcher.VIDEO_MEDIA; 118 import static com.android.providers.media.LocalUriMatcher.VIDEO_MEDIA_ID; 119 import static com.android.providers.media.LocalUriMatcher.VIDEO_MEDIA_ID_THUMBNAIL; 120 import static com.android.providers.media.LocalUriMatcher.VIDEO_THUMBNAILS; 121 import static com.android.providers.media.LocalUriMatcher.VIDEO_THUMBNAILS_ID; 122 import static com.android.providers.media.LocalUriMatcher.VOLUMES; 123 import static com.android.providers.media.LocalUriMatcher.VOLUMES_ID; 124 import static com.android.providers.media.PickerUriResolver.getMediaUri; 125 import static com.android.providers.media.scan.MediaScanner.REASON_DEMAND; 126 import static com.android.providers.media.scan.MediaScanner.REASON_IDLE; 127 import static com.android.providers.media.util.DatabaseUtils.bindList; 128 import static com.android.providers.media.util.FileUtils.DEFAULT_FOLDER_NAMES; 129 import static com.android.providers.media.util.FileUtils.PATTERN_PENDING_FILEPATH_FOR_SQL; 130 import static com.android.providers.media.util.FileUtils.buildPrimaryVolumeFile; 131 import static com.android.providers.media.util.FileUtils.extractDisplayName; 132 import static com.android.providers.media.util.FileUtils.extractFileExtension; 133 import static com.android.providers.media.util.FileUtils.extractFileName; 134 import static com.android.providers.media.util.FileUtils.extractOwnerPackageNameFromRelativePath; 135 import static com.android.providers.media.util.FileUtils.extractPathOwnerPackageName; 136 import static com.android.providers.media.util.FileUtils.extractRelativePath; 137 import static com.android.providers.media.util.FileUtils.extractRelativePathWithDisplayName; 138 import static com.android.providers.media.util.FileUtils.extractTopLevelDir; 139 import static com.android.providers.media.util.FileUtils.extractVolumeName; 140 import static com.android.providers.media.util.FileUtils.extractVolumePath; 141 import static com.android.providers.media.util.FileUtils.fromFuseFile; 142 import static com.android.providers.media.util.FileUtils.getAbsoluteSanitizedPath; 143 import static com.android.providers.media.util.FileUtils.isCrossUserEnabled; 144 import static com.android.providers.media.util.FileUtils.isDataOrObbPath; 145 import static com.android.providers.media.util.FileUtils.isDataOrObbRelativePath; 146 import static com.android.providers.media.util.FileUtils.isDownload; 147 import static com.android.providers.media.util.FileUtils.isExternalMediaDirectory; 148 import static com.android.providers.media.util.FileUtils.isObbOrChildRelativePath; 149 import static com.android.providers.media.util.FileUtils.sanitizePath; 150 import static com.android.providers.media.util.FileUtils.toFuseFile; 151 import static com.android.providers.media.util.Logging.LOGV; 152 import static com.android.providers.media.util.Logging.TAG; 153 import static com.android.providers.media.util.PermissionUtils.checkPermissionSelf; 154 import static com.android.providers.media.util.PermissionUtils.checkPermissionShell; 155 import static com.android.providers.media.util.StringUtils.componentStateToString; 156 import static com.android.providers.media.util.SyntheticPathUtils.REDACTED_URI_ID_PREFIX; 157 import static com.android.providers.media.util.SyntheticPathUtils.REDACTED_URI_ID_SIZE; 158 import static com.android.providers.media.util.SyntheticPathUtils.createSparseFile; 159 import static com.android.providers.media.util.SyntheticPathUtils.extractSyntheticRelativePathSegements; 160 import static com.android.providers.media.util.SyntheticPathUtils.getRedactedRelativePath; 161 import static com.android.providers.media.util.SyntheticPathUtils.isPickerPath; 162 import static com.android.providers.media.util.SyntheticPathUtils.isRedactedPath; 163 import static com.android.providers.media.util.SyntheticPathUtils.isSyntheticPath; 164 165 import android.Manifest; 166 import android.annotation.IntDef; 167 import android.app.AppOpsManager; 168 import android.app.AppOpsManager.OnOpActiveChangedListener; 169 import android.app.AppOpsManager.OnOpChangedListener; 170 import android.app.DownloadManager; 171 import android.app.PendingIntent; 172 import android.app.RecoverableSecurityException; 173 import android.app.RemoteAction; 174 import android.app.compat.CompatChanges; 175 import android.compat.annotation.ChangeId; 176 import android.compat.annotation.EnabledAfter; 177 import android.content.BroadcastReceiver; 178 import android.content.ClipData; 179 import android.content.ClipDescription; 180 import android.content.ComponentName; 181 import android.content.ContentProvider; 182 import android.content.ContentProviderClient; 183 import android.content.ContentProviderOperation; 184 import android.content.ContentProviderResult; 185 import android.content.ContentResolver; 186 import android.content.ContentUris; 187 import android.content.ContentValues; 188 import android.content.Context; 189 import android.content.Intent; 190 import android.content.IntentFilter; 191 import android.content.OperationApplicationException; 192 import android.content.SharedPreferences; 193 import android.content.pm.ApplicationInfo; 194 import android.content.pm.PackageInstaller.SessionInfo; 195 import android.content.pm.PackageManager; 196 import android.content.pm.PackageManager.NameNotFoundException; 197 import android.content.pm.PermissionGroupInfo; 198 import android.content.pm.ProviderInfo; 199 import android.content.res.AssetFileDescriptor; 200 import android.content.res.Configuration; 201 import android.content.res.Resources; 202 import android.database.Cursor; 203 import android.database.MatrixCursor; 204 import android.database.sqlite.SQLiteConstraintException; 205 import android.database.sqlite.SQLiteDatabase; 206 import android.graphics.Bitmap; 207 import android.graphics.BitmapFactory; 208 import android.graphics.drawable.Icon; 209 import android.icu.util.ULocale; 210 import android.media.ExifInterface; 211 import android.media.ThumbnailUtils; 212 import android.mtp.MtpConstants; 213 import android.net.Uri; 214 import android.os.Binder; 215 import android.os.Binder.ProxyTransactListener; 216 import android.os.Build; 217 import android.os.Bundle; 218 import android.os.CancellationSignal; 219 import android.os.Environment; 220 import android.os.IBinder; 221 import android.os.ParcelFileDescriptor; 222 import android.os.ParcelFileDescriptor.OnCloseListener; 223 import android.os.Parcelable; 224 import android.os.Process; 225 import android.os.RemoteException; 226 import android.os.SystemClock; 227 import android.os.Trace; 228 import android.os.UserHandle; 229 import android.os.UserManager; 230 import android.os.storage.StorageManager; 231 import android.os.storage.StorageManager.StorageVolumeCallback; 232 import android.os.storage.StorageVolume; 233 import android.preference.PreferenceManager; 234 import android.provider.AsyncContentProvider; 235 import android.provider.BaseColumns; 236 import android.provider.Column; 237 import android.provider.DocumentsContract; 238 import android.provider.ExportedSince; 239 import android.provider.IAsyncContentProvider; 240 import android.provider.MediaStore; 241 import android.provider.MediaStore.Audio; 242 import android.provider.MediaStore.Audio.AudioColumns; 243 import android.provider.MediaStore.Audio.Playlists; 244 import android.provider.MediaStore.Downloads; 245 import android.provider.MediaStore.Files; 246 import android.provider.MediaStore.Files.FileColumns; 247 import android.provider.MediaStore.Images; 248 import android.provider.MediaStore.Images.ImageColumns; 249 import android.provider.MediaStore.MediaColumns; 250 import android.provider.MediaStore.Video; 251 import android.system.ErrnoException; 252 import android.system.Os; 253 import android.system.OsConstants; 254 import android.system.StructStat; 255 import android.text.TextUtils; 256 import android.text.format.DateUtils; 257 import android.util.ArrayMap; 258 import android.util.ArraySet; 259 import android.util.DisplayMetrics; 260 import android.util.Log; 261 import android.util.LongSparseArray; 262 import android.util.Pair; 263 import android.util.Size; 264 import android.util.SparseArray; 265 import android.webkit.MimeTypeMap; 266 267 import androidx.annotation.GuardedBy; 268 import androidx.annotation.Keep; 269 import androidx.annotation.NonNull; 270 import androidx.annotation.Nullable; 271 import androidx.annotation.RequiresApi; 272 import androidx.annotation.VisibleForTesting; 273 274 import com.android.modules.utils.BackgroundThread; 275 import com.android.modules.utils.build.SdkLevel; 276 import com.android.providers.media.DatabaseHelper.OnFilesChangeListener; 277 import com.android.providers.media.DatabaseHelper.OnLegacyMigrationListener; 278 import com.android.providers.media.dao.FileRow; 279 import com.android.providers.media.fuse.ExternalStorageServiceImpl; 280 import com.android.providers.media.fuse.FuseDaemon; 281 import com.android.providers.media.metrics.PulledMetrics; 282 import com.android.providers.media.photopicker.PhotoPickerActivity; 283 import com.android.providers.media.photopicker.PickerDataLayer; 284 import com.android.providers.media.photopicker.PickerSyncController; 285 import com.android.providers.media.photopicker.data.ExternalDbFacade; 286 import com.android.providers.media.photopicker.data.PickerDbFacade; 287 import com.android.providers.media.playlist.Playlist; 288 import com.android.providers.media.scan.MediaScanner; 289 import com.android.providers.media.scan.MediaScanner.ScanReason; 290 import com.android.providers.media.scan.ModernMediaScanner; 291 import com.android.providers.media.util.CachedSupplier; 292 import com.android.providers.media.util.DatabaseUtils; 293 import com.android.providers.media.util.FileUtils; 294 import com.android.providers.media.util.ForegroundThread; 295 import com.android.providers.media.util.IsoInterface; 296 import com.android.providers.media.util.Logging; 297 import com.android.providers.media.util.LongArray; 298 import com.android.providers.media.util.Metrics; 299 import com.android.providers.media.util.MimeUtils; 300 import com.android.providers.media.util.PermissionUtils; 301 import com.android.providers.media.util.Preconditions; 302 import com.android.providers.media.util.SQLiteQueryBuilder; 303 import com.android.providers.media.util.SpecialFormatDetector; 304 import com.android.providers.media.util.StringUtils; 305 import com.android.providers.media.util.UserCache; 306 import com.android.providers.media.util.XAttrUtils; 307 import com.android.providers.media.util.XmpInterface; 308 309 import com.google.common.base.Strings; 310 import com.google.common.hash.Hashing; 311 312 import java.io.File; 313 import java.io.FileDescriptor; 314 import java.io.FileInputStream; 315 import java.io.FileNotFoundException; 316 import java.io.FileOutputStream; 317 import java.io.IOException; 318 import java.io.OutputStream; 319 import java.io.PrintWriter; 320 import java.lang.annotation.Retention; 321 import java.lang.annotation.RetentionPolicy; 322 import java.lang.reflect.InvocationTargetException; 323 import java.lang.reflect.Method; 324 import java.nio.charset.StandardCharsets; 325 import java.nio.file.Path; 326 import java.util.ArrayList; 327 import java.util.Arrays; 328 import java.util.Collection; 329 import java.util.Collections; 330 import java.util.HashSet; 331 import java.util.LinkedHashMap; 332 import java.util.List; 333 import java.util.Locale; 334 import java.util.Map; 335 import java.util.Objects; 336 import java.util.Optional; 337 import java.util.Set; 338 import java.util.UUID; 339 import java.util.concurrent.CountDownLatch; 340 import java.util.concurrent.ExecutionException; 341 import java.util.concurrent.TimeUnit; 342 import java.util.concurrent.TimeoutException; 343 import java.util.function.Consumer; 344 import java.util.function.Supplier; 345 import java.util.function.UnaryOperator; 346 import java.util.regex.Matcher; 347 import java.util.regex.Pattern; 348 import java.util.stream.Collectors; 349 350 /** 351 * Media content provider. See {@link android.provider.MediaStore} for details. 352 * Separate databases are kept for each external storage card we see (using the 353 * card's ID as an index). The content visible at content://media/external/... 354 * changes with the card. 355 */ 356 public class MediaProvider extends ContentProvider { 357 /** 358 * Enables checks to stop apps from inserting and updating to private files via media provider. 359 */ 360 @ChangeId 361 @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.R) 362 static final long ENABLE_CHECKS_FOR_PRIVATE_FILES = 172100307L; 363 364 /** 365 * Regex of a selection string that matches a specific ID. 366 */ 367 static final Pattern PATTERN_SELECTION_ID = Pattern.compile( 368 "(?:image_id|video_id)\\s*=\\s*(\\d+)"); 369 370 /** File access by uid requires the transcoding transform */ 371 private static final int FLAG_TRANSFORM_TRANSCODING = 1 << 0; 372 373 /** File access by uid is a synthetic path corresponding to a redacted URI */ 374 private static final int FLAG_TRANSFORM_REDACTION = 1 << 1; 375 376 /** File access by uid is a synthetic path corresponding to a picker URI */ 377 private static final int FLAG_TRANSFORM_PICKER = 1 << 2; 378 379 /** 380 * These directory names aren't declared in Environment as final variables, and so we need to 381 * have the same values in separate final variables in order to have them considered constant 382 * expressions. 383 * These directory names are intentionally in lower case to ease the case insensitive path 384 * comparison. 385 */ 386 private static final String DIRECTORY_MUSIC_LOWER_CASE = "music"; 387 private static final String DIRECTORY_PODCASTS_LOWER_CASE = "podcasts"; 388 private static final String DIRECTORY_RINGTONES_LOWER_CASE = "ringtones"; 389 private static final String DIRECTORY_ALARMS_LOWER_CASE = "alarms"; 390 private static final String DIRECTORY_NOTIFICATIONS_LOWER_CASE = "notifications"; 391 private static final String DIRECTORY_PICTURES_LOWER_CASE = "pictures"; 392 private static final String DIRECTORY_MOVIES_LOWER_CASE = "movies"; 393 private static final String DIRECTORY_DOWNLOADS_LOWER_CASE = "download"; 394 private static final String DIRECTORY_DCIM_LOWER_CASE = "dcim"; 395 private static final String DIRECTORY_DOCUMENTS_LOWER_CASE = "documents"; 396 private static final String DIRECTORY_AUDIOBOOKS_LOWER_CASE = "audiobooks"; 397 private static final String DIRECTORY_RECORDINGS_LOWER_CASE = "recordings"; 398 private static final String DIRECTORY_ANDROID_LOWER_CASE = "android"; 399 400 private static final String DIRECTORY_MEDIA = "media"; 401 private static final String DIRECTORY_THUMBNAILS = ".thumbnails"; 402 403 /** 404 * Hard-coded filename where the current value of 405 * {@link DatabaseHelper#getOrCreateUuid} is persisted on a physical SD card 406 * to help identify stale thumbnail collections. 407 */ 408 private static final String FILE_DATABASE_UUID = ".database_uuid"; 409 410 /** 411 * Specify what default directories the caller gets full access to. By default, the caller 412 * shouldn't get full access to any default dirs. 413 * But for example, we do an exception for System Gallery apps and allow them full access to: 414 * DCIM, Pictures, Movies. 415 */ 416 static final String INCLUDED_DEFAULT_DIRECTORIES = 417 "android:included-default-directories"; 418 419 /** 420 * Value indicating that operations should include database rows matching the criteria defined 421 * by this key only when calling package has write permission to the database row or column is 422 * {@column MediaColumns#IS_PENDING} and is set by FUSE. 423 * <p> 424 * Note that items <em>not</em> matching the criteria will also be included, and as part of this 425 * match no additional write permission checks are carried out for those items. 426 */ 427 private static final int MATCH_VISIBLE_FOR_FILEPATH = 32; 428 429 private static final int NON_HIDDEN_CACHE_SIZE = 50; 430 431 /** 432 * This is required as idle maintenance maybe stopped anytime; we do not want to query 433 * and accumulate values to update for a long time, instead we want to batch query and update 434 * by a limited number. 435 */ 436 private static final int IDLE_MAINTENANCE_ROWS_LIMIT = 1000; 437 438 /** 439 * Where clause to match pending files from FUSE. Pending files from FUSE will not have 440 * PATTERN_PENDING_FILEPATH_FOR_SQL pattern. 441 */ 442 private static final String MATCH_PENDING_FROM_FUSE = String.format("lower(%s) NOT REGEXP '%s'", 443 MediaColumns.DATA, PATTERN_PENDING_FILEPATH_FOR_SQL); 444 445 /** 446 * This flag is replaced with {@link MediaStore#QUERY_ARG_DEFER_SCAN} from S onwards and only 447 * kept around for app compatibility in R. 448 */ 449 private static final String QUERY_ARG_DO_ASYNC_SCAN = "android:query-arg-do-async-scan"; 450 451 /** 452 * Time between two polling attempts for availability of FuseDaemon thread. 453 */ 454 private static final long POLLING_TIME_IN_MILLIS = 100; 455 456 /** 457 * Enable option to defer the scan triggered as part of MediaProvider#update() 458 */ 459 @ChangeId 460 @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.R) 461 static final long ENABLE_DEFERRED_SCAN = 180326732L; 462 463 /** 464 * Enable option to include database rows of files from recently unmounted 465 * volume in MediaProvider#query 466 */ 467 @ChangeId 468 @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.R) 469 static final long ENABLE_INCLUDE_ALL_VOLUMES = 182734110L; 470 471 /** 472 * Set of {@link Cursor} columns that refer to raw filesystem paths. 473 */ 474 private static final ArrayMap<String, Object> sDataColumns = new ArrayMap<>(); 475 476 static { sDataColumns.put(MediaStore.MediaColumns.DATA, null)477 sDataColumns.put(MediaStore.MediaColumns.DATA, null); sDataColumns.put(MediaStore.Images.Thumbnails.DATA, null)478 sDataColumns.put(MediaStore.Images.Thumbnails.DATA, null); sDataColumns.put(MediaStore.Video.Thumbnails.DATA, null)479 sDataColumns.put(MediaStore.Video.Thumbnails.DATA, null); sDataColumns.put(MediaStore.Audio.PlaylistsColumns.DATA, null)480 sDataColumns.put(MediaStore.Audio.PlaylistsColumns.DATA, null); sDataColumns.put(MediaStore.Audio.AlbumColumns.ALBUM_ART, null)481 sDataColumns.put(MediaStore.Audio.AlbumColumns.ALBUM_ART, null); 482 } 483 484 private static final int sUserId = UserHandle.myUserId(); 485 486 /** 487 * Please use {@link getDownloadsProviderAuthority()} instead of using this directly. 488 */ 489 private static final String DOWNLOADS_PROVIDER_AUTHORITY = "downloads"; 490 491 @GuardedBy("mPendingOpenInfo") 492 private final Map<Integer, PendingOpenInfo> mPendingOpenInfo = new ArrayMap<>(); 493 494 @GuardedBy("mNonHiddenPaths") 495 private final LRUCache<String, Integer> mNonHiddenPaths = new LRUCache<>(NON_HIDDEN_CACHE_SIZE); 496 updateVolumes()497 public void updateVolumes() { 498 mVolumeCache.update(); 499 // Update filters to reflect mounted volumes so users don't get 500 // confused by metadata from ejected volumes 501 ForegroundThread.getExecutor().execute(() -> { 502 mExternalDatabase.setFilterVolumeNames(mVolumeCache.getExternalVolumeNames()); 503 }); 504 } 505 506 @NonNull getVolume(@onNull String volumeName)507 public MediaVolume getVolume(@NonNull String volumeName) throws FileNotFoundException { 508 return mVolumeCache.findVolume(volumeName, mCallingIdentity.get().getUser()); 509 } 510 511 @NonNull getVolumePath(@onNull String volumeName)512 public File getVolumePath(@NonNull String volumeName) throws FileNotFoundException { 513 // Ugly hack to keep unit tests passing, where we don't always have a 514 // Context to discover volumes with 515 if (getContext() == null) { 516 return Environment.getExternalStorageDirectory(); 517 } 518 519 return mVolumeCache.getVolumePath(volumeName, mCallingIdentity.get().getUser()); 520 } 521 522 @NonNull getAllowedVolumePaths(String volumeName)523 private Collection<File> getAllowedVolumePaths(String volumeName) 524 throws FileNotFoundException { 525 // This method is used to verify whether a path belongs to a certain volume name; 526 // we can't always use the calling user's identity here to determine exactly which 527 // volume is meant, because the MediaScanner may scan paths belonging to another user, 528 // eg a clone user. 529 // So, for volumes like external_primary, just return allowed paths for all users. 530 List<UserHandle> users = mUserCache.getUsersCached(); 531 ArrayList<File> allowedPaths = new ArrayList<>(); 532 for (UserHandle user : users) { 533 try { 534 Collection<File> volumeScanPaths = mVolumeCache.getVolumeScanPaths(volumeName, 535 user); 536 allowedPaths.addAll(volumeScanPaths); 537 } catch (FileNotFoundException e) { 538 Log.e(TAG, volumeName + " has no associated path for user: " + user); 539 } 540 } 541 542 return allowedPaths; 543 } 544 545 /** 546 * Frees any cache held by MediaProvider. 547 * 548 * @param bytes number of bytes which need to be freed 549 */ freeCache(long bytes)550 public void freeCache(long bytes) { 551 mTranscodeHelper.freeCache(bytes); 552 } 553 onAnrDelayStarted(@onNull String packageName, int uid, int tid, int reason)554 public void onAnrDelayStarted(@NonNull String packageName, int uid, int tid, int reason) { 555 mTranscodeHelper.onAnrDelayStarted(packageName, uid, tid, reason); 556 } 557 558 private volatile Locale mLastLocale = Locale.getDefault(); 559 560 private StorageManager mStorageManager; 561 private PackageManager mPackageManager; 562 private UserManager mUserManager; 563 private PickerUriResolver mPickerUriResolver; 564 565 private UserCache mUserCache; 566 private VolumeCache mVolumeCache; 567 568 private int mExternalStorageAuthorityAppId; 569 private int mDownloadsAuthorityAppId; 570 private Size mThumbSize; 571 572 /** 573 * Map from UID to cached {@link LocalCallingIdentity}. Values are only 574 * maintained in this map while the UID is actively working with a 575 * performance-critical component, such as camera. 576 */ 577 @GuardedBy("mCachedCallingIdentity") 578 private final SparseArray<LocalCallingIdentity> mCachedCallingIdentity = new SparseArray<>(); 579 580 private final OnOpActiveChangedListener mActiveListener = (code, uid, packageName, active) -> { 581 synchronized (mCachedCallingIdentity) { 582 if (active) { 583 // TODO moltmann: Set correct featureId 584 mCachedCallingIdentity.put(uid, 585 LocalCallingIdentity.fromExternal(getContext(), mUserCache, uid, 586 packageName, null)); 587 } else { 588 mCachedCallingIdentity.remove(uid); 589 } 590 } 591 }; 592 593 /** 594 * Map from UID to cached {@link LocalCallingIdentity}. Values are only 595 * maintained in this map until there's any change in the appops needed or packages 596 * used in the {@link LocalCallingIdentity}. 597 */ 598 @GuardedBy("mCachedCallingIdentityForFuse") 599 private final SparseArray<LocalCallingIdentity> mCachedCallingIdentityForFuse = 600 new SparseArray<>(); 601 602 private final OnOpChangedListener mModeListener = new OnOpChangedListener() { 603 604 /** 605 * Callback method called as part of {@link OnOpChangedListener}. 606 * Calls {@link #onOpChanged(String, String, int)} with cached userId(s). 607 * 608 * @param packageName - package for which AppOp changed 609 * @param op - AppOp for which the mode changed. 610 */ 611 public void onOpChanged(String op, String packageName) { 612 // In case no userId is supplied, we drop grants for all cached users. 613 List<UserHandle> userHandles = mUserCache.getUsersCached(); 614 for (UserHandle user : userHandles) { 615 onOpChanged(op, packageName, user.getIdentifier()); 616 } 617 } 618 619 /** 620 * Callback method called as part of {@link OnOpChangedListener}. 621 * When an AppOp is written - 622 * 1. We invalidate saved LocalCallingIdentity object for the package. This 623 * is needed to ensure we read the new permission state 624 * 2. If the AppOp change was on the read media appOps, we clear any stale 625 * grants, 626 * 627 * @param packageName - package for which AppOp changed 628 * @param op - AppOp for which the mode changed. 629 * @param userId - userSpace where the package is located 630 */ 631 public void onOpChanged(String op, String packageName, int userId) { 632 invalidateLocalCallingIdentityCache(packageName, "op " + op /* reason */); 633 removeMediaGrantsOnModeChange(packageName, op, userId); 634 } 635 }; 636 637 /** 638 * Removes media_grants for the given {@code packageName} and {@code userId} if the AppOp 639 * change resulted in a state of "Allow All" or "Deny All" for read 640 * permission. 641 */ removeMediaGrantsOnModeChange(String packageName, String op, int userId)642 private void removeMediaGrantsOnModeChange(String packageName, String op, int userId) { 643 // b/265963379: onModeChanged is always called with op=OPSTR_READ_EXTERNAL_STORAGE even if 644 // the appOp mode changed for other read media app ops. Handle all read media app op changes 645 // until the bug is fixed. 646 if (!SdkLevel.isAtLeastU() || !isReadMediaAppOp(op)) { 647 return; 648 } 649 Context context = getContext(); 650 PackageManager packageManager = context.getPackageManager(); 651 try { 652 int uid = packageManager.getPackageUidAsUser(packageName, 653 PackageManager.PackageInfoFlags.of(0), userId); 654 if (!LocalCallingIdentity.fromExternal(context, mUserCache, uid) 655 .checkCallingPermissionUserSelected()) { 656 // Revoke media grants if permission state is not "Select flow". 657 mMediaGrants.removeAllMediaGrantsForPackage( 658 packageName, 659 /*reason=*/ "Mode changed: " + op, 660 userId); 661 } 662 } catch (NameNotFoundException e) { 663 Log.d(TAG, "Unable to resolve uid. Ignoring the AppOp change for " 664 + packageName + ", User : " + userId); 665 } 666 } 667 668 /** 669 * Returns {@code true} if the given {@code op} is one of the appOp 670 * related to read media appOps 671 */ isReadMediaAppOp(String op)672 private boolean isReadMediaAppOp(String op) { 673 return AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE.equals(op) 674 || AppOpsManager.OPSTR_READ_MEDIA_IMAGES.equals(op) 675 || AppOpsManager.OPSTR_READ_MEDIA_VIDEO.equals(op) 676 || AppOpsManager.OPSTR_READ_MEDIA_VISUAL_USER_SELECTED.equals(op); 677 } 678 679 /** 680 * Retrieves a cached calling identity or creates a new one. Also, always sets the app-op 681 * description for the calling identity. 682 */ getCachedCallingIdentityForFuse(int uid)683 private LocalCallingIdentity getCachedCallingIdentityForFuse(int uid) { 684 synchronized (mCachedCallingIdentityForFuse) { 685 PermissionUtils.setOpDescription("via FUSE"); 686 LocalCallingIdentity identity = mCachedCallingIdentityForFuse.get(uid); 687 if (identity == null) { 688 identity = LocalCallingIdentity.fromExternal(getContext(), mUserCache, uid); 689 if (uidToUserId(uid) == sUserId) { 690 mCachedCallingIdentityForFuse.put(uid, identity); 691 } else { 692 // In some app cloning designs, MediaProvider user 0 may 693 // serve requests for apps running as a "clone" user; in 694 // those cases, don't keep a cache for the clone user, since 695 // we don't get any invalidation events for these users. 696 } 697 } 698 return identity; 699 } 700 } 701 702 /** 703 * Calling identity state about on the current thread. Populated on demand, 704 * and invalidated by {@link #onCallingPackageChanged()} when each remote 705 * call is finished. 706 */ 707 private final ThreadLocal<LocalCallingIdentity> mCallingIdentity = ThreadLocal 708 .withInitial(() -> { 709 PermissionUtils.setOpDescription("via MediaProvider"); 710 synchronized (mCachedCallingIdentity) { 711 final LocalCallingIdentity cached = mCachedCallingIdentity 712 .get(Binder.getCallingUid()); 713 return (cached != null) ? cached 714 : LocalCallingIdentity.fromBinder(getContext(), this, mUserCache); 715 } 716 }); 717 718 /** 719 * We simply propagate the UID that is being tracked by 720 * {@link LocalCallingIdentity}, which means we accurately blame both 721 * incoming Binder calls and FUSE calls. 722 */ 723 private final ProxyTransactListener mTransactListener = new ProxyTransactListener() { 724 @Override 725 public Object onTransactStarted(IBinder binder, int transactionCode) { 726 if (LOGV) Trace.beginSection(Thread.currentThread().getStackTrace()[5].getMethodName()); 727 // Check if mCallindIdentity was created within a fuse or content provider transaction 728 if (mCallingIdentity.get().isValidProviderOrFuseCallingIdentity()) { 729 return Binder.setCallingWorkSourceUid(mCallingIdentity.get().uid); 730 } 731 // If mCallingIdentity was not created for a fuse or content provider transaction, 732 // we should reset it, the next time it is retrieved it will be created for the 733 // appropriate caller. 734 mCallingIdentity.remove(); 735 return Binder.setCallingWorkSourceUid(Binder.getCallingUid()); 736 } 737 738 @Override 739 public void onTransactEnded(Object session) { 740 final long token = (long) session; 741 Binder.restoreCallingWorkSource(token); 742 if (LOGV) Trace.endSection(); 743 } 744 }; 745 746 // In memory cache of path<->id mappings, to speed up inserts during media scan 747 @GuardedBy("mDirectoryCache") 748 private final ArrayMap<String, Long> mDirectoryCache = new ArrayMap<>(); 749 750 private static final String[] sDataOnlyColumn = new String[] { 751 FileColumns.DATA 752 }; 753 754 private static final String ID_NOT_PARENT_CLAUSE = 755 "_id NOT IN (SELECT parent FROM files WHERE parent IS NOT NULL)"; 756 757 private static final String CANONICAL = "canonical"; 758 759 private static final String ALL_VOLUMES = "all_volumes"; 760 761 private final BroadcastReceiver mPackageReceiver = new BroadcastReceiver() { 762 @Override 763 public void onReceive(Context context, Intent intent) { 764 switch (intent.getAction()) { 765 case Intent.ACTION_PACKAGE_REMOVED: 766 case Intent.ACTION_PACKAGE_ADDED: 767 Uri uri = intent.getData(); 768 String pkg = uri != null ? uri.getSchemeSpecificPart() : null; 769 if (pkg != null) { 770 invalidateLocalCallingIdentityCache(pkg, "package " + intent.getAction()); 771 if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) { 772 mUserCache.invalidateWorkProfileOwnerApps(pkg); 773 mPickerSyncController.notifyPackageRemoval(pkg); 774 invalidateDentryForExternalStorage(pkg); 775 } 776 } else { 777 Log.w(TAG, "Failed to retrieve package from intent: " + intent.getAction()); 778 } 779 break; 780 } 781 } 782 }; 783 invalidateDentryForExternalStorage(String packageName)784 private void invalidateDentryForExternalStorage(String packageName) { 785 for (MediaVolume vol : mVolumeCache.getExternalVolumes()) { 786 try { 787 invalidateFuseDentry(String.format(Locale.ROOT, 788 "%s/Android/media/%s/", getVolumePath(vol.getName()).getAbsolutePath(), 789 packageName)); 790 } catch (FileNotFoundException e) { 791 Log.e(TAG, "External volume path not found for " + vol.getName(), e); 792 } 793 } 794 } 795 796 private final BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() { 797 @Override 798 public void onReceive(Context context, Intent intent) { 799 switch (intent.getAction()) { 800 case Intent.ACTION_USER_REMOVED: 801 /** 802 * Removing media files for user being deleted. This would impact if the deleted 803 * user have been using same MediaProvider as the current user i.e. when 804 * isMediaSharedWithParent is true.On removal of such user profile, 805 * the owner's MediaProvider would need to clean any media files stored 806 * by the removed user profile. 807 */ 808 UserHandle userToBeRemoved = intent.getParcelableExtra(Intent.EXTRA_USER); 809 if(userToBeRemoved.getIdentifier() != sUserId){ 810 mExternalDatabase.runWithTransaction((db) -> { 811 db.execSQL("delete from files where _user_id=?", 812 new String[]{String.valueOf(userToBeRemoved.getIdentifier())}); 813 return null ; 814 }); 815 } 816 break; 817 } 818 } 819 }; 820 invalidateLocalCallingIdentityCache(String packageName, String reason)821 private void invalidateLocalCallingIdentityCache(String packageName, String reason) { 822 synchronized (mCachedCallingIdentityForFuse) { 823 try { 824 int packageUid = getContext().getPackageManager().getPackageUid(packageName, 0); 825 if (mCachedCallingIdentityForFuse.contains(packageUid)) { 826 mCachedCallingIdentityForFuse.get(packageUid).dump(reason); 827 mCachedCallingIdentityForFuse.remove(packageUid); 828 } 829 } catch (NameNotFoundException ignored) { 830 } 831 } 832 } 833 updateQuotaTypeForUri(@onNull Uri uri, int mediaType, @NonNull String volumeName)834 private void updateQuotaTypeForUri(@NonNull Uri uri, int mediaType, 835 @NonNull String volumeName) { 836 // Quota type is only updated for external primary volume 837 if (!MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)) { 838 return; 839 } 840 841 Trace.beginSection("MP.updateQuotaTypeForUri"); 842 File file; 843 try { 844 file = queryForDataFile(uri, null); 845 if (!file.exists()) { 846 // This can happen if an item is inserted in MediaStore before it is created 847 return; 848 } 849 850 if (mediaType == FileColumns.MEDIA_TYPE_NONE) { 851 // This might be because the file is hidden; but we still want to 852 // attribute its quota to the correct type, so get the type from 853 // the extension instead. 854 mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(file)); 855 } 856 857 updateQuotaTypeForFileInternal(file, mediaType); 858 } catch (FileNotFoundException | IllegalArgumentException e) { 859 // Ignore 860 Log.w(TAG, "Failed to update quota for uri: " + uri, e); 861 } finally { 862 Trace.endSection(); 863 } 864 } 865 updateQuotaTypeForFileInternal(File file, int mediaType)866 private void updateQuotaTypeForFileInternal(File file, int mediaType) { 867 try { 868 switch (mediaType) { 869 case FileColumns.MEDIA_TYPE_AUDIO: 870 mStorageManager.updateExternalStorageFileQuotaType(file, 871 StorageManager.QUOTA_TYPE_MEDIA_AUDIO); 872 break; 873 case FileColumns.MEDIA_TYPE_VIDEO: 874 mStorageManager.updateExternalStorageFileQuotaType(file, 875 StorageManager.QUOTA_TYPE_MEDIA_VIDEO); 876 break; 877 case FileColumns.MEDIA_TYPE_IMAGE: 878 mStorageManager.updateExternalStorageFileQuotaType(file, 879 StorageManager.QUOTA_TYPE_MEDIA_IMAGE); 880 break; 881 default: 882 mStorageManager.updateExternalStorageFileQuotaType(file, 883 StorageManager.QUOTA_TYPE_MEDIA_NONE); 884 break; 885 } 886 } catch (IOException e) { 887 Log.w(TAG, "Failed to update quota type for " + file.getPath(), e); 888 } 889 } 890 891 /** 892 * Since these operations are in the critical path of apps working with 893 * media, we only collect the {@link Uri} that need to be notified, and all 894 * other side-effect operations are delegated to {@link BackgroundThread} so 895 * that we return as quickly as possible. 896 */ 897 private final OnFilesChangeListener mFilesListener = new OnFilesChangeListener() { 898 @Override 899 public void onInsert(@NonNull DatabaseHelper helper, @NonNull FileRow insertedRow) { 900 if (helper.isDatabaseRecovering()) { 901 // Do not perform any trigger operation if database is recovering 902 return; 903 } 904 905 handleInsertedRowForFuse(insertedRow.getId()); 906 acceptWithExpansion(helper::notifyInsert, insertedRow.getVolumeName(), 907 insertedRow.getId(), insertedRow.getMediaType(), insertedRow.isDownload()); 908 909 mDatabaseBackupAndRecovery.updateNextRowIdXattr(helper, insertedRow.getId()); 910 911 helper.postBackground(() -> { 912 if (helper.isExternal() && !isFuseThread()) { 913 // Update the quota type on the filesystem 914 Uri fileUri = MediaStore.Files.getContentUri(insertedRow.getVolumeName(), 915 insertedRow.getId()); 916 updateQuotaTypeForUri(fileUri, insertedRow.getMediaType(), 917 insertedRow.getVolumeName()); 918 } 919 920 // Tell our SAF provider so it knows when views are no longer empty 921 MediaDocumentsProvider.onMediaStoreInsert(getContext(), insertedRow.getVolumeName(), 922 insertedRow.getMediaType(), insertedRow.getId()); 923 924 if (mExternalDbFacade.onFileInserted(insertedRow.getMediaType(), 925 insertedRow.isPending())) { 926 mPickerSyncController.notifyMediaEvent(); 927 } 928 929 mDatabaseBackupAndRecovery.backupVolumeDbData(helper, insertedRow); 930 }); 931 } 932 933 @Override 934 public void onUpdate(@NonNull DatabaseHelper helper, @NonNull FileRow oldRow, 935 @NonNull FileRow newRow) { 936 if (helper.isDatabaseRecovering()) { 937 // Do not perform any trigger operation if database is recovering 938 return; 939 } 940 941 final boolean isDownload = oldRow.isDownload() || newRow.isDownload(); 942 final Uri fileUri = MediaStore.Files.getContentUri(oldRow.getVolumeName(), 943 oldRow.getId()); 944 handleUpdatedRowForFuse(oldRow.getPath(), oldRow.getOwnerPackageName(), oldRow.getId(), 945 newRow.getId()); 946 handleOwnerPackageNameChange(oldRow.getPath(), oldRow.getOwnerPackageName(), 947 newRow.getOwnerPackageName()); 948 acceptWithExpansion(helper::notifyUpdate, oldRow.getVolumeName(), oldRow.getId(), 949 oldRow.getMediaType(), isDownload); 950 951 mDatabaseBackupAndRecovery.updateNextRowIdAndSetDirty(helper, oldRow, newRow); 952 953 helper.postBackground(() -> { 954 if (helper.isExternal()) { 955 // Update the quota type on the filesystem 956 updateQuotaTypeForUri(fileUri, newRow.getMediaType(), oldRow.getVolumeName()); 957 } 958 959 if (mExternalDbFacade.onFileUpdated(oldRow.getId(), 960 oldRow.getMediaType(), newRow.getMediaType(), 961 oldRow.isTrashed(), newRow.isTrashed(), 962 oldRow.isPending(), newRow.isPending(), 963 oldRow.isFavorite(), newRow.isFavorite(), 964 oldRow.getSpecialFormat(), newRow.getSpecialFormat())) { 965 mPickerSyncController.notifyMediaEvent(); 966 } 967 968 mDatabaseBackupAndRecovery.updateBackup(helper, oldRow, newRow); 969 }); 970 971 if (newRow.getMediaType() != oldRow.getMediaType()) { 972 acceptWithExpansion(helper::notifyUpdate, oldRow.getVolumeName(), oldRow.getId(), 973 newRow.getMediaType(), isDownload); 974 975 helper.postBackground(() -> { 976 // Invalidate any thumbnails when the media type changes 977 invalidateThumbnails(fileUri); 978 }); 979 } 980 } 981 982 @Override 983 public void onDelete(@NonNull DatabaseHelper helper, @NonNull FileRow deletedRow) { 984 if (helper.isDatabaseRecovering()) { 985 // Do not perform any trigger operation if database is recovering 986 return; 987 } 988 989 handleDeletedRowForFuse(deletedRow.getPath(), deletedRow.getOwnerPackageName(), 990 deletedRow.getId()); 991 acceptWithExpansion(helper::notifyDelete, deletedRow.getVolumeName(), 992 deletedRow.getId(), deletedRow.getMediaType(), deletedRow.isDownload()); 993 // Remove cached transcoded file if any 994 mTranscodeHelper.deleteCachedTranscodeFile(deletedRow.getId()); 995 996 helper.postBackground(() -> { 997 // Item no longer exists, so revoke all access to it 998 Trace.beginSection("MP.revokeUriPermission"); 999 try { 1000 acceptWithExpansion((uri) -> getContext().revokeUriPermission(uri, ~0), 1001 deletedRow.getVolumeName(), deletedRow.getId(), 1002 deletedRow.getMediaType(), deletedRow.isDownload()); 1003 } finally { 1004 Trace.endSection(); 1005 } 1006 1007 switch (deletedRow.getMediaType()) { 1008 case FileColumns.MEDIA_TYPE_PLAYLIST: 1009 case FileColumns.MEDIA_TYPE_AUDIO: 1010 if (helper.isExternal()) { 1011 removePlaylistMembers(deletedRow.getMediaType(), deletedRow.getId()); 1012 } 1013 } 1014 1015 // Invalidate any thumbnails now that media is gone 1016 invalidateThumbnails(MediaStore.Files.getContentUri(deletedRow.getVolumeName(), 1017 deletedRow.getId())); 1018 1019 // Tell our SAF provider so it can revoke too 1020 MediaDocumentsProvider.onMediaStoreDelete(getContext(), deletedRow.getVolumeName(), 1021 deletedRow.getMediaType(), deletedRow.getId()); 1022 1023 if (mExternalDbFacade.onFileDeleted(deletedRow.getId(), 1024 deletedRow.getMediaType())) { 1025 mPickerSyncController.notifyMediaEvent(); 1026 } 1027 1028 mDatabaseBackupAndRecovery.deleteFromDbBackup(helper, deletedRow); 1029 }); 1030 } 1031 }; 1032 1033 private final UnaryOperator<String> mIdGenerator = path -> { 1034 final long rowId = mCallingIdentity.get().getDeletedRowId(path); 1035 if (rowId != -1 && isFuseThread()) { 1036 return String.valueOf(rowId); 1037 } 1038 return null; 1039 }; 1040 1041 /** {@hide} */ 1042 public static final OnLegacyMigrationListener MIGRATION_LISTENER = 1043 new OnLegacyMigrationListener() { 1044 @Override 1045 public void onStarted(ContentProviderClient client, String volumeName) { 1046 MediaStore.startLegacyMigration(ContentResolver.wrap(client), volumeName); 1047 } 1048 1049 @Override 1050 public void onProgress(ContentProviderClient client, String volumeName, 1051 long progress, long total) { 1052 // TODO: notify blocked threads of progress once we can change APIs 1053 } 1054 1055 @Override 1056 public void onFinished(ContentProviderClient client, String volumeName) { 1057 MediaStore.finishLegacyMigration(ContentResolver.wrap(client), volumeName); 1058 } 1059 }; 1060 1061 /** 1062 * Apply {@link Consumer#accept} to the given item. 1063 * <p> 1064 * Since media items can be exposed through multiple collections or views, 1065 * this method expands the single item being accepted to also accept all 1066 * relevant views. 1067 */ acceptWithExpansion(@onNull Consumer<Uri> consumer, @NonNull String volumeName, long id, int mediaType, boolean isDownload)1068 private void acceptWithExpansion(@NonNull Consumer<Uri> consumer, @NonNull String volumeName, 1069 long id, int mediaType, boolean isDownload) { 1070 switch (mediaType) { 1071 case FileColumns.MEDIA_TYPE_AUDIO: 1072 consumer.accept(MediaStore.Audio.Media.getContentUri(volumeName, id)); 1073 1074 // Any changing audio items mean we probably need to invalidate all 1075 // indexed views built from that media 1076 consumer.accept(Audio.Genres.getContentUri(volumeName)); 1077 consumer.accept(Audio.Playlists.getContentUri(volumeName)); 1078 consumer.accept(Audio.Artists.getContentUri(volumeName)); 1079 consumer.accept(Audio.Albums.getContentUri(volumeName)); 1080 break; 1081 1082 case FileColumns.MEDIA_TYPE_VIDEO: 1083 consumer.accept(MediaStore.Video.Media.getContentUri(volumeName, id)); 1084 break; 1085 1086 case FileColumns.MEDIA_TYPE_IMAGE: 1087 consumer.accept(MediaStore.Images.Media.getContentUri(volumeName, id)); 1088 break; 1089 1090 case FileColumns.MEDIA_TYPE_PLAYLIST: 1091 consumer.accept(ContentUris.withAppendedId( 1092 MediaStore.Audio.Playlists.getContentUri(volumeName), id)); 1093 break; 1094 } 1095 1096 // Also notify through any generic views 1097 consumer.accept(MediaStore.Files.getContentUri(volumeName, id)); 1098 if (isDownload) { 1099 consumer.accept(MediaStore.Downloads.getContentUri(volumeName, id)); 1100 } 1101 1102 // Rinse and repeat through any synthetic views 1103 switch (volumeName) { 1104 case MediaStore.VOLUME_INTERNAL: 1105 case MediaStore.VOLUME_EXTERNAL: 1106 // Already a top-level view, no need to expand 1107 break; 1108 default: 1109 acceptWithExpansion(consumer, MediaStore.VOLUME_EXTERNAL, 1110 id, mediaType, isDownload); 1111 break; 1112 } 1113 } 1114 1115 /** 1116 * Ensure that default folders are created on mounted storage devices. 1117 * We only do this once per volume so we don't annoy the user if deleted 1118 * manually. 1119 */ ensureDefaultFolders(@onNull MediaVolume volume, @NonNull SQLiteDatabase db)1120 private void ensureDefaultFolders(@NonNull MediaVolume volume, @NonNull SQLiteDatabase db) { 1121 if (volume.shouldSkipDefaultDirCreation()) { 1122 // Default folders should not be automatically created inside volumes managed from 1123 // outside Android. 1124 return; 1125 } 1126 final String volumeName = volume.getName(); 1127 String key; 1128 if (volumeName.equals(MediaStore.VOLUME_EXTERNAL_PRIMARY)) { 1129 // For the primary volume, we use the ID, because we may be handling 1130 // the primary volume for multiple users 1131 key = "created_default_folders_" + volume.getId(); 1132 } else { 1133 // For others, like public volumes, just use the name, because the id 1134 // might not change when re-formatted 1135 key = "created_default_folders_" + volumeName; 1136 } 1137 1138 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); 1139 if (prefs.getInt(key, 0) == 0) { 1140 for (String folderName : DEFAULT_FOLDER_NAMES) { 1141 final File folder = new File(volume.getPath(), folderName); 1142 if (!folder.exists()) { 1143 folder.mkdirs(); 1144 insertDirectory(db, folder.getAbsolutePath()); 1145 } 1146 } 1147 1148 SharedPreferences.Editor editor = prefs.edit(); 1149 editor.putInt(key, 1); 1150 editor.commit(); 1151 } 1152 } 1153 1154 /** 1155 * Ensure that any thumbnail collections on the given storage volume can be 1156 * used with the given {@link DatabaseHelper}. If the 1157 * {@link DatabaseHelper#getOrCreateUuid} doesn't match the UUID found on 1158 * disk, then all thumbnails will be considered stable and will be deleted. 1159 */ ensureThumbnailsValid(@onNull MediaVolume volume, @NonNull SQLiteDatabase db)1160 private void ensureThumbnailsValid(@NonNull MediaVolume volume, @NonNull SQLiteDatabase db) { 1161 if (volume.shouldSkipDefaultDirCreation()) { 1162 // Default folders and thumbnail directories should not be automatically created inside 1163 // volumes managed from outside Android, and there is no need to ensure the validity of 1164 // their thumbnails here. 1165 return; 1166 } 1167 final String uuidFromDatabase = DatabaseHelper.getOrCreateUuid(db); 1168 try { 1169 for (File dir : getThumbnailDirectories(volume)) { 1170 if (!dir.exists()) { 1171 dir.mkdirs(); 1172 } 1173 1174 final File file = new File(dir, FILE_DATABASE_UUID); 1175 final Optional<String> uuidFromDisk = FileUtils.readString(file); 1176 1177 final boolean updateUuid; 1178 if (!uuidFromDisk.isPresent()) { 1179 // For newly inserted volumes or upgrading of existing volumes, 1180 // assume that our current UUID is valid 1181 updateUuid = true; 1182 } else if (!Objects.equals(uuidFromDatabase, uuidFromDisk.get())) { 1183 // The UUID of database disagrees with the one on disk, 1184 // which means we can't trust any thumbnails 1185 Log.d(TAG, "Invalidating all thumbnails under " + dir); 1186 FileUtils.walkFileTreeContents(dir.toPath(), this::deleteAndInvalidate); 1187 updateUuid = true; 1188 } else { 1189 updateUuid = false; 1190 } 1191 1192 if (updateUuid) { 1193 FileUtils.writeString(file, Optional.of(uuidFromDatabase)); 1194 } 1195 } 1196 } catch (IOException e) { 1197 Log.w(TAG, "Failed to ensure thumbnails valid for " + volume.getName(), e); 1198 } 1199 } 1200 1201 @Override attachInfo(Context context, ProviderInfo info)1202 public void attachInfo(Context context, ProviderInfo info) { 1203 Log.v(TAG, "Attached " + info.authority + " from " + info.applicationInfo.packageName); 1204 1205 mUriMatcher = new LocalUriMatcher(info.authority); 1206 1207 super.attachInfo(context, info); 1208 } 1209 1210 @Nullable 1211 private static MediaProvider sInstance; 1212 1213 @Nullable getInstance()1214 static synchronized MediaProvider getInstance() { 1215 return sInstance; 1216 } 1217 1218 @Override onCreate()1219 public boolean onCreate() { 1220 synchronized (MediaProvider.class) { 1221 sInstance = this; 1222 } 1223 1224 final Context context = getContext(); 1225 1226 mUserCache = new UserCache(context); 1227 1228 // Shift call statistics back to the original caller 1229 Binder.setProxyTransactListener(mTransactListener); 1230 1231 mStorageManager = context.getSystemService(StorageManager.class); 1232 AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class); 1233 mPackageManager = context.getPackageManager(); 1234 mUserManager = context.getSystemService(UserManager.class); 1235 mVolumeCache = new VolumeCache(context, mUserCache); 1236 1237 // Reasonable thumbnail size is half of the smallest screen edge width 1238 final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); 1239 final int thumbSize = Math.min(metrics.widthPixels, metrics.heightPixels) / 2; 1240 mThumbSize = new Size(thumbSize, thumbSize); 1241 1242 mConfigStore = createConfigStore(); 1243 mDatabaseBackupAndRecovery = createDatabaseBackupAndRecovery(); 1244 1245 mMediaScanner = new ModernMediaScanner(context); 1246 mProjectionHelper = new ProjectionHelper(Column.class, ExportedSince.class); 1247 mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, false, false, 1248 mProjectionHelper, Metrics::logSchemaChange, mFilesListener, 1249 MIGRATION_LISTENER, mIdGenerator, true, mDatabaseBackupAndRecovery); 1250 mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME, false, false, 1251 mProjectionHelper, Metrics::logSchemaChange, mFilesListener, 1252 MIGRATION_LISTENER, mIdGenerator, true, mDatabaseBackupAndRecovery); 1253 mExternalDbFacade = new ExternalDbFacade(getContext(), mExternalDatabase, mVolumeCache); 1254 mPickerDbFacade = new PickerDbFacade(context); 1255 1256 mMediaGrants = new MediaGrants(mExternalDatabase); 1257 1258 mPickerSyncController = new PickerSyncController(context, mPickerDbFacade, mConfigStore); 1259 mPickerDataLayer = new PickerDataLayer(context, mPickerDbFacade, mPickerSyncController); 1260 mPickerUriResolver = new PickerUriResolver(context, mPickerDbFacade, mProjectionHelper); 1261 1262 if (SdkLevel.isAtLeastS()) { 1263 mTranscodeHelper = new TranscodeHelperImpl(context, this, mConfigStore); 1264 } else { 1265 mTranscodeHelper = new TranscodeHelperNoOp(); 1266 } 1267 1268 // Create dir for redacted and picker URI paths. 1269 buildPrimaryVolumeFile(uidToUserId(MY_UID), getRedactedRelativePath()).mkdirs(); 1270 1271 final IntentFilter packageFilter = new IntentFilter(); 1272 packageFilter.setPriority(10); 1273 packageFilter.addDataScheme("package"); 1274 packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 1275 packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); 1276 context.registerReceiver(mPackageReceiver, packageFilter); 1277 1278 // Creating intent broadcast receiver for user actions like Intent.ACTION_USER_REMOVED, 1279 // where we would need to remove files stored by removed user. 1280 final IntentFilter userIntentFilter = new IntentFilter(); 1281 userIntentFilter.addAction(Intent.ACTION_USER_REMOVED); 1282 context.registerReceiver(mUserIntentReceiver, userIntentFilter); 1283 1284 // Watch for invalidation of cached volumes 1285 mStorageManager.registerStorageVolumeCallback(context.getMainExecutor(), 1286 new StorageVolumeCallback() { 1287 @Override 1288 public void onStateChanged(@NonNull StorageVolume volume) { 1289 updateVolumes(); 1290 } 1291 }); 1292 1293 if (SdkLevel.isAtLeastT()) { 1294 try { 1295 mStorageManager.setCloudMediaProvider(mPickerSyncController.getCloudProvider()); 1296 } catch (SecurityException e) { 1297 // This can happen in unit tests 1298 Log.w(TAG, "Failed to update the system_server with the latest cloud provider", e); 1299 } 1300 } 1301 1302 updateVolumes(); 1303 attachVolume(MediaVolume.fromInternal(), /* validate */ false); 1304 for (MediaVolume volume : mVolumeCache.getExternalVolumes()) { 1305 attachVolume(volume, /* validate */ false); 1306 } 1307 1308 // Watch for performance-sensitive activity 1309 appOpsManager.startWatchingActive(new String[] { 1310 AppOpsManager.OPSTR_CAMERA 1311 }, context.getMainExecutor(), mActiveListener); 1312 1313 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE, 1314 null /* all packages */, mModeListener); 1315 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_AUDIO, 1316 null /* all packages */, mModeListener); 1317 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_IMAGES, 1318 null /* all packages */, mModeListener); 1319 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_VIDEO, 1320 null /* all packages */, mModeListener); 1321 if (SdkLevel.isAtLeastU()) { 1322 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_VISUAL_USER_SELECTED, 1323 null /* all packages */, mModeListener); 1324 } 1325 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_EXTERNAL_STORAGE, 1326 null /* all packages */, mModeListener); 1327 appOpsManager.startWatchingMode(permissionToOp(ACCESS_MEDIA_LOCATION), 1328 null /* all packages */, mModeListener); 1329 // Legacy apps 1330 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_LEGACY_STORAGE, 1331 null /* all packages */, mModeListener); 1332 // File managers 1333 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_MANAGE_EXTERNAL_STORAGE, 1334 null /* all packages */, mModeListener); 1335 // Default gallery changes 1336 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES, 1337 null /* all packages */, mModeListener); 1338 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO, 1339 null /* all packages */, mModeListener); 1340 try { 1341 // Here we are forced to depend on the non-public API of AppOpsManager. If 1342 // OPSTR_NO_ISOLATED_STORAGE app op is not defined in AppOpsManager, then this call will 1343 // throw an IllegalArgumentException during MediaProvider startup. In combination with 1344 // MediaProvider's CTS tests it should give us guarantees that OPSTR_NO_ISOLATED_STORAGE 1345 // is defined. 1346 appOpsManager.startWatchingMode(AppOpsManager.OPSTR_NO_ISOLATED_STORAGE, 1347 null /* all packages */, mModeListener); 1348 } catch (IllegalArgumentException e) { 1349 Log.w(TAG, "Failed to start watching " + AppOpsManager.OPSTR_NO_ISOLATED_STORAGE, e); 1350 } 1351 1352 ProviderInfo provider = mPackageManager.resolveContentProvider( 1353 getDownloadsProviderAuthority(), PackageManager.MATCH_DIRECT_BOOT_AWARE 1354 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE); 1355 if (provider != null) { 1356 mDownloadsAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid); 1357 } 1358 1359 provider = mPackageManager.resolveContentProvider(getExternalStorageProviderAuthority(), 1360 PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE); 1361 if (provider != null) { 1362 mExternalStorageAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid); 1363 } 1364 1365 storageNativeBootPropertyChangeListener(); 1366 mConfigStore.addOnChangeListener( 1367 BackgroundThread.getExecutor(), this::storageNativeBootPropertyChangeListener); 1368 1369 // media_grants are cleared on device reboot, and onCreate is a good signal for this. 1370 ForegroundThread.getExecutor().execute(() -> { 1371 mMediaGrants.removeAllMediaGrants(); 1372 }); 1373 1374 PulledMetrics.initialize(context); 1375 return true; 1376 } 1377 1378 @VisibleForTesting storageNativeBootPropertyChangeListener()1379 protected void storageNativeBootPropertyChangeListener() { 1380 boolean isGetContentTakeoverEnabled; 1381 if (SdkLevel.isAtLeastT()) { 1382 isGetContentTakeoverEnabled = true; 1383 } else { 1384 isGetContentTakeoverEnabled = mConfigStore.isGetContentTakeOverEnabled(); 1385 } 1386 setComponentEnabledSetting("PhotoPickerGetContentActivity", isGetContentTakeoverEnabled); 1387 1388 setComponentEnabledSetting("PhotoPickerUserSelectActivity", 1389 mConfigStore.isUserSelectForAppEnabled()); 1390 1391 mDatabaseBackupAndRecovery.onConfigPropertyChangeListener(); 1392 } 1393 getDatabaseBackupAndRecovery()1394 public DatabaseBackupAndRecovery getDatabaseBackupAndRecovery() { 1395 return mDatabaseBackupAndRecovery; 1396 } 1397 setComponentEnabledSetting(@onNull String activityName, boolean isEnabled)1398 private void setComponentEnabledSetting(@NonNull String activityName, boolean isEnabled) { 1399 final String activityFullName = 1400 PhotoPickerActivity.class.getPackage().getName() + "." + activityName; 1401 final ComponentName componentName = new ComponentName(getContext().getPackageName(), 1402 activityFullName); 1403 1404 final int expectedState = isEnabled 1405 ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED 1406 : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; 1407 1408 Log.i(TAG, "Changed " + activityName + " component state to " 1409 + componentStateToString(expectedState)); 1410 1411 getContext().getPackageManager().setComponentEnabledSetting(componentName, expectedState, 1412 PackageManager.DONT_KILL_APP); 1413 } 1414 getDatabaseHelper(String dbName)1415 Optional<DatabaseHelper> getDatabaseHelper(String dbName) { 1416 if (dbName.equalsIgnoreCase(INTERNAL_DATABASE_NAME)) { 1417 return Optional.of(mInternalDatabase); 1418 } else if (dbName.equalsIgnoreCase(EXTERNAL_DATABASE_NAME)) { 1419 return Optional.of(mExternalDatabase); 1420 } 1421 1422 return Optional.empty(); 1423 } 1424 1425 @Override onCallingPackageChanged()1426 public void onCallingPackageChanged() { 1427 // Identity of the current thread has changed, so invalidate caches 1428 mCallingIdentity.remove(); 1429 } 1430 clearLocalCallingIdentity()1431 public LocalCallingIdentity clearLocalCallingIdentity() { 1432 // We retain the user part of the calling identity, since we are executing 1433 // the call on behalf of that user, and we need to maintain the user context 1434 // to correctly resolve things like volumes 1435 UserHandle user = mCallingIdentity.get().getUser(); 1436 return clearLocalCallingIdentity(LocalCallingIdentity.fromSelfAsUser(getContext(), user)); 1437 } 1438 clearLocalCallingIdentity(LocalCallingIdentity replacement)1439 public LocalCallingIdentity clearLocalCallingIdentity(LocalCallingIdentity replacement) { 1440 final LocalCallingIdentity token = mCallingIdentity.get(); 1441 mCallingIdentity.set(replacement); 1442 return token; 1443 } 1444 restoreLocalCallingIdentity(LocalCallingIdentity token)1445 public void restoreLocalCallingIdentity(LocalCallingIdentity token) { 1446 mCallingIdentity.set(token); 1447 } 1448 isPackageKnown(@onNull String packageName, int userId)1449 private boolean isPackageKnown(@NonNull String packageName, int userId) { 1450 final Context context = mUserCache.getContextForUser(UserHandle.of(userId)); 1451 final PackageManager pm = context.getPackageManager(); 1452 1453 // First, is the app actually installed? 1454 try { 1455 pm.getPackageInfo(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES); 1456 return true; 1457 } catch (NameNotFoundException ignored) { 1458 } 1459 1460 // Second, is the app pending, probably from a backup/restore operation? 1461 for (SessionInfo si : pm.getPackageInstaller().getAllSessions()) { 1462 if (Objects.equals(packageName, si.getAppPackageName())) { 1463 return true; 1464 } 1465 } 1466 1467 // I've never met this package in my life 1468 return false; 1469 } 1470 onIdleMaintenance(@onNull CancellationSignal signal)1471 public void onIdleMaintenance(@NonNull CancellationSignal signal) { 1472 final long startTime = SystemClock.elapsedRealtime(); 1473 // Trim any stale log files before we emit new events below 1474 Logging.trimPersistent(); 1475 1476 // Scan all volumes to resolve any staleness 1477 for (MediaVolume volume : mVolumeCache.getExternalVolumes()) { 1478 // Possibly bail before digging into each volume 1479 signal.throwIfCanceled(); 1480 1481 try { 1482 MediaService.onScanVolume(getContext(), volume, REASON_IDLE); 1483 } catch (IOException e) { 1484 Log.w(TAG, e); 1485 } 1486 1487 // Ensure that our thumbnails are valid 1488 mExternalDatabase.runWithTransaction((db) -> { 1489 ensureThumbnailsValid(volume, db); 1490 return null; 1491 }); 1492 } 1493 1494 // Delete any stale thumbnails 1495 final int staleThumbnails = mExternalDatabase.runWithTransaction((db) -> { 1496 return pruneThumbnails(db, signal); 1497 }); 1498 Log.d(TAG, "Pruned " + staleThumbnails + " unknown thumbnails"); 1499 1500 // Finished orphaning any content whose package no longer exists 1501 pruneStalePackages(signal); 1502 1503 // Delete the expired items or extend them on mounted volumes 1504 final int[] result = deleteOrExtendExpiredItems(signal); 1505 final int deletedExpiredMedia = result[0]; 1506 Log.d(TAG, "Deleted " + deletedExpiredMedia + " expired items"); 1507 Log.d(TAG, "Extended " + result[1] + " expired items"); 1508 1509 // Forget any stale volumes 1510 deleteStaleVolumes(signal); 1511 1512 final long itemCount = mExternalDatabase.runWithTransaction(DatabaseHelper::getItemCount); 1513 1514 // Cleaning media files for users that have been removed 1515 cleanMediaFilesForRemovedUser(signal); 1516 1517 // Calculate standard_mime_type_extension column for files which have SPECIAL_FORMAT column 1518 // value as NULL, and update the same in the picker db 1519 detectSpecialFormat(signal); 1520 1521 final long durationMillis = (SystemClock.elapsedRealtime() - startTime); 1522 Metrics.logIdleMaintenance(MediaStore.VOLUME_EXTERNAL, itemCount, 1523 durationMillis, staleThumbnails, deletedExpiredMedia); 1524 } 1525 1526 /** 1527 * This function find and clean the files related to user who have been removed 1528 */ cleanMediaFilesForRemovedUser(CancellationSignal signal)1529 private void cleanMediaFilesForRemovedUser(CancellationSignal signal) { 1530 //Finding userIds that are available in database 1531 final List<String> userIds = mExternalDatabase.runWithTransaction((db) -> { 1532 final List<String> userIdsPresent = new ArrayList<>(); 1533 try (Cursor c = db.query(true, "files", new String[] { "_user_id" }, 1534 null, null, null, null, null, 1535 null, signal)) { 1536 while (c.moveToNext()) { 1537 final String userId = c.getString(0); 1538 userIdsPresent.add(userId); 1539 } 1540 } 1541 return userIdsPresent; 1542 }); 1543 1544 // removing calling userId 1545 userIds.remove(String.valueOf(sUserId)); 1546 // removing all the valid/existing user, remaining userIds would be users who would have 1547 // been removed 1548 userIds.removeAll(mUserManager.getEnabledProfiles().stream() 1549 .map(userHandle -> String.valueOf(userHandle.getIdentifier())).collect( 1550 Collectors.toList())); 1551 1552 // Cleaning media files of users who have been removed 1553 mExternalDatabase.runWithTransaction((db) -> { 1554 userIds.stream().forEach(userId ->{ 1555 Log.d(TAG, "Removing media files associated with user : " + userId); 1556 db.execSQL("delete from files where _user_id=?", 1557 new String[]{String.valueOf(userId)}); 1558 }); 1559 return null ; 1560 }); 1561 } 1562 pruneStalePackages(CancellationSignal signal)1563 private void pruneStalePackages(CancellationSignal signal) { 1564 final int stalePackages = mExternalDatabase.runWithTransaction((db) -> { 1565 final ArraySet<Pair<String, Integer>> unknownPackages = new ArraySet<>(); 1566 try (Cursor c = db.query(true, "files", 1567 new String[] { "owner_package_name", "_user_id" }, 1568 null, null, null, null, null, null, signal)) { 1569 while (c.moveToNext()) { 1570 final String packageName = c.getString(0); 1571 if (TextUtils.isEmpty(packageName)) continue; 1572 1573 final int userId = c.getInt(1); 1574 1575 if (!isPackageKnown(packageName, userId)) { 1576 unknownPackages.add(Pair.create(packageName, userId)); 1577 } 1578 } 1579 } 1580 for (Pair<String, Integer> pair : unknownPackages) { 1581 onPackageOrphaned(db, pair.first, pair.second); 1582 } 1583 return unknownPackages.size(); 1584 }); 1585 Log.d(TAG, "Pruned " + stalePackages + " unknown packages"); 1586 } 1587 deleteStaleVolumes(CancellationSignal signal)1588 private void deleteStaleVolumes(CancellationSignal signal) { 1589 mExternalDatabase.runWithTransaction((db) -> { 1590 final Set<String> recentVolumeNames = MediaStore 1591 .getRecentExternalVolumeNames(getContext()); 1592 final Set<String> knownVolumeNames = new ArraySet<>(); 1593 try (Cursor c = db.query(true, "files", new String[] { MediaColumns.VOLUME_NAME }, 1594 null, null, null, null, null, null, signal)) { 1595 while (c.moveToNext()) { 1596 knownVolumeNames.add(c.getString(0)); 1597 } 1598 } 1599 final Set<String> staleVolumeNames = new ArraySet<>(); 1600 staleVolumeNames.addAll(knownVolumeNames); 1601 staleVolumeNames.removeAll(recentVolumeNames); 1602 for (String staleVolumeName : staleVolumeNames) { 1603 final int num = db.delete("files", FileColumns.VOLUME_NAME + "=?", 1604 new String[] { staleVolumeName }); 1605 Log.d(TAG, "Forgot " + num + " stale items from " + staleVolumeName); 1606 } 1607 return null; 1608 }); 1609 1610 synchronized (mDirectoryCache) { 1611 mDirectoryCache.clear(); 1612 } 1613 } 1614 1615 @VisibleForTesting setUriResolver(PickerUriResolver resolver)1616 public void setUriResolver(PickerUriResolver resolver) { 1617 Log.w(TAG, "Changing the PickerUriResolver!!! Should only be called during test"); 1618 mPickerUriResolver = resolver; 1619 } 1620 1621 @VisibleForTesting detectSpecialFormat(@onNull CancellationSignal signal)1622 void detectSpecialFormat(@NonNull CancellationSignal signal) { 1623 // Picker sync and special format update can execute concurrently and run into a deadlock. 1624 // Acquiring a lock before execution of each flow to avoid this. 1625 PickerSyncController.sIdleMaintenanceSyncLock.lock(); 1626 try { 1627 mExternalDatabase.runWithTransaction((db) -> { 1628 updateSpecialFormatColumn(db, signal); 1629 return null; 1630 }); 1631 } finally { 1632 PickerSyncController.sIdleMaintenanceSyncLock.unlock(); 1633 } 1634 } 1635 updateSpecialFormatColumn(SQLiteDatabase db, @NonNull CancellationSignal signal)1636 private void updateSpecialFormatColumn(SQLiteDatabase db, @NonNull CancellationSignal signal) { 1637 // This is to ensure we only do a bounded iteration over the rows as updates can fail, and 1638 // we don't want to keep running the query/update indefinitely. 1639 final int totalRowsToUpdate = getPendingSpecialFormatRowsCount(db, signal); 1640 for (int i = 0; i < totalRowsToUpdate; i += IDLE_MAINTENANCE_ROWS_LIMIT) { 1641 try (PickerDbFacade.UpdateMediaOperation operation = 1642 mPickerDbFacade.beginUpdateMediaOperation( 1643 PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)) { 1644 updateSpecialFormatForLimitedRows(db, signal, operation); 1645 operation.setSuccess(); 1646 } 1647 } 1648 } 1649 getPendingSpecialFormatRowsCount(SQLiteDatabase db, @NonNull CancellationSignal signal)1650 private int getPendingSpecialFormatRowsCount(SQLiteDatabase db, 1651 @NonNull CancellationSignal signal) { 1652 try (Cursor c = queryForPendingSpecialFormatColumns(db, /* limit */ null, signal)) { 1653 if (c == null) { 1654 return 0; 1655 } 1656 return c.getCount(); 1657 } 1658 } 1659 updateSpecialFormatForLimitedRows(SQLiteDatabase externalDb, @NonNull CancellationSignal signal, PickerDbFacade.UpdateMediaOperation operation)1660 private void updateSpecialFormatForLimitedRows(SQLiteDatabase externalDb, 1661 @NonNull CancellationSignal signal, PickerDbFacade.UpdateMediaOperation operation) { 1662 // Accumulate all the new SPECIAL_FORMAT updates with their ids 1663 ArrayMap<Long, Integer> newSpecialFormatValues = new ArrayMap<>(); 1664 final String limit = String.valueOf(IDLE_MAINTENANCE_ROWS_LIMIT); 1665 try (Cursor c = queryForPendingSpecialFormatColumns(externalDb, limit, signal)) { 1666 while (c.moveToNext() && !signal.isCanceled()) { 1667 final long id = c.getLong(0); 1668 final String path = c.getString(1); 1669 newSpecialFormatValues.put(id, getSpecialFormatValue(path)); 1670 } 1671 } 1672 1673 // Now, update all the new SPECIAL_FORMAT values in both external db and picker db. 1674 final ContentValues pickerDbValues = new ContentValues(); 1675 final ContentValues externalDbValues = new ContentValues(); 1676 int count = 0; 1677 for (long id : newSpecialFormatValues.keySet()) { 1678 if (signal.isCanceled()) { 1679 return; 1680 } 1681 1682 int specialFormat = newSpecialFormatValues.get(id); 1683 1684 pickerDbValues.clear(); 1685 pickerDbValues.put(PickerDbFacade.KEY_STANDARD_MIME_TYPE_EXTENSION, specialFormat); 1686 boolean pickerDbWriteSuccess = operation.execute(String.valueOf(id), pickerDbValues); 1687 1688 externalDbValues.clear(); 1689 externalDbValues.put(_SPECIAL_FORMAT, specialFormat); 1690 final String externalDbSelection = MediaColumns._ID + "=?"; 1691 final String[] externalDbSelectionArgs = new String[]{String.valueOf(id)}; 1692 boolean externalDbWriteSuccess = 1693 externalDb.update("files", externalDbValues, externalDbSelection, 1694 externalDbSelectionArgs) 1695 == 1; 1696 1697 if (pickerDbWriteSuccess && externalDbWriteSuccess) { 1698 count++; 1699 } 1700 } 1701 Log.d(TAG, "Updated standard_mime_type_extension for " + count + " items"); 1702 } 1703 getSpecialFormatValue(String path)1704 private int getSpecialFormatValue(String path) { 1705 final File file = new File(path); 1706 if (!file.exists()) { 1707 // We always update special format to none if the file is not found or there is an 1708 // error, this is so that we do not repeat over the same column again and again. 1709 return _SPECIAL_FORMAT_NONE; 1710 } 1711 1712 try { 1713 return SpecialFormatDetector.detect(file); 1714 } catch (Exception e) { 1715 // we tried our best, no need to run special detection again and again if it 1716 // throws exception once, it is likely to do so everytime. 1717 Log.d(TAG, "Failed to detect special format for file: " + file, e); 1718 return _SPECIAL_FORMAT_NONE; 1719 } 1720 } 1721 queryForPendingSpecialFormatColumns(SQLiteDatabase db, String limit, @NonNull CancellationSignal signal)1722 private Cursor queryForPendingSpecialFormatColumns(SQLiteDatabase db, String limit, 1723 @NonNull CancellationSignal signal) { 1724 // Run special detection for images only 1725 final String selection = _SPECIAL_FORMAT + " IS NULL AND " 1726 + MEDIA_TYPE + "=" + MEDIA_TYPE_IMAGE; 1727 final String[] projection = new String[] { MediaColumns._ID, MediaColumns.DATA }; 1728 return db.query(/* distinct */ true, "files", projection, selection, null, null, null, 1729 null, limit, signal); 1730 } 1731 1732 /** 1733 * Delete any expired content on mounted volumes. The expired content on unmounted 1734 * volumes will be deleted when we forget any stale volumes; we're cautious about 1735 * wildly changing clocks, so only delete items within the last week. 1736 * If the items are expired more than one week, extend the expired time of them 1737 * another one week to avoid data loss with incorrect time zone data. We will 1738 * delete it when it is expired next time. 1739 * 1740 * @param signal the cancellation signal 1741 * @return the integer array includes total deleted count and total extended count 1742 */ 1743 @NonNull deleteOrExtendExpiredItems(@onNull CancellationSignal signal)1744 private int[] deleteOrExtendExpiredItems(@NonNull CancellationSignal signal) { 1745 final long expiredOneWeek = 1746 ((System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS) / 1000); 1747 final long now = (System.currentTimeMillis() / 1000); 1748 final long expiredTime = now + (FileUtils.DEFAULT_DURATION_EXTENDED / 1000); 1749 return mExternalDatabase.runWithTransaction((db) -> { 1750 String selection = FileColumns.DATE_EXPIRES + " < " + now; 1751 selection += " AND volume_name in " + bindList(MediaStore.getExternalVolumeNames( 1752 getContext()).toArray()); 1753 String[] projection = new String[]{"volume_name", "_id", 1754 FileColumns.DATE_EXPIRES, FileColumns.DATA}; 1755 try (Cursor c = db.query(true, "files", projection, selection, null, null, null, null, 1756 null, signal)) { 1757 int totalDeleteCount = 0; 1758 int totalExtendedCount = 0; 1759 int index = 0; 1760 while (c.moveToNext()) { 1761 final String volumeName = c.getString(0); 1762 final long id = c.getLong(1); 1763 final long dateExpires = c.getLong(2); 1764 // we only delete the items that expire in one week 1765 if (dateExpires > expiredOneWeek) { 1766 totalDeleteCount += delete(Files.getContentUri(volumeName, id), null, null); 1767 } else { 1768 final String oriPath = c.getString(3); 1769 1770 final boolean success = extendExpiredItem(db, oriPath, id, expiredTime, 1771 expiredTime + index); 1772 if (success) { 1773 totalExtendedCount++; 1774 } 1775 index++; 1776 } 1777 } 1778 return new int[]{totalDeleteCount, totalExtendedCount}; 1779 } 1780 }); 1781 } 1782 1783 /** 1784 * Extend the expired items by renaming the file to new path with new timestamp and updating the 1785 * database for {@link FileColumns#DATA} and {@link FileColumns#DATE_EXPIRES}. If there is 1786 * UNIQUE constraint error for FileColumns.DATA, use adjustedExpiredTime and generate the new 1787 * path by adjustedExpiredTime. 1788 */ extendExpiredItem(@onNull SQLiteDatabase db, @NonNull String originalPath, long id, long newExpiredTime, long adjustedExpiredTime)1789 private boolean extendExpiredItem(@NonNull SQLiteDatabase db, @NonNull String originalPath, 1790 long id, long newExpiredTime, long adjustedExpiredTime) { 1791 String newPath = FileUtils.getAbsoluteExtendedPath(originalPath, newExpiredTime); 1792 if (newPath == null) { 1793 Log.e(TAG, "Couldn't compute path for " + originalPath + " and expired time " 1794 + newExpiredTime); 1795 return false; 1796 } 1797 1798 try { 1799 if (updateDatabaseForExpiredItem(db, newPath, id, newExpiredTime)) { 1800 return renameInLowerFsAndInvalidateFuseDentry(originalPath, newPath); 1801 } 1802 return false; 1803 } catch (SQLiteConstraintException e) { 1804 final String errorMessage = 1805 "Update database _data from " + originalPath + " to " + newPath + " failed."; 1806 Log.d(TAG, errorMessage, e); 1807 } 1808 1809 // When we update the database for newPath with newExpiredTime, if the new path already 1810 // exists in the database, it may raise SQLiteConstraintException. 1811 // If there are two expired items that have the same display name in the same directory, 1812 // but they have different expired time. E.g. .trashed-123-A.jpg and .trashed-456-A.jpg. 1813 // After we rename .trashed-123-A.jpg to .trashed-newExpiredTime-A.jpg, then we rename 1814 // .trashed-456-A.jpg to .trashed-newExpiredTime-A.jpg, it raises the exception. For 1815 // this case, we will retry it with the adjustedExpiredTime again. 1816 newPath = FileUtils.getAbsoluteExtendedPath(originalPath, adjustedExpiredTime); 1817 Log.i(TAG, "Retrying to extend expired item with the new path = " + newPath); 1818 try { 1819 if (updateDatabaseForExpiredItem(db, newPath, id, adjustedExpiredTime)) { 1820 return renameInLowerFsAndInvalidateFuseDentry(originalPath, newPath); 1821 } 1822 } catch (SQLiteConstraintException e) { 1823 // If we want to rename one expired item E.g. .trashed-123-A.jpg., and there is another 1824 // non-expired trashed/pending item has the same name. E.g. 1825 // .trashed-adjustedExpiredTime-A.jpg. When we rename .trashed-123-A.jpg to 1826 // .trashed-adjustedExpiredTime-A.jpg, it raises the SQLiteConstraintException. 1827 // The smallest unit of the expired time we use is second. It is a very rare case. 1828 // When this case is happened, we can handle it in next idle maintenance. 1829 final String errorMessage = 1830 "Update database _data from " + originalPath + " to " + newPath + " failed."; 1831 Log.d(TAG, errorMessage, e); 1832 } 1833 1834 return false; 1835 } 1836 updateDatabaseForExpiredItem(@onNull SQLiteDatabase db, @NonNull String path, long id, long expiredTime)1837 private boolean updateDatabaseForExpiredItem(@NonNull SQLiteDatabase db, 1838 @NonNull String path, long id, long expiredTime) { 1839 final String table = "files"; 1840 final String whereClause = MediaColumns._ID + "=?"; 1841 final String[] whereArgs = new String[]{String.valueOf(id)}; 1842 final ContentValues values = new ContentValues(); 1843 values.put(FileColumns.DATA, path); 1844 values.put(FileColumns.DATE_EXPIRES, expiredTime); 1845 final int count = db.update(table, values, whereClause, whereArgs); 1846 return count == 1; 1847 } 1848 renameInLowerFsAndInvalidateFuseDentry(@onNull String originalPath, @NonNull String newPath)1849 private boolean renameInLowerFsAndInvalidateFuseDentry(@NonNull String originalPath, 1850 @NonNull String newPath) { 1851 try { 1852 Os.rename(originalPath, newPath); 1853 invalidateFuseDentry(originalPath); 1854 invalidateFuseDentry(newPath); 1855 return true; 1856 } catch (ErrnoException e) { 1857 final String errorMessage = "Rename " + originalPath + " to " + newPath 1858 + " in lower file system for extending item failed."; 1859 Log.e(TAG, errorMessage, e); 1860 } 1861 return false; 1862 } 1863 onIdleMaintenanceStopped()1864 public void onIdleMaintenanceStopped() { 1865 mMediaScanner.onIdleScanStopped(); 1866 } 1867 1868 /** 1869 * Orphan any content of the given package. This will delete Android/media orphaned files from 1870 * the database. 1871 */ onPackageOrphaned(String packageName, int uid)1872 public void onPackageOrphaned(String packageName, int uid) { 1873 mExternalDatabase.runWithTransaction((db) -> { 1874 final int userId = uid / PER_USER_RANGE; 1875 onPackageOrphaned(db, packageName, userId); 1876 return null; 1877 }); 1878 } 1879 1880 /** 1881 * Orphan any content of the given package from the given database. This will delete 1882 * Android/media files from the database if the underlying file no longer exists. 1883 */ onPackageOrphaned(@onNull SQLiteDatabase db, @NonNull String packageName, int userId)1884 public void onPackageOrphaned(@NonNull SQLiteDatabase db, 1885 @NonNull String packageName, int userId) { 1886 // Delete Android/media entries. 1887 deleteAndroidMediaEntries(db, packageName, userId); 1888 // Orphan rest of entries. 1889 orphanEntries(db, packageName, userId); 1890 mDatabaseBackupAndRecovery.removeOwnerIdToPackageRelation(packageName, userId); 1891 // TODO(b/260685885): Add e2e tests to ensure these are cleared when a package is removed. 1892 mMediaGrants.removeAllMediaGrantsForPackage(packageName, /* reason */ "Package orphaned", 1893 userId); 1894 } 1895 deleteAndroidMediaEntries(SQLiteDatabase db, String packageName, int userId)1896 private void deleteAndroidMediaEntries(SQLiteDatabase db, String packageName, int userId) { 1897 String relativePath = "Android/media/" + DatabaseUtils.escapeForLike(packageName) + "/%"; 1898 try (Cursor cursor = db.query( 1899 "files", 1900 new String[] { MediaColumns._ID, MediaColumns.DATA }, 1901 "relative_path LIKE ? ESCAPE '\\' AND owner_package_name=? AND _user_id=?", 1902 new String[] { relativePath, packageName, "" + userId }, 1903 /* groupBy= */ null, 1904 /* having= */ null, 1905 /* orderBy= */null, 1906 /* limit= */ null)) { 1907 int countDeleted = 0; 1908 if (cursor != null) { 1909 while (cursor.moveToNext()) { 1910 File file = new File(cursor.getString(1)); 1911 // We check for existence to be sure we don't delete files that still exist. 1912 // This can happen even if the pair (package, userid) is unknown, 1913 // since some framework implementations may rely on special userids. 1914 if (!file.exists()) { 1915 countDeleted += 1916 db.delete("files", "_id=?", new String[]{cursor.getString(0)}); 1917 } 1918 } 1919 } 1920 Log.d(TAG, "Deleted " + countDeleted + " Android/media items belonging to " 1921 + packageName + " on " + db.getPath()); 1922 } 1923 } 1924 orphanEntries( @onNull SQLiteDatabase db, @NonNull String packageName, int userId)1925 private void orphanEntries( 1926 @NonNull SQLiteDatabase db, @NonNull String packageName, int userId) { 1927 final ContentValues values = new ContentValues(); 1928 values.putNull(FileColumns.OWNER_PACKAGE_NAME); 1929 1930 final int countOrphaned = db.update("files", values, 1931 "owner_package_name=? AND _user_id=?", new String[] { packageName, "" + userId }); 1932 if (countOrphaned > 0) { 1933 Log.d(TAG, "Orphaned " + countOrphaned + " items belonging to " 1934 + packageName + " on " + db.getPath()); 1935 } 1936 } 1937 scanDirectory(@onNull File dir, @ScanReason int reason)1938 public void scanDirectory(@NonNull File dir, @ScanReason int reason) { 1939 mMediaScanner.scanDirectory(dir, reason); 1940 } 1941 scanFile(@onNull File file, @ScanReason int reason)1942 public Uri scanFile(@NonNull File file, @ScanReason int reason) { 1943 return mMediaScanner.scanFile(file, reason); 1944 } 1945 scanFileAsMediaProvider(File file, int reason)1946 private Uri scanFileAsMediaProvider(File file, int reason) { 1947 final LocalCallingIdentity tokenInner = clearLocalCallingIdentity(); 1948 try { 1949 return scanFile(file, REASON_DEMAND); 1950 } finally { 1951 restoreLocalCallingIdentity(tokenInner); 1952 } 1953 } 1954 1955 /** 1956 * Called when a new file is created through FUSE 1957 * 1958 * @param path path of the file that was created 1959 * 1960 * Called from JNI in jni/MediaProviderWrapper.cpp 1961 */ 1962 @Keep onFileCreatedForFuse(String path)1963 public void onFileCreatedForFuse(String path) { 1964 // Make sure we update the quota type of the file 1965 BackgroundThread.getExecutor().execute(() -> { 1966 File file = new File(path); 1967 int mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(file)); 1968 updateQuotaTypeForFileInternal(file, mediaType); 1969 }); 1970 } 1971 isAppCloneUserPair(int userId1, int userId2)1972 private boolean isAppCloneUserPair(int userId1, int userId2) { 1973 UserHandle user1 = UserHandle.of(userId1); 1974 UserHandle user2 = UserHandle.of(userId2); 1975 if (SdkLevel.isAtLeastS()) { 1976 if (mUserCache.userSharesMediaWithParent(user1) 1977 || mUserCache.userSharesMediaWithParent(user2)) { 1978 return true; 1979 } 1980 if (Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.S) { 1981 // If we're on S or higher, and we shipped with S or higher, only allow the new 1982 // app cloning functionality 1983 return false; 1984 } 1985 // else, fall back to deprecated solution below on updating devices 1986 } 1987 try { 1988 Method isAppCloneUserPair = StorageManager.class.getMethod("isAppCloneUserPair", 1989 int.class, int.class); 1990 return (Boolean) isAppCloneUserPair.invoke(mStorageManager, userId1, userId2); 1991 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 1992 Log.w(TAG, "isAppCloneUserPair failed. Users: " + userId1 + " and " + userId2); 1993 return false; 1994 } 1995 } 1996 1997 /** 1998 * Determines whether the passed in userId forms an app clone user pair with user 0. 1999 * 2000 * @param userId user ID to check 2001 * 2002 * Called from JNI in jni/MediaProviderWrapper.cpp 2003 */ 2004 @Keep isAppCloneUserForFuse(int userId)2005 public boolean isAppCloneUserForFuse(int userId) { 2006 if (!isCrossUserEnabled()) { 2007 Log.d(TAG, "CrossUser not enabled."); 2008 return false; 2009 } 2010 boolean result = isAppCloneUserPair(0, userId); 2011 2012 Log.w(TAG, "isAppCloneUserPair for user " + userId + ": " + result); 2013 2014 return result; 2015 } 2016 2017 /** 2018 * Determines if to allow FUSE_LOOKUP for uid. Might allow uids that don't belong to the 2019 * MediaProvider user, depending on OEM configuration. 2020 * 2021 * @param uid linux uid to check 2022 * 2023 * Called from JNI in jni/MediaProviderWrapper.cpp 2024 */ 2025 @Keep shouldAllowLookupForFuse(int uid, int pathUserId)2026 public boolean shouldAllowLookupForFuse(int uid, int pathUserId) { 2027 int callingUserId = uidToUserId(uid); 2028 if (!isCrossUserEnabled()) { 2029 Log.d(TAG, "CrossUser not enabled. Users: " + callingUserId + " and " + pathUserId); 2030 return false; 2031 } 2032 2033 if (callingUserId != pathUserId && callingUserId != 0 && pathUserId != 0) { 2034 Log.w(TAG, "CrossUser at least one user is 0 check failed. Users: " + callingUserId 2035 + " and " + pathUserId); 2036 return false; 2037 } 2038 2039 if (mUserCache.isWorkProfile(callingUserId) || mUserCache.isWorkProfile(pathUserId)) { 2040 // Cross-user lookup not allowed if one user in the pair has a profile owner app 2041 Log.w(TAG, "CrossUser work profile check failed. Users: " + callingUserId + " and " 2042 + pathUserId); 2043 return false; 2044 } 2045 2046 boolean result = isAppCloneUserPair(pathUserId, callingUserId); 2047 if (result) { 2048 Log.i(TAG, "CrossUser allowed. Users: " + callingUserId + " and " + pathUserId); 2049 } else { 2050 Log.w(TAG, "CrossUser isAppCloneUserPair check failed. Users: " + callingUserId 2051 + " and " + pathUserId); 2052 } 2053 2054 return result; 2055 } 2056 2057 /** 2058 * Called from FUSE to transform a file 2059 * 2060 * A transform can change the file contents for {@code uid} from {@code src} to {@code dst} 2061 * depending on {@code flags}. This allows the FUSE daemon serve different file contents for 2062 * the same file to different apps. 2063 * 2064 * The only supported transform for now is transcoding which re-encodes a file taken in a modern 2065 * format like HEVC to a legacy format like AVC. 2066 * 2067 * @param src file path to transform 2068 * @param dst file path to save transformed file 2069 * @param flags determines the kind of transform 2070 * @param readUid app that called us requesting transform 2071 * @param openUid app that originally made the open call 2072 * @param mediaCapabilitiesUid app for which the transform decision was made, 2073 * 0 if decision was made with openUid 2074 * 2075 * Called from JNI in jni/MediaProviderWrapper.cpp 2076 */ 2077 @Keep transformForFuse(String src, String dst, int transforms, int transformsReason, int readUid, int openUid, int mediaCapabilitiesUid)2078 public boolean transformForFuse(String src, String dst, int transforms, int transformsReason, 2079 int readUid, int openUid, int mediaCapabilitiesUid) { 2080 if ((transforms & FLAG_TRANSFORM_TRANSCODING) != 0) { 2081 if (mTranscodeHelper.isTranscodeFileCached(src, dst)) { 2082 Log.d(TAG, "Using transcode cache for " + src); 2083 return true; 2084 } 2085 2086 // In general we always mark the opener as causing transcoding. 2087 // However, if the mediaCapabilitiesUid is available then we mark the reader as causing 2088 // transcoding. This handles the case where a malicious app might want to take 2089 // advantage of mediaCapabilitiesUid by setting it to another app's uid and reading the 2090 // media contents itself; in such cases we'd mark the reader (malicious app) for the 2091 // cost of transcoding. 2092 // 2093 // openUid readUid mediaCapabilitiesUid 2094 // ------------------------------------------------------------------------------------- 2095 // using picker SAF app app 2096 // abusive case bad app bad app victim 2097 // modern to lega- 2098 // -cy sharing modern legacy legacy 2099 // 2100 // we'd not be here in the below case. 2101 // legacy to mode- 2102 // -rn sharing legacy modern modern 2103 2104 int transcodeUid = openUid; 2105 if (mediaCapabilitiesUid > 0) { 2106 Log.d(TAG, "Fix up transcodeUid to " + readUid + ". openUid " + openUid 2107 + ", mediaCapabilitiesUid " + mediaCapabilitiesUid); 2108 transcodeUid = readUid; 2109 } 2110 return mTranscodeHelper.transcode(src, dst, transcodeUid, transformsReason); 2111 } 2112 return true; 2113 } 2114 2115 /** 2116 * Called from FUSE to get {@link FileLookupResult} for a {@code path} and {@code uid} 2117 * 2118 * {@link FileLookupResult} contains transforms, transforms completion status and ioPath 2119 * for transform lookup query for a file and uid. 2120 * 2121 * @param path file path to get transforms for 2122 * @param uid app requesting IO form kernel 2123 * @param tid FUSE thread id handling IO request from kernel 2124 * 2125 * Called from JNI in jni/MediaProviderWrapper.cpp 2126 */ 2127 @Keep onFileLookupForFuse(String path, int uid, int tid)2128 public FileLookupResult onFileLookupForFuse(String path, int uid, int tid) { 2129 uid = getBinderUidForFuse(uid, tid); 2130 final int userId = uidToUserId(uid); 2131 2132 if (isSyntheticPath(path, userId)) { 2133 if (isRedactedPath(path, userId)) { 2134 return handleRedactedFileLookup(uid, path); 2135 } else if (isPickerPath(path, userId)) { 2136 return handlePickerFileLookup(userId, uid, path); 2137 } 2138 2139 throw new IllegalStateException("Unexpected synthetic path: " + path); 2140 } 2141 2142 if (mTranscodeHelper.supportsTranscode(path)) { 2143 return handleTranscodedFileLookup(path, uid, tid); 2144 } 2145 2146 return new FileLookupResult(/* transforms */ 0, uid, /* ioPath */ ""); 2147 } 2148 handleTranscodedFileLookup(String path, int uid, int tid)2149 private FileLookupResult handleTranscodedFileLookup(String path, int uid, int tid) { 2150 final int transformsReason; 2151 final PendingOpenInfo info; 2152 2153 synchronized (mPendingOpenInfo) { 2154 info = mPendingOpenInfo.get(tid); 2155 } 2156 2157 if (info != null && info.uid == uid) { 2158 transformsReason = info.transcodeReason; 2159 } else { 2160 transformsReason = mTranscodeHelper.shouldTranscode(path, uid, null /* bundle */); 2161 } 2162 2163 if (transformsReason > 0) { 2164 final String ioPath = mTranscodeHelper.prepareIoPath(path, uid); 2165 final boolean transformsComplete = mTranscodeHelper.isTranscodeFileCached(path, ioPath); 2166 2167 return new FileLookupResult(FLAG_TRANSFORM_TRANSCODING, transformsReason, uid, 2168 transformsComplete, /* transformsSupported */ true, ioPath); 2169 } 2170 2171 return new FileLookupResult(/* transforms */ 0, transformsReason, uid, 2172 /* transformsComplete */ true, /* transformsSupported */ true, ""); 2173 } 2174 handleRedactedFileLookup(int uid, @NonNull String path)2175 private FileLookupResult handleRedactedFileLookup(int uid, @NonNull String path) { 2176 final LocalCallingIdentity token = clearLocalCallingIdentity(); 2177 final String fileName = extractFileName(path); 2178 2179 final DatabaseHelper helper; 2180 try { 2181 helper = getDatabaseForUri(FileUtils.getContentUriForPath(path)); 2182 } catch (VolumeNotFoundException e) { 2183 throw new IllegalStateException("Volume not found for file: " + path); 2184 } 2185 2186 try (final Cursor c = helper.runWithoutTransaction( 2187 (db) -> db.query("files", new String[]{MediaColumns.DATA}, 2188 FileColumns.REDACTED_URI_ID + "=?", new String[]{fileName}, null, null, 2189 null))) { 2190 if (c.moveToFirst()) { 2191 return new FileLookupResult(FLAG_TRANSFORM_REDACTION, uid, c.getString(0)); 2192 } 2193 2194 throw new IllegalStateException("Failed to fetch synthetic redacted path: " + path); 2195 } finally { 2196 restoreLocalCallingIdentity(token); 2197 } 2198 } 2199 2200 /** TODO(b/242153950) :Add negative tests for permission check of file lookup of synthetic 2201 * paths. */ handlePickerFileLookup(int userId, int uid, @NonNull String path)2202 private FileLookupResult handlePickerFileLookup(int userId, int uid, @NonNull String path) { 2203 final File file = new File(path); 2204 final List<String> syntheticRelativePathSegments = 2205 extractSyntheticRelativePathSegements(path, userId); 2206 final int segmentCount = syntheticRelativePathSegments.size(); 2207 2208 if (segmentCount < 1 || segmentCount > 5) { 2209 throw new IllegalStateException("Unexpected synthetic picker path: " + file); 2210 } 2211 2212 final String lastSegment = syntheticRelativePathSegments.get(segmentCount - 1); 2213 2214 boolean result = false; 2215 switch (segmentCount) { 2216 case 1: 2217 // .../picker 2218 if (lastSegment.equals("picker")) { 2219 result = file.exists() || file.mkdir(); 2220 } 2221 break; 2222 case 2: 2223 // .../picker/<user-id> 2224 try { 2225 Integer.parseInt(lastSegment); 2226 result = file.exists() || file.mkdir(); 2227 } catch (NumberFormatException e) { 2228 Log.w(TAG, "Invalid user id for picker file lookup: " + lastSegment 2229 + ". File: " + file); 2230 } 2231 break; 2232 case 3: 2233 // .../picker/<user-id>/<authority> 2234 result = preparePickerAuthorityPathSegment(file, lastSegment, uid); 2235 break; 2236 case 4: 2237 // .../picker/<user-id>/<authority>/media 2238 if (lastSegment.equals("media")) { 2239 result = file.exists() || file.mkdir(); 2240 } 2241 break; 2242 case 5: 2243 // .../picker/<user-id>/<authority>/media/<media-id.extension> 2244 final String fileUserId = syntheticRelativePathSegments.get(1); 2245 final String authority = syntheticRelativePathSegments.get(2); 2246 result = preparePickerMediaIdPathSegment(file, authority, lastSegment, fileUserId, 2247 uid); 2248 break; 2249 } 2250 2251 if (result) { 2252 return new FileLookupResult(FLAG_TRANSFORM_PICKER, uid, path); 2253 } 2254 throw new IllegalStateException("Failed to prepare synthetic picker path: " + file); 2255 } 2256 handlePickerFileOpen(String path, int uid)2257 private FileOpenResult handlePickerFileOpen(String path, int uid) { 2258 final String[] segments = path.split("/"); 2259 if (segments.length != 11) { 2260 Log.e(TAG, "Picker file open failed. Unexpected segments: " + path); 2261 return new FileOpenResult(OsConstants.ENOENT /* status */, uid, /* transformsUid */ 0, 2262 new long[0]); 2263 } 2264 2265 // ['', 'storage', 'emulated', '0', 'transforms', 'synthetic', 'picker', '<user-id>', 2266 // '<host>', 'media', '<fileName>'] 2267 final String userId = segments[7]; 2268 final String fileName = segments[10]; 2269 final String host = segments[8]; 2270 final String authority = userId + "@" + host; 2271 final int lastDotIndex = fileName.lastIndexOf('.'); 2272 2273 if (lastDotIndex == -1) { 2274 Log.e(TAG, "Picker file open failed. No file extension: " + path); 2275 return FileOpenResult.createError(OsConstants.ENOENT, uid); 2276 } 2277 2278 final String mediaId = fileName.substring(0, lastDotIndex); 2279 final Uri uri = getMediaUri(authority).buildUpon().appendPath(mediaId).build(); 2280 2281 IBinder binder = getContext().getContentResolver() 2282 .call(uri, METHOD_GET_ASYNC_CONTENT_PROVIDER, null, null) 2283 .getBinder(EXTRA_ASYNC_CONTENT_PROVIDER); 2284 if (binder == null) { 2285 Log.e(TAG, "Picker file open failed. No cloud media provider found."); 2286 return FileOpenResult.createError(OsConstants.ENOENT, uid); 2287 } 2288 IAsyncContentProvider iAsyncContentProvider = IAsyncContentProvider.Stub.asInterface( 2289 binder); 2290 AsyncContentProvider asyncContentProvider = new AsyncContentProvider(iAsyncContentProvider); 2291 final ParcelFileDescriptor pfd; 2292 try { 2293 pfd = asyncContentProvider.openMedia(uri, "r"); 2294 } catch (FileNotFoundException | ExecutionException | InterruptedException 2295 | TimeoutException | RemoteException e) { 2296 Log.e(TAG, "Picker file open failed. Failed to open URI: " + uri, e); 2297 return FileOpenResult.createError(OsConstants.ENOENT, uid); 2298 } 2299 2300 try (FileInputStream fis = new FileInputStream(pfd.getFileDescriptor())) { 2301 final String mimeType = MimeUtils.resolveMimeType(new File(path)); 2302 final long[] redactionRanges = getRedactionRanges(fis, mimeType).redactionRanges; 2303 return new FileOpenResult(0 /* status */, uid, /* transformsUid */ 0, 2304 /* nativeFd */ pfd.detachFd(), redactionRanges); 2305 } catch (IOException e) { 2306 Log.e(TAG, "Picker file open failed. No file extension: " + path, e); 2307 return FileOpenResult.createError(OsConstants.ENOENT, uid); 2308 } 2309 } 2310 preparePickerAuthorityPathSegment(File file, String authority, int uid)2311 private boolean preparePickerAuthorityPathSegment(File file, String authority, int uid) { 2312 if (mPickerSyncController.isProviderEnabled(authority)) { 2313 return file.exists() || file.mkdir(); 2314 } 2315 2316 return false; 2317 } 2318 preparePickerMediaIdPathSegment(File file, String authority, String fileName, String userId, int uid)2319 private boolean preparePickerMediaIdPathSegment(File file, String authority, String fileName, 2320 String userId, int uid) { 2321 final String mediaId = extractFileName(fileName); 2322 final String[] projection = new String[] { MediaStore.PickerMediaColumns.SIZE }; 2323 2324 final Uri uri = Uri.parse("content://media/picker/" + userId + "/" + authority + "/media/" 2325 + mediaId); 2326 try (Cursor cursor = mPickerUriResolver.query(uri, projection, /* callingPid */0, uid, 2327 mCallingIdentity.get().getPackageName())) { 2328 if (cursor != null && cursor.moveToFirst()) { 2329 final int sizeBytesIdx = cursor.getColumnIndex(MediaStore.PickerMediaColumns.SIZE); 2330 2331 if (sizeBytesIdx != -1) { 2332 return createSparseFile(file, cursor.getLong(sizeBytesIdx)); 2333 } 2334 } 2335 } 2336 2337 return false; 2338 } 2339 getBinderUidForFuse(int uid, int tid)2340 public int getBinderUidForFuse(int uid, int tid) { 2341 if (uid != MY_UID) { 2342 return uid; 2343 } 2344 2345 synchronized (mPendingOpenInfo) { 2346 PendingOpenInfo info = mPendingOpenInfo.get(tid); 2347 if (info == null) { 2348 return uid; 2349 } 2350 return info.uid; 2351 } 2352 } 2353 uidToUserId(int uid)2354 private static int uidToUserId(int uid) { 2355 return uid / PER_USER_RANGE; 2356 } 2357 2358 /** 2359 * Returns true if the app denoted by the given {@code uid} and {@code packageName} is allowed 2360 * to clear other apps' cache directories. 2361 */ hasPermissionToClearCaches(Context context, ApplicationInfo ai)2362 static boolean hasPermissionToClearCaches(Context context, ApplicationInfo ai) { 2363 PermissionUtils.setOpDescription("clear app cache"); 2364 try { 2365 return PermissionUtils.checkPermissionManager(context, /* pid */ -1, ai.uid, 2366 ai.packageName, /* attributionTag */ null); 2367 } finally { 2368 PermissionUtils.clearOpDescription(); 2369 } 2370 } 2371 2372 @VisibleForTesting computeAudioLocalizedValues(ContentValues values)2373 void computeAudioLocalizedValues(ContentValues values) { 2374 try { 2375 final String title = values.getAsString(AudioColumns.TITLE); 2376 final String titleRes = values.getAsString(AudioColumns.TITLE_RESOURCE_URI); 2377 2378 if (!TextUtils.isEmpty(titleRes)) { 2379 final String localized = getLocalizedTitle(titleRes); 2380 if (!TextUtils.isEmpty(localized)) { 2381 values.put(AudioColumns.TITLE, localized); 2382 } 2383 } else { 2384 final String localized = getLocalizedTitle(title); 2385 if (!TextUtils.isEmpty(localized)) { 2386 values.put(AudioColumns.TITLE, localized); 2387 values.put(AudioColumns.TITLE_RESOURCE_URI, title); 2388 } 2389 } 2390 } catch (Exception e) { 2391 Log.w(TAG, "Failed to localize title", e); 2392 } 2393 } 2394 2395 @VisibleForTesting computeAudioKeyValues(ContentValues values)2396 static void computeAudioKeyValues(ContentValues values) { 2397 computeAudioKeyValue(values, AudioColumns.TITLE, AudioColumns.TITLE_KEY, /* focusId */ 2398 null, /* hashValue */ 0); 2399 computeAudioKeyValue(values, AudioColumns.ARTIST, AudioColumns.ARTIST_KEY, 2400 AudioColumns.ARTIST_ID, /* hashValue */ 0); 2401 computeAudioKeyValue(values, AudioColumns.GENRE, AudioColumns.GENRE_KEY, 2402 AudioColumns.GENRE_ID, /* hashValue */ 0); 2403 computeAudioAlbumKeyValue(values); 2404 } 2405 2406 /** 2407 * To distinguish same-named albums, we append a hash. The hash is 2408 * based on the "album artist" tag if present, otherwise on the path of 2409 * the parent directory of the audio file. 2410 */ computeAudioAlbumKeyValue(ContentValues values)2411 private static void computeAudioAlbumKeyValue(ContentValues values) { 2412 int hashCode = 0; 2413 2414 final String albumArtist = values.getAsString(MediaColumns.ALBUM_ARTIST); 2415 if (!TextUtils.isEmpty(albumArtist)) { 2416 hashCode = albumArtist.hashCode(); 2417 } else { 2418 final String path = values.getAsString(MediaColumns.DATA); 2419 if (!TextUtils.isEmpty(path)) { 2420 hashCode = path.substring(0, path.lastIndexOf('/')).hashCode(); 2421 } 2422 } 2423 2424 computeAudioKeyValue(values, AudioColumns.ALBUM, AudioColumns.ALBUM_KEY, 2425 AudioColumns.ALBUM_ID, hashCode); 2426 } 2427 computeAudioKeyValue(@onNull ContentValues values, @NonNull String focus, @Nullable String focusKey, @Nullable String focusId, int hashValue)2428 private static void computeAudioKeyValue(@NonNull ContentValues values, @NonNull String focus, 2429 @Nullable String focusKey, @Nullable String focusId, int hashValue) { 2430 if (focusKey != null) values.remove(focusKey); 2431 if (focusId != null) values.remove(focusId); 2432 2433 final String value = values.getAsString(focus); 2434 if (TextUtils.isEmpty(value)) return; 2435 2436 final String key = Audio.keyFor(value); 2437 if (key == null) return; 2438 2439 if (focusKey != null) { 2440 values.put(focusKey, key); 2441 } 2442 if (focusId != null) { 2443 // Many apps break if we generate negative IDs, so trim off the 2444 // highest bit to ensure we're always unsigned 2445 final long id = Hashing.farmHashFingerprint64().hashString(key + hashValue, 2446 StandardCharsets.UTF_8).asLong() & ~(1L << 63); 2447 values.put(focusId, id); 2448 } 2449 } 2450 2451 @Override canonicalize(Uri uri)2452 public Uri canonicalize(Uri uri) { 2453 final boolean allowHidden = isCallingPackageAllowedHidden(); 2454 final int match = matchUri(uri, allowHidden); 2455 2456 // Skip when we have nothing to canonicalize 2457 if ("1".equals(uri.getQueryParameter(CANONICAL))) { 2458 return uri; 2459 } 2460 2461 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) { 2462 switch (match) { 2463 case AUDIO_MEDIA_ID: { 2464 final String title = getDefaultTitleFromCursor(c); 2465 if (!TextUtils.isEmpty(title)) { 2466 final Uri.Builder builder = uri.buildUpon(); 2467 builder.appendQueryParameter(AudioColumns.TITLE, title); 2468 builder.appendQueryParameter(CANONICAL, "1"); 2469 return builder.build(); 2470 } 2471 break; 2472 } 2473 case VIDEO_MEDIA_ID: 2474 case IMAGES_MEDIA_ID: { 2475 final String documentId = c 2476 .getString(c.getColumnIndexOrThrow(MediaColumns.DOCUMENT_ID)); 2477 if (!TextUtils.isEmpty(documentId)) { 2478 final Uri.Builder builder = uri.buildUpon(); 2479 builder.appendQueryParameter(MediaColumns.DOCUMENT_ID, documentId); 2480 builder.appendQueryParameter(CANONICAL, "1"); 2481 return builder.build(); 2482 } 2483 break; 2484 } 2485 } 2486 } catch (FileNotFoundException e) { 2487 Log.w(TAG, e.getMessage()); 2488 } 2489 return null; 2490 } 2491 2492 @Override uncanonicalize(Uri uri)2493 public Uri uncanonicalize(Uri uri) { 2494 final boolean allowHidden = isCallingPackageAllowedHidden(); 2495 final int match = matchUri(uri, allowHidden); 2496 2497 // Skip when we have nothing to uncanonicalize 2498 if (!"1".equals(uri.getQueryParameter(CANONICAL))) { 2499 return uri; 2500 } 2501 2502 // Extract values and then clear to avoid recursive lookups 2503 final String title = uri.getQueryParameter(AudioColumns.TITLE); 2504 final String documentId = uri.getQueryParameter(MediaColumns.DOCUMENT_ID); 2505 uri = uri.buildUpon().clearQuery().build(); 2506 2507 switch (match) { 2508 case AUDIO_MEDIA_ID: { 2509 // First check for an exact match 2510 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) { 2511 if (Objects.equals(title, getDefaultTitleFromCursor(c))) { 2512 return uri; 2513 } 2514 } catch (FileNotFoundException e) { 2515 Log.w(TAG, "Trouble resolving " + uri + "; falling back to search: " + e); 2516 } 2517 2518 // Otherwise fallback to searching 2519 final Uri baseUri = ContentUris.removeId(uri); 2520 try (Cursor c = queryForSingleItem(baseUri, 2521 new String[] { BaseColumns._ID }, 2522 AudioColumns.TITLE + "=?", new String[] { title }, null)) { 2523 return ContentUris.withAppendedId(baseUri, c.getLong(0)); 2524 } catch (FileNotFoundException e) { 2525 Log.w(TAG, "Failed to resolve " + uri + ": " + e); 2526 return null; 2527 } 2528 } 2529 case VIDEO_MEDIA_ID: 2530 case IMAGES_MEDIA_ID: { 2531 // First check for an exact match 2532 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) { 2533 if (Objects.equals(title, getDefaultTitleFromCursor(c))) { 2534 return uri; 2535 } 2536 } catch (FileNotFoundException e) { 2537 Log.w(TAG, "Trouble resolving " + uri + "; falling back to search: " + e); 2538 } 2539 2540 // Otherwise fallback to searching 2541 final Uri baseUri = ContentUris.removeId(uri); 2542 try (Cursor c = queryForSingleItem(baseUri, 2543 new String[] { BaseColumns._ID }, 2544 MediaColumns.DOCUMENT_ID + "=?", new String[] { documentId }, null)) { 2545 return ContentUris.withAppendedId(baseUri, c.getLong(0)); 2546 } catch (FileNotFoundException e) { 2547 Log.w(TAG, "Failed to resolve " + uri + ": " + e); 2548 return null; 2549 } 2550 } 2551 } 2552 2553 return uri; 2554 } 2555 safeUncanonicalize(Uri uri)2556 private Uri safeUncanonicalize(Uri uri) { 2557 Uri newUri = uncanonicalize(uri); 2558 if (newUri != null) { 2559 return newUri; 2560 } 2561 return uri; 2562 } 2563 2564 /** 2565 * @return where clause to exclude database rows where 2566 * <ul> 2567 * <li> {@code column} is set or 2568 * <li> {@code column} is {@link MediaColumns#IS_PENDING} and is set by FUSE and not owned by 2569 * calling package. 2570 * <li> {@code column} is {@link MediaColumns#IS_PENDING}, is unset and is waiting for 2571 * metadata update from a deferred scan. 2572 * </ul> 2573 */ getWhereClauseForMatchExclude(@onNull String column)2574 private String getWhereClauseForMatchExclude(@NonNull String column) { 2575 if (column.equalsIgnoreCase(MediaColumns.IS_PENDING)) { 2576 // Don't include rows that are pending for metadata 2577 final String pendingForMetadata = FileColumns._MODIFIER + "=" 2578 + FileColumns._MODIFIER_CR_PENDING_METADATA; 2579 final String notPending = String.format("(%s=0 AND NOT %s)", column, 2580 pendingForMetadata); 2581 2582 // Include owned pending files from Fuse 2583 final String pendingFromFuse = String.format("(%s=1 AND %s AND %s)", column, 2584 MATCH_PENDING_FROM_FUSE, getWhereForOwnerPackageMatch(mCallingIdentity.get())); 2585 2586 return "(" + notPending + " OR " + pendingFromFuse + ")"; 2587 } 2588 return column + "=0"; 2589 } 2590 2591 /** 2592 * @return where clause to include database rows where 2593 * <ul> 2594 * <li> {@code column} is not set or 2595 * <li> {@code column} is set and calling package has write permission to corresponding db row 2596 * or {@code column} is {@link MediaColumns#IS_PENDING} and is set by FUSE. 2597 * </ul> 2598 * The method is used to match db rows corresponding to writable pending and trashed files. 2599 */ 2600 @Nullable getWhereClauseForMatchableVisibleFromFilePath(@onNull Uri uri, @NonNull String column)2601 private String getWhereClauseForMatchableVisibleFromFilePath(@NonNull Uri uri, 2602 @NonNull String column) { 2603 if (checkCallingPermissionGlobal(uri, /*forWrite*/ true)) { 2604 // No special filtering needed 2605 return null; 2606 } 2607 2608 int uriType = matchUri(uri, isCallingPackageAllowedHidden()); 2609 if (hasAccessToCollection(mCallingIdentity.get(), uriType, /* forWrite */ true)) { 2610 // has direct write access to whole collection, no special filtering needed. 2611 return null; 2612 } 2613 2614 final String writeAccessCheckSql = getWhereForConstrainedAccess(mCallingIdentity.get(), 2615 uriType, /* forWrite */ true, Bundle.EMPTY); 2616 2617 final String matchWritableRowsClause = String.format("%s=0 OR (%s=1 AND (%s OR %s))", 2618 column, column, MATCH_PENDING_FROM_FUSE, writeAccessCheckSql); 2619 2620 return matchWritableRowsClause; 2621 } 2622 2623 /** 2624 * Gets list of files in {@code path} from media provider database. 2625 * 2626 * @param path path of the directory. 2627 * @param uid UID of the calling process. 2628 * @return a list of file names in the given directory path. 2629 * An empty list is returned if no files are visible to the calling app or the given directory 2630 * does not have any files. 2631 * A list with ["/"] is returned if the path is not indexed by MediaProvider database or 2632 * calling package is a legacy app and has appropriate storage permissions for the given path. 2633 * In both scenarios file names should be obtained from lower file system. 2634 * A list with empty string[""] is returned if the calling package doesn't have access to the 2635 * given path. 2636 * 2637 * <p>Directory names are always obtained from lower file system. 2638 * 2639 * Called from JNI in jni/MediaProviderWrapper.cpp 2640 */ 2641 @Keep getFilesInDirectoryForFuse(String path, int uid)2642 public String[] getFilesInDirectoryForFuse(String path, int uid) { 2643 final LocalCallingIdentity token = 2644 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 2645 PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path); 2646 2647 try { 2648 if (isPrivatePackagePathNotAccessibleByCaller(path)) { 2649 return new String[] {""}; 2650 } 2651 2652 if (shouldBypassFuseRestrictions(/*forWrite*/ false, path)) { 2653 return new String[] {"/"}; 2654 } 2655 2656 // Do not allow apps to list Android/data or Android/obb dirs. 2657 // On primary volumes, apps that get special access to these directories get it via 2658 // mount views of lowerfs. On secondary volumes, such apps would return early from 2659 // shouldBypassFuseRestrictions above. 2660 if (isDataOrObbPath(path)) { 2661 return new String[] {""}; 2662 } 2663 2664 // Legacy apps that made is this far don't have the right storage permission and hence 2665 // are not allowed to access anything other than their external app directory 2666 if (isCallingPackageRequestingLegacy()) { 2667 return new String[] {""}; 2668 } 2669 2670 // Get relative path for the contents of given directory. 2671 String relativePath = extractRelativePathWithDisplayName(path); 2672 if (relativePath == null) { 2673 // Path is /storage/emulated/, if relativePath is null, MediaProvider doesn't 2674 // have any details about the given directory. Use lower file system to obtain 2675 // files and directories in the given directory. 2676 return new String[] {"/"}; 2677 } 2678 // Getting UserId from the directory path, as clone user shares the MediaProvider 2679 // of user 0. 2680 int userIdFromPath = FileUtils.extractUserId(path); 2681 // In some cases, like querying public volumes, userId is not available in path. We 2682 // take userId from the user running MediaProvider process (sUserId). 2683 if (userIdFromPath == -1) { 2684 userIdFromPath = sUserId; 2685 } 2686 // For all other paths, get file names from media provider database. 2687 // Return media and non-media files visible to the calling package. 2688 ArrayList<String> fileNamesList = new ArrayList<>(); 2689 2690 // Only FileColumns.DATA contains actual name of the file. 2691 String[] projection = {MediaColumns.DATA}; 2692 2693 Bundle queryArgs = new Bundle(); 2694 queryArgs.putString(QUERY_ARG_SQL_SELECTION, MediaColumns.RELATIVE_PATH + 2695 " =? and " + FileColumns._USER_ID + " =? and mime_type not like 'null'"); 2696 queryArgs.putStringArray(QUERY_ARG_SQL_SELECTION_ARGS, new String[] {relativePath, 2697 String.valueOf(userIdFromPath)}); 2698 // Get database entries for files from MediaProvider database with 2699 // MediaColumns.RELATIVE_PATH as the given path. 2700 try (final Cursor cursor = query(FileUtils.getContentUriForPath(path), projection, 2701 queryArgs, null)) { 2702 while(cursor.moveToNext()) { 2703 fileNamesList.add(extractDisplayName(cursor.getString(0))); 2704 } 2705 } 2706 return fileNamesList.toArray(new String[fileNamesList.size()]); 2707 } finally { 2708 restoreLocalCallingIdentity(token); 2709 } 2710 } 2711 2712 /** 2713 * Scan files during directory renames for the following reasons: 2714 * <ul> 2715 * <li>Because we don't update db rows for directories, we scan the oldPath to discard stale 2716 * directory db rows. This prevents conflicts during subsequent db operations with oldPath. 2717 * <li>We need to scan newPath as well, because the new directory may have become hidden 2718 * or unhidden, in which case we need to update the media types of the contained files 2719 * </ul> 2720 */ scanRenamedDirectoryForFuse(@onNull String oldPath, @NonNull String newPath)2721 private void scanRenamedDirectoryForFuse(@NonNull String oldPath, @NonNull String newPath) { 2722 scanFileAsMediaProvider(new File(oldPath), REASON_DEMAND); 2723 scanFileAsMediaProvider(new File(newPath), REASON_DEMAND); 2724 } 2725 2726 /** 2727 * Checks if given {@code mimeType} is supported in {@code path}. 2728 */ isMimeTypeSupportedInPath(String path, String mimeType)2729 private boolean isMimeTypeSupportedInPath(String path, String mimeType) { 2730 final String supportedPrimaryMimeType; 2731 final int match = matchUri(getContentUriForFile(path, mimeType), true); 2732 switch (match) { 2733 case AUDIO_MEDIA: 2734 supportedPrimaryMimeType = "audio"; 2735 break; 2736 case VIDEO_MEDIA: 2737 supportedPrimaryMimeType = "video"; 2738 break; 2739 case IMAGES_MEDIA: 2740 supportedPrimaryMimeType = "image"; 2741 break; 2742 default: 2743 supportedPrimaryMimeType = ClipDescription.MIMETYPE_UNKNOWN; 2744 } 2745 return (supportedPrimaryMimeType.equalsIgnoreCase(ClipDescription.MIMETYPE_UNKNOWN) || 2746 StringUtils.startsWithIgnoreCase(mimeType, supportedPrimaryMimeType)); 2747 } 2748 2749 /** 2750 * Removes owner package for the renamed path if the calling package doesn't own the db row 2751 * 2752 * When oldPath is renamed to newPath, if newPath exists in the database, and caller is not the 2753 * owner of the file, owner package is set to 'null'. This prevents previous owner of newPath 2754 * from accessing renamed file. 2755 * @return {@code true} if 2756 * <ul> 2757 * <li> there is no corresponding database row for given {@code path} 2758 * <li> shared calling package is the owner of the database row 2759 * <li> owner package name is already set to 'null' 2760 * <li> updating owner package name to 'null' was successful. 2761 * </ul> 2762 * Returns {@code false} otherwise. 2763 */ maybeRemoveOwnerPackageForFuseRename(@onNull DatabaseHelper helper, @NonNull String path)2764 private boolean maybeRemoveOwnerPackageForFuseRename(@NonNull DatabaseHelper helper, 2765 @NonNull String path) { 2766 final Uri uri = FileUtils.getContentUriForPath(path); 2767 final int match = matchUri(uri, isCallingPackageAllowedHidden()); 2768 final String ownerPackageName; 2769 final String selection = MediaColumns.DATA + " =? AND " 2770 + MediaColumns.OWNER_PACKAGE_NAME + " != 'null'"; 2771 final String[] selectionArgs = new String[] {path}; 2772 2773 final SQLiteQueryBuilder qbForQuery = 2774 getQueryBuilder(TYPE_QUERY, match, uri, Bundle.EMPTY, null); 2775 try (Cursor c = qbForQuery.query(helper, new String[] {FileColumns.OWNER_PACKAGE_NAME}, 2776 selection, selectionArgs, null, null, null, null, null)) { 2777 if (!c.moveToFirst()) { 2778 // We don't need to remove owner_package from db row if path doesn't exist in 2779 // database or owner_package is already set to 'null' 2780 return true; 2781 } 2782 ownerPackageName = c.getString(0); 2783 if (isCallingIdentitySharedPackageName(ownerPackageName)) { 2784 // We don't need to remove owner_package from db row if calling package is the owner 2785 // of the database row 2786 return true; 2787 } 2788 } 2789 2790 final SQLiteQueryBuilder qbForUpdate = 2791 getQueryBuilder(TYPE_UPDATE, match, uri, Bundle.EMPTY, null); 2792 ContentValues values = new ContentValues(); 2793 values.put(FileColumns.OWNER_PACKAGE_NAME, "null"); 2794 return qbForUpdate.update(helper, values, selection, selectionArgs) == 1; 2795 } 2796 updateDatabaseForFuseRename(@onNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values)2797 private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper, 2798 @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values) { 2799 return updateDatabaseForFuseRename(helper, oldPath, newPath, values, Bundle.EMPTY); 2800 } 2801 updateDatabaseForFuseRename(@onNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values, @NonNull Bundle qbExtras)2802 private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper, 2803 @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values, 2804 @NonNull Bundle qbExtras) { 2805 return updateDatabaseForFuseRename(helper, oldPath, newPath, values, qbExtras, 2806 FileUtils.getContentUriForPath(oldPath)); 2807 } 2808 2809 /** 2810 * Updates database entry for given {@code path} with {@code values} 2811 */ updateDatabaseForFuseRename(@onNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values, @NonNull Bundle qbExtras, Uri uriOldPath)2812 private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper, 2813 @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values, 2814 @NonNull Bundle qbExtras, Uri uriOldPath) { 2815 boolean allowHidden = isCallingPackageAllowedHidden(); 2816 final SQLiteQueryBuilder qbForUpdate = getQueryBuilder(TYPE_UPDATE, 2817 matchUri(uriOldPath, allowHidden), uriOldPath, qbExtras, null); 2818 2819 // uriOldPath may use Files uri which doesn't allow modifying AudioColumns. Include 2820 // AudioColumns projection map if we are modifying any audio columns while renaming 2821 // database rows. 2822 if (values.containsKey(AudioColumns.IS_RINGTONE)) { 2823 qbForUpdate.setProjectionMap(getProjectionMap(AudioColumns.class, FileColumns.class)); 2824 } 2825 2826 if (values.containsKey(FileColumns._MODIFIER)) { 2827 qbForUpdate.allowColumn(FileColumns._MODIFIER); 2828 } 2829 2830 final String selection = MediaColumns.DATA + " =? "; 2831 int count = 0; 2832 boolean retryUpdateWithReplace = false; 2833 2834 try { 2835 // TODO(b/146777893): System gallery apps can rename a media directory containing 2836 // non-media files. This update doesn't support updating non-media files that are not 2837 // owned by system gallery app. 2838 count = qbForUpdate.update(helper, values, selection, new String[]{oldPath}); 2839 } catch (SQLiteConstraintException e) { 2840 Log.w(TAG, "Database update failed while renaming " + oldPath, e); 2841 retryUpdateWithReplace = true; 2842 } 2843 2844 if (retryUpdateWithReplace) { 2845 if (deleteForFuseRename(helper, oldPath, newPath, qbExtras, selection, allowHidden)) { 2846 Log.i(TAG, "Retrying database update after deleting conflicting entry"); 2847 count = qbForUpdate.update(helper, values, selection, new String[]{oldPath}); 2848 } else { 2849 return false; 2850 } 2851 } 2852 return count == 1; 2853 } 2854 deleteForFuseRename(DatabaseHelper helper, String oldPath, String newPath, Bundle qbExtras, String selection, boolean allowHidden)2855 private boolean deleteForFuseRename(DatabaseHelper helper, String oldPath, 2856 String newPath, Bundle qbExtras, String selection, boolean allowHidden) { 2857 // We are replacing file in newPath with file in oldPath. If calling package has 2858 // write permission for newPath, delete existing database entry and retry update. 2859 final Uri uriNewPath = FileUtils.getContentUriForPath(oldPath); 2860 final SQLiteQueryBuilder qbForDelete = getQueryBuilder(TYPE_DELETE, 2861 matchUri(uriNewPath, allowHidden), uriNewPath, qbExtras, null); 2862 if (qbForDelete.delete(helper, selection, new String[] {newPath}) == 1) { 2863 return true; 2864 } 2865 // Check if delete can be done using other URI grants 2866 final String[] projection = new String[] { 2867 FileColumns.MEDIA_TYPE, 2868 FileColumns.DATA, 2869 FileColumns._ID, 2870 FileColumns.IS_DOWNLOAD, 2871 FileColumns.MIME_TYPE, 2872 }; 2873 return 2874 deleteWithOtherUriGrants( 2875 FileUtils.getContentUriForPath(newPath), 2876 helper, projection, selection, new String[] {newPath}, qbExtras) == 1; 2877 } 2878 2879 /** 2880 * Gets {@link ContentValues} for updating database entry to {@code path}. 2881 */ getContentValuesForFuseRename(String path, String newMimeType, boolean wasHidden, boolean isHidden, boolean isSameMimeType)2882 private ContentValues getContentValuesForFuseRename(String path, String newMimeType, 2883 boolean wasHidden, boolean isHidden, boolean isSameMimeType) { 2884 ContentValues values = new ContentValues(); 2885 values.put(MediaColumns.MIME_TYPE, newMimeType); 2886 values.put(MediaColumns.DATA, path); 2887 2888 if (isHidden) { 2889 values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE); 2890 } else { 2891 int mediaType = MimeUtils.resolveMediaType(newMimeType); 2892 values.put(FileColumns.MEDIA_TYPE, mediaType); 2893 } 2894 2895 if ((!isHidden && wasHidden) || !isSameMimeType) { 2896 // Set the modifier as MODIFIER_FUSE so that apps can scan the file to update the 2897 // metadata. Otherwise, scan will skip scanning this file because rename() doesn't 2898 // change lastModifiedTime and scan assumes there is no change in the file. 2899 values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_FUSE); 2900 } 2901 2902 if (MimeUtils.isAudioMimeType(newMimeType) && !values.containsKey(FileColumns._MODIFIER)) { 2903 computeAudioLocalizedValues(values); 2904 computeAudioKeyValues(values); 2905 FileUtils.computeAudioTypeValuesFromData(path, values::put); 2906 } 2907 2908 FileUtils.computeValuesFromData(values, isFuseThread()); 2909 return values; 2910 } 2911 getIncludedDefaultDirectories()2912 private ArrayList<String> getIncludedDefaultDirectories() { 2913 final ArrayList<String> includedDefaultDirs = new ArrayList<>(); 2914 if (mCallingIdentity.get().checkCallingPermissionVideo(/* forWrite */ true)) { 2915 includedDefaultDirs.add(Environment.DIRECTORY_DCIM); 2916 includedDefaultDirs.add(Environment.DIRECTORY_PICTURES); 2917 includedDefaultDirs.add(Environment.DIRECTORY_MOVIES); 2918 } else if (mCallingIdentity.get().checkCallingPermissionImages(/* forWrite */ true)) { 2919 includedDefaultDirs.add(Environment.DIRECTORY_DCIM); 2920 includedDefaultDirs.add(Environment.DIRECTORY_PICTURES); 2921 } 2922 return includedDefaultDirs; 2923 } 2924 2925 /** 2926 * Gets all files in the given {@code path} and subdirectories of the given {@code path}. 2927 */ getAllFilesForRenameDirectory(String oldPath)2928 private ArrayList<String> getAllFilesForRenameDirectory(String oldPath) { 2929 final String selection = FileColumns.DATA + " LIKE ? ESCAPE '\\'" 2930 + " and mime_type not like 'null'"; 2931 final String[] selectionArgs = new String[] {DatabaseUtils.escapeForLike(oldPath) + "/%"}; 2932 ArrayList<String> fileList = new ArrayList<>(); 2933 2934 final LocalCallingIdentity token = clearLocalCallingIdentity(); 2935 try (final Cursor c = query(FileUtils.getContentUriForPath(oldPath), 2936 new String[] {MediaColumns.DATA}, selection, selectionArgs, null)) { 2937 while (c.moveToNext()) { 2938 String filePath = c.getString(0); 2939 filePath = filePath.replaceFirst(Pattern.quote(oldPath + "/"), ""); 2940 fileList.add(filePath); 2941 } 2942 } finally { 2943 restoreLocalCallingIdentity(token); 2944 } 2945 return fileList; 2946 } 2947 2948 /** 2949 * Gets files in the given {@code path} and subdirectories of the given {@code path} for which 2950 * calling package has write permissions. 2951 * 2952 * This method throws {@code IllegalArgumentException} if the directory has one or more 2953 * files for which calling package doesn't have write permission or if file type is not 2954 * supported in {@code newPath} 2955 */ getWritableFilesForRenameDirectory(String oldPath, String newPath)2956 private ArrayList<String> getWritableFilesForRenameDirectory(String oldPath, String newPath) 2957 throws IllegalArgumentException { 2958 // Try a simple check to see if the caller has full access to the given collections first 2959 // before falling back to performing a query to probe for access. 2960 final String oldRelativePath = extractRelativePathWithDisplayName(oldPath); 2961 final String newRelativePath = extractRelativePathWithDisplayName(newPath); 2962 boolean hasFullAccessToOldPath = false; 2963 boolean hasFullAccessToNewPath = false; 2964 for (String defaultDir : getIncludedDefaultDirectories()) { 2965 if (oldRelativePath.startsWith(defaultDir)) hasFullAccessToOldPath = true; 2966 if (newRelativePath.startsWith(defaultDir)) hasFullAccessToNewPath = true; 2967 } 2968 if (hasFullAccessToNewPath && hasFullAccessToOldPath) { 2969 return getAllFilesForRenameDirectory(oldPath); 2970 } 2971 2972 final int countAllFilesInDirectory; 2973 final String selection = FileColumns.DATA + " LIKE ? ESCAPE '\\'" 2974 + " and mime_type not like 'null'"; 2975 final String[] selectionArgs = new String[] {DatabaseUtils.escapeForLike(oldPath) + "/%"}; 2976 2977 final Uri uriOldPath = FileUtils.getContentUriForPath(oldPath); 2978 2979 final LocalCallingIdentity token = clearLocalCallingIdentity(); 2980 try (final Cursor c = query(uriOldPath, new String[] {MediaColumns._ID}, selection, 2981 selectionArgs, null)) { 2982 // get actual number of files in the given directory. 2983 countAllFilesInDirectory = c.getCount(); 2984 } finally { 2985 restoreLocalCallingIdentity(token); 2986 } 2987 2988 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, 2989 matchUri(uriOldPath, isCallingPackageAllowedHidden()), uriOldPath, Bundle.EMPTY, 2990 null); 2991 final DatabaseHelper helper; 2992 try { 2993 helper = getDatabaseForUri(uriOldPath); 2994 } catch (VolumeNotFoundException e) { 2995 throw new IllegalStateException("Volume not found while querying files for renaming " 2996 + oldPath); 2997 } 2998 2999 ArrayList<String> fileList = new ArrayList<>(); 3000 final String[] projection = {MediaColumns.DATA, MediaColumns.MIME_TYPE}; 3001 try (Cursor c = qb.query(helper, projection, selection, selectionArgs, null, null, null, 3002 null, null)) { 3003 // Check if the calling package has write permission to all files in the given 3004 // directory. If calling package has write permission to all files in the directory, the 3005 // query with update uri should return same number of files as previous query. 3006 if (c.getCount() != countAllFilesInDirectory) { 3007 throw new IllegalArgumentException("Calling package doesn't have write permission " 3008 + " to rename one or more files in " + oldPath); 3009 } 3010 while(c.moveToNext()) { 3011 String filePath = c.getString(0); 3012 filePath = filePath.replaceFirst(Pattern.quote(oldPath + "/"), ""); 3013 3014 final String mimeType = c.getString(1); 3015 if (!isMimeTypeSupportedInPath(newPath + "/" + filePath, mimeType)) { 3016 throw new IllegalArgumentException("Can't rename " + oldPath + "/" + filePath 3017 + ". Mime type " + mimeType + " not supported in " + newPath); 3018 } 3019 fileList.add(filePath); 3020 } 3021 } 3022 return fileList; 3023 } 3024 renameInLowerFs(String oldPath, String newPath)3025 private int renameInLowerFs(String oldPath, String newPath) { 3026 try { 3027 Os.rename(oldPath, newPath); 3028 return 0; 3029 } catch (ErrnoException e) { 3030 final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed."; 3031 Log.e(TAG, errorMessage, e); 3032 return e.errno; 3033 } 3034 } 3035 3036 /** 3037 * Rename directory from {@code oldPath} to {@code newPath}. 3038 * 3039 * Renaming a directory is only allowed if calling package has write permission to all files in 3040 * the given directory tree and all file types in the given directory tree are supported by the 3041 * top level directory of new path. Renaming a directory is split into three steps: 3042 * 1. Check calling package's permissions for all files in the given directory tree. Also check 3043 * file type support for all files in the {@code newPath}. 3044 * 2. Try updating database for all files in the directory. 3045 * 3. Rename the directory in lower file system. If rename in the lower file system is 3046 * successful, commit database update. 3047 * 3048 * @param oldPath path of the directory to be renamed. 3049 * @param newPath new path of directory to be renamed. 3050 * @return 0 on successful rename, appropriate negated errno value if the rename is not allowed. 3051 * <ul> 3052 * <li>{@link OsConstants#EPERM} Renaming a directory with file types not supported by 3053 * {@code newPath} or renaming a directory with files for which calling package doesn't have 3054 * write permission. 3055 * This method can also return errno returned from {@code Os.rename} function. 3056 */ renameDirectoryCheckedForFuse(String oldPath, String newPath)3057 private int renameDirectoryCheckedForFuse(String oldPath, String newPath) { 3058 final ArrayList<String> fileList; 3059 try { 3060 fileList = getWritableFilesForRenameDirectory(oldPath, newPath); 3061 } catch (IllegalArgumentException e) { 3062 final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed. "; 3063 Log.e(TAG, errorMessage, e); 3064 return OsConstants.EPERM; 3065 } 3066 3067 return renameDirectoryUncheckedForFuse(oldPath, newPath, fileList); 3068 } 3069 renameDirectoryUncheckedForFuse(String oldPath, String newPath, ArrayList<String> fileList)3070 private int renameDirectoryUncheckedForFuse(String oldPath, String newPath, 3071 ArrayList<String> fileList) { 3072 final DatabaseHelper helper; 3073 try { 3074 helper = getDatabaseForUri(FileUtils.getContentUriForPath(oldPath)); 3075 } catch (VolumeNotFoundException e) { 3076 throw new IllegalStateException("Volume not found while trying to update database for " 3077 + oldPath, e); 3078 } 3079 3080 helper.beginTransaction(); 3081 try { 3082 final Bundle qbExtras = new Bundle(); 3083 qbExtras.putStringArrayList(INCLUDED_DEFAULT_DIRECTORIES, 3084 getIncludedDefaultDirectories()); 3085 final boolean wasHidden = FileUtils.shouldDirBeHidden(new File(oldPath)); 3086 final boolean isHidden = FileUtils.shouldDirBeHidden(new File(newPath)); 3087 for (String filePath : fileList) { 3088 final String newFilePath = newPath + "/" + filePath; 3089 final String mimeType = MimeUtils.resolveMimeType(new File(newFilePath)); 3090 if(!updateDatabaseForFuseRename(helper, oldPath + "/" + filePath, newFilePath, 3091 getContentValuesForFuseRename(newFilePath, mimeType, wasHidden, isHidden, 3092 /* isSameMimeType */ true), 3093 qbExtras)) { 3094 Log.e(TAG, "Calling package doesn't have write permission to rename file."); 3095 return OsConstants.EPERM; 3096 } 3097 } 3098 3099 // Rename the directory in lower file system. 3100 int errno = renameInLowerFs(oldPath, newPath); 3101 if (errno == 0) { 3102 helper.setTransactionSuccessful(); 3103 } else { 3104 return errno; 3105 } 3106 } finally { 3107 helper.endTransaction(); 3108 } 3109 // Directory movement might have made new/old path hidden. 3110 scanRenamedDirectoryForFuse(oldPath, newPath); 3111 return 0; 3112 } 3113 3114 /** 3115 * Rename a file from {@code oldPath} to {@code newPath}. 3116 * 3117 * Renaming a file is split into three parts: 3118 * 1. Check if {@code newPath} supports new file type. 3119 * 2. Try updating database entry from {@code oldPath} to {@code newPath}. This update may fail 3120 * if calling package doesn't have write permission for {@code oldPath} and {@code newPath}. 3121 * 3. Rename the file in lower file system. If Rename in lower file system succeeds, commit 3122 * database update. 3123 * @param oldPath path of the file to be renamed. 3124 * @param newPath new path of the file to be renamed. 3125 * @return 0 on successful rename, appropriate negated errno value if the rename is not allowed. 3126 * <ul> 3127 * <li>{@link OsConstants#EPERM} Calling package doesn't have write permission for 3128 * {@code oldPath} or {@code newPath}, or file type is not supported by {@code newPath}. 3129 * This method can also return errno returned from {@code Os.rename} function. 3130 */ renameFileCheckedForFuse(String oldPath, String newPath)3131 private int renameFileCheckedForFuse(String oldPath, String newPath) { 3132 // Check if new mime type is supported in new path. 3133 final String newMimeType = MimeUtils.resolveMimeType(new File(newPath)); 3134 if (!isMimeTypeSupportedInPath(newPath, newMimeType)) { 3135 return OsConstants.EPERM; 3136 } 3137 return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ false) ; 3138 } 3139 renameFileUncheckedForFuse(String oldPath, String newPath)3140 private int renameFileUncheckedForFuse(String oldPath, String newPath) { 3141 return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ true) ; 3142 } 3143 renameFileForFuse(String oldPath, String newPath, boolean bypassRestrictions)3144 private int renameFileForFuse(String oldPath, String newPath, boolean bypassRestrictions) { 3145 final DatabaseHelper helper; 3146 try { 3147 helper = getDatabaseForUri(FileUtils.getContentUriForPath(oldPath)); 3148 } catch (VolumeNotFoundException e) { 3149 throw new IllegalStateException("Failed to update database row with " + oldPath, e); 3150 } 3151 3152 final boolean wasHidden = FileUtils.shouldFileBeHidden(new File(oldPath)); 3153 final boolean isHidden = FileUtils.shouldFileBeHidden(new File(newPath)); 3154 helper.beginTransaction(); 3155 try { 3156 final String newMimeType = MimeUtils.resolveMimeType(new File(newPath)); 3157 final String oldMimeType = MimeUtils.resolveMimeType(new File(oldPath)); 3158 final boolean isSameMimeType = newMimeType.equalsIgnoreCase(oldMimeType); 3159 ContentValues contentValues = getContentValuesForFuseRename(newPath, newMimeType, 3160 wasHidden, isHidden, isSameMimeType); 3161 if (!updateDatabaseForFuseRename(helper, oldPath, newPath, contentValues)) { 3162 if (!bypassRestrictions) { 3163 // Check for other URI format grants for oldPath only. Check right before 3164 // returning EPERM, to leave positive case performance unaffected. 3165 if (!renameWithOtherUriGrants(helper, oldPath, newPath, contentValues)) { 3166 Log.e(TAG, "Calling package doesn't have write permission to rename file."); 3167 return OsConstants.EPERM; 3168 } 3169 } else if (!maybeRemoveOwnerPackageForFuseRename(helper, newPath)) { 3170 Log.wtf(TAG, "Couldn't clear owner package name for " + newPath); 3171 return OsConstants.EPERM; 3172 } 3173 } 3174 3175 // Try renaming oldPath to newPath in lower file system. 3176 int errno = renameInLowerFs(oldPath, newPath); 3177 if (errno == 0) { 3178 helper.setTransactionSuccessful(); 3179 } else { 3180 return errno; 3181 } 3182 } finally { 3183 helper.endTransaction(); 3184 } 3185 // The above code should have taken are of the mime/media type of the new file, 3186 // even if it was moved to/from a hidden directory. 3187 // This leaves cases where the source/dest of the move is a .nomedia file itself. Eg: 3188 // 1) /sdcard/foo/.nomedia => /sdcard/foo/bar.mp3 3189 // in this case, the code above has given bar.mp3 the correct mime type, but we should 3190 // still can /sdcard/foo, because it's now no longer hidden 3191 // 2) /sdcard/foo/.nomedia => /sdcard/bar/.nomedia 3192 // in this case, we need to scan both /sdcard/foo and /sdcard/bar/ 3193 // 3) /sdcard/foo/bar.mp3 => /sdcard/foo/.nomedia 3194 // in this case, we need to scan all of /sdcard/foo 3195 if (extractDisplayName(oldPath).equals(".nomedia")) { 3196 scanFileAsMediaProvider(new File(oldPath).getParentFile(), REASON_DEMAND); 3197 } 3198 if (extractDisplayName(newPath).equals(".nomedia")) { 3199 scanFileAsMediaProvider(new File(newPath).getParentFile(), REASON_DEMAND); 3200 } 3201 3202 return 0; 3203 } 3204 3205 /** 3206 * Rename file by checking for other URI grants on oldPath 3207 * 3208 * We don't support replace scenario by checking for other URI grants on newPath (if it exists). 3209 */ renameWithOtherUriGrants(DatabaseHelper helper, String oldPath, String newPath, ContentValues contentValues)3210 private boolean renameWithOtherUriGrants(DatabaseHelper helper, String oldPath, String newPath, 3211 ContentValues contentValues) { 3212 final Uri oldPathGrantedUri = getOtherUriGrantsForPath(oldPath, /* forWrite */ true); 3213 if (oldPathGrantedUri == null) { 3214 return false; 3215 } 3216 return updateDatabaseForFuseRename(helper, oldPath, newPath, contentValues, Bundle.EMPTY, 3217 oldPathGrantedUri); 3218 } 3219 3220 /** 3221 * Rename file/directory without imposing any restrictions. 3222 * 3223 * We don't impose any rename restrictions for apps that bypass scoped storage restrictions. 3224 * However, we update database entries for renamed files to keep the database consistent. 3225 */ renameUncheckedForFuse(String oldPath, String newPath)3226 private int renameUncheckedForFuse(String oldPath, String newPath) { 3227 if (new File(oldPath).isFile()) { 3228 return renameFileUncheckedForFuse(oldPath, newPath); 3229 } else { 3230 return renameDirectoryUncheckedForFuse(oldPath, newPath, 3231 getAllFilesForRenameDirectory(oldPath)); 3232 } 3233 } 3234 3235 /** 3236 * Rename file or directory from {@code oldPath} to {@code newPath}. 3237 * 3238 * @param oldPath path of the file or directory to be renamed. 3239 * @param newPath new path of the file or directory to be renamed. 3240 * @param uid UID of the calling package. 3241 * @return 0 on successful rename, appropriate errno value if the rename is not allowed. 3242 * <ul> 3243 * <li>{@link OsConstants#ENOENT} Renaming a non-existing file or renaming a file from path that 3244 * is not indexed by MediaProvider database. 3245 * <li>{@link OsConstants#EPERM} Renaming a default directory or renaming a file to a file type 3246 * not supported by new path. 3247 * This method can also return errno returned from {@code Os.rename} function. 3248 * 3249 * Called from JNI in jni/MediaProviderWrapper.cpp 3250 */ 3251 @Keep renameForFuse(String oldPath, String newPath, int uid)3252 public int renameForFuse(String oldPath, String newPath, int uid) { 3253 final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed. "; 3254 final LocalCallingIdentity token = 3255 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 3256 PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), oldPath); 3257 3258 try { 3259 if (isPrivatePackagePathNotAccessibleByCaller(oldPath) 3260 || isPrivatePackagePathNotAccessibleByCaller(newPath)) { 3261 return OsConstants.EACCES; 3262 } 3263 3264 if (!newPath.equals(getAbsoluteSanitizedPath(newPath))) { 3265 Log.e(TAG, "New path name contains invalid characters."); 3266 return OsConstants.EPERM; 3267 } 3268 3269 if (shouldBypassDatabaseAndSetDirtyForFuse(uid, oldPath) 3270 && shouldBypassDatabaseAndSetDirtyForFuse(uid, newPath)) { 3271 return renameInLowerFs(oldPath, newPath); 3272 } 3273 3274 if (shouldBypassFuseRestrictions(/*forWrite*/ true, oldPath) 3275 && shouldBypassFuseRestrictions(/*forWrite*/ true, newPath)) { 3276 return renameUncheckedForFuse(oldPath, newPath); 3277 } 3278 // Legacy apps that made is this far don't have the right storage permission and hence 3279 // are not allowed to access anything other than their external app directory 3280 if (isCallingPackageRequestingLegacy()) { 3281 return OsConstants.EACCES; 3282 } 3283 3284 final String[] oldRelativePath = sanitizePath(extractRelativePath(oldPath)); 3285 final String[] newRelativePath = sanitizePath(extractRelativePath(newPath)); 3286 if (oldRelativePath.length == 0 || newRelativePath.length == 0) { 3287 // Rename not allowed on paths that can't be translated to RELATIVE_PATH. 3288 Log.e(TAG, errorMessage + "Invalid path."); 3289 return OsConstants.EPERM; 3290 } 3291 if (oldRelativePath.length == 1 && TextUtils.isEmpty(oldRelativePath[0])) { 3292 // Allow rename of files/folders other than default directories. 3293 final String displayName = extractDisplayName(oldPath); 3294 for (String defaultFolder : DEFAULT_FOLDER_NAMES) { 3295 if (displayName.equals(defaultFolder)) { 3296 Log.e(TAG, errorMessage + oldPath + " is a default folder." 3297 + " Renaming a default folder is not allowed."); 3298 return OsConstants.EPERM; 3299 } 3300 } 3301 } 3302 if (newRelativePath.length == 1 && TextUtils.isEmpty(newRelativePath[0])) { 3303 Log.e(TAG, errorMessage + newPath + " is in root folder." 3304 + " Renaming a file/directory to root folder is not allowed"); 3305 return OsConstants.EPERM; 3306 } 3307 3308 final File directoryAndroid = new File( 3309 extractVolumePath(oldPath).toLowerCase(Locale.ROOT), 3310 DIRECTORY_ANDROID_LOWER_CASE 3311 ); 3312 final File directoryAndroidMedia = new File(directoryAndroid, DIRECTORY_MEDIA); 3313 String newPathLowerCase = newPath.toLowerCase(Locale.ROOT); 3314 if (directoryAndroidMedia.getAbsolutePath().equalsIgnoreCase(oldPath)) { 3315 // Don't allow renaming 'Android/media' directory. 3316 // Android/[data|obb] are bind mounted and these paths don't go through FUSE. 3317 Log.e(TAG, errorMessage + oldPath + " is a default folder in app external " 3318 + "directory. Renaming a default folder is not allowed."); 3319 return OsConstants.EPERM; 3320 } else if (FileUtils.contains(directoryAndroid, new File(newPathLowerCase))) { 3321 if (newRelativePath.length <= 2) { 3322 // Path is directly under Android, Android/media, Android/data, Android/obb or 3323 // some other directory under Android. Don't allow moving files and directories 3324 // in these paths. Files and directories are only allowed to move to path 3325 // Android/media/<app_specific_directory>/* 3326 Log.e(TAG, errorMessage + newPath + " is in app external directory. " 3327 + "Renaming a file/directory to app external directory is not " 3328 + "allowed."); 3329 return OsConstants.EPERM; 3330 } else if (!FileUtils.contains(directoryAndroidMedia, new File(newPathLowerCase))) { 3331 // New path is not in Android/media/*. Don't allow moving of files or 3332 // directories to app external directory other than media directory. 3333 Log.e(TAG, errorMessage + newPath + " is not in external media directory." 3334 + "File/directory can only be renamed to a path in external media " 3335 + "directory. Renaming file/directory to path in other external " 3336 + "directories is not allowed"); 3337 return OsConstants.EPERM; 3338 } 3339 } 3340 3341 // Continue renaming files/directories if rename of oldPath to newPath is allowed. 3342 if (new File(oldPath).isFile()) { 3343 return renameFileCheckedForFuse(oldPath, newPath); 3344 } else { 3345 return renameDirectoryCheckedForFuse(oldPath, newPath); 3346 } 3347 } finally { 3348 restoreLocalCallingIdentity(token); 3349 } 3350 } 3351 3352 @Override checkUriPermission(@onNull Uri uri, int uid, int modeFlags)3353 public int checkUriPermission(@NonNull Uri uri, int uid, 3354 /* @Intent.AccessUriMode */ int modeFlags) { 3355 final LocalCallingIdentity token = clearLocalCallingIdentity( 3356 LocalCallingIdentity.fromExternal(getContext(), mUserCache, uid)); 3357 3358 if (isRedactedUri(uri)) { 3359 if ((modeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) { 3360 // we don't allow write grants on redacted uris. 3361 return PackageManager.PERMISSION_DENIED; 3362 } 3363 3364 uri = getUriForRedactedUri(uri); 3365 } 3366 3367 if (isPickerUri(uri)) { 3368 // Do not allow implicit access (by the virtue of ownership/permission) to picker uris. 3369 // Picker uris should have explicit permission grants. 3370 // If the calling app A has an explicit grant on picker uri, UriGrantsManagerService 3371 // will check the grant status and allow app A to grant the uri to app B (without 3372 // calling into MediaProvider) 3373 return PackageManager.PERMISSION_DENIED; 3374 } 3375 3376 try { 3377 final boolean allowHidden = isCallingPackageAllowedHidden(); 3378 final int table = matchUri(uri, allowHidden); 3379 3380 final DatabaseHelper helper; 3381 try { 3382 helper = getDatabaseForUri(uri); 3383 } catch (VolumeNotFoundException e) { 3384 return PackageManager.PERMISSION_DENIED; 3385 } 3386 3387 final int type; 3388 if ((modeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) { 3389 type = TYPE_UPDATE; 3390 } else { 3391 type = TYPE_QUERY; 3392 } 3393 3394 final SQLiteQueryBuilder qb = getQueryBuilder(type, table, uri, Bundle.EMPTY, null); 3395 try (Cursor c = qb.query(helper, 3396 new String[] { BaseColumns._ID }, null, null, null, null, null, null, null)) { 3397 if (c.getCount() == 1) { 3398 c.moveToFirst(); 3399 final long cursorId = c.getLong(0); 3400 3401 long uriId = -1; 3402 try { 3403 uriId = ContentUris.parseId(uri); 3404 } catch (NumberFormatException ignored) { 3405 // if the id is not a number, the uri doesn't have a valid ID at the end of 3406 // the uri, (i.e., uri is uri of the table not of the item/row) 3407 } 3408 3409 if (uriId != -1 && cursorId == uriId) { 3410 return PackageManager.PERMISSION_GRANTED; 3411 } 3412 } 3413 } 3414 3415 // For the uri with id cases, if it isn't returned in above query section, the result 3416 // isn't as expected. Don't grant the permission. 3417 switch (table) { 3418 case AUDIO_MEDIA_ID: 3419 case IMAGES_MEDIA_ID: 3420 case VIDEO_MEDIA_ID: 3421 case DOWNLOADS_ID: 3422 case FILES_ID: 3423 case AUDIO_MEDIA_ID_GENRES_ID: 3424 case AUDIO_GENRES_ID: 3425 case AUDIO_PLAYLISTS_ID: 3426 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 3427 case AUDIO_ARTISTS_ID: 3428 case AUDIO_ALBUMS_ID: 3429 return PackageManager.PERMISSION_DENIED; 3430 default: 3431 // continue below 3432 } 3433 3434 // If the uri is a valid content uri and doesn't have a valid ID at the end of the uri, 3435 // (i.e., uri is uri of the table not of the item/row), and app doesn't request prefix 3436 // grant, we are willing to grant this uri permission since this doesn't grant them any 3437 // extra access. This grant will only grant permissions on given uri, it will not grant 3438 // access to db rows of the corresponding table. 3439 if ((modeFlags & Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) == 0) { 3440 return PackageManager.PERMISSION_GRANTED; 3441 } 3442 } finally { 3443 restoreLocalCallingIdentity(token); 3444 } 3445 return PackageManager.PERMISSION_DENIED; 3446 } 3447 3448 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)3449 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 3450 String sortOrder) { 3451 return query(uri, projection, 3452 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, sortOrder), null); 3453 } 3454 3455 @Override query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal)3456 public Cursor query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal) { 3457 return query(uri, projection, queryArgs, signal, /* forSelf */ false); 3458 } 3459 query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal, boolean forSelf)3460 private Cursor query(Uri uri, String[] projection, Bundle queryArgs, 3461 CancellationSignal signal, boolean forSelf) { 3462 Trace.beginSection("MP.query [" + uri + ']'); 3463 try { 3464 return queryInternal(uri, projection, queryArgs, signal, forSelf); 3465 } catch (FallbackException e) { 3466 return e.translateForQuery(getCallingPackageTargetSdkVersion()); 3467 } finally { 3468 Trace.endSection(); 3469 } 3470 } 3471 queryInternal(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal, boolean forSelf)3472 private Cursor queryInternal(Uri uri, String[] projection, Bundle queryArgs, 3473 CancellationSignal signal, boolean forSelf) throws FallbackException { 3474 if (isPickerUri(uri)) { 3475 return mPickerUriResolver.query(uri, projection, mCallingIdentity.get().pid, 3476 mCallingIdentity.get().uid, mCallingIdentity.get().getPackageName()); 3477 } 3478 3479 final String volumeName = getVolumeName(uri); 3480 PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName); 3481 queryArgs = (queryArgs != null) ? queryArgs : new Bundle(); 3482 3483 // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. 3484 queryArgs.remove(INCLUDED_DEFAULT_DIRECTORIES); 3485 3486 final ArraySet<String> honoredArgs = new ArraySet<>(); 3487 DatabaseUtils.resolveQueryArgs(queryArgs, honoredArgs::add, this::ensureCustomCollator); 3488 3489 Uri redactedUri = null; 3490 // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider. 3491 queryArgs.remove(QUERY_ARG_REDACTED_URI); 3492 if (isRedactedUri(uri)) { 3493 redactedUri = uri; 3494 uri = getUriForRedactedUri(uri); 3495 queryArgs.putParcelable(QUERY_ARG_REDACTED_URI, redactedUri); 3496 } 3497 3498 uri = safeUncanonicalize(uri); 3499 3500 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 3501 final boolean allowHidden = isCallingPackageAllowedHidden(); 3502 final int table = matchUri(uri, allowHidden); 3503 3504 // handle MEDIA_SCANNER before calling getDatabaseForUri() 3505 if (table == MEDIA_SCANNER) { 3506 // create a cursor to return volume currently being scanned by the media scanner 3507 MatrixCursor c = new MatrixCursor(new String[] {MediaStore.MEDIA_SCANNER_VOLUME}); 3508 c.addRow(new String[] {mMediaScannerVolume}); 3509 return c; 3510 } 3511 3512 // Used temporarily (until we have unique media IDs) to get an identifier 3513 // for the current sd card, so that the music app doesn't have to use the 3514 // non-public getFatVolumeId method 3515 if (table == FS_ID) { 3516 MatrixCursor c = new MatrixCursor(new String[] {"fsid"}); 3517 // current FAT volume ID 3518 int volumeId = -1; 3519 c.addRow(new Integer[] {volumeId}); 3520 return c; 3521 } 3522 3523 if (table == VERSION) { 3524 MatrixCursor c = new MatrixCursor(new String[] {"version"}); 3525 c.addRow(new Integer[] {DatabaseHelper.getDatabaseVersion(getContext())}); 3526 return c; 3527 } 3528 3529 // TODO(b/195008831): Add test to verify that apps can't access 3530 if (table == PICKER_INTERNAL_MEDIA_ALL) { 3531 return mPickerDataLayer.fetchAllMedia(queryArgs); 3532 } else if (table == PICKER_INTERNAL_MEDIA_LOCAL) { 3533 return mPickerDataLayer.fetchLocalMedia(queryArgs); 3534 } else if (table == PICKER_INTERNAL_ALBUMS_ALL) { 3535 return mPickerDataLayer.fetchAllAlbums(queryArgs); 3536 } else if (table == PICKER_INTERNAL_ALBUMS_LOCAL) { 3537 return mPickerDataLayer.fetchLocalAlbums(queryArgs); 3538 } 3539 3540 final DatabaseHelper helper = getDatabaseForUri(uri); 3541 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, queryArgs, 3542 honoredArgs::add); 3543 // Allowing hidden column _user_id for this query to support Cloned Profile use case. 3544 if (table == FILES) { 3545 qb.allowColumn(FileColumns._USER_ID); 3546 } 3547 3548 if (targetSdkVersion < Build.VERSION_CODES.R) { 3549 // Some apps are abusing "ORDER BY" clauses to inject "LIMIT" 3550 // clauses; gracefully lift them out. 3551 DatabaseUtils.recoverAbusiveSortOrder(queryArgs); 3552 3553 // Some apps are abusing the Uri query parameters to inject LIMIT 3554 // clauses; gracefully lift them out. 3555 DatabaseUtils.recoverAbusiveLimit(uri, queryArgs); 3556 } 3557 3558 if (targetSdkVersion < Build.VERSION_CODES.Q) { 3559 // Some apps are abusing the "WHERE" clause by injecting "GROUP BY" 3560 // clauses; gracefully lift them out. 3561 DatabaseUtils.recoverAbusiveSelection(queryArgs); 3562 3563 // Some apps are abusing the first column to inject "DISTINCT"; 3564 // gracefully lift them out. 3565 if ((projection != null) && (projection.length > 0) 3566 && projection[0].startsWith("DISTINCT ")) { 3567 projection[0] = projection[0].substring("DISTINCT ".length()); 3568 qb.setDistinct(true); 3569 } 3570 3571 // Some apps are generating thumbnails with getThumbnail(), but then 3572 // ignoring the returned Bitmap and querying the raw table; give 3573 // them a row with enough information to find the original image. 3574 final String selection = queryArgs.getString(QUERY_ARG_SQL_SELECTION); 3575 if ((table == IMAGES_THUMBNAILS || table == VIDEO_THUMBNAILS) 3576 && !TextUtils.isEmpty(selection)) { 3577 final Matcher matcher = PATTERN_SELECTION_ID.matcher(selection); 3578 if (matcher.matches()) { 3579 final long id = Long.parseLong(matcher.group(1)); 3580 3581 final Uri fullUri; 3582 if (table == IMAGES_THUMBNAILS) { 3583 fullUri = ContentUris.withAppendedId( 3584 Images.Media.getContentUri(volumeName), id); 3585 } else if (table == VIDEO_THUMBNAILS) { 3586 fullUri = ContentUris.withAppendedId( 3587 Video.Media.getContentUri(volumeName), id); 3588 } else { 3589 throw new IllegalArgumentException(); 3590 } 3591 3592 final MatrixCursor cursor = new MatrixCursor(projection); 3593 final File file = ContentResolver.encodeToFile( 3594 fullUri.buildUpon().appendPath("thumbnail").build()); 3595 final String data = file.getAbsolutePath(); 3596 cursor.newRow().add(MediaColumns._ID, null) 3597 .add(Images.Thumbnails.IMAGE_ID, id) 3598 .add(Video.Thumbnails.VIDEO_ID, id) 3599 .add(MediaColumns.DATA, data); 3600 return cursor; 3601 } 3602 } 3603 } 3604 3605 // Update locale if necessary. 3606 if (helper.isInternal() && !Locale.getDefault().equals(mLastLocale)) { 3607 Log.i(TAG, "Updating locale within queryInternal"); 3608 onLocaleChanged(false); 3609 } 3610 3611 Cursor c = qb.query(helper, projection, queryArgs, signal); 3612 3613 // Starting U, we are filtering owner package names for apps without visibility 3614 // on other packages. Apps with QUERY_ALL_PACKAGES permission are not affected. 3615 if (shouldFilterOwnerPackageNameFlag() && isApplicableForOwnerPackageNameFiltering(c)) { 3616 final long startTime = SystemClock.elapsedRealtime(); 3617 final String[] resultOwnerPackageNames = getOwnerPackageNames(c); 3618 if (resultOwnerPackageNames.length != 0) { 3619 final Set<String> queryablePackages = getQueryablePackages(resultOwnerPackageNames); 3620 if (resultOwnerPackageNames.length != queryablePackages.size()) { 3621 c = filterOwnerPackageNames(c, queryablePackages); 3622 } 3623 } 3624 final long durationMillis = SystemClock.elapsedRealtime() - startTime; 3625 Log.d(TAG, "Filtering owner package names took " + durationMillis + " ms"); 3626 } 3627 3628 if (c != null && !forSelf) { 3629 // As a performance optimization, only configure notifications when 3630 // resulting cursor will leave our process 3631 final boolean callerIsRemote = mCallingIdentity.get().pid != android.os.Process.myPid(); 3632 if (callerIsRemote && !isFuseThread()) { 3633 c.setNotificationUri(getContext().getContentResolver(), uri); 3634 } 3635 3636 final Bundle extras = new Bundle(); 3637 extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, 3638 honoredArgs.toArray(new String[honoredArgs.size()])); 3639 c.setExtras(extras); 3640 } 3641 3642 // Query was on a redacted URI, update the sensitive information such as the _ID, DATA etc. 3643 if (redactedUri != null && c != null) { 3644 try { 3645 return getRedactedUriCursor(redactedUri, c); 3646 } finally { 3647 c.close(); 3648 } 3649 } 3650 3651 return c; 3652 } 3653 3654 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) getQueryablePackages(String[] packageNames)3655 private Set<String> getQueryablePackages(String[] packageNames) { 3656 final boolean[] canPackageBeQueried; 3657 try { 3658 canPackageBeQueried = mPackageManager.canPackageQuery( 3659 mCallingIdentity.get().getPackageName(), packageNames); 3660 } catch (NameNotFoundException e) { 3661 Log.e(TAG, "Invalid package name", e); 3662 // If package manager throws an error, only assume calling package as queryable package 3663 return new HashSet<>(Arrays.asList(mCallingIdentity.get().getPackageName())); 3664 } 3665 3666 final Set<String> queryablePackages = new HashSet<>(); 3667 for (int i = 0; i < packageNames.length; i++) { 3668 if (canPackageBeQueried[i]) { 3669 queryablePackages.add(packageNames[i]); 3670 } 3671 } 3672 return queryablePackages; 3673 } 3674 getOwnerPackageNames(Cursor c)3675 private String[] getOwnerPackageNames(Cursor c) { 3676 final Set<String> ownerPackageNames = new HashSet<>(); 3677 final int ownerPackageNameColIndex = c.getColumnIndex(MediaColumns.OWNER_PACKAGE_NAME); 3678 3679 while (c.moveToNext()) { 3680 final String ownerPackageName = c.getString(ownerPackageNameColIndex); 3681 if (!Strings.isNullOrEmpty(ownerPackageName)) { 3682 ownerPackageNames.add(ownerPackageName); 3683 } 3684 } 3685 c.moveToPosition(-1); 3686 3687 return ownerPackageNames.toArray(new String[0]); 3688 } 3689 filterOwnerPackageNames(@onNull Cursor c, @NonNull Set<String> queryablePackages)3690 private Cursor filterOwnerPackageNames(@NonNull Cursor c, 3691 @NonNull Set<String> queryablePackages) { 3692 final MatrixCursor filteredCursor = new MatrixCursor(c.getColumnNames(), c.getCount()); 3693 3694 while (c.moveToNext()) { 3695 final MatrixCursor.RowBuilder row = filteredCursor.newRow(); 3696 for (int colIndex = 0; colIndex < c.getColumnCount(); colIndex++) { 3697 if (OWNER_PACKAGE_NAME.equals(c.getColumnName(colIndex)) 3698 && !queryablePackages.contains(c.getString(colIndex))) { 3699 row.add(null); 3700 } else if (c.getType(colIndex) == FIELD_TYPE_BLOB) { 3701 row.add(c.getBlob(colIndex)); 3702 } else { 3703 row.add(c.getString(colIndex)); 3704 } 3705 } 3706 } 3707 return filteredCursor; 3708 } 3709 isApplicableForOwnerPackageNameFiltering(Cursor c)3710 private boolean isApplicableForOwnerPackageNameFiltering(Cursor c) { 3711 return SdkLevel.isAtLeastU() && c != null 3712 && Arrays.asList(c.getColumnNames()).contains(MediaColumns.OWNER_PACKAGE_NAME) 3713 && getContext().checkPermission(QUERY_ALL_PACKAGES, 3714 mCallingIdentity.get().pid, mCallingIdentity.get().uid) == PERMISSION_DENIED; 3715 } 3716 shouldFilterOwnerPackageNameFlag()3717 private boolean shouldFilterOwnerPackageNameFlag() { 3718 return true; 3719 } 3720 isUriSupportedForRedaction(Uri uri)3721 private boolean isUriSupportedForRedaction(Uri uri) { 3722 final int match = matchUri(uri, true); 3723 return REDACTED_URI_SUPPORTED_TYPES.contains(match); 3724 } 3725 getRedactedUriCursor(Uri redactedUri, @NonNull Cursor c)3726 private Cursor getRedactedUriCursor(Uri redactedUri, @NonNull Cursor c) { 3727 final HashSet<String> columnNames = new HashSet<>(Arrays.asList(c.getColumnNames())); 3728 final MatrixCursor redactedUriCursor = new MatrixCursor(c.getColumnNames()); 3729 final String redactedUriId = redactedUri.getLastPathSegment(); 3730 3731 if (!c.moveToFirst()) { 3732 return redactedUriCursor; 3733 } 3734 3735 // NOTE: It is safe to assume that there will only be one entry corresponding to a 3736 // redacted URI as it corresponds to a unique DB entry. 3737 if (c.getCount() != 1) { 3738 throw new AssertionError("Two rows corresponding to " + redactedUri.toString() 3739 + " found, when only one expected"); 3740 } 3741 3742 final MatrixCursor.RowBuilder row = redactedUriCursor.newRow(); 3743 for (String columnName : c.getColumnNames()) { 3744 final int colIndex = c.getColumnIndex(columnName); 3745 if (c.getType(colIndex) == FIELD_TYPE_BLOB) { 3746 row.add(c.getBlob(colIndex)); 3747 } else { 3748 row.add(c.getString(colIndex)); 3749 } 3750 } 3751 3752 String ext = getFileExtensionFromCursor(c, columnNames); 3753 ext = ext == null ? "" : "." + ext; 3754 final String displayName = redactedUriId + ext; 3755 final String data = buildPrimaryVolumeFile(uidToUserId(Binder.getCallingUid()), 3756 getRedactedRelativePath(), displayName).getAbsolutePath(); 3757 3758 updateRow(columnNames, MediaColumns._ID, row, redactedUriId); 3759 updateRow(columnNames, MediaColumns.DISPLAY_NAME, row, displayName); 3760 updateRow(columnNames, MediaColumns.RELATIVE_PATH, row, getRedactedRelativePath()); 3761 updateRow(columnNames, MediaColumns.BUCKET_DISPLAY_NAME, row, getRedactedRelativePath()); 3762 updateRow(columnNames, MediaColumns.DATA, row, data); 3763 updateRow(columnNames, MediaColumns.DOCUMENT_ID, row, null); 3764 updateRow(columnNames, MediaColumns.INSTANCE_ID, row, null); 3765 updateRow(columnNames, MediaColumns.BUCKET_ID, row, null); 3766 3767 return redactedUriCursor; 3768 } 3769 3770 @Nullable getFileExtensionFromCursor(@onNull Cursor c, @NonNull HashSet<String> columnNames)3771 private static String getFileExtensionFromCursor(@NonNull Cursor c, 3772 @NonNull HashSet<String> columnNames) { 3773 if (columnNames.contains(MediaColumns.DATA)) { 3774 return extractFileExtension(c.getString(c.getColumnIndex(MediaColumns.DATA))); 3775 } 3776 if (columnNames.contains(MediaColumns.DISPLAY_NAME)) { 3777 return extractFileExtension(c.getString(c.getColumnIndex(MediaColumns.DISPLAY_NAME))); 3778 } 3779 return null; 3780 } 3781 updateRow(HashSet<String> columnNames, String columnName, MatrixCursor.RowBuilder row, Object val)3782 private void updateRow(HashSet<String> columnNames, String columnName, 3783 MatrixCursor.RowBuilder row, Object val) { 3784 if (columnNames.contains(columnName)) { 3785 row.add(columnName, val); 3786 } 3787 } 3788 getUriForRedactedUri(Uri redactedUri)3789 private Uri getUriForRedactedUri(Uri redactedUri) { 3790 final Uri.Builder builder = redactedUri.buildUpon(); 3791 builder.path(null); 3792 final List<String> segments = redactedUri.getPathSegments(); 3793 for (int i = 0; i < segments.size() - 1; i++) { 3794 builder.appendPath(segments.get(i)); 3795 } 3796 3797 DatabaseHelper helper; 3798 try { 3799 helper = getDatabaseForUri(redactedUri); 3800 } catch (VolumeNotFoundException e) { 3801 throw e.rethrowAsIllegalArgumentException(); 3802 } 3803 3804 try (final Cursor c = helper.runWithoutTransaction( 3805 (db) -> db.query("files", new String[]{MediaColumns._ID}, 3806 FileColumns.REDACTED_URI_ID + "=?", 3807 new String[]{redactedUri.getLastPathSegment()}, null, null, null))) { 3808 if (!c.moveToFirst()) { 3809 throw new IllegalArgumentException( 3810 "Uri: " + redactedUri.toString() + " not found."); 3811 } 3812 3813 builder.appendPath(c.getString(0)); 3814 return builder.build(); 3815 } 3816 } 3817 isRedactedUri(Uri uri)3818 private boolean isRedactedUri(Uri uri) { 3819 String id = uri.getLastPathSegment(); 3820 return id != null && id.startsWith(REDACTED_URI_ID_PREFIX) 3821 && id.length() == REDACTED_URI_ID_SIZE; 3822 } 3823 3824 @Override getType(Uri url)3825 public String getType(Uri url) { 3826 final int match = matchUri(url, true); 3827 switch (match) { 3828 case IMAGES_MEDIA_ID: 3829 case AUDIO_MEDIA_ID: 3830 case AUDIO_PLAYLISTS_ID: 3831 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 3832 case VIDEO_MEDIA_ID: 3833 case DOWNLOADS_ID: 3834 case FILES_ID: 3835 if (SdkLevel.isAtLeastU()) { 3836 // Starting Android 14, there is permission check for 3837 // getting types requiring internal query. 3838 return queryForTypeAsCaller(url); 3839 } else { 3840 return queryForTypeAsSelf(url); 3841 } 3842 3843 case IMAGES_MEDIA: 3844 case IMAGES_THUMBNAILS: 3845 return Images.Media.CONTENT_TYPE; 3846 3847 case AUDIO_ALBUMART_ID: 3848 case AUDIO_ALBUMART_FILE_ID: 3849 case IMAGES_THUMBNAILS_ID: 3850 case VIDEO_THUMBNAILS_ID: 3851 return "image/jpeg"; 3852 3853 case AUDIO_MEDIA: 3854 case AUDIO_GENRES_ID_MEMBERS: 3855 case AUDIO_PLAYLISTS_ID_MEMBERS: 3856 return Audio.Media.CONTENT_TYPE; 3857 3858 case AUDIO_GENRES: 3859 case AUDIO_MEDIA_ID_GENRES: 3860 return Audio.Genres.CONTENT_TYPE; 3861 case AUDIO_GENRES_ID: 3862 case AUDIO_MEDIA_ID_GENRES_ID: 3863 return Audio.Genres.ENTRY_CONTENT_TYPE; 3864 case AUDIO_PLAYLISTS: 3865 return Audio.Playlists.CONTENT_TYPE; 3866 3867 case VIDEO_MEDIA: 3868 return Video.Media.CONTENT_TYPE; 3869 case DOWNLOADS: 3870 return Downloads.CONTENT_TYPE; 3871 3872 case PICKER_ID: 3873 return mPickerUriResolver.getType(url, Binder.getCallingPid(), 3874 Binder.getCallingUid()); 3875 } 3876 throw new IllegalStateException("Unknown URL : " + url); 3877 } 3878 queryForTypeAsSelf(Uri url)3879 private String queryForTypeAsSelf(Uri url) { 3880 final LocalCallingIdentity token = clearLocalCallingIdentity(); 3881 try { 3882 return queryForTypeAsCaller(url); 3883 } finally { 3884 restoreLocalCallingIdentity(token); 3885 } 3886 } 3887 queryForTypeAsCaller(Uri url)3888 private String queryForTypeAsCaller(Uri url) { 3889 try (Cursor cursor = queryForSingleItem(url, 3890 new String[] { MediaColumns.MIME_TYPE }, null, null, null)) { 3891 return cursor.getString(0); 3892 } catch (FileNotFoundException e) { 3893 throw new IllegalArgumentException(e.getMessage()); 3894 } 3895 } 3896 3897 @VisibleForTesting ensureFileColumns(@onNull Uri uri, @NonNull ContentValues values)3898 void ensureFileColumns(@NonNull Uri uri, @NonNull ContentValues values) 3899 throws VolumeArgumentException, VolumeNotFoundException { 3900 final LocalUriMatcher matcher = new LocalUriMatcher(MediaStore.AUTHORITY); 3901 final int match = matcher.matchUri(uri, true); 3902 ensureNonUniqueFileColumns(match, uri, Bundle.EMPTY, values, null /* currentPath */); 3903 } 3904 ensureUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath)3905 private void ensureUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, 3906 @NonNull ContentValues values, @Nullable String currentPath) 3907 throws VolumeArgumentException, VolumeNotFoundException { 3908 ensureFileColumns(match, uri, extras, values, true, currentPath); 3909 } 3910 ensureNonUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath)3911 private void ensureNonUniqueFileColumns(int match, @NonNull Uri uri, 3912 @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath) 3913 throws VolumeArgumentException, VolumeNotFoundException { 3914 ensureFileColumns(match, uri, extras, values, false, currentPath); 3915 } 3916 3917 /** 3918 * Get the various file-related {@link MediaColumns} in the given 3919 * {@link ContentValues} into a consistent condition. Also validates that defined 3920 * columns are valid for the given {@link Uri}, such as ensuring that only 3921 * {@code image/*} can be inserted into 3922 * {@link android.provider.MediaStore.Images}. 3923 */ ensureFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath)3924 private void ensureFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, 3925 @NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath) 3926 throws VolumeArgumentException, VolumeNotFoundException { 3927 Trace.beginSection("MP.ensureFileColumns"); 3928 3929 Objects.requireNonNull(uri); 3930 Objects.requireNonNull(extras); 3931 Objects.requireNonNull(values); 3932 3933 // Figure out defaults based on Uri being modified 3934 String defaultMimeType = ClipDescription.MIMETYPE_UNKNOWN; 3935 int defaultMediaType = FileColumns.MEDIA_TYPE_NONE; 3936 String defaultPrimary = Environment.DIRECTORY_DOWNLOADS; 3937 String defaultSecondary = null; 3938 List<String> allowedPrimary = Arrays.asList( 3939 Environment.DIRECTORY_DOWNLOADS, 3940 Environment.DIRECTORY_DOCUMENTS); 3941 switch (match) { 3942 case AUDIO_MEDIA: 3943 case AUDIO_MEDIA_ID: 3944 defaultMimeType = "audio/mpeg"; 3945 defaultMediaType = FileColumns.MEDIA_TYPE_AUDIO; 3946 defaultPrimary = Environment.DIRECTORY_MUSIC; 3947 if (SdkLevel.isAtLeastS()) { 3948 allowedPrimary = Arrays.asList( 3949 Environment.DIRECTORY_ALARMS, 3950 Environment.DIRECTORY_AUDIOBOOKS, 3951 Environment.DIRECTORY_MUSIC, 3952 Environment.DIRECTORY_NOTIFICATIONS, 3953 Environment.DIRECTORY_PODCASTS, 3954 Environment.DIRECTORY_RECORDINGS, 3955 Environment.DIRECTORY_RINGTONES); 3956 } else { 3957 allowedPrimary = Arrays.asList( 3958 Environment.DIRECTORY_ALARMS, 3959 Environment.DIRECTORY_AUDIOBOOKS, 3960 Environment.DIRECTORY_MUSIC, 3961 Environment.DIRECTORY_NOTIFICATIONS, 3962 Environment.DIRECTORY_PODCASTS, 3963 FileUtils.DIRECTORY_RECORDINGS, 3964 Environment.DIRECTORY_RINGTONES); 3965 } 3966 break; 3967 case VIDEO_MEDIA: 3968 case VIDEO_MEDIA_ID: 3969 defaultMimeType = "video/mp4"; 3970 defaultMediaType = FileColumns.MEDIA_TYPE_VIDEO; 3971 defaultPrimary = Environment.DIRECTORY_MOVIES; 3972 allowedPrimary = Arrays.asList( 3973 Environment.DIRECTORY_DCIM, 3974 Environment.DIRECTORY_MOVIES, 3975 Environment.DIRECTORY_PICTURES); 3976 break; 3977 case IMAGES_MEDIA: 3978 case IMAGES_MEDIA_ID: 3979 defaultMimeType = "image/jpeg"; 3980 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE; 3981 defaultPrimary = Environment.DIRECTORY_PICTURES; 3982 allowedPrimary = Arrays.asList( 3983 Environment.DIRECTORY_DCIM, 3984 Environment.DIRECTORY_PICTURES); 3985 break; 3986 case AUDIO_ALBUMART: 3987 case AUDIO_ALBUMART_ID: 3988 defaultMimeType = "image/jpeg"; 3989 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE; 3990 defaultPrimary = Environment.DIRECTORY_MUSIC; 3991 allowedPrimary = Collections.singletonList(defaultPrimary); 3992 defaultSecondary = DIRECTORY_THUMBNAILS; 3993 break; 3994 case VIDEO_THUMBNAILS: 3995 case VIDEO_THUMBNAILS_ID: 3996 defaultMimeType = "image/jpeg"; 3997 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE; 3998 defaultPrimary = Environment.DIRECTORY_MOVIES; 3999 allowedPrimary = Collections.singletonList(defaultPrimary); 4000 defaultSecondary = DIRECTORY_THUMBNAILS; 4001 break; 4002 case IMAGES_THUMBNAILS: 4003 case IMAGES_THUMBNAILS_ID: 4004 defaultMimeType = "image/jpeg"; 4005 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE; 4006 defaultPrimary = Environment.DIRECTORY_PICTURES; 4007 allowedPrimary = Collections.singletonList(defaultPrimary); 4008 defaultSecondary = DIRECTORY_THUMBNAILS; 4009 break; 4010 case AUDIO_PLAYLISTS: 4011 case AUDIO_PLAYLISTS_ID: 4012 defaultMimeType = "audio/mpegurl"; 4013 defaultMediaType = FileColumns.MEDIA_TYPE_PLAYLIST; 4014 defaultPrimary = Environment.DIRECTORY_MUSIC; 4015 allowedPrimary = Arrays.asList( 4016 Environment.DIRECTORY_MUSIC, 4017 Environment.DIRECTORY_MOVIES); 4018 break; 4019 case DOWNLOADS: 4020 case DOWNLOADS_ID: 4021 defaultPrimary = Environment.DIRECTORY_DOWNLOADS; 4022 allowedPrimary = Collections.singletonList(defaultPrimary); 4023 break; 4024 case FILES: 4025 case FILES_ID: 4026 // Use defaults above 4027 break; 4028 default: 4029 Log.w(TAG, "Unhandled location " + uri + "; assuming generic files"); 4030 break; 4031 } 4032 4033 final String resolvedVolumeName = resolveVolumeName(uri); 4034 4035 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA)) 4036 && MediaStore.VOLUME_INTERNAL.equals(resolvedVolumeName)) { 4037 // TODO: promote this to top-level check 4038 throw new UnsupportedOperationException( 4039 "Writing to internal storage is not supported."); 4040 } 4041 4042 // Force values when raw path provided 4043 if (!TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) { 4044 FileUtils.computeValuesFromData(values, isFuseThread()); 4045 } 4046 4047 final boolean isTargetSdkROrHigher = 4048 getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R; 4049 final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME); 4050 final String mimeTypeFromExt = TextUtils.isEmpty(displayName) ? null : 4051 MimeUtils.resolveMimeType(new File(displayName)); 4052 4053 if (TextUtils.isEmpty(values.getAsString(MediaColumns.MIME_TYPE))) { 4054 if (isTargetSdkROrHigher) { 4055 // Extract the MIME type from the display name if we couldn't resolve it from the 4056 // raw path 4057 if (mimeTypeFromExt != null) { 4058 values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt); 4059 } else { 4060 // We couldn't resolve mimeType, it means that both display name and MIME type 4061 // were missing in values, so we use defaultMimeType. 4062 values.put(MediaColumns.MIME_TYPE, defaultMimeType); 4063 } 4064 } else if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) { 4065 values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt); 4066 } else { 4067 // We don't use mimeTypeFromExt to preserve legacy behavior. 4068 values.put(MediaColumns.MIME_TYPE, defaultMimeType); 4069 } 4070 } 4071 4072 String mimeType = values.getAsString(MediaColumns.MIME_TYPE); 4073 if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) { 4074 // We allow any mimeType for generic uri with default media type as MEDIA_TYPE_NONE. 4075 } else if (mimeType != null && 4076 MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) == null) { 4077 if (mimeTypeFromExt != null && 4078 defaultMediaType == MimeUtils.resolveMediaType(mimeTypeFromExt)) { 4079 // If mimeType from extension matches the defaultMediaType of uri, we use mimeType 4080 // from file extension as mimeType. This is an effort to guess the mimeType when we 4081 // get unsupported mimeType. 4082 // Note: We can't force defaultMimeType because when we force defaultMimeType, we 4083 // will force the file extension as well. For example, if DISPLAY_NAME=Foo.png and 4084 // mimeType="image/*". If we force mimeType to be "image/jpeg", we append the file 4085 // name with the new file extension i.e., "Foo.png.jpg" where as the expected file 4086 // name was "Foo.png" 4087 values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt); 4088 } else if (isTargetSdkROrHigher) { 4089 // We are here because given mimeType is unsupported also we couldn't guess valid 4090 // mimeType from file extension. 4091 throw new IllegalArgumentException("Unsupported MIME type " + mimeType); 4092 } else { 4093 // We can't throw error for legacy apps, so we try to use defaultMimeType. 4094 values.put(MediaColumns.MIME_TYPE, defaultMimeType); 4095 } 4096 } 4097 4098 // Give ourselves reasonable defaults when missing 4099 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DISPLAY_NAME))) { 4100 values.put(MediaColumns.DISPLAY_NAME, 4101 String.valueOf(System.currentTimeMillis())); 4102 } 4103 final Integer formatObject = values.getAsInteger(FileColumns.FORMAT); 4104 final int format = formatObject == null ? 0 : formatObject; 4105 if (format == MtpConstants.FORMAT_ASSOCIATION) { 4106 values.putNull(MediaColumns.MIME_TYPE); 4107 } 4108 4109 mimeType = values.getAsString(MediaColumns.MIME_TYPE); 4110 // Quick check MIME type against table 4111 if (mimeType != null) { 4112 PulledMetrics.logMimeTypeAccess(getCallingUidOrSelf(), mimeType); 4113 final int actualMediaType = MimeUtils.resolveMediaType(mimeType); 4114 if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) { 4115 // Give callers an opportunity to work with playlists and 4116 // subtitles using the generic files table 4117 switch (actualMediaType) { 4118 case FileColumns.MEDIA_TYPE_PLAYLIST: 4119 defaultMimeType = "audio/mpegurl"; 4120 defaultMediaType = FileColumns.MEDIA_TYPE_PLAYLIST; 4121 defaultPrimary = Environment.DIRECTORY_MUSIC; 4122 allowedPrimary = new ArrayList<>(allowedPrimary); 4123 allowedPrimary.add(Environment.DIRECTORY_MUSIC); 4124 allowedPrimary.add(Environment.DIRECTORY_MOVIES); 4125 break; 4126 case FileColumns.MEDIA_TYPE_SUBTITLE: 4127 defaultMimeType = "application/x-subrip"; 4128 defaultMediaType = FileColumns.MEDIA_TYPE_SUBTITLE; 4129 defaultPrimary = Environment.DIRECTORY_MOVIES; 4130 allowedPrimary = new ArrayList<>(allowedPrimary); 4131 allowedPrimary.add(Environment.DIRECTORY_MUSIC); 4132 allowedPrimary.add(Environment.DIRECTORY_MOVIES); 4133 break; 4134 } 4135 } else if (defaultMediaType != actualMediaType) { 4136 final String[] split = defaultMimeType.split("/"); 4137 throw new IllegalArgumentException( 4138 "MIME type " + mimeType + " cannot be inserted into " + uri 4139 + "; expected MIME type under " + split[0] + "/*"); 4140 } 4141 } 4142 4143 // Use default directories when missing 4144 if (TextUtils.isEmpty(values.getAsString(MediaColumns.RELATIVE_PATH))) { 4145 if (defaultSecondary != null) { 4146 values.put(MediaColumns.RELATIVE_PATH, 4147 defaultPrimary + '/' + defaultSecondary + '/'); 4148 } else { 4149 values.put(MediaColumns.RELATIVE_PATH, 4150 defaultPrimary + '/'); 4151 } 4152 } 4153 4154 // Generate path when undefined 4155 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) { 4156 // Note that just the volume name isn't enough to determine the path, 4157 // since we can manage different volumes with the same name for 4158 // different users. Instead, if we have a current path (which implies 4159 // an already existing file to be renamed), use that to derive the 4160 // user-id of the file, and in turn use that to derive the correct 4161 // volume. Cross-user renames are not supported without a specified 4162 // DATA column. 4163 File volumePath; 4164 UserHandle userHandle = mCallingIdentity.get().getUser(); 4165 if (currentPath != null) { 4166 int userId = FileUtils.extractUserId(currentPath); 4167 if (userId != -1) { 4168 userHandle = UserHandle.of(userId); 4169 } 4170 } 4171 try { 4172 volumePath = mVolumeCache.getVolumePath(resolvedVolumeName, userHandle); 4173 } catch (FileNotFoundException e) { 4174 throw new IllegalArgumentException(e); 4175 } 4176 4177 FileUtils.sanitizeValues(values, /*rewriteHiddenFileName*/ !isFuseThread()); 4178 FileUtils.computeDataFromValues(values, volumePath, isFuseThread()); 4179 4180 // Create result file 4181 File res = new File(values.getAsString(MediaColumns.DATA)); 4182 try { 4183 if (makeUnique) { 4184 res = FileUtils.buildUniqueFile(res.getParentFile(), 4185 mimeType, res.getName()); 4186 } else { 4187 res = FileUtils.buildNonUniqueFile(res.getParentFile(), 4188 mimeType, res.getName()); 4189 } 4190 } catch (FileNotFoundException e) { 4191 throw new IllegalStateException( 4192 "Failed to build unique file: " + res + " " + values); 4193 } 4194 4195 // Require that content lives under well-defined directories to help 4196 // keep the user's content organized 4197 4198 // Start by saying unchanged directories are valid 4199 final String currentDir = (currentPath != null) 4200 ? new File(currentPath).getParent() : null; 4201 boolean validPath = res.getParent().equals(currentDir); 4202 4203 // Next, consider allowing based on allowed primary directory 4204 final String[] relativePath = values.getAsString(MediaColumns.RELATIVE_PATH).split("/"); 4205 final String primary = extractTopLevelDir(relativePath); 4206 if (!validPath) { 4207 validPath = containsIgnoreCase(allowedPrimary, primary); 4208 } 4209 4210 // Next, consider allowing paths when referencing a related item 4211 final Uri relatedUri = extras.getParcelable(QUERY_ARG_RELATED_URI); 4212 if (!validPath && relatedUri != null) { 4213 try (Cursor c = queryForSingleItem(relatedUri, new String[] { 4214 MediaColumns.MIME_TYPE, 4215 MediaColumns.RELATIVE_PATH, 4216 }, null, null, null)) { 4217 // If top-level MIME type matches, and relative path 4218 // matches, then allow caller to place things here 4219 4220 final String expectedType = MimeUtils.extractPrimaryType( 4221 c.getString(0)); 4222 final String actualType = MimeUtils.extractPrimaryType( 4223 values.getAsString(MediaColumns.MIME_TYPE)); 4224 if (!Objects.equals(expectedType, actualType)) { 4225 throw new IllegalArgumentException("Placement of " + actualType 4226 + " item not allowed in relation to " + expectedType + " item"); 4227 } 4228 4229 final String expectedPath = c.getString(1); 4230 final String actualPath = values.getAsString(MediaColumns.RELATIVE_PATH); 4231 if (!Objects.equals(expectedPath, actualPath)) { 4232 throw new IllegalArgumentException("Placement of " + actualPath 4233 + " item not allowed in relation to " + expectedPath + " item"); 4234 } 4235 4236 // If we didn't see any trouble above, then we'll allow it 4237 validPath = true; 4238 } catch (FileNotFoundException e) { 4239 Log.w(TAG, "Failed to find related item " + relatedUri + ": " + e); 4240 } 4241 } 4242 4243 // Consider allowing external media directory of calling package 4244 if (!validPath) { 4245 final String pathOwnerPackage = extractPathOwnerPackageName(res.getAbsolutePath()); 4246 if (pathOwnerPackage != null) { 4247 validPath = isExternalMediaDirectory(res.getAbsolutePath()) && 4248 isCallingIdentitySharedPackageName(pathOwnerPackage); 4249 } 4250 } 4251 4252 // Allow apps with MANAGE_EXTERNAL_STORAGE to create files anywhere 4253 if (!validPath) { 4254 validPath = isCallingPackageManager(); 4255 } 4256 4257 // Allow system gallery to create image/video files. 4258 if (!validPath) { 4259 // System gallery can create image/video files in any existing directory, it can 4260 // also create subdirectories in any existing top-level directory. However, system 4261 // gallery is not allowed to create non-default top level directory. 4262 final boolean createNonDefaultTopLevelDir = primary != null && 4263 !FileUtils.buildPath(volumePath, primary).exists(); 4264 validPath = !createNonDefaultTopLevelDir && canSystemGalleryAccessTheFile( 4265 res.getAbsolutePath()); 4266 } 4267 4268 // Nothing left to check; caller can't use this path 4269 if (!validPath) { 4270 throw new IllegalArgumentException( 4271 "Primary directory " + primary + " not allowed for " + uri 4272 + "; allowed directories are " + allowedPrimary); 4273 } 4274 4275 boolean isFuseThread = isFuseThread(); 4276 // Check if the following are true: 4277 // 1. Not a FUSE thread 4278 // 2. |res| is a child of a default dir and the default dir is missing 4279 // If true, we want to update the mTime of the volume root, after creating the dir 4280 // on the lower filesystem. This fixes some FileManagers relying on the mTime change 4281 // for UI updates 4282 File defaultDirVolumePath = 4283 isFuseThread ? null : checkDefaultDirMissing(resolvedVolumeName, res); 4284 // Ensure all parent folders of result file exist 4285 res.getParentFile().mkdirs(); 4286 if (!res.getParentFile().exists()) { 4287 throw new IllegalStateException("Failed to create directory: " + res); 4288 } 4289 touchFusePath(defaultDirVolumePath); 4290 4291 values.put(MediaColumns.DATA, res.getAbsolutePath()); 4292 // buildFile may have changed the file name, compute values to extract new DISPLAY_NAME. 4293 // Note: We can't extract displayName from res.getPath() because for pending & trashed 4294 // files DISPLAY_NAME will not be same as file name. 4295 FileUtils.computeValuesFromData(values, isFuseThread); 4296 } else { 4297 assertFileColumnsConsistent(match, uri, values); 4298 } 4299 4300 assertPrivatePathNotInValues(values); 4301 4302 // Drop columns that aren't relevant for special tables 4303 switch (match) { 4304 case AUDIO_ALBUMART: 4305 case VIDEO_THUMBNAILS: 4306 case IMAGES_THUMBNAILS: 4307 final Set<String> valid = getProjectionMap(MediaStore.Images.Thumbnails.class) 4308 .keySet(); 4309 for (String key : new ArraySet<>(values.keySet())) { 4310 if (!valid.contains(key)) { 4311 values.remove(key); 4312 } 4313 } 4314 break; 4315 } 4316 4317 Trace.endSection(); 4318 } 4319 4320 /** 4321 * For apps targetSdk >= S: Check that values does not contain any external private path. 4322 * For all apps: Check that values does not contain any other app's external private paths. 4323 */ assertPrivatePathNotInValues(ContentValues values)4324 private void assertPrivatePathNotInValues(ContentValues values) 4325 throws IllegalArgumentException { 4326 ArrayList<String> relativePaths = new ArrayList<String>(); 4327 relativePaths.add(extractRelativePath(values.getAsString(MediaColumns.DATA))); 4328 relativePaths.add(values.getAsString(MediaColumns.RELATIVE_PATH)); 4329 4330 for (final String relativePath : relativePaths) { 4331 if (!isDataOrObbRelativePath(relativePath)) { 4332 continue; 4333 } 4334 4335 /** 4336 * Don't allow apps to insert/update database row to files in Android/data or 4337 * Android/obb dirs. These are app private directories and files in these private 4338 * directories can't be added to public media collection. 4339 * 4340 * Note: For backwards compatibility we allow apps with targetSdk < S to insert private 4341 * files to MediaProvider 4342 */ 4343 if (CompatChanges.isChangeEnabled(ENABLE_CHECKS_FOR_PRIVATE_FILES, 4344 Binder.getCallingUid())) { 4345 throw new IllegalArgumentException( 4346 "Inserting private file: " + relativePath + " is not allowed."); 4347 } 4348 4349 /** 4350 * Restrict all (legacy and non-legacy) apps from inserting paths in other 4351 * app's private directories. 4352 * Allow legacy apps to insert/update files in app private directories for backward 4353 * compatibility but don't allow them to do so in other app's private directories. 4354 */ 4355 if (!isCallingIdentityAllowedAccessToDataOrObbPath(relativePath)) { 4356 throw new IllegalArgumentException( 4357 "Inserting private file: " + relativePath + " is not allowed."); 4358 } 4359 } 4360 } 4361 4362 /** 4363 * @return the default dir if {@code file} is a child of default dir and it's missing, 4364 * {@code null} otherwise. 4365 */ checkDefaultDirMissing(String volumeName, File file)4366 private File checkDefaultDirMissing(String volumeName, File file) { 4367 String topLevelDir = FileUtils.extractTopLevelDir(file.getPath()); 4368 if (topLevelDir != null && FileUtils.isDefaultDirectoryName(topLevelDir)) { 4369 try { 4370 File volumePath = getVolumePath(volumeName); 4371 if (!new File(volumePath, topLevelDir).exists()) { 4372 return volumePath; 4373 } 4374 } catch (FileNotFoundException e) { 4375 Log.w(TAG, "Failed to checkDefaultDirMissing for " + file, e); 4376 } 4377 } 4378 return null; 4379 } 4380 4381 /** Updates mTime of {@code path} on the FUSE filesystem */ touchFusePath(@ullable File path)4382 private void touchFusePath(@Nullable File path) { 4383 if (path != null) { 4384 // Touch root of volume to update mTime on FUSE filesystem 4385 // This allows FileManagers that may be relying on mTime changes to update their UI 4386 File fusePath = toFuseFile(path); 4387 Log.i(TAG, "Touching FUSE path " + fusePath); 4388 fusePath.setLastModified(System.currentTimeMillis()); 4389 } 4390 } 4391 4392 /** 4393 * Check that any requested {@link MediaColumns#DATA} paths actually 4394 * live on the storage volume being targeted. 4395 */ assertFileColumnsConsistent(int match, Uri uri, ContentValues values)4396 private void assertFileColumnsConsistent(int match, Uri uri, ContentValues values) 4397 throws VolumeArgumentException, VolumeNotFoundException { 4398 if (!values.containsKey(MediaColumns.DATA)) return; 4399 4400 final String volumeName = resolveVolumeName(uri); 4401 try { 4402 // Quick check that the requested path actually lives on volume 4403 final Collection<File> allowed = getAllowedVolumePaths(volumeName); 4404 final File actual = new File(values.getAsString(MediaColumns.DATA)) 4405 .getCanonicalFile(); 4406 if (!FileUtils.contains(allowed, actual)) { 4407 throw new VolumeArgumentException(actual, allowed); 4408 } 4409 } catch (IOException e) { 4410 throw new VolumeNotFoundException(volumeName); 4411 } 4412 } 4413 4414 @Override bulkInsert(Uri uri, ContentValues[] values)4415 public int bulkInsert(Uri uri, ContentValues[] values) { 4416 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 4417 final boolean allowHidden = isCallingPackageAllowedHidden(); 4418 final int match = matchUri(uri, allowHidden); 4419 4420 if (match == VOLUMES) { 4421 return super.bulkInsert(uri, values); 4422 } 4423 4424 if (match == AUDIO_PLAYLISTS_ID || match == AUDIO_PLAYLISTS_ID_MEMBERS) { 4425 final String resolvedVolumeName = resolveVolumeName(uri); 4426 4427 final long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 4428 final Uri playlistUri = ContentUris.withAppendedId( 4429 MediaStore.Audio.Playlists.getContentUri(resolvedVolumeName), playlistId); 4430 4431 final String audioVolumeName = 4432 MediaStore.VOLUME_INTERNAL.equals(resolvedVolumeName) 4433 ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL; 4434 4435 // Require that caller has write access to underlying media 4436 enforceCallingPermission(playlistUri, Bundle.EMPTY, true); 4437 for (ContentValues each : values) { 4438 final long audioId = each.getAsLong(Audio.Playlists.Members.AUDIO_ID); 4439 final Uri audioUri = Audio.Media.getContentUri(audioVolumeName, audioId); 4440 enforceCallingPermission(audioUri, Bundle.EMPTY, false); 4441 } 4442 4443 return bulkInsertPlaylist(playlistUri, values); 4444 } 4445 4446 final DatabaseHelper helper; 4447 try { 4448 helper = getDatabaseForUri(uri); 4449 } catch (VolumeNotFoundException e) { 4450 return e.translateForUpdateDelete(targetSdkVersion); 4451 } 4452 4453 helper.beginTransaction(); 4454 try { 4455 final int result = super.bulkInsert(uri, values); 4456 helper.setTransactionSuccessful(); 4457 return result; 4458 } finally { 4459 helper.endTransaction(); 4460 } 4461 } 4462 bulkInsertPlaylist(@onNull Uri uri, @NonNull ContentValues[] values)4463 private int bulkInsertPlaylist(@NonNull Uri uri, @NonNull ContentValues[] values) { 4464 Trace.beginSection("MP.bulkInsertPlaylist"); 4465 try { 4466 try { 4467 return addPlaylistMembers(uri, values); 4468 } catch (SQLiteConstraintException e) { 4469 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) { 4470 throw e; 4471 } else { 4472 return 0; 4473 } 4474 } 4475 } catch (FallbackException e) { 4476 return e.translateForBulkInsert(getCallingPackageTargetSdkVersion()); 4477 } finally { 4478 Trace.endSection(); 4479 } 4480 } 4481 insertDirectory(@onNull SQLiteDatabase db, @NonNull String path)4482 private long insertDirectory(@NonNull SQLiteDatabase db, @NonNull String path) { 4483 if (LOGV) Log.v(TAG, "inserting directory " + path); 4484 ContentValues values = new ContentValues(); 4485 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION); 4486 values.put(FileColumns.DATA, path); 4487 values.put(FileColumns.PARENT, getParent(db, path)); 4488 values.put(FileColumns.OWNER_PACKAGE_NAME, extractPathOwnerPackageName(path)); 4489 values.put(FileColumns.VOLUME_NAME, extractVolumeName(path)); 4490 values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path)); 4491 values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path)); 4492 values.put(FileColumns.IS_DOWNLOAD, isDownload(path) ? 1 : 0); 4493 4494 // Getting UserId from the directory path, as clone user shares the MediaProvider 4495 // of user 0. 4496 int userIdFromPath = FileUtils.extractUserId(path); 4497 // In some cases, like querying public volumes, userId is not available in path. We 4498 // take userId from the user running MediaProvider process (sUserId). 4499 if (userIdFromPath != -1) { 4500 if (isAppCloneUserForFuse(userIdFromPath)) { 4501 values.put(FileColumns._USER_ID, userIdFromPath); 4502 } else { 4503 values.put(FileColumns._USER_ID, sUserId); 4504 } 4505 } 4506 4507 File file = new File(path); 4508 if (file.exists()) { 4509 values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000); 4510 } 4511 return db.insert("files", FileColumns.DATE_MODIFIED, values); 4512 } 4513 getParent(@onNull SQLiteDatabase db, @NonNull String path)4514 private long getParent(@NonNull SQLiteDatabase db, @NonNull String path) { 4515 final String parentPath = new File(path).getParent(); 4516 if (Objects.equals("/", parentPath)) { 4517 return -1; 4518 } else { 4519 synchronized (mDirectoryCache) { 4520 Long id = mDirectoryCache.get(parentPath); 4521 if (id != null) { 4522 return id; 4523 } 4524 } 4525 4526 final long id; 4527 try (Cursor c = db.query("files", new String[] { FileColumns._ID }, 4528 FileColumns.DATA + "=?", new String[] { parentPath }, null, null, null)) { 4529 if (c.moveToFirst()) { 4530 id = c.getLong(0); 4531 } else { 4532 id = insertDirectory(db, parentPath); 4533 } 4534 } 4535 4536 synchronized (mDirectoryCache) { 4537 mDirectoryCache.put(parentPath, id); 4538 } 4539 return id; 4540 } 4541 } 4542 4543 /** 4544 * @param c the Cursor whose title to retrieve 4545 * @return the result of {@link #getDefaultTitle(String)} if the result is valid; otherwise 4546 * the value of the {@code MediaStore.Audio.Media.TITLE} column 4547 */ getDefaultTitleFromCursor(Cursor c)4548 private String getDefaultTitleFromCursor(Cursor c) { 4549 String title = null; 4550 final int columnIndex = c.getColumnIndex("title_resource_uri"); 4551 // Necessary to check for existence because we may be reading from an old DB version 4552 if (columnIndex > -1) { 4553 final String titleResourceUri = c.getString(columnIndex); 4554 if (titleResourceUri != null) { 4555 try { 4556 title = getDefaultTitle(titleResourceUri); 4557 } catch (Exception e) { 4558 // Best attempt only 4559 } 4560 } 4561 } 4562 if (title == null) { 4563 title = c.getString(c.getColumnIndex(MediaStore.Audio.Media.TITLE)); 4564 } 4565 return title; 4566 } 4567 4568 /** 4569 * @param title_resource_uri The title resource for which to retrieve the default localization 4570 * @return The title localized to {@code Locale.US}, or {@code null} if unlocalizable 4571 * @throws Exception Thrown if the title appears to be localizable, but the localization failed 4572 * for any reason. For example, the application from which the localized title is fetched is not 4573 * installed, or it does not have the resource which needs to be localized 4574 */ getDefaultTitle(String title_resource_uri)4575 private String getDefaultTitle(String title_resource_uri) throws Exception{ 4576 try { 4577 return getTitleFromResourceUri(title_resource_uri, false); 4578 } catch (Exception e) { 4579 Log.e(TAG, "Error getting default title for " + title_resource_uri, e); 4580 throw e; 4581 } 4582 } 4583 4584 /** 4585 * @param title_resource_uri The title resource to localize 4586 * @return The localized title, or {@code null} if unlocalizable 4587 * @throws Exception Thrown if the title appears to be localizable, but the localization failed 4588 * for any reason. For example, the application from which the localized title is fetched is not 4589 * installed, or it does not have the resource which needs to be localized 4590 */ getLocalizedTitle(String title_resource_uri)4591 private String getLocalizedTitle(String title_resource_uri) throws Exception { 4592 try { 4593 return getTitleFromResourceUri(title_resource_uri, true); 4594 } catch (Exception e) { 4595 Log.e(TAG, "Error getting localized title for " + title_resource_uri, e); 4596 throw e; 4597 } 4598 } 4599 4600 /** 4601 * Localizable titles conform to this URI pattern: 4602 * Scheme: {@link ContentResolver.SCHEME_ANDROID_RESOURCE} 4603 * Authority: Package Name of ringtone title provider 4604 * First Path Segment: Type of resource (must be "string") 4605 * Second Path Segment: Resource name of title 4606 * 4607 * @param title_resource_uri The title resource to retrieve 4608 * @param localize Whether or not to localize the title 4609 * @return The title, or {@code null} if unlocalizable 4610 * @throws Exception Thrown if the title appears to be localizable, but the localization failed 4611 * for any reason. For example, the application from which the localized title is fetched is not 4612 * installed, or it does not have the resource which needs to be localized 4613 */ getTitleFromResourceUri(String title_resource_uri, boolean localize)4614 private String getTitleFromResourceUri(String title_resource_uri, boolean localize) 4615 throws Exception { 4616 if (TextUtils.isEmpty(title_resource_uri)) { 4617 return null; 4618 } 4619 final Uri titleUri = Uri.parse(title_resource_uri); 4620 final String scheme = titleUri.getScheme(); 4621 if (!ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) { 4622 return null; 4623 } 4624 final List<String> pathSegments = titleUri.getPathSegments(); 4625 if (pathSegments.size() != 2) { 4626 Log.e(TAG, "Error getting localized title for " + title_resource_uri 4627 + ", must have 2 path segments"); 4628 return null; 4629 } 4630 final String type = pathSegments.get(0); 4631 if (!"string".equals(type)) { 4632 Log.e(TAG, "Error getting localized title for " + title_resource_uri 4633 + ", first path segment must be \"string\""); 4634 return null; 4635 } 4636 final String packageName = titleUri.getAuthority(); 4637 final Resources resources; 4638 if (localize) { 4639 resources = mPackageManager.getResourcesForApplication(packageName); 4640 } else { 4641 final Context packageContext = getContext().createPackageContext(packageName, 0); 4642 final Configuration configuration = packageContext.getResources().getConfiguration(); 4643 configuration.setLocale(Locale.US); 4644 resources = packageContext.createConfigurationContext(configuration).getResources(); 4645 } 4646 final String resourceIdentifier = pathSegments.get(1); 4647 final int id = resources.getIdentifier(resourceIdentifier, type, packageName); 4648 return resources.getString(id); 4649 } 4650 onLocaleChanged()4651 public void onLocaleChanged() { 4652 onLocaleChanged(true); 4653 } 4654 onLocaleChanged(boolean forceUpdate)4655 private void onLocaleChanged(boolean forceUpdate) { 4656 mInternalDatabase.runWithTransaction((db) -> { 4657 if (forceUpdate || !mLastLocale.equals(Locale.getDefault())) { 4658 localizeTitles(db); 4659 mLastLocale = Locale.getDefault(); 4660 } 4661 return null; 4662 }); 4663 } 4664 localizeTitles(@onNull SQLiteDatabase db)4665 private void localizeTitles(@NonNull SQLiteDatabase db) { 4666 try (Cursor c = db.query("files", new String[]{"_id", "title_resource_uri"}, 4667 "title_resource_uri IS NOT NULL", null, null, null, null)) { 4668 while (c.moveToNext()) { 4669 final String id = c.getString(0); 4670 final String titleResourceUri = c.getString(1); 4671 final ContentValues values = new ContentValues(); 4672 try { 4673 values.put(AudioColumns.TITLE_RESOURCE_URI, titleResourceUri); 4674 computeAudioLocalizedValues(values); 4675 computeAudioKeyValues(values); 4676 db.update("files", values, "_id=?", new String[]{id}); 4677 } catch (Exception e) { 4678 Log.e(TAG, "Error updating localized title for " + titleResourceUri 4679 + ", keeping old localization"); 4680 } 4681 } 4682 } 4683 } 4684 insertFile(@onNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper, int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, int mediaType)4685 private Uri insertFile(@NonNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper, 4686 int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, 4687 int mediaType) throws VolumeArgumentException, VolumeNotFoundException { 4688 boolean wasPathEmpty = !values.containsKey(MediaStore.MediaColumns.DATA) 4689 || TextUtils.isEmpty(values.getAsString(MediaStore.MediaColumns.DATA)); 4690 4691 // Make sure all file-related columns are defined 4692 ensureUniqueFileColumns(match, uri, extras, values, null); 4693 4694 switch (mediaType) { 4695 case FileColumns.MEDIA_TYPE_AUDIO: { 4696 computeAudioLocalizedValues(values); 4697 computeAudioKeyValues(values); 4698 break; 4699 } 4700 } 4701 4702 // compute bucket_id and bucket_display_name for all files 4703 String path = values.getAsString(MediaStore.MediaColumns.DATA); 4704 FileUtils.computeValuesFromData(values, isFuseThread()); 4705 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 4706 4707 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 4708 if (title == null && path != null) { 4709 title = extractFileName(path); 4710 } 4711 values.put(FileColumns.TITLE, title); 4712 4713 String mimeType = null; 4714 int format = MtpConstants.FORMAT_ASSOCIATION; 4715 if (path != null && new File(path).isDirectory()) { 4716 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION); 4717 values.putNull(MediaStore.MediaColumns.MIME_TYPE); 4718 } else { 4719 mimeType = values.getAsString(MediaStore.MediaColumns.MIME_TYPE); 4720 final Integer formatObject = values.getAsInteger(FileColumns.FORMAT); 4721 format = (formatObject == null ? 0 : formatObject); 4722 } 4723 4724 if (format == 0) { 4725 format = MimeUtils.resolveFormatCode(mimeType); 4726 } 4727 if (path != null && path.endsWith("/")) { 4728 // TODO: convert to using FallbackException once VERSION_CODES.S is defined 4729 Log.e(TAG, "directory has trailing slash: " + path); 4730 return null; 4731 } 4732 if (format != 0) { 4733 values.put(FileColumns.FORMAT, format); 4734 } 4735 4736 if (mimeType == null && path != null && format != MtpConstants.FORMAT_ASSOCIATION) { 4737 mimeType = MimeUtils.resolveMimeType(new File(path)); 4738 } 4739 4740 if (mimeType != null) { 4741 values.put(FileColumns.MIME_TYPE, mimeType); 4742 if (isCallingPackageSelf() && values.containsKey(FileColumns.MEDIA_TYPE)) { 4743 // Leave FileColumns.MEDIA_TYPE untouched if the caller is ModernMediaScanner and 4744 // FileColumns.MEDIA_TYPE is already populated. 4745 } else if (isFuseThread() && path != null 4746 && FileUtils.shouldFileBeHidden(new File(path))) { 4747 // We should only mark MEDIA_TYPE as MEDIA_TYPE_NONE for Fuse Thread. 4748 // MediaProvider#insert() returns the uri by appending the "rowId" to the given 4749 // uri, hence to ensure the correct working of the returned uri, we shouldn't 4750 // change the MEDIA_TYPE in insert operation and let scan change it for us. 4751 values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE); 4752 } else { 4753 values.put(FileColumns.MEDIA_TYPE, MimeUtils.resolveMediaType(mimeType)); 4754 } 4755 } else { 4756 values.put(FileColumns.MEDIA_TYPE, mediaType); 4757 } 4758 4759 qb.allowColumn(FileColumns._MODIFIER); 4760 if (isCallingPackageSelf() && values.containsKey(FileColumns._MODIFIER)) { 4761 // We can't identify if the call is coming from media scan, hence 4762 // we let ModernMediaScanner send FileColumns._MODIFIER value. 4763 } else if (isFuseThread()) { 4764 values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_FUSE); 4765 } else { 4766 values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_CR); 4767 } 4768 4769 // There is no meaning of an owner in the internal storage. It is shared by all users. 4770 // So we only set the user_id field in the database for external storage. 4771 qb.allowColumn(FileColumns._USER_ID); 4772 int ownerUserId = FileUtils.extractUserId(path); 4773 if (helper.isExternal()) { 4774 if (isAppCloneUserForFuse(ownerUserId)) { 4775 values.put(FileColumns._USER_ID, ownerUserId); 4776 } else { 4777 values.put(FileColumns._USER_ID, sUserId); 4778 } 4779 } 4780 4781 final long rowId; 4782 Uri newUri = uri; 4783 { 4784 if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 4785 String name = values.getAsString(Audio.Playlists.NAME); 4786 if (name == null && path == null) { 4787 // MediaScanner will compute the name from the path if we have one 4788 throw new IllegalArgumentException( 4789 "no name was provided when inserting abstract playlist"); 4790 } 4791 } else { 4792 if (path == null) { 4793 // path might be null for playlists created on the device 4794 // or transfered via MTP 4795 throw new IllegalArgumentException( 4796 "no path was provided when inserting new file"); 4797 } 4798 } 4799 4800 // make sure modification date and size are set 4801 if (path != null) { 4802 File file = new File(path); 4803 if (file.exists()) { 4804 values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000); 4805 if (!values.containsKey(FileColumns.SIZE)) { 4806 values.put(FileColumns.SIZE, file.length()); 4807 } 4808 } 4809 // Checking if the file/directory is hidden can be expensive based on the depth of 4810 // the directory tree. Call shouldFileBeHidden() only when the caller of insert() 4811 // cares about returned uri. 4812 if (!isCallingPackageSelf() && !isFuseThread() 4813 && FileUtils.shouldFileBeHidden(file)) { 4814 newUri = MediaStore.Files.getContentUri(MediaStore.getVolumeName(uri)); 4815 } 4816 } 4817 4818 rowId = insertAllowingUpsert(qb, helper, values, path); 4819 } 4820 if (format == MtpConstants.FORMAT_ASSOCIATION) { 4821 synchronized (mDirectoryCache) { 4822 mDirectoryCache.put(path, rowId); 4823 } 4824 } 4825 4826 return ContentUris.withAppendedId(newUri, rowId); 4827 } 4828 4829 /** 4830 * Inserts a new row in MediaProvider database with {@code values}. Treats insert as upsert for 4831 * double inserts from same package. 4832 */ insertAllowingUpsert(@onNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper, @NonNull ContentValues values, String path)4833 private long insertAllowingUpsert(@NonNull SQLiteQueryBuilder qb, 4834 @NonNull DatabaseHelper helper, @NonNull ContentValues values, String path) 4835 throws SQLiteConstraintException { 4836 return helper.runWithTransaction((db) -> { 4837 Long parent = values.getAsLong(FileColumns.PARENT); 4838 if (parent == null) { 4839 if (path != null) { 4840 final long parentId = getParent(db, path); 4841 values.put(FileColumns.PARENT, parentId); 4842 } 4843 } 4844 4845 try { 4846 return qb.insert(helper, values); 4847 } catch (SQLiteConstraintException e) { 4848 final String packages = getAllowedPackagesForUpsert( 4849 values.getAsString(MediaColumns.OWNER_PACKAGE_NAME)); 4850 SQLiteQueryBuilder qbForUpsert = getQueryBuilderForUpsert(path); 4851 final long rowId = getIdIfPathOwnedByPackages(qbForUpsert, helper, path, packages); 4852 // Apps sometimes create a file via direct path and then insert it into 4853 // MediaStore via ContentResolver. The former should create a database entry, 4854 // so we have to treat the latter as an upsert. 4855 // TODO(b/149917493) Perform all INSERT operations as UPSERT. 4856 if (rowId != -1 && qbForUpsert.update(helper, values, "_id=?", 4857 new String[]{Long.toString(rowId)}) == 1) { 4858 return rowId; 4859 } 4860 // Rethrow SQLiteConstraintException on failed upsert. 4861 throw e; 4862 } 4863 }); 4864 } 4865 4866 /** 4867 * @return row id of the entry with path {@code path} if the owner is one of {@code packages}. 4868 */ 4869 private long getIdIfPathOwnedByPackages(@NonNull SQLiteQueryBuilder qb, 4870 @NonNull DatabaseHelper helper, String path, String packages) { 4871 final String[] projection = new String[] {FileColumns._ID}; 4872 final String ownerPackageMatchClause = DatabaseUtils.bindSelection( 4873 MediaColumns.OWNER_PACKAGE_NAME + " IN " + packages); 4874 final String selection = FileColumns.DATA + " =? AND " + ownerPackageMatchClause; 4875 4876 try (Cursor c = qb.query(helper, projection, selection, new String[] {path}, null, null, 4877 null, null, null)) { 4878 if (c.moveToFirst()) { 4879 return c.getLong(0); 4880 } 4881 } 4882 return -1; 4883 } 4884 4885 /** 4886 * Gets packages that should match to upsert a db row. 4887 * 4888 * A database row can be upserted if 4889 * <ul> 4890 * <li> Calling package or one of the shared packages owns the db row. 4891 * <li> {@code givenOwnerPackage} owns the db row. This is useful when DownloadProvider 4892 * requests upsert on behalf of another app 4893 * </ul> 4894 */ 4895 private String getAllowedPackagesForUpsert(@Nullable String givenOwnerPackage) { 4896 ArrayList<String> packages = new ArrayList<>( 4897 Arrays.asList(mCallingIdentity.get().getSharedPackageNamesArray())); 4898 4899 // If givenOwnerPackage is CallingIdentity, packages list would already have shared package 4900 // names of givenOwnerPackage. If givenOwnerPackage is not CallingIdentity, since 4901 // DownloadProvider can upsert a row on behalf of app, we should include all shared packages 4902 // of givenOwnerPackage. 4903 if (givenOwnerPackage != null && isCallingPackageDelegator() && 4904 !isCallingIdentitySharedPackageName(givenOwnerPackage)) { 4905 // Allow DownloadProvider to Upsert if givenOwnerPackage is owner of the db row. 4906 packages.addAll(Arrays.asList(getSharedPackagesForPackage(givenOwnerPackage))); 4907 } 4908 return bindList((Object[]) packages.toArray()); 4909 } 4910 4911 /** 4912 * @return {@link SQLiteQueryBuilder} for upsert with Files uri. This disables strict columns 4913 * check to allow upsert to update any column with Files uri. 4914 */ 4915 private SQLiteQueryBuilder getQueryBuilderForUpsert(@NonNull String path) { 4916 final boolean allowHidden = isCallingPackageAllowedHidden(); 4917 Bundle extras = new Bundle(); 4918 extras.putInt(QUERY_ARG_MATCH_PENDING, MATCH_INCLUDE); 4919 extras.putInt(QUERY_ARG_MATCH_TRASHED, MATCH_INCLUDE); 4920 4921 // When Fuse inserts a file to database it doesn't set is_download column. When app tries 4922 // insert with Downloads uri, upsert fails because getIdIfPathExistsForCallingPackage can't 4923 // find a row ID with is_download=1. Use Files uri to get queryBuilder & update any existing 4924 // row irrespective of is_download=1. 4925 final Uri uri = FileUtils.getContentUriForPath(path); 4926 SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, matchUri(uri, allowHidden), uri, 4927 extras, null); 4928 4929 // We won't be able to update columns that are not part of projection map of Files table. We 4930 // have already checked strict columns in previous insert operation which failed with 4931 // exception. Any malicious column usage would have got caught in insert operation, hence we 4932 // can safely disable strict column check for upsert. 4933 qb.setStrictColumns(false); 4934 return qb; 4935 } 4936 4937 private void maybePut(@NonNull ContentValues values, @NonNull String key, 4938 @Nullable String value) { 4939 if (value != null) { 4940 values.put(key, value); 4941 } 4942 } 4943 4944 private boolean maybeMarkAsDownload(@NonNull ContentValues values) { 4945 final String path = values.getAsString(MediaColumns.DATA); 4946 if (path != null && isDownload(path)) { 4947 values.put(FileColumns.IS_DOWNLOAD, 1); 4948 return true; 4949 } 4950 return false; 4951 } 4952 4953 @NonNull 4954 private static String resolveVolumeName(@NonNull Uri uri) { 4955 final String volumeName = getVolumeName(uri); 4956 if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) { 4957 return MediaStore.VOLUME_EXTERNAL_PRIMARY; 4958 } else { 4959 return volumeName; 4960 } 4961 } 4962 4963 /** 4964 * @deprecated all operations should be routed through the overload that 4965 * accepts a {@link Bundle} of extras. 4966 */ 4967 @Override 4968 @Deprecated 4969 public Uri insert(Uri uri, ContentValues values) { 4970 return insert(uri, values, null); 4971 } 4972 4973 @Override 4974 @Nullable 4975 public Uri insert(@NonNull Uri uri, @Nullable ContentValues values, 4976 @Nullable Bundle extras) { 4977 Trace.beginSection("MP.insert [" + uri + ']'); 4978 try { 4979 try { 4980 return insertInternal(uri, values, extras); 4981 } catch (SQLiteConstraintException e) { 4982 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) { 4983 throw e; 4984 } else { 4985 return null; 4986 } 4987 } 4988 } catch (FallbackException e) { 4989 return e.translateForInsert(getCallingPackageTargetSdkVersion()); 4990 } finally { 4991 Trace.endSection(); 4992 } 4993 } 4994 4995 @Nullable 4996 private Uri insertInternal(@NonNull Uri uri, @Nullable ContentValues initialValues, 4997 @Nullable Bundle extras) throws FallbackException { 4998 final String originalVolumeName = getVolumeName(uri); 4999 PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), originalVolumeName); 5000 5001 extras = (extras != null) ? extras : new Bundle(); 5002 // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider. 5003 extras.remove(QUERY_ARG_REDACTED_URI); 5004 5005 // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. 5006 extras.remove(INCLUDED_DEFAULT_DIRECTORIES); 5007 5008 final boolean allowHidden = isCallingPackageAllowedHidden(); 5009 final int match = matchUri(uri, allowHidden); 5010 5011 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 5012 final String resolvedVolumeName = resolveVolumeName(uri); 5013 5014 // handle MEDIA_SCANNER before calling getDatabaseForUri() 5015 if (match == MEDIA_SCANNER) { 5016 mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME); 5017 5018 final DatabaseHelper helper = getDatabaseForUri( 5019 MediaStore.Files.getContentUri(mMediaScannerVolume)); 5020 5021 helper.mScanStartTime = SystemClock.elapsedRealtime(); 5022 return MediaStore.getMediaScannerUri(); 5023 } 5024 5025 if (match == VOLUMES) { 5026 String name = initialValues.getAsString("name"); 5027 MediaVolume volume = null; 5028 try { 5029 volume = getVolume(name); 5030 Uri attachedVolume = attachVolume(volume, /* validate */ true); 5031 if (mMediaScannerVolume != null && mMediaScannerVolume.equals(name)) { 5032 final DatabaseHelper helper = getDatabaseForUri( 5033 MediaStore.Files.getContentUri(mMediaScannerVolume)); 5034 helper.mScanStartTime = SystemClock.elapsedRealtime(); 5035 } 5036 return attachedVolume; 5037 } catch (FileNotFoundException e) { 5038 Log.w(TAG, "Couldn't find volume with name " + volume.getName()); 5039 return null; 5040 } 5041 } 5042 5043 final DatabaseHelper helper = getDatabaseForUri(uri); 5044 switch (match) { 5045 case AUDIO_PLAYLISTS_ID: 5046 case AUDIO_PLAYLISTS_ID_MEMBERS: { 5047 final long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 5048 final Uri playlistUri = ContentUris.withAppendedId( 5049 MediaStore.Audio.Playlists.getContentUri(resolvedVolumeName), playlistId); 5050 5051 final long audioId = initialValues 5052 .getAsLong(MediaStore.Audio.Playlists.Members.AUDIO_ID); 5053 final String audioVolumeName = 5054 MediaStore.VOLUME_INTERNAL.equals(resolvedVolumeName) 5055 ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL; 5056 final Uri audioUri = ContentUris.withAppendedId( 5057 MediaStore.Audio.Media.getContentUri(audioVolumeName), audioId); 5058 5059 // Require that caller has write access to underlying media 5060 enforceCallingPermission(playlistUri, Bundle.EMPTY, true); 5061 enforceCallingPermission(audioUri, Bundle.EMPTY, false); 5062 5063 // Playlist contents are always persisted directly into playlist 5064 // files on disk to ensure that we can reliably migrate between 5065 // devices and recover from database corruption 5066 final long id = addPlaylistMembers(playlistUri, initialValues); 5067 acceptWithExpansion(helper::notifyInsert, resolvedVolumeName, playlistId, 5068 FileColumns.MEDIA_TYPE_PLAYLIST, false); 5069 return ContentUris.withAppendedId(MediaStore.Audio.Playlists.Members 5070 .getContentUri(originalVolumeName, playlistId), id); 5071 } 5072 } 5073 5074 String path = null; 5075 String ownerPackageName = null; 5076 if (initialValues != null) { 5077 // IDs are forever; nobody should be editing them 5078 initialValues.remove(MediaColumns._ID); 5079 5080 // Expiration times are hard-coded; let's derive them 5081 FileUtils.computeDateExpires(initialValues); 5082 5083 // Ignore or augment incoming raw filesystem paths 5084 for (String column : sDataColumns.keySet()) { 5085 if (!initialValues.containsKey(column)) continue; 5086 5087 if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) { 5088 // Mutation allowed 5089 } else if (isCallingPackageManager()) { 5090 // Apps with MANAGE_EXTERNAL_STORAGE have all files access, hence they are 5091 // allowed to insert files anywhere. 5092 } else if (getCallingPackageTargetSdkVersion() >= 5093 Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 5094 // Throwing an exception so that it doesn't result in some unexpected 5095 // behavior for apps and make them aware of what is happening. 5096 throw new IllegalArgumentException("Mutation of " + column 5097 + " is not allowed."); 5098 } else { 5099 Log.w(TAG, "Ignoring mutation of " + column + " from " 5100 + getCallingPackageOrSelf()); 5101 initialValues.remove(column); 5102 } 5103 } 5104 5105 path = initialValues.getAsString(MediaStore.MediaColumns.DATA); 5106 5107 if (!isCallingPackageSelf()) { 5108 initialValues.remove(FileColumns.IS_DOWNLOAD); 5109 } 5110 5111 // We no longer track location metadata 5112 if (initialValues.containsKey(ImageColumns.LATITUDE)) { 5113 initialValues.putNull(ImageColumns.LATITUDE); 5114 } 5115 if (initialValues.containsKey(ImageColumns.LONGITUDE)) { 5116 initialValues.putNull(ImageColumns.LONGITUDE); 5117 } 5118 if (getCallingPackageTargetSdkVersion() <= Build.VERSION_CODES.Q) { 5119 // These columns are removed in R. 5120 if (initialValues.containsKey("primary_directory")) { 5121 initialValues.remove("primary_directory"); 5122 } 5123 if (initialValues.containsKey("secondary_directory")) { 5124 initialValues.remove("secondary_directory"); 5125 } 5126 } 5127 5128 if (isCallingPackageSelf() || isCallingPackageShell()) { 5129 // When media inserted by ourselves during a scan, or by the 5130 // shell, the best we can do is guess ownership based on path 5131 // when it's not explicitly provided 5132 ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME); 5133 if (TextUtils.isEmpty(ownerPackageName)) { 5134 ownerPackageName = extractPathOwnerPackageName(path); 5135 } 5136 } else if (isCallingPackageDelegator()) { 5137 // When caller is a delegator, we handle ownership as a hybrid 5138 // of the two other cases: we're willing to accept any ownership 5139 // transfer attempted during insert, but we fall back to using 5140 // the Binder identity if they don't request a specific owner 5141 ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME); 5142 if (TextUtils.isEmpty(ownerPackageName)) { 5143 ownerPackageName = getCallingPackageOrSelf(); 5144 } 5145 } else { 5146 // Remote callers have no direct control over owner column; we force 5147 // it be whoever is creating the content. 5148 initialValues.remove(FileColumns.OWNER_PACKAGE_NAME); 5149 ownerPackageName = getCallingPackageOrSelf(); 5150 } 5151 } 5152 5153 long rowId = -1; 5154 Uri newUri = null; 5155 5156 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_INSERT, match, uri, extras, null); 5157 5158 switch (match) { 5159 case IMAGES_MEDIA: { 5160 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 5161 final boolean isDownload = maybeMarkAsDownload(initialValues); 5162 newUri = insertFile(qb, helper, match, uri, extras, initialValues, 5163 FileColumns.MEDIA_TYPE_IMAGE); 5164 break; 5165 } 5166 5167 case IMAGES_THUMBNAILS: { 5168 if (helper.isInternal()) { 5169 throw new UnsupportedOperationException( 5170 "Writing to internal storage is not supported."); 5171 } 5172 5173 // Require that caller has write access to underlying media 5174 final long imageId = initialValues.getAsLong(MediaStore.Images.Thumbnails.IMAGE_ID); 5175 enforceCallingPermission(ContentUris.withAppendedId( 5176 MediaStore.Images.Media.getContentUri(resolvedVolumeName), imageId), 5177 extras, true); 5178 5179 ensureUniqueFileColumns(match, uri, extras, initialValues, null); 5180 5181 rowId = qb.insert(helper, initialValues); 5182 if (rowId > 0) { 5183 newUri = ContentUris.withAppendedId(Images.Thumbnails. 5184 getContentUri(originalVolumeName), rowId); 5185 } 5186 break; 5187 } 5188 5189 case VIDEO_THUMBNAILS: { 5190 if (helper.isInternal()) { 5191 throw new UnsupportedOperationException( 5192 "Writing to internal storage is not supported."); 5193 } 5194 5195 // Require that caller has write access to underlying media 5196 final long videoId = initialValues.getAsLong(MediaStore.Video.Thumbnails.VIDEO_ID); 5197 enforceCallingPermission(ContentUris.withAppendedId( 5198 MediaStore.Video.Media.getContentUri(resolvedVolumeName), videoId), 5199 Bundle.EMPTY, true); 5200 5201 ensureUniqueFileColumns(match, uri, extras, initialValues, null); 5202 5203 rowId = qb.insert(helper, initialValues); 5204 if (rowId > 0) { 5205 newUri = ContentUris.withAppendedId(Video.Thumbnails. 5206 getContentUri(originalVolumeName), rowId); 5207 } 5208 break; 5209 } 5210 5211 case AUDIO_MEDIA: { 5212 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 5213 final boolean isDownload = maybeMarkAsDownload(initialValues); 5214 newUri = insertFile(qb, helper, match, uri, extras, initialValues, 5215 FileColumns.MEDIA_TYPE_AUDIO); 5216 break; 5217 } 5218 5219 case AUDIO_MEDIA_ID_GENRES: { 5220 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R); 5221 } 5222 5223 case AUDIO_GENRES: { 5224 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R); 5225 } 5226 5227 case AUDIO_GENRES_ID_MEMBERS: { 5228 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R); 5229 } 5230 5231 case AUDIO_PLAYLISTS: { 5232 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 5233 final boolean isDownload = maybeMarkAsDownload(initialValues); 5234 ContentValues values = new ContentValues(initialValues); 5235 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000); 5236 // Playlist names are stored as display names, but leave 5237 // values untouched if the caller is ModernMediaScanner 5238 if (!isCallingPackageSelf()) { 5239 if (values.containsKey(Playlists.NAME)) { 5240 values.put(MediaColumns.DISPLAY_NAME, values.getAsString(Playlists.NAME)); 5241 } 5242 if (!values.containsKey(MediaColumns.MIME_TYPE)) { 5243 values.put(MediaColumns.MIME_TYPE, "audio/mpegurl"); 5244 } 5245 } 5246 newUri = insertFile(qb, helper, match, uri, extras, values, 5247 FileColumns.MEDIA_TYPE_PLAYLIST); 5248 if (newUri != null) { 5249 // Touch empty playlist file on disk so its ready for renames 5250 if (Binder.getCallingUid() != android.os.Process.myUid()) { 5251 try (OutputStream out = ContentResolver.wrap(this) 5252 .openOutputStream(newUri)) { 5253 } catch (IOException ignored) { 5254 } 5255 } 5256 } 5257 break; 5258 } 5259 5260 case VIDEO_MEDIA: { 5261 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 5262 final boolean isDownload = maybeMarkAsDownload(initialValues); 5263 newUri = insertFile(qb, helper, match, uri, extras, initialValues, 5264 FileColumns.MEDIA_TYPE_VIDEO); 5265 break; 5266 } 5267 5268 case AUDIO_ALBUMART: { 5269 if (helper.isInternal()) { 5270 throw new UnsupportedOperationException("no internal album art allowed"); 5271 } 5272 5273 ensureUniqueFileColumns(match, uri, extras, initialValues, null); 5274 5275 rowId = qb.insert(helper, initialValues); 5276 if (rowId > 0) { 5277 newUri = ContentUris.withAppendedId(uri, rowId); 5278 } 5279 break; 5280 } 5281 5282 case FILES: { 5283 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 5284 final boolean isDownload = maybeMarkAsDownload(initialValues); 5285 final String mimeType = initialValues.getAsString(MediaColumns.MIME_TYPE); 5286 final int mediaType = MimeUtils.resolveMediaType(mimeType); 5287 newUri = insertFile(qb, helper, match, uri, extras, initialValues, 5288 mediaType); 5289 break; 5290 } 5291 5292 case DOWNLOADS: 5293 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 5294 initialValues.put(FileColumns.IS_DOWNLOAD, 1); 5295 newUri = insertFile(qb, helper, match, uri, extras, initialValues, 5296 FileColumns.MEDIA_TYPE_NONE); 5297 break; 5298 5299 default: 5300 throw new UnsupportedOperationException("Invalid URI " + uri); 5301 } 5302 5303 // Remember that caller is owner of this item, to speed up future 5304 // permission checks for this caller 5305 mCallingIdentity.get().setOwned(rowId, true); 5306 5307 if (path != null && path.toLowerCase(Locale.ROOT).endsWith("/.nomedia")) { 5308 scanFileAsMediaProvider(new File(path).getParentFile(), REASON_DEMAND); 5309 } 5310 5311 return newUri; 5312 } 5313 5314 @Override 5315 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 5316 throws OperationApplicationException { 5317 // Open transactions on databases for requested volumes 5318 final Set<DatabaseHelper> transactions = new ArraySet<>(); 5319 try { 5320 for (ContentProviderOperation op : operations) { 5321 final DatabaseHelper helper = getDatabaseForUri(op.getUri()); 5322 if (transactions.contains(helper)) continue; 5323 5324 if (!helper.isTransactionActive()) { 5325 helper.beginTransaction(); 5326 transactions.add(helper); 5327 } else { 5328 // We normally don't allow nested transactions (since we 5329 // don't have a good way to selectively roll them back) but 5330 // if the incoming operation is ignoring exceptions, then we 5331 // don't need to worry about partial rollback and can 5332 // piggyback on the larger active transaction 5333 if (!op.isExceptionAllowed()) { 5334 throw new IllegalStateException("Nested transactions not supported"); 5335 } 5336 } 5337 } 5338 5339 final ContentProviderResult[] result = super.applyBatch(operations); 5340 for (DatabaseHelper helper : transactions) { 5341 helper.setTransactionSuccessful(); 5342 } 5343 return result; 5344 } catch (VolumeNotFoundException e) { 5345 throw e.rethrowAsIllegalArgumentException(); 5346 } finally { 5347 for (DatabaseHelper helper : transactions) { 5348 helper.endTransaction(); 5349 } 5350 } 5351 } 5352 5353 private void appendWhereStandaloneMatch(@NonNull SQLiteQueryBuilder qb, 5354 @NonNull String column, /* @Match */ int match, Uri uri) { 5355 switch (match) { 5356 case MATCH_INCLUDE: 5357 // No special filtering needed 5358 break; 5359 case MATCH_EXCLUDE: 5360 appendWhereStandalone(qb, getWhereClauseForMatchExclude(column)); 5361 break; 5362 case MATCH_ONLY: 5363 appendWhereStandalone(qb, column + "=?", 1); 5364 break; 5365 case MATCH_VISIBLE_FOR_FILEPATH: 5366 final String whereClause = 5367 getWhereClauseForMatchableVisibleFromFilePath(uri, column); 5368 if (whereClause != null) { 5369 appendWhereStandalone(qb, whereClause); 5370 } 5371 break; 5372 default: 5373 throw new IllegalArgumentException(); 5374 } 5375 } 5376 5377 private static void appendWhereStandalone(@NonNull SQLiteQueryBuilder qb, 5378 @Nullable String selection, @Nullable Object... selectionArgs) { 5379 qb.appendWhereStandalone(DatabaseUtils.bindSelection(selection, selectionArgs)); 5380 } 5381 5382 private static void appendWhereStandaloneFilter(@NonNull SQLiteQueryBuilder qb, 5383 @NonNull String[] columns, @Nullable String filter) { 5384 if (TextUtils.isEmpty(filter)) return; 5385 for (String filterWord : filter.split("\\s+")) { 5386 appendWhereStandalone(qb, String.join("||", columns) + " LIKE ? ESCAPE '\\'", 5387 "%" + DatabaseUtils.escapeForLike(Audio.keyFor(filterWord)) + "%"); 5388 } 5389 } 5390 5391 /** 5392 * Gets {@link LocalCallingIdentity} for the calling package 5393 * TODO(b/170465810) Change the method name after refactoring. 5394 */ 5395 LocalCallingIdentity getCachedCallingIdentityForTranscoding(int uid) { 5396 return getCachedCallingIdentityForFuse(uid); 5397 } 5398 5399 /** 5400 * Gets shared packages names for given {@code packageName} 5401 */ 5402 private String[] getSharedPackagesForPackage(String packageName) { 5403 try { 5404 final int packageUid = getContext().getPackageManager() 5405 .getPackageUid(packageName, 0); 5406 return getContext().getPackageManager().getPackagesForUid(packageUid); 5407 } catch (NameNotFoundException ignored) { 5408 return new String[] {packageName}; 5409 } 5410 } 5411 5412 private static final int TYPE_QUERY = 0; 5413 private static final int TYPE_INSERT = 1; 5414 private static final int TYPE_UPDATE = 2; 5415 private static final int TYPE_DELETE = 3; 5416 5417 /** 5418 * Creating a new method for Transcoding to avoid any merge conflicts. 5419 * TODO(b/170465810): Remove this when getQueryBuilder code is refactored. 5420 */ 5421 @NonNull SQLiteQueryBuilder getQueryBuilderForTranscoding(int type, int match, 5422 @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) { 5423 // Force MediaProvider calling identity when accessing the db from transcoding to avoid 5424 // generating 'strict' SQL e.g forcing owner_package_name matches 5425 // We already handle the required permission checks for the app before we get here 5426 final LocalCallingIdentity token = clearLocalCallingIdentity(); 5427 try { 5428 return getQueryBuilder(type, match, uri, extras, honored); 5429 } finally { 5430 restoreLocalCallingIdentity(token); 5431 } 5432 } 5433 5434 /** 5435 * Generate a {@link SQLiteQueryBuilder} that is filtered based on the 5436 * runtime permissions and/or {@link Uri} grants held by the caller. 5437 * <ul> 5438 * <li>If caller holds a {@link Uri} grant, access is allowed according to 5439 * that grant. 5440 * <li>If caller holds the write permission for a collection, they can 5441 * read/write all contents of that collection. 5442 * <li>If caller holds the read permission for a collection, they can read 5443 * all contents of that collection, but writes are limited to content they 5444 * own. 5445 * <li>If caller holds no permissions for a collection, all reads/write are 5446 * limited to content they own. 5447 * </ul> 5448 */ 5449 private @NonNull SQLiteQueryBuilder getQueryBuilder(int type, int match, 5450 @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) { 5451 Trace.beginSection("MP.getQueryBuilder"); 5452 try { 5453 return getQueryBuilderInternal(type, match, uri, extras, honored); 5454 } finally { 5455 Trace.endSection(); 5456 } 5457 } 5458 5459 private @NonNull SQLiteQueryBuilder getQueryBuilderInternal(int type, int match, 5460 @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) { 5461 final boolean forWrite; 5462 switch (type) { 5463 case TYPE_QUERY: forWrite = false; break; 5464 case TYPE_INSERT: forWrite = true; break; 5465 case TYPE_UPDATE: forWrite = true; break; 5466 case TYPE_DELETE: forWrite = true; break; 5467 default: throw new IllegalStateException(); 5468 } 5469 5470 if (forWrite) { 5471 final Uri redactedUri = extras.getParcelable(QUERY_ARG_REDACTED_URI); 5472 if (redactedUri != null) { 5473 throw new UnsupportedOperationException( 5474 "Writes on: " + redactedUri.toString() + " are not supported"); 5475 } 5476 } 5477 5478 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 5479 if (uri.getBooleanQueryParameter("distinct", false)) { 5480 qb.setDistinct(true); 5481 } 5482 qb.setStrict(true); 5483 if (isCallingPackageSelf()) { 5484 // When caller is system, such as the media scanner, we're willing 5485 // to let them access any columns they want 5486 } else { 5487 qb.setTargetSdkVersion(getCallingPackageTargetSdkVersion()); 5488 qb.setStrictColumns(true); 5489 qb.setStrictGrammar(true); 5490 } 5491 5492 // TODO: throw when requesting a currently unmounted volume 5493 final String volumeName = MediaStore.getVolumeName(uri); 5494 final String includeVolumes; 5495 if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) { 5496 includeVolumes = bindList(mVolumeCache.getExternalVolumeNames().toArray()); 5497 } else { 5498 includeVolumes = bindList(volumeName); 5499 } 5500 5501 int matchPending = extras.getInt(QUERY_ARG_MATCH_PENDING, MATCH_DEFAULT); 5502 int matchTrashed = extras.getInt(QUERY_ARG_MATCH_TRASHED, MATCH_DEFAULT); 5503 int matchFavorite = extras.getInt(QUERY_ARG_MATCH_FAVORITE, MATCH_DEFAULT); 5504 5505 5506 // Handle callers using legacy arguments 5507 if (MediaStore.getIncludePending(uri)) matchPending = MATCH_INCLUDE; 5508 5509 // Resolve any remaining default options 5510 final int defaultMatchForPendingAndTrashed; 5511 if (isFuseThread()) { 5512 // Write operations always check for file ownership, we don't need additional write 5513 // permission check for is_pending and is_trashed. 5514 defaultMatchForPendingAndTrashed = 5515 forWrite ? MATCH_INCLUDE : MATCH_VISIBLE_FOR_FILEPATH; 5516 } else { 5517 defaultMatchForPendingAndTrashed = MATCH_EXCLUDE; 5518 } 5519 if (matchPending == MATCH_DEFAULT) matchPending = defaultMatchForPendingAndTrashed; 5520 if (matchTrashed == MATCH_DEFAULT) matchTrashed = defaultMatchForPendingAndTrashed; 5521 if (matchFavorite == MATCH_DEFAULT) matchFavorite = MATCH_INCLUDE; 5522 5523 // Handle callers using legacy filtering 5524 final String filter = uri.getQueryParameter("filter"); 5525 5526 // Only accept ALL_VOLUMES parameter up until R, because we're not convinced we want 5527 // to commit to this as an API. 5528 final boolean includeAllVolumes = shouldIncludeRecentlyUnmountedVolumes(uri, extras); 5529 5530 appendAccessCheckQuery(qb, forWrite, uri, match, extras, volumeName); 5531 5532 switch (match) { 5533 case IMAGES_MEDIA_ID: 5534 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 5535 matchPending = MATCH_INCLUDE; 5536 matchTrashed = MATCH_INCLUDE; 5537 // fall-through 5538 case IMAGES_MEDIA: { 5539 if (type == TYPE_QUERY) { 5540 qb.setTables("images"); 5541 qb.setProjectionMap( 5542 getProjectionMap(Images.Media.class)); 5543 } else { 5544 qb.setTables("files"); 5545 qb.setProjectionMap( 5546 getProjectionMap(Images.Media.class, Files.FileColumns.class)); 5547 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 5548 FileColumns.MEDIA_TYPE_IMAGE); 5549 } 5550 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); 5551 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); 5552 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 5553 if (honored != null) { 5554 honored.accept(QUERY_ARG_MATCH_PENDING); 5555 honored.accept(QUERY_ARG_MATCH_TRASHED); 5556 honored.accept(QUERY_ARG_MATCH_FAVORITE); 5557 } 5558 if (!includeAllVolumes) { 5559 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 5560 } 5561 break; 5562 } 5563 case IMAGES_THUMBNAILS_ID: 5564 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 5565 // fall-through 5566 case IMAGES_THUMBNAILS: { 5567 qb.setTables("thumbnails"); 5568 5569 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 5570 getProjectionMap(Images.Thumbnails.class)); 5571 projectionMap.put(Images.Thumbnails.THUMB_DATA, 5572 "NULL AS " + Images.Thumbnails.THUMB_DATA); 5573 qb.setProjectionMap(projectionMap); 5574 5575 break; 5576 } 5577 case AUDIO_MEDIA_ID: 5578 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 5579 matchPending = MATCH_INCLUDE; 5580 matchTrashed = MATCH_INCLUDE; 5581 // fall-through 5582 case AUDIO_MEDIA: { 5583 if (type == TYPE_QUERY) { 5584 qb.setTables("audio"); 5585 qb.setProjectionMap( 5586 getProjectionMap(Audio.Media.class)); 5587 } else { 5588 qb.setTables("files"); 5589 qb.setProjectionMap( 5590 getProjectionMap(Audio.Media.class, Files.FileColumns.class)); 5591 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 5592 FileColumns.MEDIA_TYPE_AUDIO); 5593 } 5594 appendWhereStandaloneFilter(qb, new String[] { 5595 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY 5596 }, filter); 5597 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); 5598 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); 5599 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 5600 if (honored != null) { 5601 honored.accept(QUERY_ARG_MATCH_PENDING); 5602 honored.accept(QUERY_ARG_MATCH_TRASHED); 5603 honored.accept(QUERY_ARG_MATCH_FAVORITE); 5604 } 5605 if (!includeAllVolumes) { 5606 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 5607 } 5608 break; 5609 } 5610 case AUDIO_MEDIA_ID_GENRES_ID: 5611 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(5)); 5612 // fall-through 5613 case AUDIO_MEDIA_ID_GENRES: { 5614 if (type == TYPE_QUERY) { 5615 qb.setTables("audio_genres"); 5616 qb.setProjectionMap(getProjectionMap(Audio.Genres.class)); 5617 } else { 5618 throw new UnsupportedOperationException("Genres cannot be directly modified"); 5619 } 5620 appendWhereStandalone(qb, "_id IN (SELECT genre_id FROM " + 5621 "audio WHERE _id=?)", uri.getPathSegments().get(3)); 5622 break; 5623 } 5624 case AUDIO_GENRES_ID: 5625 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 5626 // fall-through 5627 case AUDIO_GENRES: { 5628 qb.setTables("audio_genres"); 5629 qb.setProjectionMap(getProjectionMap(Audio.Genres.class)); 5630 break; 5631 } 5632 case AUDIO_GENRES_ID_MEMBERS: 5633 appendWhereStandalone(qb, "genre_id=?", uri.getPathSegments().get(3)); 5634 // fall-through 5635 case AUDIO_GENRES_ALL_MEMBERS: { 5636 if (type == TYPE_QUERY) { 5637 qb.setTables("audio"); 5638 5639 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 5640 getProjectionMap(Audio.Genres.Members.class)); 5641 projectionMap.put(Audio.Genres.Members.AUDIO_ID, 5642 "_id AS " + Audio.Genres.Members.AUDIO_ID); 5643 qb.setProjectionMap(projectionMap); 5644 } else { 5645 throw new UnsupportedOperationException("Genres cannot be directly modified"); 5646 } 5647 appendWhereStandaloneFilter(qb, new String[] { 5648 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY 5649 }, filter); 5650 // In order to be consistent with other audio views like audio_artist, audio_albums, 5651 // and audio_genres, exclude pending and trashed item 5652 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, MATCH_EXCLUDE, uri); 5653 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, MATCH_EXCLUDE, uri); 5654 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 5655 if (honored != null) { 5656 honored.accept(QUERY_ARG_MATCH_FAVORITE); 5657 } 5658 if (!includeAllVolumes) { 5659 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 5660 } 5661 break; 5662 } 5663 case AUDIO_PLAYLISTS_ID: 5664 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 5665 matchPending = MATCH_INCLUDE; 5666 matchTrashed = MATCH_INCLUDE; 5667 // fall-through 5668 case AUDIO_PLAYLISTS: { 5669 if (type == TYPE_QUERY) { 5670 qb.setTables("audio_playlists"); 5671 qb.setProjectionMap( 5672 getProjectionMap(Audio.Playlists.class)); 5673 } else { 5674 qb.setTables("files"); 5675 qb.setProjectionMap( 5676 getProjectionMap(Audio.Playlists.class, Files.FileColumns.class)); 5677 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 5678 FileColumns.MEDIA_TYPE_PLAYLIST); 5679 } 5680 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); 5681 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); 5682 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 5683 if (honored != null) { 5684 honored.accept(QUERY_ARG_MATCH_PENDING); 5685 honored.accept(QUERY_ARG_MATCH_TRASHED); 5686 honored.accept(QUERY_ARG_MATCH_FAVORITE); 5687 } 5688 if (!includeAllVolumes) { 5689 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 5690 } 5691 break; 5692 } 5693 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 5694 appendWhereStandalone(qb, "audio_playlists_map._id=?", 5695 uri.getPathSegments().get(5)); 5696 // fall-through 5697 case AUDIO_PLAYLISTS_ID_MEMBERS: { 5698 appendWhereStandalone(qb, "playlist_id=?", uri.getPathSegments().get(3)); 5699 if (type == TYPE_QUERY) { 5700 qb.setTables("audio_playlists_map, audio"); 5701 5702 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 5703 getProjectionMap(Audio.Playlists.Members.class)); 5704 projectionMap.put(Audio.Playlists.Members._ID, 5705 "audio_playlists_map._id AS " + Audio.Playlists.Members._ID); 5706 qb.setProjectionMap(projectionMap); 5707 5708 appendWhereStandalone(qb, "audio._id = audio_id"); 5709 // Since we use audio table along with audio_playlists_map 5710 // for querying, we should only include database rows of 5711 // the attached volumes. 5712 if (!includeAllVolumes) { 5713 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " 5714 + includeVolumes); 5715 } 5716 } else { 5717 qb.setTables("audio_playlists_map"); 5718 qb.setProjectionMap(getProjectionMap(Audio.Playlists.Members.class)); 5719 } 5720 appendWhereStandaloneFilter(qb, new String[] { 5721 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY 5722 }, filter); 5723 break; 5724 } 5725 case AUDIO_ALBUMART_ID: 5726 appendWhereStandalone(qb, "album_id=?", uri.getPathSegments().get(3)); 5727 // fall-through 5728 case AUDIO_ALBUMART: { 5729 qb.setTables("album_art"); 5730 5731 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 5732 getProjectionMap(Audio.Thumbnails.class)); 5733 projectionMap.put(Audio.Thumbnails._ID, 5734 "album_id AS " + Audio.Thumbnails._ID); 5735 qb.setProjectionMap(projectionMap); 5736 5737 break; 5738 } 5739 case AUDIO_ARTISTS_ID_ALBUMS: { 5740 if (type == TYPE_QUERY) { 5741 qb.setTables("audio_artists_albums"); 5742 qb.setProjectionMap(getProjectionMap(Audio.Artists.Albums.class)); 5743 5744 final String artistId = uri.getPathSegments().get(3); 5745 appendWhereStandalone(qb, "artist_id=?", artistId); 5746 } else { 5747 throw new UnsupportedOperationException("Albums cannot be directly modified"); 5748 } 5749 appendWhereStandaloneFilter(qb, new String[] { 5750 AudioColumns.ALBUM_KEY 5751 }, filter); 5752 break; 5753 } 5754 case AUDIO_ARTISTS_ID: 5755 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 5756 // fall-through 5757 case AUDIO_ARTISTS: { 5758 if (type == TYPE_QUERY) { 5759 qb.setTables("audio_artists"); 5760 qb.setProjectionMap(getProjectionMap(Audio.Artists.class)); 5761 } else { 5762 throw new UnsupportedOperationException("Artists cannot be directly modified"); 5763 } 5764 appendWhereStandaloneFilter(qb, new String[] { 5765 AudioColumns.ARTIST_KEY 5766 }, filter); 5767 break; 5768 } 5769 case AUDIO_ALBUMS_ID: 5770 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 5771 // fall-through 5772 case AUDIO_ALBUMS: { 5773 if (type == TYPE_QUERY) { 5774 qb.setTables("audio_albums"); 5775 qb.setProjectionMap(getProjectionMap(Audio.Albums.class)); 5776 } else { 5777 throw new UnsupportedOperationException("Albums cannot be directly modified"); 5778 } 5779 appendWhereStandaloneFilter(qb, new String[] { 5780 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY 5781 }, filter); 5782 break; 5783 } 5784 case VIDEO_MEDIA_ID: 5785 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 5786 matchPending = MATCH_INCLUDE; 5787 matchTrashed = MATCH_INCLUDE; 5788 // fall-through 5789 case VIDEO_MEDIA: { 5790 if (type == TYPE_QUERY) { 5791 qb.setTables("video"); 5792 qb.setProjectionMap( 5793 getProjectionMap(Video.Media.class)); 5794 } else { 5795 qb.setTables("files"); 5796 qb.setProjectionMap( 5797 getProjectionMap(Video.Media.class, Files.FileColumns.class)); 5798 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 5799 FileColumns.MEDIA_TYPE_VIDEO); 5800 } 5801 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); 5802 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); 5803 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 5804 if (honored != null) { 5805 honored.accept(QUERY_ARG_MATCH_PENDING); 5806 honored.accept(QUERY_ARG_MATCH_TRASHED); 5807 honored.accept(QUERY_ARG_MATCH_FAVORITE); 5808 } 5809 if (!includeAllVolumes) { 5810 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 5811 } 5812 break; 5813 } 5814 case VIDEO_THUMBNAILS_ID: 5815 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 5816 // fall-through 5817 case VIDEO_THUMBNAILS: { 5818 qb.setTables("videothumbnails"); 5819 qb.setProjectionMap(getProjectionMap(Video.Thumbnails.class)); 5820 break; 5821 } 5822 case FILES_ID: 5823 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2)); 5824 matchPending = MATCH_INCLUDE; 5825 matchTrashed = MATCH_INCLUDE; 5826 // fall-through 5827 case FILES: { 5828 qb.setTables("files"); 5829 qb.setProjectionMap(getProjectionMap(Files.FileColumns.class)); 5830 5831 appendWhereStandaloneFilter(qb, new String[] { 5832 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY 5833 }, filter); 5834 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); 5835 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); 5836 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 5837 if (honored != null) { 5838 honored.accept(QUERY_ARG_MATCH_PENDING); 5839 honored.accept(QUERY_ARG_MATCH_TRASHED); 5840 honored.accept(QUERY_ARG_MATCH_FAVORITE); 5841 } 5842 if (!includeAllVolumes) { 5843 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 5844 } 5845 break; 5846 } 5847 case DOWNLOADS_ID: 5848 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2)); 5849 matchPending = MATCH_INCLUDE; 5850 matchTrashed = MATCH_INCLUDE; 5851 // fall-through 5852 case DOWNLOADS: { 5853 if (type == TYPE_QUERY) { 5854 qb.setTables("downloads"); 5855 qb.setProjectionMap( 5856 getProjectionMap(Downloads.class)); 5857 } else { 5858 qb.setTables("files"); 5859 qb.setProjectionMap( 5860 getProjectionMap(Downloads.class, Files.FileColumns.class)); 5861 appendWhereStandalone(qb, FileColumns.IS_DOWNLOAD + "=1"); 5862 } 5863 5864 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); 5865 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); 5866 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 5867 if (honored != null) { 5868 honored.accept(QUERY_ARG_MATCH_PENDING); 5869 honored.accept(QUERY_ARG_MATCH_TRASHED); 5870 honored.accept(QUERY_ARG_MATCH_FAVORITE); 5871 } 5872 if (!includeAllVolumes) { 5873 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 5874 } 5875 break; 5876 } 5877 default: 5878 throw new UnsupportedOperationException( 5879 "Unknown or unsupported URL: " + uri.toString()); 5880 } 5881 5882 // To ensure we're enforcing our security model, all operations must 5883 // have a projection map configured 5884 if (qb.getProjectionMap() == null) { 5885 throw new IllegalStateException("All queries must have a projection map"); 5886 } 5887 5888 // If caller is an older app, we're willing to let through a 5889 // greylist of technically invalid columns 5890 if (getCallingPackageTargetSdkVersion() < Build.VERSION_CODES.Q) { 5891 qb.setProjectionGreylist(sGreylist); 5892 } 5893 5894 // Starting U, if owner package name is used in query arguments, 5895 // we are restricting result set to only self-owned packages. 5896 if (shouldFilterOwnerPackageNameFlag() && shouldFilterByOwnerPackageName(extras, type)) { 5897 Log.d(TAG, "Restricting result set to only packages owned by calling package: " 5898 + mCallingIdentity.get().getSharedPackagesAsString()); 5899 final String ownerPackageMatchClause = getWhereForOwnerPackageMatch( 5900 mCallingIdentity.get()); 5901 appendWhereStandalone(qb, ownerPackageMatchClause); 5902 } 5903 5904 return qb; 5905 } 5906 5907 private boolean shouldFilterByOwnerPackageName(Bundle queryArgs, int type) { 5908 return type == TYPE_QUERY && SdkLevel.isAtLeastU() && containsOwnerPackageName(queryArgs) 5909 && getContext().checkPermission(QUERY_ALL_PACKAGES, mCallingIdentity.get().pid, 5910 mCallingIdentity.get().uid) == PERMISSION_DENIED; 5911 } 5912 5913 private boolean containsOwnerPackageName(Bundle queryArgs) { 5914 final String selection = queryArgs.getString(QUERY_ARG_SQL_SELECTION, "") 5915 .toLowerCase(Locale.ROOT); 5916 final String groupBy = queryArgs.getString(QUERY_ARG_SQL_GROUP_BY, "") 5917 .toLowerCase(Locale.ROOT); 5918 final String sort = queryArgs.getString(QUERY_ARG_SQL_SORT_ORDER, "") 5919 .toLowerCase(Locale.ROOT); 5920 final String having = queryArgs.getString(QUERY_ARG_SQL_HAVING, "") 5921 .toLowerCase(Locale.ROOT); 5922 5923 return selection.contains(OWNER_PACKAGE_NAME) || groupBy.contains(OWNER_PACKAGE_NAME) 5924 || sort.contains(OWNER_PACKAGE_NAME) || having.contains(OWNER_PACKAGE_NAME); 5925 } 5926 5927 private void appendAccessCheckQuery(@NonNull SQLiteQueryBuilder qb, boolean forWrite, 5928 @NonNull Uri uri, int uriType, @NonNull Bundle extras, @NonNull String volumeName) { 5929 Objects.requireNonNull(extras); 5930 final Uri redactedUri = extras.getParcelable(QUERY_ARG_REDACTED_URI); 5931 5932 final boolean allowGlobal; 5933 if (redactedUri != null) { 5934 allowGlobal = checkCallingPermissionGlobal(redactedUri, false); 5935 } else { 5936 allowGlobal = checkCallingPermissionGlobal(uri, forWrite); 5937 } 5938 5939 if (allowGlobal) { 5940 return; 5941 } 5942 5943 if (hasAccessToCollection(mCallingIdentity.get(), uriType, forWrite)) { 5944 // has direct access to whole collection, no special filtering needed. 5945 return; 5946 } 5947 5948 final ArrayList<String> options = new ArrayList<>(); 5949 if (!MediaStore.VOLUME_INTERNAL.equals(volumeName) 5950 && hasUserSelectedAccess(mCallingIdentity.get(), uriType, forWrite)) { 5951 // If app has READ_MEDIA_VISUAL_USER_SELECTED permission, allow access on files granted 5952 // via PhotoPicker launched for Permission. These grants are defined in media_grants 5953 // table. 5954 // We exclude volume internal from the query because media_grants are not supported. 5955 options.add(getWhereForUserSelectedAccess(mCallingIdentity.get(), uriType)); 5956 } 5957 5958 // Allow access to files which are owned by the caller. Or allow access to files based on 5959 // legacy or any other special access permissions. 5960 options.add(getWhereForConstrainedAccess(mCallingIdentity.get(), uriType, forWrite, 5961 extras)); 5962 5963 appendWhereStandalone(qb, TextUtils.join(" OR ", options)); 5964 } 5965 5966 /** 5967 * @return {@code true} if app requests to include database rows from 5968 * recently unmounted volume. 5969 * {@code false} otherwise. 5970 */ 5971 private boolean shouldIncludeRecentlyUnmountedVolumes(Uri uri, Bundle extras) { 5972 if (isFuseThread()) { 5973 // File path requests don't require to query from unmounted volumes. 5974 return false; 5975 } 5976 5977 boolean isIncludeVolumesChangeEnabled = SdkLevel.isAtLeastS() && 5978 CompatChanges.isChangeEnabled(ENABLE_INCLUDE_ALL_VOLUMES, Binder.getCallingUid()); 5979 if ("1".equals(uri.getQueryParameter(ALL_VOLUMES))) { 5980 // Support uri parameter only in R OS and below. Apps should use 5981 // MediaStore#QUERY_ARG_RECENTLY_UNMOUNTED_VOLUMES on S OS onwards. 5982 if (!isIncludeVolumesChangeEnabled) { 5983 return true; 5984 } 5985 throw new IllegalArgumentException("Unsupported uri parameter \"all_volumes\""); 5986 } 5987 if (isIncludeVolumesChangeEnabled) { 5988 // MediaStore#QUERY_ARG_INCLUDE_RECENTLY_UNMOUNTED_VOLUMES is only supported on S OS and 5989 // for app targeting targetSdk>=S. 5990 return extras.getBoolean(MediaStore.QUERY_ARG_INCLUDE_RECENTLY_UNMOUNTED_VOLUMES, 5991 false); 5992 } 5993 return false; 5994 } 5995 5996 /** 5997 * Determine if given {@link Uri} has a 5998 * {@link MediaColumns#OWNER_PACKAGE_NAME} column. 5999 */ 6000 private boolean hasOwnerPackageName(Uri uri) { 6001 // It's easier to maintain this as an inverted list 6002 final int table = matchUri(uri, true); 6003 switch (table) { 6004 case IMAGES_THUMBNAILS_ID: 6005 case IMAGES_THUMBNAILS: 6006 case VIDEO_THUMBNAILS_ID: 6007 case VIDEO_THUMBNAILS: 6008 case AUDIO_ALBUMART: 6009 case AUDIO_ALBUMART_ID: 6010 case AUDIO_ALBUMART_FILE_ID: 6011 return false; 6012 default: 6013 return true; 6014 } 6015 } 6016 6017 /** 6018 * @deprecated all operations should be routed through the overload that 6019 * accepts a {@link Bundle} of extras. 6020 */ 6021 @Override 6022 @Deprecated 6023 public int delete(Uri uri, String selection, String[] selectionArgs) { 6024 return delete(uri, 6025 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null)); 6026 } 6027 6028 @Override 6029 public int delete(@NonNull Uri uri, @Nullable Bundle extras) { 6030 Trace.beginSection("MP.delete [" + uri + ']'); 6031 try { 6032 return deleteInternal(uri, extras); 6033 } catch (FallbackException e) { 6034 return e.translateForUpdateDelete(getCallingPackageTargetSdkVersion()); 6035 } finally { 6036 Trace.endSection(); 6037 } 6038 } 6039 6040 private int deleteInternal(@NonNull Uri uri, @Nullable Bundle extras) 6041 throws FallbackException { 6042 final String volumeName = getVolumeName(uri); 6043 PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName); 6044 6045 extras = (extras != null) ? extras : new Bundle(); 6046 // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider. 6047 extras.remove(QUERY_ARG_REDACTED_URI); 6048 6049 if (isRedactedUri(uri)) { 6050 // we don't support deletion on redacted uris. 6051 return 0; 6052 } 6053 6054 // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. 6055 extras.remove(INCLUDED_DEFAULT_DIRECTORIES); 6056 6057 uri = safeUncanonicalize(uri); 6058 final boolean allowHidden = isCallingPackageAllowedHidden(); 6059 final int match = matchUri(uri, allowHidden); 6060 6061 switch (match) { 6062 case AUDIO_MEDIA_ID: 6063 case AUDIO_PLAYLISTS_ID: 6064 case VIDEO_MEDIA_ID: 6065 case IMAGES_MEDIA_ID: 6066 case DOWNLOADS_ID: 6067 case FILES_ID: { 6068 if (!isFuseThread() && getCachedCallingIdentityForFuse(Binder.getCallingUid()). 6069 removeDeletedRowId(Long.parseLong(uri.getLastPathSegment()))) { 6070 // Apps sometimes delete the file via filePath and then try to delete the db row 6071 // using MediaProvider#delete. Since we would have already deleted the db row 6072 // during the filePath operation, the latter will result in a security 6073 // exception. Apps which don't expect an exception will break here. Since we 6074 // have already deleted the db row, silently return zero as deleted count. 6075 return 0; 6076 } 6077 } 6078 break; 6079 default: 6080 // For other match types, given uri will not correspond to a valid file. 6081 break; 6082 } 6083 6084 final String userWhere = extras.getString(QUERY_ARG_SQL_SELECTION); 6085 final String[] userWhereArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS); 6086 6087 int count = 0; 6088 6089 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 6090 6091 // handle MEDIA_SCANNER before calling getDatabaseForUri() 6092 if (match == MEDIA_SCANNER) { 6093 if (mMediaScannerVolume == null) { 6094 return 0; 6095 } 6096 6097 final DatabaseHelper helper = getDatabaseForUri( 6098 MediaStore.Files.getContentUri(mMediaScannerVolume)); 6099 6100 helper.mScanStopTime = SystemClock.elapsedRealtime(); 6101 6102 mMediaScannerVolume = null; 6103 return 1; 6104 } 6105 6106 if (match == VOLUMES_ID) { 6107 detachVolume(uri); 6108 count = 1; 6109 } 6110 6111 final DatabaseHelper helper = getDatabaseForUri(uri); 6112 switch (match) { 6113 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 6114 extras.putString(QUERY_ARG_SQL_SELECTION, 6115 BaseColumns._ID + "=" + uri.getPathSegments().get(5)); 6116 // fall-through 6117 case AUDIO_PLAYLISTS_ID_MEMBERS: { 6118 final long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 6119 final Uri playlistUri = ContentUris.withAppendedId( 6120 MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId); 6121 6122 // Playlist contents are always persisted directly into playlist 6123 // files on disk to ensure that we can reliably migrate between 6124 // devices and recover from database corruption 6125 int numOfRemovedPlaylistMembers = removePlaylistMembers(playlistUri, extras); 6126 if (numOfRemovedPlaylistMembers > 0) { 6127 acceptWithExpansion(helper::notifyDelete, volumeName, playlistId, 6128 FileColumns.MEDIA_TYPE_PLAYLIST, false); 6129 } 6130 return numOfRemovedPlaylistMembers; 6131 } 6132 } 6133 6134 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, match, uri, extras, null); 6135 6136 { 6137 // Give callers interacting with a specific media item a chance to 6138 // escalate access if they don't already have it 6139 switch (match) { 6140 case AUDIO_MEDIA_ID: 6141 case VIDEO_MEDIA_ID: 6142 case IMAGES_MEDIA_ID: 6143 enforceCallingPermission(uri, extras, true); 6144 } 6145 6146 final String[] projection = new String[] { 6147 FileColumns.MEDIA_TYPE, 6148 FileColumns.DATA, 6149 FileColumns._ID, 6150 FileColumns.IS_DOWNLOAD, 6151 FileColumns.MIME_TYPE, 6152 }; 6153 final boolean isFilesTable = qb.getTables().equals("files"); 6154 final LongSparseArray<String> deletedDownloadIds = new LongSparseArray<>(); 6155 final int[] countPerMediaType = new int[FileColumns.MEDIA_TYPE_COUNT]; 6156 if (isFilesTable) { 6157 String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA); 6158 if (deleteparam == null || ! deleteparam.equals("false")) { 6159 Cursor c = qb.query(helper, projection, userWhere, userWhereArgs, 6160 null, null, null, null, null); 6161 try { 6162 while (c.moveToNext()) { 6163 final int mediaType = c.getInt(0); 6164 final String data = c.getString(1); 6165 final long id = c.getLong(2); 6166 final int isDownload = c.getInt(3); 6167 final String mimeType = c.getString(4); 6168 6169 // TODO(b/188782594) Consider logging mime type access on delete too. 6170 6171 // Forget that caller is owner of this item 6172 mCallingIdentity.get().setOwned(id, false); 6173 6174 deleteIfAllowed(uri, extras, data); 6175 int res = qb.delete(helper, BaseColumns._ID + "=" + id, null); 6176 count += res; 6177 // Avoid ArrayIndexOutOfBounds if more mediaTypes are added, 6178 // but mediaTypeSize is not updated 6179 if (res > 0 && mediaType < countPerMediaType.length) { 6180 countPerMediaType[mediaType] += res; 6181 } 6182 6183 if (isDownload == 1) { 6184 deletedDownloadIds.put(id, mimeType); 6185 } 6186 } 6187 } finally { 6188 FileUtils.closeQuietly(c); 6189 } 6190 // Do not allow deletion if the file/object is referenced as parent 6191 // by some other entries. It could cause database corruption. 6192 appendWhereStandalone(qb, ID_NOT_PARENT_CLAUSE); 6193 } 6194 } 6195 6196 switch (match) { 6197 case AUDIO_GENRES_ID_MEMBERS: 6198 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R); 6199 6200 case IMAGES_THUMBNAILS_ID: 6201 case IMAGES_THUMBNAILS: 6202 case VIDEO_THUMBNAILS_ID: 6203 case VIDEO_THUMBNAILS: 6204 // Delete the referenced files first. 6205 Cursor c = qb.query(helper, sDataOnlyColumn, userWhere, userWhereArgs, null, 6206 null, null, null, null); 6207 if (c != null) { 6208 try { 6209 while (c.moveToNext()) { 6210 deleteIfAllowed(uri, extras, c.getString(0)); 6211 } 6212 } finally { 6213 FileUtils.closeQuietly(c); 6214 } 6215 } 6216 count += deleteRecursive(qb, helper, userWhere, userWhereArgs); 6217 break; 6218 6219 default: 6220 count += deleteRecursive(qb, helper, userWhere, userWhereArgs); 6221 break; 6222 } 6223 6224 if (deletedDownloadIds.size() > 0) { 6225 notifyDownloadManagerOnDelete(helper, deletedDownloadIds); 6226 } 6227 6228 // Check for other URI format grants for File API call only. Check right before 6229 // returning count = 0, to leave positive cases performance unaffected. 6230 if (count == 0 && isFuseThread()) { 6231 count += deleteWithOtherUriGrants(uri, helper, projection, userWhere, userWhereArgs, 6232 extras); 6233 } 6234 6235 if (isFilesTable && !isCallingPackageSelf()) { 6236 Metrics.logDeletion(volumeName, mCallingIdentity.get().uid, 6237 getCallingPackageOrSelf(), count, countPerMediaType); 6238 } 6239 } 6240 6241 return count; 6242 } 6243 6244 private int deleteWithOtherUriGrants(@NonNull Uri uri, DatabaseHelper helper, 6245 String[] projection, String userWhere, String[] userWhereArgs, 6246 @Nullable Bundle extras) { 6247 try (Cursor c = queryForSingleItemAsMediaProvider(uri, projection, userWhere, userWhereArgs, 6248 null)) { 6249 final int mediaType = c.getInt(0); 6250 final String data = c.getString(1); 6251 final long id = c.getLong(2); 6252 final int isDownload = c.getInt(3); 6253 final String mimeType = c.getString(4); 6254 6255 final Uri uriGranted = getOtherUriGrantsForPath(data, mediaType, Long.toString(id), 6256 /* forWrite */ true); 6257 if (uriGranted != null) { 6258 // 1. delete file 6259 deleteIfAllowed(uriGranted, extras, data); 6260 // 2. delete file row from the db 6261 final boolean allowHidden = isCallingPackageAllowedHidden(); 6262 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, 6263 matchUri(uriGranted, allowHidden), uriGranted, extras, null); 6264 int count = qb.delete(helper, BaseColumns._ID + "=" + id, null); 6265 6266 if (isDownload == 1) { 6267 final LongSparseArray<String> deletedDownloadIds = new LongSparseArray<>(); 6268 deletedDownloadIds.put(id, mimeType); 6269 notifyDownloadManagerOnDelete(helper, deletedDownloadIds); 6270 } 6271 return count; 6272 } 6273 } catch (FileNotFoundException ignored) { 6274 // Do nothing. Returns 0 files deleted. 6275 } 6276 return 0; 6277 } 6278 6279 private void notifyDownloadManagerOnDelete(DatabaseHelper helper, 6280 LongSparseArray<String> deletedDownloadIds) { 6281 // Do this on a background thread, since we don't want to make binder 6282 // calls as part of a FUSE call. 6283 helper.postBackground(() -> { 6284 DownloadManager dm = getContext().getSystemService(DownloadManager.class); 6285 if (dm != null) { 6286 dm.onMediaStoreDownloadsDeleted(deletedDownloadIds); 6287 } 6288 }); 6289 } 6290 6291 /** 6292 * Executes identical delete repeatedly within a single transaction until 6293 * stability is reached. Combined with {@link #ID_NOT_PARENT_CLAUSE}, this 6294 * can be used to recursively delete all matching entries, since it only 6295 * deletes parents when no references remaining. 6296 */ 6297 private int deleteRecursive(SQLiteQueryBuilder qb, DatabaseHelper helper, String userWhere, 6298 String[] userWhereArgs) { 6299 return helper.runWithTransaction((db) -> { 6300 synchronized (mDirectoryCache) { 6301 mDirectoryCache.clear(); 6302 } 6303 6304 int n = 0; 6305 int total = 0; 6306 do { 6307 n = qb.delete(helper, userWhere, userWhereArgs); 6308 total += n; 6309 } while (n > 0); 6310 return total; 6311 }); 6312 } 6313 6314 @Nullable 6315 @VisibleForTesting 6316 Uri getRedactedUri(@NonNull Uri uri) { 6317 if (!isUriSupportedForRedaction(uri)) { 6318 return null; 6319 } 6320 6321 DatabaseHelper helper; 6322 try { 6323 helper = getDatabaseForUri(uri); 6324 } catch (VolumeNotFoundException e) { 6325 throw e.rethrowAsIllegalArgumentException(); 6326 } 6327 6328 try (final Cursor c = helper.runWithoutTransaction( 6329 (db) -> db.query("files", 6330 new String[]{FileColumns.REDACTED_URI_ID}, FileColumns._ID + "=?", 6331 new String[]{uri.getLastPathSegment()}, null, null, null))) { 6332 // Database entry for uri not found. 6333 if (!c.moveToFirst()) return null; 6334 6335 String redactedUriID = c.getString(c.getColumnIndex(FileColumns.REDACTED_URI_ID)); 6336 if (redactedUriID == null) { 6337 // No redacted has even been created for this uri. Create a new redacted URI ID for 6338 // the uri and store it in the DB. 6339 redactedUriID = REDACTED_URI_ID_PREFIX + UUID.randomUUID().toString().replace("-", 6340 ""); 6341 6342 ContentValues cv = new ContentValues(); 6343 cv.put(FileColumns.REDACTED_URI_ID, redactedUriID); 6344 int rowsAffected = helper.runWithTransaction( 6345 (db) -> db.update("files", cv, FileColumns._ID + "=?", 6346 new String[]{uri.getLastPathSegment()})); 6347 if (rowsAffected == 0) { 6348 // this shouldn't happen ideally, only reason this might happen is if the db 6349 // entry got deleted in b/w in which case we should return null. 6350 return null; 6351 } 6352 } 6353 6354 // Create and return a uri with ID = redactedUriID. 6355 final Uri.Builder builder = ContentUris.removeId(uri).buildUpon(); 6356 builder.appendPath(redactedUriID); 6357 6358 return builder.build(); 6359 } 6360 } 6361 6362 @NonNull 6363 @VisibleForTesting 6364 List<Uri> getRedactedUri(@NonNull List<Uri> uris) { 6365 ArrayList<Uri> redactedUris = new ArrayList<>(); 6366 for (Uri uri : uris) { 6367 redactedUris.add(getRedactedUri(uri)); 6368 } 6369 6370 return redactedUris; 6371 } 6372 6373 @Override 6374 public Bundle call(String method, String arg, Bundle extras) { 6375 Trace.beginSection("MP.call [" + method + ']'); 6376 try { 6377 return callInternal(method, arg, extras); 6378 } finally { 6379 Trace.endSection(); 6380 } 6381 } 6382 6383 private Bundle callInternal(String method, String arg, Bundle extras) { 6384 switch (method) { 6385 case MediaStore.RESOLVE_PLAYLIST_MEMBERS_CALL: { 6386 final LocalCallingIdentity token = clearLocalCallingIdentity(); 6387 final CallingIdentity providerToken = clearCallingIdentity(); 6388 try { 6389 final Uri playlistUri = extras.getParcelable(MediaStore.EXTRA_URI); 6390 resolvePlaylistMembers(playlistUri); 6391 } finally { 6392 restoreCallingIdentity(providerToken); 6393 restoreLocalCallingIdentity(token); 6394 } 6395 return null; 6396 } 6397 case MediaStore.RUN_IDLE_MAINTENANCE_CALL: { 6398 // Protect ourselves from random apps by requiring a generic 6399 // permission held by common debugging components, such as shell 6400 getContext().enforceCallingOrSelfPermission( 6401 android.Manifest.permission.DUMP, TAG); 6402 final LocalCallingIdentity token = clearLocalCallingIdentity(); 6403 final CallingIdentity providerToken = clearCallingIdentity(); 6404 try { 6405 onIdleMaintenance(new CancellationSignal()); 6406 } finally { 6407 restoreCallingIdentity(providerToken); 6408 restoreLocalCallingIdentity(token); 6409 } 6410 return null; 6411 } 6412 case MediaStore.WAIT_FOR_IDLE_CALL: { 6413 // TODO(b/195009139): Remove after overriding wait for idle in test to sync picker 6414 // Syncing the picker while waiting for idle fixes tests with the picker db 6415 // flag enabled because the picker db is in a consistent state with the external 6416 // db after the sync 6417 syncAllMedia(); 6418 ForegroundThread.waitForIdle(); 6419 final CountDownLatch latch = new CountDownLatch(1); 6420 BackgroundThread.getExecutor().execute(latch::countDown); 6421 try { 6422 latch.await(30, TimeUnit.SECONDS); 6423 } catch (InterruptedException e) { 6424 throw new IllegalStateException(e); 6425 } 6426 return null; 6427 } 6428 case MediaStore.SCAN_FILE_CALL: { 6429 final LocalCallingIdentity token = clearLocalCallingIdentity(); 6430 final CallingIdentity providerToken = clearCallingIdentity(); 6431 6432 final String filePath = arg; 6433 final Uri uri; 6434 try { 6435 File file; 6436 try { 6437 file = FileUtils.getCanonicalFile(filePath); 6438 } catch (IOException e) { 6439 file = null; 6440 } 6441 6442 uri = file != null ? scanFile(file, REASON_DEMAND) : null; 6443 } finally { 6444 restoreCallingIdentity(providerToken); 6445 restoreLocalCallingIdentity(token); 6446 } 6447 6448 // TODO(b/262244882): maybe enforceCallingPermissionInternal(uri, ...) 6449 6450 final Bundle res = new Bundle(); 6451 res.putParcelable(Intent.EXTRA_STREAM, uri); 6452 return res; 6453 } 6454 case MediaStore.SCAN_VOLUME_CALL: { 6455 final int userId = uidToUserId(Binder.getCallingUid()); 6456 final LocalCallingIdentity token = clearLocalCallingIdentity(); 6457 final CallingIdentity providerToken = clearCallingIdentity(); 6458 6459 final String volumeName = arg; 6460 try { 6461 final MediaVolume volume = mVolumeCache.findVolume(volumeName, 6462 UserHandle.of(userId)); 6463 MediaService.onScanVolume(getContext(), volume, REASON_DEMAND); 6464 } catch (FileNotFoundException e) { 6465 Log.w(TAG, "Failed to find volume " + volumeName, e); 6466 } catch (IOException e) { 6467 throw new RuntimeException(e); 6468 } finally { 6469 restoreCallingIdentity(providerToken); 6470 restoreLocalCallingIdentity(token); 6471 } 6472 return Bundle.EMPTY; 6473 } 6474 case MediaStore.GET_VERSION_CALL: { 6475 final String volumeName = extras.getString(Intent.EXTRA_TEXT); 6476 6477 final DatabaseHelper helper; 6478 try { 6479 helper = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName)); 6480 } catch (VolumeNotFoundException e) { 6481 throw e.rethrowAsIllegalArgumentException(); 6482 } 6483 6484 final String version = helper.runWithoutTransaction((db) -> 6485 db.getVersion() + ":" + DatabaseHelper.getOrCreateUuid(db)); 6486 6487 final Bundle res = new Bundle(); 6488 res.putString(Intent.EXTRA_TEXT, version); 6489 return res; 6490 } 6491 case MediaStore.GET_GENERATION_CALL: { 6492 final String volumeName = extras.getString(Intent.EXTRA_TEXT); 6493 6494 final DatabaseHelper helper; 6495 try { 6496 helper = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName)); 6497 } catch (VolumeNotFoundException e) { 6498 throw e.rethrowAsIllegalArgumentException(); 6499 } 6500 6501 final long generation = helper.runWithoutTransaction(DatabaseHelper::getGeneration); 6502 6503 final Bundle res = new Bundle(); 6504 res.putLong(Intent.EXTRA_INDEX, generation); 6505 return res; 6506 } 6507 case MediaStore.GET_DOCUMENT_URI_CALL: { 6508 final Uri mediaUri = extras.getParcelable(MediaStore.EXTRA_URI); 6509 enforceCallingPermission(mediaUri, extras, false); 6510 6511 final Uri fileUri; 6512 final LocalCallingIdentity token = clearLocalCallingIdentity(); 6513 try { 6514 fileUri = Uri.fromFile(queryForDataFile(mediaUri, null)); 6515 } catch (FileNotFoundException e) { 6516 throw new IllegalArgumentException(e); 6517 } finally { 6518 restoreLocalCallingIdentity(token); 6519 } 6520 6521 try (ContentProviderClient client = getContext().getContentResolver() 6522 .acquireUnstableContentProviderClient( 6523 getExternalStorageProviderAuthority())) { 6524 extras.putParcelable(MediaStore.EXTRA_URI, fileUri); 6525 return client.call(method, null, extras); 6526 } catch (RemoteException e) { 6527 throw new IllegalStateException(e); 6528 } 6529 } 6530 case MediaStore.GET_MEDIA_URI_CALL: { 6531 final Uri documentUri = extras.getParcelable(MediaStore.EXTRA_URI); 6532 getContext().enforceCallingUriPermission(documentUri, 6533 Intent.FLAG_GRANT_READ_URI_PERMISSION, TAG); 6534 6535 final int callingPid = mCallingIdentity.get().pid; 6536 final int callingUid = mCallingIdentity.get().uid; 6537 final String callingPackage = getCallingPackage(); 6538 final CallingIdentity token = clearCallingIdentity(); 6539 final String authority = documentUri.getAuthority(); 6540 6541 if (!authority.equals(MediaDocumentsProvider.AUTHORITY) && 6542 !authority.equals(DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) { 6543 throw new IllegalArgumentException("Provider for this Uri is not supported."); 6544 } 6545 6546 try (ContentProviderClient client = getContext().getContentResolver() 6547 .acquireUnstableContentProviderClient(authority)) { 6548 final Bundle clientRes = client.call(method, null, extras); 6549 final Uri fileUri = clientRes.getParcelable(MediaStore.EXTRA_URI); 6550 final Bundle res = new Bundle(); 6551 final Uri mediaStoreUri = fileUri.getAuthority().equals(MediaStore.AUTHORITY) ? 6552 fileUri : queryForMediaUri(new File(fileUri.getPath()), null); 6553 copyUriPermissionGrants(documentUri, mediaStoreUri, callingPid, 6554 callingUid, callingPackage); 6555 res.putParcelable(MediaStore.EXTRA_URI, mediaStoreUri); 6556 return res; 6557 } catch (FileNotFoundException e) { 6558 throw new IllegalArgumentException(e); 6559 } catch (RemoteException e) { 6560 throw new IllegalStateException(e); 6561 } finally { 6562 restoreCallingIdentity(token); 6563 } 6564 } 6565 case MediaStore.GET_REDACTED_MEDIA_URI_CALL: { 6566 final Uri uri = extras.getParcelable(MediaStore.EXTRA_URI); 6567 // NOTE: It is ok to update the DB and return a redacted URI for the cases when 6568 // the user code only has read access, hence we don't check for write permission. 6569 enforceCallingPermission(uri, Bundle.EMPTY, false); 6570 final LocalCallingIdentity token = clearLocalCallingIdentity(); 6571 try { 6572 final Bundle res = new Bundle(); 6573 res.putParcelable(MediaStore.EXTRA_URI, getRedactedUri(uri)); 6574 return res; 6575 } finally { 6576 restoreLocalCallingIdentity(token); 6577 } 6578 } 6579 case MediaStore.GET_REDACTED_MEDIA_URI_LIST_CALL: { 6580 final List<Uri> uris = extras.getParcelableArrayList(MediaStore.EXTRA_URI_LIST); 6581 // NOTE: It is ok to update the DB and return a redacted URI for the cases when 6582 // the user code only has read access, hence we don't check for write permission. 6583 enforceCallingPermission(uris, false); 6584 final LocalCallingIdentity token = clearLocalCallingIdentity(); 6585 try { 6586 final Bundle res = new Bundle(); 6587 res.putParcelableArrayList(MediaStore.EXTRA_URI_LIST, 6588 (ArrayList<? extends Parcelable>) getRedactedUri(uris)); 6589 return res; 6590 } finally { 6591 restoreLocalCallingIdentity(token); 6592 } 6593 } 6594 case MediaStore.GRANT_MEDIA_READ_FOR_PACKAGE_CALL: { 6595 final int caller = Binder.getCallingUid(); 6596 int userId; 6597 final List<Uri> uris; 6598 String packageName; 6599 if (checkPermissionSelf(caller)) { 6600 // If the caller is MediaProvider the accepted parameters are EXTRA_URI_LIST 6601 // and EXTRA_UID. 6602 if (!extras.containsKey( 6603 MediaStore.EXTRA_URI_LIST) 6604 && !extras.containsKey(Intent.EXTRA_UID)) { 6605 throw new IllegalArgumentException( 6606 "Missing required extras arguments: EXTRA_URI_LIST or" 6607 + " EXTRA_UID"); 6608 } 6609 uris = extras.getParcelableArrayList(MediaStore.EXTRA_URI_LIST); 6610 final PackageManager pm = getContext().getPackageManager(); 6611 final int packageUid = extras.getInt(Intent.EXTRA_UID); 6612 packageName = pm.getNameForUid(packageUid); 6613 // Get the userId from packageUid as the initiator could be a cloned app, which 6614 // accesses Media via MP of its parent user and Binder's callingUid reflects 6615 // the latter. 6616 userId = uidToUserId(packageUid); 6617 if (packageName.contains(":")) { 6618 // Check if the package name includes the package uid. This is expected 6619 // for packages that are referencing a shared user. PackageManager will 6620 // return a string such as <packagename>:<uid> in this instance. 6621 packageName = packageName.split(":")[0]; 6622 } 6623 } else if (checkPermissionShell(caller)) { 6624 // If the caller is the shell, the accepted parameters are EXTRA_URI (as string) 6625 // and EXTRA_PACKAGE_NAME (as string). 6626 if (!extras.containsKey(MediaStore.EXTRA_URI) 6627 && !extras.containsKey(Intent.EXTRA_PACKAGE_NAME)) { 6628 throw new IllegalArgumentException( 6629 "Missing required extras arguments: EXTRA_URI or" 6630 + " EXTRA_PACKAGE_NAME"); 6631 } 6632 packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME); 6633 uris = List.of(Uri.parse(extras.getString(MediaStore.EXTRA_URI))); 6634 userId = uidToUserId(caller); 6635 } else { 6636 // All other callers are unauthorized. 6637 throw new SecurityException("Create media grants not allowed. " 6638 + " Calling app ID:" + UserHandle.getAppId(Binder.getCallingUid()) 6639 + " Calling UID:" + Binder.getCallingUid() 6640 + " Media Provider app ID:" + UserHandle.getAppId(MY_UID) 6641 + " Media Provider UID:" + MY_UID); 6642 } 6643 6644 mMediaGrants.addMediaGrantsForPackage(packageName, uris, userId); 6645 return null; 6646 } 6647 case MediaStore.CREATE_WRITE_REQUEST_CALL: 6648 case MediaStore.CREATE_FAVORITE_REQUEST_CALL: 6649 case MediaStore.CREATE_TRASH_REQUEST_CALL: 6650 case MediaStore.CREATE_DELETE_REQUEST_CALL: { 6651 final PendingIntent pi = createRequest(method, extras); 6652 final Bundle res = new Bundle(); 6653 res.putParcelable(MediaStore.EXTRA_RESULT, pi); 6654 return res; 6655 } 6656 case MediaStore.IS_SYSTEM_GALLERY_CALL: 6657 final LocalCallingIdentity token = clearLocalCallingIdentity(); 6658 try { 6659 String packageName = arg; 6660 int uid = extras.getInt(MediaStore.EXTRA_IS_SYSTEM_GALLERY_UID); 6661 boolean isSystemGallery = PermissionUtils.checkWriteImagesOrVideoAppOps( 6662 getContext(), uid, packageName, getContext().getAttributionTag()); 6663 Bundle res = new Bundle(); 6664 res.putBoolean(MediaStore.EXTRA_IS_SYSTEM_GALLERY_RESPONSE, isSystemGallery); 6665 return res; 6666 } finally { 6667 restoreLocalCallingIdentity(token); 6668 } 6669 case MediaStore.GET_CLOUD_PROVIDER_CALL: { 6670 // TODO(b/245746037): replace UID check with Permission(MANAGE_CLOUD_MEDIA_PROVIDER) 6671 // PhotoPickerSettingsActivity will run as either the primary or the managed user. 6672 // Since the activity shows both personal and work tabs, it will have to make get 6673 // cloud provider IPC call to both instances of Media Provider - one running as 6674 // primary profile and the other as managed profile. Hence, UID check will not be 6675 // feasible here. 6676 if (!checkPermissionSelf(Binder.getCallingUid())) { 6677 throw new SecurityException("Get cloud provider not allowed. " 6678 + " Calling app ID:" + UserHandle.getAppId(Binder.getCallingUid()) 6679 + " Calling UID:" + Binder.getCallingUid() 6680 + " Media Provider app ID:" + UserHandle.getAppId(MY_UID) 6681 + " Media Provider UID:" + MY_UID); 6682 } 6683 final Bundle bundle = new Bundle(); 6684 bundle.putString(MediaStore.GET_CLOUD_PROVIDER_RESULT, 6685 mPickerSyncController.getCloudProvider()); 6686 return bundle; 6687 } 6688 case MediaStore.SET_CLOUD_PROVIDER_CALL: { 6689 // TODO(b/267327327): Add permission check before updating cloud provider. Also 6690 // validate the new cloud provider before setting it by using 6691 // PickerSyncController#setCloudProvider instead of 6692 // PickerSyncController#forceSetCloudProvider. 6693 final String cloudProvider = extras.getString(MediaStore.EXTRA_CLOUD_PROVIDER); 6694 Log.i(TAG, "Request received to set cloud provider to " + cloudProvider); 6695 mPickerSyncController.forceSetCloudProvider(cloudProvider); 6696 Log.i(TAG, "Completed request to set cloud provider to " + cloudProvider); 6697 6698 // Cannot start sync here yet because currently sync and other picker related 6699 // queries like SET_CLOUD_PROVIDER_CALL and GET_CLOUD_PROVIDER use the same lock. 6700 // If we start sync here and then user tries to return to the Picker or change the 6701 // provider again, Picker will ANR and crash. 6702 return new Bundle(); 6703 } 6704 case MediaStore.SYNC_PROVIDERS_CALL: { 6705 syncAllMedia(); 6706 return new Bundle(); 6707 } 6708 case MediaStore.IS_SUPPORTED_CLOUD_PROVIDER_CALL: { 6709 final boolean isSupported = mPickerSyncController.isProviderSupported(arg, 6710 Binder.getCallingUid()); 6711 6712 Bundle bundle = new Bundle(); 6713 bundle.putBoolean(MediaStore.EXTRA_CLOUD_PROVIDER_RESULT, isSupported); 6714 return bundle; 6715 } 6716 case MediaStore.IS_CURRENT_CLOUD_PROVIDER_CALL: { 6717 final boolean isEnabled = mPickerSyncController.isProviderEnabled(arg, 6718 Binder.getCallingUid()); 6719 6720 Bundle bundle = new Bundle(); 6721 bundle.putBoolean(MediaStore.EXTRA_CLOUD_PROVIDER_RESULT, isEnabled); 6722 return bundle; 6723 } 6724 case MediaStore.NOTIFY_CLOUD_MEDIA_CHANGED_EVENT_CALL: { 6725 final boolean notifyCloudEventResult; 6726 if (mPickerSyncController.isProviderEnabled(arg, Binder.getCallingUid())) { 6727 mPickerSyncController.notifyMediaEvent(); 6728 notifyCloudEventResult = true; 6729 } else { 6730 notifyCloudEventResult = false; 6731 } 6732 6733 Bundle bundle = new Bundle(); 6734 bundle.putBoolean(MediaStore.EXTRA_CLOUD_PROVIDER_RESULT, 6735 notifyCloudEventResult); 6736 return bundle; 6737 } 6738 case MediaStore.USES_FUSE_PASSTHROUGH: { 6739 boolean isEnabled = false; 6740 try { 6741 FuseDaemon daemon = getFuseDaemonForFile(new File(arg), mVolumeCache); 6742 if (daemon != null) { 6743 isEnabled = daemon.usesFusePassthrough(); 6744 } 6745 } catch (FileNotFoundException e) { 6746 } 6747 6748 Bundle bundle = new Bundle(); 6749 bundle.putBoolean(MediaStore.USES_FUSE_PASSTHROUGH_RESULT, isEnabled); 6750 return bundle; 6751 } 6752 case MediaStore.RUN_IDLE_MAINTENANCE_FOR_STABLE_URIS: { 6753 backupDatabases(null); 6754 return new Bundle(); 6755 } 6756 case MediaStore.READ_BACKED_UP_FILE_PATHS: { 6757 getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE, 6758 "Permission missing to call READ_BACKED_UP_FILE_PATHS by " 6759 + "uid:" + Binder.getCallingUid()); 6760 List<String> cumulatedValues = new ArrayList<String>(); 6761 String[] backedUpFilePaths; 6762 String lastReadValue = ""; 6763 while (true) { 6764 backedUpFilePaths = mDatabaseBackupAndRecovery.readBackedUpFilePaths(arg, 6765 lastReadValue, LEVEL_DB_READ_LIMIT); 6766 if (backedUpFilePaths.length <= 0) { 6767 break; 6768 } 6769 cumulatedValues.addAll(Arrays.asList(backedUpFilePaths)); 6770 if (backedUpFilePaths.length < LEVEL_DB_READ_LIMIT) { 6771 break; 6772 } 6773 lastReadValue = backedUpFilePaths[backedUpFilePaths.length - 1]; 6774 } 6775 6776 Bundle bundle = new Bundle(); 6777 Object[] values = cumulatedValues.toArray(); 6778 String[] resultArray = Arrays.copyOf(values, values.length, String[].class); 6779 bundle.putStringArray(READ_BACKED_UP_FILE_PATHS, resultArray); 6780 return bundle; 6781 } 6782 case MediaStore.DELETE_BACKED_UP_FILE_PATHS: 6783 getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE, 6784 "Permission missing to call DELETE_BACKED_UP_FILE_PATHS by " 6785 + "uid:" + Binder.getCallingUid()); 6786 mDatabaseBackupAndRecovery.deleteBackupForVolume(arg); 6787 return new Bundle(); 6788 case MediaStore.GET_BACKUP_FILES: 6789 getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE, 6790 "Permission missing to call GET_BACKUP_FILES by " 6791 + "uid:" + Binder.getCallingUid()); 6792 List<File> backupFiles = mDatabaseBackupAndRecovery.getBackupFiles(); 6793 List<String> fileNames = new ArrayList<>(); 6794 for (File file : backupFiles) { 6795 fileNames.add(file.getName()); 6796 } 6797 Bundle bundle = new Bundle(); 6798 Object[] values = fileNames.toArray(); 6799 String[] resultArray = Arrays.copyOf(values, values.length, String[].class); 6800 bundle.putStringArray(GET_BACKUP_FILES, resultArray); 6801 return bundle; 6802 default: 6803 throw new UnsupportedOperationException("Unsupported call: " + method); 6804 } 6805 } 6806 6807 public void backupDatabases(CancellationSignal signal) { 6808 mDatabaseBackupAndRecovery.backupDatabases(mInternalDatabase, mExternalDatabase, signal); 6809 } 6810 6811 private void syncAllMedia() { 6812 // Clear the binder calling identity so that we can sync the unexported 6813 // local_provider while running as MediaProvider 6814 final long t = Binder.clearCallingIdentity(); 6815 try { 6816 Log.v(TAG, "Test initiated cloud provider sync"); 6817 mPickerSyncController.syncAllMedia(); 6818 } finally { 6819 Binder.restoreCallingIdentity(t); 6820 } 6821 } 6822 6823 private AssetFileDescriptor getOriginalMediaFormatFileDescriptor(Bundle extras) 6824 throws FileNotFoundException { 6825 try (ParcelFileDescriptor inputPfd = 6826 extras.getParcelable(MediaStore.EXTRA_FILE_DESCRIPTOR)) { 6827 File file = getFileFromFileDescriptor(inputPfd); 6828 // Convert from FUSE file to lower fs file because the supportsTranscode() check below 6829 // expects a lower fs file format 6830 file = fromFuseFile(file); 6831 if (!mTranscodeHelper.supportsTranscode(file.getPath())) { 6832 // Note that we should be checking if a file is a modern format and not just 6833 // that it supports transcoding, unfortunately, checking modern format 6834 // requires either a db query or media scan which can lead to ANRs if apps 6835 // or the system implicitly call this method as part of a 6836 // MediaPlayer#setDataSource. 6837 throw new FileNotFoundException("Input file descriptor is already original"); 6838 } 6839 6840 FuseDaemon fuseDaemon = getFuseDaemonForFile(file, mVolumeCache); 6841 int uid = Binder.getCallingUid(); 6842 6843 FdAccessResult result = fuseDaemon.checkFdAccess(inputPfd, uid); 6844 if (!result.isSuccess()) { 6845 throw new FileNotFoundException("Invalid path for original media format file"); 6846 } 6847 6848 String outputPath = result.filePath; 6849 boolean shouldRedact = result.shouldRedact; 6850 6851 int posixMode = Os.fcntlInt(inputPfd.getFileDescriptor(), F_GETFL, 6852 0 /* args */); 6853 int modeBits = FileUtils.translateModePosixToPfd(posixMode); 6854 6855 ParcelFileDescriptor pfd = openWithFuse(outputPath, uid, 0 /* mediaCapabilitiesUid */, 6856 modeBits, shouldRedact, false /* shouldTranscode */, 6857 0 /* transcodeReason */); 6858 return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH); 6859 } catch (IOException e) { 6860 throw new FileNotFoundException("Failed to fetch original file descriptor"); 6861 } catch (ErrnoException e) { 6862 Log.w(TAG, "Failed to fetch access mode for file descriptor", e); 6863 throw new FileNotFoundException("Failed to fetch access mode for file descriptor"); 6864 } 6865 } 6866 6867 /** 6868 * Grant similar read/write access for mediaStoreUri as the caller has for documentsUri. 6869 * 6870 * Note: This function assumes that read permission check for documentsUri is already enforced. 6871 * Note: This function currently does not check/grant for persisted Uris. Support for this can 6872 * be added eventually, but the calling application will have to call 6873 * ContentResolver#takePersistableUriPermission(Uri, int) for the mediaStoreUri to persist. 6874 * 6875 * @param documentsUri DocumentsProvider format content Uri 6876 * @param mediaStoreUri MediaStore format content Uri 6877 * @param callingPid pid of the caller 6878 * @param callingUid uid of the caller 6879 * @param callingPackage package name of the caller 6880 */ 6881 private void copyUriPermissionGrants(Uri documentsUri, Uri mediaStoreUri, 6882 int callingPid, int callingUid, String callingPackage) { 6883 // No need to check for read permission, as we enforce it already. 6884 int modeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION; 6885 if (getContext().checkUriPermission(documentsUri, callingPid, callingUid, 6886 Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == PERMISSION_GRANTED) { 6887 modeFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; 6888 } 6889 getContext().grantUriPermission(callingPackage, mediaStoreUri, modeFlags); 6890 } 6891 6892 static List<Uri> collectUris(ClipData clipData) { 6893 final ArrayList<Uri> res = new ArrayList<>(); 6894 for (int i = 0; i < clipData.getItemCount(); i++) { 6895 res.add(clipData.getItemAt(i).getUri()); 6896 } 6897 return res; 6898 } 6899 6900 /** 6901 * Return the filesystem path of the real file on disk that is represented 6902 * by the given {@link ParcelFileDescriptor}. 6903 * 6904 * Note that the file may be a FUSE or lower fs file and depending on the purpose might need 6905 * to be converted with {@link FileUtils#toFuseFile} or {@link FileUtils#fromFuseFile}. 6906 * 6907 * Copied from {@link ParcelFileDescriptor#getFile} 6908 */ 6909 private static File getFileFromFileDescriptor(ParcelFileDescriptor fileDescriptor) 6910 throws IOException { 6911 try { 6912 final String path = Os.readlink("/proc/self/fd/" + fileDescriptor.getFd()); 6913 if (OsConstants.S_ISREG(Os.stat(path).st_mode)) { 6914 return new File(path); 6915 } else { 6916 throw new IOException("Not a regular file: " + path); 6917 } 6918 } catch (ErrnoException e) { 6919 throw e.rethrowAsIOException(); 6920 } 6921 } 6922 6923 /** 6924 * Generate the {@link PendingIntent} for the given grant request. This 6925 * method also checks the incoming arguments for security purposes 6926 * before creating the privileged {@link PendingIntent}. 6927 */ 6928 @NonNull 6929 private PendingIntent createRequest(@NonNull String method, @NonNull Bundle extras) { 6930 final ClipData clipData = extras.getParcelable(MediaStore.EXTRA_CLIP_DATA); 6931 final List<Uri> uris = collectUris(clipData); 6932 6933 for (Uri uri : uris) { 6934 final int match = matchUri(uri, false); 6935 switch (match) { 6936 case IMAGES_MEDIA_ID: 6937 case AUDIO_MEDIA_ID: 6938 case VIDEO_MEDIA_ID: 6939 case AUDIO_PLAYLISTS_ID: 6940 // Caller is requesting a specific media item by its ID, 6941 // which means it's valid for requests 6942 break; 6943 case FILES_ID: 6944 // Allow only subtitle files 6945 if (!isSubtitleFile(uri)) { 6946 throw new IllegalArgumentException( 6947 "All requested items must be Media items"); 6948 } 6949 break; 6950 default: 6951 throw new IllegalArgumentException( 6952 "All requested items must be referenced by specific ID"); 6953 } 6954 } 6955 6956 // Enforce that limited set of columns can be mutated 6957 final ContentValues values = extras.getParcelable(MediaStore.EXTRA_CONTENT_VALUES); 6958 final List<String> allowedColumns; 6959 switch (method) { 6960 case MediaStore.CREATE_FAVORITE_REQUEST_CALL: 6961 allowedColumns = Collections.singletonList( 6962 MediaColumns.IS_FAVORITE); 6963 break; 6964 case MediaStore.CREATE_TRASH_REQUEST_CALL: 6965 allowedColumns = Collections.singletonList( 6966 MediaColumns.IS_TRASHED); 6967 break; 6968 default: 6969 allowedColumns = Collections.emptyList(); 6970 break; 6971 } 6972 if (values != null) { 6973 for (String key : values.keySet()) { 6974 if (!allowedColumns.contains(key)) { 6975 throw new IllegalArgumentException("Invalid column " + key); 6976 } 6977 } 6978 } 6979 6980 final Context context = getContext(); 6981 final Intent intent = new Intent(method, null, context, PermissionActivity.class); 6982 intent.putExtras(extras); 6983 return PendingIntent.getActivity(context, PermissionActivity.REQUEST_CODE, intent, 6984 FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE); 6985 } 6986 6987 /** 6988 * @return true if the given Files uri has media_type=MEDIA_TYPE_SUBTITLE 6989 */ 6990 private boolean isSubtitleFile(Uri uri) { 6991 final LocalCallingIdentity tokenInner = clearLocalCallingIdentity(); 6992 try (Cursor cursor = queryForSingleItem(uri, new String[]{FileColumns.MEDIA_TYPE}, null, 6993 null, null)) { 6994 return cursor.getInt(0) == FileColumns.MEDIA_TYPE_SUBTITLE; 6995 } catch (FileNotFoundException e) { 6996 Log.e(TAG, "Couldn't find database row for requested uri " + uri, e); 6997 } finally { 6998 restoreLocalCallingIdentity(tokenInner); 6999 } 7000 return false; 7001 } 7002 7003 /** 7004 * Ensure that all local databases have a custom collator registered for the 7005 * given {@link ULocale} locale. 7006 * 7007 * @return the corresponding custom collation name to be used in 7008 * {@code ORDER BY} clauses. 7009 */ 7010 @NonNull 7011 private String ensureCustomCollator(@NonNull String locale) { 7012 // Quick check that requested locale looks reasonable 7013 new ULocale(locale); 7014 7015 final String collationName = "custom_" + locale.replaceAll("[^a-zA-Z]", ""); 7016 synchronized (mCustomCollators) { 7017 if (!mCustomCollators.contains(collationName)) { 7018 for (DatabaseHelper helper : new DatabaseHelper[] { 7019 mInternalDatabase, 7020 mExternalDatabase 7021 }) { 7022 helper.runWithoutTransaction((db) -> { 7023 db.execPerConnectionSQL("SELECT icu_load_collation(?, ?);", 7024 new String[] { locale, collationName }); 7025 return null; 7026 }); 7027 } 7028 mCustomCollators.add(collationName); 7029 } 7030 } 7031 return collationName; 7032 } 7033 7034 private int pruneThumbnails(@NonNull SQLiteDatabase db, @NonNull CancellationSignal signal) { 7035 int prunedCount = 0; 7036 7037 // Determine all known media items 7038 final LongArray knownIds = new LongArray(); 7039 try (Cursor c = db.query(true, "files", new String[] { BaseColumns._ID }, 7040 null, null, null, null, null, null, signal)) { 7041 while (c.moveToNext()) { 7042 knownIds.add(c.getLong(0)); 7043 } 7044 } 7045 7046 final long[] knownIdsRaw = knownIds.toArray(); 7047 Arrays.sort(knownIdsRaw); 7048 7049 for (MediaVolume volume : mVolumeCache.getExternalVolumes()) { 7050 final List<File> thumbDirs; 7051 try { 7052 thumbDirs = getThumbnailDirectories(volume); 7053 } catch (FileNotFoundException e) { 7054 Log.w(TAG, "Failed to resolve volume " + volume.getName(), e); 7055 continue; 7056 } 7057 7058 // Reconcile all thumbnails, deleting stale items 7059 for (File thumbDir : thumbDirs) { 7060 // Possibly bail before digging into each directory 7061 signal.throwIfCanceled(); 7062 7063 final File[] files = thumbDir.listFiles(); 7064 for (File thumbFile : (files != null) ? files : new File[0]) { 7065 if (Objects.equals(thumbFile.getName(), FILE_DATABASE_UUID)) continue; 7066 if (Objects.equals(thumbFile.getName(), MEDIA_IGNORE_FILENAME)) continue; 7067 final String name = FileUtils.extractFileName(thumbFile.getName()); 7068 try { 7069 final long id = Long.parseLong(name); 7070 if (Arrays.binarySearch(knownIdsRaw, id) >= 0) { 7071 // Thumbnail belongs to known media, keep it 7072 continue; 7073 } 7074 } catch (NumberFormatException e) { 7075 } 7076 7077 Log.v(TAG, "Deleting stale thumbnail " + thumbFile); 7078 deleteAndInvalidate(thumbFile); 7079 prunedCount++; 7080 } 7081 } 7082 } 7083 7084 // Also delete stale items from legacy tables 7085 db.execSQL("delete from thumbnails " 7086 + "where image_id not in (select _id from images)"); 7087 db.execSQL("delete from videothumbnails " 7088 + "where video_id not in (select _id from video)"); 7089 7090 return prunedCount; 7091 } 7092 7093 abstract class Thumbnailer { 7094 final String directoryName; 7095 7096 public Thumbnailer(String directoryName) { 7097 this.directoryName = directoryName; 7098 } 7099 7100 private File getThumbnailFile(Uri uri) throws IOException { 7101 final String volumeName = resolveVolumeName(uri); 7102 final File volumePath = getVolumePath(volumeName); 7103 return FileUtils.buildPath(volumePath, directoryName, 7104 DIRECTORY_THUMBNAILS, ContentUris.parseId(uri) + ".jpg"); 7105 } 7106 7107 public abstract Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) 7108 throws IOException; 7109 7110 public ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal) 7111 throws IOException { 7112 // First attempt to fast-path by opening the thumbnail; if it 7113 // doesn't exist we fall through to create it below 7114 final File thumbFile = getThumbnailFile(uri); 7115 try { 7116 return FileUtils.openSafely(thumbFile, 7117 ParcelFileDescriptor.MODE_READ_ONLY); 7118 } catch (FileNotFoundException ignored) { 7119 } 7120 7121 final File thumbDir = thumbFile.getParentFile(); 7122 thumbDir.mkdirs(); 7123 7124 // When multiple threads race for the same thumbnail, the second 7125 // thread could return a file with a thumbnail still in 7126 // progress. We could add heavy per-ID locking to mitigate this 7127 // rare race condition, but it's simpler to have both threads 7128 // generate the same thumbnail using temporary files and rename 7129 // them into place once finished. 7130 final File thumbTempFile = File.createTempFile("thumb", null, thumbDir); 7131 7132 ParcelFileDescriptor thumbWrite = null; 7133 ParcelFileDescriptor thumbRead = null; 7134 try { 7135 // Open our temporary file twice: once for local writing, and 7136 // once for remote reading. Both FDs point at the same 7137 // underlying inode on disk, so they're stable across renames 7138 // to avoid race conditions between threads. 7139 thumbWrite = FileUtils.openSafely(thumbTempFile, 7140 ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_CREATE); 7141 thumbRead = FileUtils.openSafely(thumbTempFile, 7142 ParcelFileDescriptor.MODE_READ_ONLY); 7143 7144 final Bitmap thumbnail = getThumbnailBitmap(uri, signal); 7145 thumbnail.compress(Bitmap.CompressFormat.JPEG, 90, 7146 new FileOutputStream(thumbWrite.getFileDescriptor())); 7147 7148 try { 7149 // Use direct syscall for better failure logs 7150 Os.rename(thumbTempFile.getAbsolutePath(), thumbFile.getAbsolutePath()); 7151 } catch (ErrnoException e) { 7152 e.rethrowAsIOException(); 7153 } 7154 7155 // Everything above went peachy, so return a duplicate of our 7156 // already-opened read FD to keep our finally logic below simple 7157 return thumbRead.dup(); 7158 7159 } finally { 7160 // Regardless of success or failure, try cleaning up any 7161 // remaining temporary file and close all our local FDs 7162 FileUtils.closeQuietly(thumbWrite); 7163 FileUtils.closeQuietly(thumbRead); 7164 deleteAndInvalidate(thumbTempFile); 7165 } 7166 } 7167 7168 public void invalidateThumbnail(Uri uri) throws IOException { 7169 deleteAndInvalidate(getThumbnailFile(uri)); 7170 } 7171 } 7172 7173 private final Thumbnailer mAudioThumbnailer = new Thumbnailer(Environment.DIRECTORY_MUSIC) { 7174 @Override 7175 public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException { 7176 return ThumbnailUtils.createAudioThumbnail(queryForDataFile(uri, signal), 7177 mThumbSize, signal); 7178 } 7179 }; 7180 7181 private final Thumbnailer mVideoThumbnailer = new Thumbnailer(Environment.DIRECTORY_MOVIES) { 7182 @Override 7183 public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException { 7184 return ThumbnailUtils.createVideoThumbnail(queryForDataFile(uri, signal), 7185 mThumbSize, signal); 7186 } 7187 }; 7188 7189 private final Thumbnailer mImageThumbnailer = new Thumbnailer(Environment.DIRECTORY_PICTURES) { 7190 @Override 7191 public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException { 7192 return ThumbnailUtils.createImageThumbnail(queryForDataFile(uri, signal), 7193 mThumbSize, signal); 7194 } 7195 }; 7196 7197 private List<File> getThumbnailDirectories(MediaVolume volume) throws FileNotFoundException { 7198 final File volumePath = volume.getPath(); 7199 return Arrays.asList( 7200 FileUtils.buildPath(volumePath, Environment.DIRECTORY_MUSIC, DIRECTORY_THUMBNAILS), 7201 FileUtils.buildPath(volumePath, Environment.DIRECTORY_MOVIES, DIRECTORY_THUMBNAILS), 7202 FileUtils.buildPath(volumePath, Environment.DIRECTORY_PICTURES, 7203 DIRECTORY_THUMBNAILS)); 7204 } 7205 7206 private void invalidateThumbnails(Uri uri) { 7207 Trace.beginSection("MP.invalidateThumbnails"); 7208 try { 7209 invalidateThumbnailsInternal(uri); 7210 } finally { 7211 Trace.endSection(); 7212 } 7213 } 7214 7215 private void invalidateThumbnailsInternal(Uri uri) { 7216 final long id = ContentUris.parseId(uri); 7217 try { 7218 mAudioThumbnailer.invalidateThumbnail(uri); 7219 mVideoThumbnailer.invalidateThumbnail(uri); 7220 mImageThumbnailer.invalidateThumbnail(uri); 7221 } catch (IOException ignored) { 7222 } 7223 7224 final DatabaseHelper helper; 7225 try { 7226 helper = getDatabaseForUri(uri); 7227 } catch (VolumeNotFoundException e) { 7228 Log.w(TAG, e); 7229 return; 7230 } 7231 7232 helper.runWithTransaction((db) -> { 7233 final String idString = Long.toString(id); 7234 try (Cursor c = db.rawQuery("select _data from thumbnails where image_id=?" 7235 + " union all select _data from videothumbnails where video_id=?", 7236 new String[] { idString, idString })) { 7237 while (c.moveToNext()) { 7238 String path = c.getString(0); 7239 deleteIfAllowed(uri, Bundle.EMPTY, path); 7240 } 7241 } 7242 7243 db.execSQL("delete from thumbnails where image_id=?", new String[] { idString }); 7244 db.execSQL("delete from videothumbnails where video_id=?", new String[] { idString }); 7245 return null; 7246 }); 7247 } 7248 7249 /** 7250 * @deprecated all operations should be routed through the overload that 7251 * accepts a {@link Bundle} of extras. 7252 */ 7253 @Override 7254 @Deprecated 7255 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 7256 return update(uri, values, 7257 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null)); 7258 } 7259 7260 @Override 7261 public int update(@NonNull Uri uri, @Nullable ContentValues values, 7262 @Nullable Bundle extras) { 7263 Trace.beginSection("MP.update [" + uri + ']'); 7264 try { 7265 return updateInternal(uri, values, extras); 7266 } catch (FallbackException e) { 7267 return e.translateForUpdateDelete(getCallingPackageTargetSdkVersion()); 7268 } finally { 7269 Trace.endSection(); 7270 } 7271 } 7272 7273 private int updateInternal(@NonNull Uri uri, @Nullable ContentValues initialValues, 7274 @Nullable Bundle extras) throws FallbackException { 7275 final String volumeName = getVolumeName(uri); 7276 PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName); 7277 7278 extras = (extras != null) ? extras : new Bundle(); 7279 // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider. 7280 extras.remove(QUERY_ARG_REDACTED_URI); 7281 7282 if (isRedactedUri(uri)) { 7283 // we don't support update on redacted uris. 7284 return 0; 7285 } 7286 7287 // Related items are only considered for new media creation, and they 7288 // can't be leveraged to move existing content into blocked locations 7289 extras.remove(QUERY_ARG_RELATED_URI); 7290 // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. 7291 extras.remove(INCLUDED_DEFAULT_DIRECTORIES); 7292 7293 final String userWhere = extras.getString(QUERY_ARG_SQL_SELECTION); 7294 final String[] userWhereArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS); 7295 7296 // Limit the hacky workaround to camera targeting Q and below, to allow newer versions 7297 // of camera that does the right thing to work correctly. 7298 if ("com.google.android.GoogleCamera".equals(getCallingPackageOrSelf()) 7299 && getCallingPackageTargetSdkVersion() <= Build.VERSION_CODES.Q) { 7300 if (matchUri(uri, false) == IMAGES_MEDIA_ID) { 7301 Log.w(TAG, "Working around app bug in b/111966296"); 7302 uri = MediaStore.Files.getContentUri("external", ContentUris.parseId(uri)); 7303 } else if (matchUri(uri, false) == VIDEO_MEDIA_ID) { 7304 Log.w(TAG, "Working around app bug in b/112246630"); 7305 uri = MediaStore.Files.getContentUri("external", ContentUris.parseId(uri)); 7306 } 7307 } 7308 7309 uri = safeUncanonicalize(uri); 7310 7311 int count; 7312 7313 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 7314 final boolean allowHidden = isCallingPackageAllowedHidden(); 7315 final int match = matchUri(uri, allowHidden); 7316 final DatabaseHelper helper = getDatabaseForUri(uri); 7317 7318 switch (match) { 7319 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 7320 extras.putString(QUERY_ARG_SQL_SELECTION, 7321 BaseColumns._ID + "=" + uri.getPathSegments().get(5)); 7322 // fall-through 7323 case AUDIO_PLAYLISTS_ID_MEMBERS: { 7324 final long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 7325 final Uri playlistUri = ContentUris.withAppendedId( 7326 MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId); 7327 if (uri.getBooleanQueryParameter("move", false)) { 7328 // Convert explicit request into query; sigh, moveItem() 7329 // uses zero-based indexing instead of one-based indexing 7330 final int from = Integer.parseInt(uri.getPathSegments().get(5)) + 1; 7331 final int to = initialValues.getAsInteger(Playlists.Members.PLAY_ORDER) + 1; 7332 extras.putString(QUERY_ARG_SQL_SELECTION, 7333 Playlists.Members.PLAY_ORDER + "=" + from); 7334 initialValues.put(Playlists.Members.PLAY_ORDER, to); 7335 } 7336 7337 // Playlist contents are always persisted directly into playlist 7338 // files on disk to ensure that we can reliably migrate between 7339 // devices and recover from database corruption 7340 final int index; 7341 if (initialValues.containsKey(Playlists.Members.PLAY_ORDER)) { 7342 index = movePlaylistMembers(playlistUri, initialValues, extras); 7343 } else { 7344 index = resolvePlaylistIndex(playlistUri, extras); 7345 } 7346 if (initialValues.containsKey(Playlists.Members.AUDIO_ID)) { 7347 final Bundle queryArgs = new Bundle(); 7348 queryArgs.putString(QUERY_ARG_SQL_SELECTION, 7349 Playlists.Members.PLAY_ORDER + "=" + (index + 1)); 7350 removePlaylistMembers(playlistUri, queryArgs); 7351 7352 final ContentValues values = new ContentValues(); 7353 values.put(Playlists.Members.AUDIO_ID, 7354 initialValues.getAsString(Playlists.Members.AUDIO_ID)); 7355 values.put(Playlists.Members.PLAY_ORDER, (index + 1)); 7356 addPlaylistMembers(playlistUri, values); 7357 } 7358 7359 acceptWithExpansion(helper::notifyUpdate, volumeName, playlistId, 7360 FileColumns.MEDIA_TYPE_PLAYLIST, false); 7361 return 1; 7362 } 7363 } 7364 7365 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, match, uri, extras, null); 7366 7367 // Give callers interacting with a specific media item a chance to 7368 // escalate access if they don't already have it 7369 switch (match) { 7370 case AUDIO_MEDIA_ID: 7371 case VIDEO_MEDIA_ID: 7372 case IMAGES_MEDIA_ID: 7373 enforceCallingPermission(uri, extras, true); 7374 } 7375 7376 boolean triggerInvalidate = false; 7377 boolean triggerScan = false; 7378 boolean isUriPublished = false; 7379 if (initialValues != null) { 7380 // IDs are forever; nobody should be editing them 7381 initialValues.remove(MediaColumns._ID); 7382 7383 // Expiration times are hard-coded; let's derive them 7384 FileUtils.computeDateExpires(initialValues); 7385 7386 // Ignore or augment incoming raw filesystem paths 7387 for (String column : sDataColumns.keySet()) { 7388 if (!initialValues.containsKey(column)) continue; 7389 7390 if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) { 7391 // Mutation allowed 7392 } else { 7393 Log.w(TAG, "Ignoring mutation of " + column + " from " 7394 + getCallingPackageOrSelf()); 7395 initialValues.remove(column); 7396 } 7397 } 7398 7399 // Enforce allowed ownership transfers 7400 if (initialValues.containsKey(MediaColumns.OWNER_PACKAGE_NAME)) { 7401 if (isCallingPackageSelf() || isCallingPackageShell()) { 7402 // When the caller is the media scanner or the shell, we let 7403 // them change ownership however they see fit; nothing to do 7404 } else if (isCallingPackageDelegator()) { 7405 // When the caller is a delegator, allow them to shift 7406 // ownership only when current owner, or when ownerless 7407 final String currentOwner; 7408 final String proposedOwner = initialValues 7409 .getAsString(MediaColumns.OWNER_PACKAGE_NAME); 7410 final Uri genericUri = MediaStore.Files.getContentUri(volumeName, 7411 ContentUris.parseId(uri)); 7412 try (Cursor c = queryForSingleItem(genericUri, 7413 new String[] { MediaColumns.OWNER_PACKAGE_NAME }, null, null, null)) { 7414 currentOwner = c.getString(0); 7415 } catch (FileNotFoundException e) { 7416 throw new IllegalStateException(e); 7417 } 7418 final boolean transferAllowed = (currentOwner == null) 7419 || Arrays.asList(getSharedPackagesForPackage(getCallingPackageOrSelf())) 7420 .contains(currentOwner); 7421 if (transferAllowed) { 7422 Log.v(TAG, "Ownership transfer from " + currentOwner + " to " 7423 + proposedOwner + " allowed"); 7424 } else { 7425 Log.w(TAG, "Ownership transfer from " + currentOwner + " to " 7426 + proposedOwner + " blocked"); 7427 initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME); 7428 } 7429 } else { 7430 // Otherwise no ownership changes are allowed 7431 initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME); 7432 } 7433 } 7434 7435 if (!isCallingPackageSelf()) { 7436 Trace.beginSection("MP.filter"); 7437 7438 // We default to filtering mutable columns, except when we know 7439 // the single item being updated is pending; when it's finally 7440 // published we'll overwrite these values. 7441 final Uri finalUri = uri; 7442 final Supplier<Boolean> isPending = new CachedSupplier<>(() -> { 7443 return isPending(finalUri); 7444 }); 7445 7446 // Column values controlled by media scanner aren't writable by 7447 // apps, since any edits here don't reflect the metadata on 7448 // disk, and they'd be overwritten during a rescan. 7449 for (String column : new ArraySet<>(initialValues.keySet())) { 7450 if (sMutableColumns.contains(column)) { 7451 // Mutation normally allowed 7452 } else if (isPending.get()) { 7453 // Mutation relaxed while pending 7454 } else { 7455 Log.w(TAG, "Ignoring mutation of " + column + " from " 7456 + getCallingPackageOrSelf()); 7457 initialValues.remove(column); 7458 triggerScan = true; 7459 } 7460 7461 // If we're publishing this item, perform a blocking scan to 7462 // make sure metadata is updated 7463 if (MediaColumns.IS_PENDING.equals(column)) { 7464 triggerScan = true; 7465 isUriPublished = true; 7466 // Explicitly clear columns used to ignore no-op scans, 7467 // since we need to force a scan on publish 7468 initialValues.putNull(MediaColumns.DATE_MODIFIED); 7469 initialValues.putNull(MediaColumns.SIZE); 7470 } 7471 } 7472 7473 Trace.endSection(); 7474 } 7475 7476 if ("files".equals(qb.getTables())) { 7477 maybeMarkAsDownload(initialValues); 7478 } 7479 7480 // We no longer track location metadata 7481 if (initialValues.containsKey(ImageColumns.LATITUDE)) { 7482 initialValues.putNull(ImageColumns.LATITUDE); 7483 } 7484 if (initialValues.containsKey(ImageColumns.LONGITUDE)) { 7485 initialValues.putNull(ImageColumns.LONGITUDE); 7486 } 7487 if (getCallingPackageTargetSdkVersion() <= Build.VERSION_CODES.Q) { 7488 // These columns are removed in R. 7489 if (initialValues.containsKey("primary_directory")) { 7490 initialValues.remove("primary_directory"); 7491 } 7492 if (initialValues.containsKey("secondary_directory")) { 7493 initialValues.remove("secondary_directory"); 7494 } 7495 } 7496 } 7497 7498 // If we're not updating anything, then we can skip 7499 if (initialValues.isEmpty()) return 0; 7500 7501 final boolean isThumbnail; 7502 switch (match) { 7503 case IMAGES_THUMBNAILS: 7504 case IMAGES_THUMBNAILS_ID: 7505 case VIDEO_THUMBNAILS: 7506 case VIDEO_THUMBNAILS_ID: 7507 case AUDIO_ALBUMART: 7508 case AUDIO_ALBUMART_ID: 7509 isThumbnail = true; 7510 break; 7511 default: 7512 isThumbnail = false; 7513 break; 7514 } 7515 7516 switch (match) { 7517 case AUDIO_PLAYLISTS: 7518 case AUDIO_PLAYLISTS_ID: 7519 // Playlist names are stored as display names, but leave 7520 // values untouched if the caller is ModernMediaScanner 7521 if (!isCallingPackageSelf()) { 7522 if (initialValues.containsKey(Playlists.NAME)) { 7523 initialValues.put(MediaColumns.DISPLAY_NAME, 7524 initialValues.getAsString(Playlists.NAME)); 7525 } 7526 if (!initialValues.containsKey(MediaColumns.MIME_TYPE)) { 7527 initialValues.put(MediaColumns.MIME_TYPE, "audio/mpegurl"); 7528 } 7529 } 7530 break; 7531 } 7532 7533 // If we're touching columns that would change placement of a file, 7534 // blend in current values and recalculate path 7535 final boolean allowMovement = extras.getBoolean(MediaStore.QUERY_ARG_ALLOW_MOVEMENT, 7536 !isCallingPackageSelf()); 7537 if (containsAny(initialValues.keySet(), sPlacementColumns) 7538 && !initialValues.containsKey(MediaColumns.DATA) 7539 && !isThumbnail 7540 && allowMovement) { 7541 Trace.beginSection("MP.movement"); 7542 7543 // We only support movement under well-defined collections 7544 switch (match) { 7545 case AUDIO_MEDIA_ID: 7546 case AUDIO_PLAYLISTS_ID: 7547 case VIDEO_MEDIA_ID: 7548 case IMAGES_MEDIA_ID: 7549 case DOWNLOADS_ID: 7550 case FILES_ID: 7551 break; 7552 default: 7553 throw new IllegalArgumentException("Movement of " + uri 7554 + " which isn't part of well-defined collection not allowed"); 7555 } 7556 7557 final LocalCallingIdentity token = clearLocalCallingIdentity(); 7558 final Uri genericUri = MediaStore.Files.getContentUri(volumeName, 7559 ContentUris.parseId(uri)); 7560 try (Cursor c = queryForSingleItem(genericUri, 7561 sPlacementColumns.toArray(new String[0]), userWhere, userWhereArgs, null)) { 7562 for (int i = 0; i < c.getColumnCount(); i++) { 7563 final String column = c.getColumnName(i); 7564 if (!initialValues.containsKey(column)) { 7565 initialValues.put(column, c.getString(i)); 7566 } 7567 } 7568 } catch (FileNotFoundException e) { 7569 throw new IllegalStateException(e); 7570 } finally { 7571 restoreLocalCallingIdentity(token); 7572 } 7573 7574 // Regenerate path using blended values; this will throw if caller 7575 // is attempting to place file into invalid location 7576 final String beforePath = initialValues.getAsString(MediaColumns.DATA); 7577 final String beforeVolume = extractVolumeName(beforePath); 7578 final String beforeOwner = extractPathOwnerPackageName(beforePath); 7579 7580 initialValues.remove(MediaColumns.DATA); 7581 ensureNonUniqueFileColumns(match, uri, extras, initialValues, beforePath); 7582 7583 final String probePath = initialValues.getAsString(MediaColumns.DATA); 7584 final String probeVolume = extractVolumeName(probePath); 7585 final String probeOwner = extractPathOwnerPackageName(probePath); 7586 if (Objects.equals(beforePath, probePath)) { 7587 Log.d(TAG, "Identical paths " + beforePath + "; not moving"); 7588 } else if (!Objects.equals(beforeVolume, probeVolume)) { 7589 throw new IllegalArgumentException("Changing volume from " + beforePath + " to " 7590 + probePath + " not allowed"); 7591 } else if (!isUpdateAllowedForOwnedPath(beforeOwner, probeOwner, beforePath, 7592 probePath)) { 7593 throw new IllegalArgumentException("Changing ownership from " + beforePath + " to " 7594 + probePath + " not allowed"); 7595 } else { 7596 // Now that we've confirmed an actual movement is taking place, 7597 // ensure we have a unique destination 7598 initialValues.remove(MediaColumns.DATA); 7599 ensureUniqueFileColumns(match, uri, extras, initialValues, beforePath); 7600 7601 String afterPath = initialValues.getAsString(MediaColumns.DATA); 7602 7603 if (isCrossUserEnabled()) { 7604 String afterVolume = extractVolumeName(afterPath); 7605 String afterVolumePath = extractVolumePath(afterPath); 7606 String beforeVolumePath = extractVolumePath(beforePath); 7607 7608 if (MediaStore.VOLUME_EXTERNAL_PRIMARY.equals(beforeVolume) 7609 && beforeVolume.equals(afterVolume) 7610 && !beforeVolumePath.equals(afterVolumePath)) { 7611 // On cross-user enabled devices, it can happen that a rename intended as 7612 // /storage/emulated/999/foo -> /storage/emulated/999/foo can end up as 7613 // /storage/emulated/999/foo -> /storage/emulated/0/foo. We now fix-up 7614 afterPath = afterPath.replaceFirst(afterVolumePath, beforeVolumePath); 7615 } 7616 } 7617 7618 Log.d(TAG, "Moving " + beforePath + " to " + afterPath); 7619 try { 7620 Os.rename(beforePath, afterPath); 7621 invalidateFuseDentry(beforePath); 7622 invalidateFuseDentry(afterPath); 7623 } catch (ErrnoException e) { 7624 if (e.errno == OsConstants.ENOENT) { 7625 Log.d(TAG, "Missing file at " + beforePath + "; continuing anyway"); 7626 } else { 7627 throw new IllegalStateException(e); 7628 } 7629 } 7630 initialValues.put(MediaColumns.DATA, afterPath); 7631 7632 // Some indexed metadata may have been derived from the path on 7633 // disk, so scan this item again to update it 7634 triggerScan = true; 7635 } 7636 7637 Trace.endSection(); 7638 } 7639 7640 assertPrivatePathNotInValues(initialValues); 7641 7642 // Make sure any updated paths look consistent 7643 assertFileColumnsConsistent(match, uri, initialValues); 7644 7645 if (initialValues.containsKey(FileColumns.DATA)) { 7646 // If we're changing paths, invalidate any thumbnails 7647 triggerInvalidate = true; 7648 7649 // If the new file exists, trigger a scan to adjust any metadata 7650 // that might be derived from the path 7651 final String data = initialValues.getAsString(FileColumns.DATA); 7652 if (!TextUtils.isEmpty(data) && new File(data).exists()) { 7653 triggerScan = true; 7654 } 7655 } 7656 7657 // If we're already doing this update from an internal scan, no need to 7658 // kick off another no-op scan 7659 if (isCallingPackageSelf()) { 7660 triggerScan = false; 7661 } 7662 7663 // Since the update mutation may prevent us from matching items after 7664 // it's applied, we need to snapshot affected IDs here 7665 final LongArray updatedIds = new LongArray(); 7666 if (triggerInvalidate || triggerScan) { 7667 Trace.beginSection("MP.snapshot"); 7668 final LocalCallingIdentity token = clearLocalCallingIdentity(); 7669 try (Cursor c = qb.query(helper, new String[] { FileColumns._ID }, 7670 userWhere, userWhereArgs, null, null, null, null, null)) { 7671 while (c.moveToNext()) { 7672 updatedIds.add(c.getLong(0)); 7673 } 7674 } finally { 7675 restoreLocalCallingIdentity(token); 7676 Trace.endSection(); 7677 } 7678 } 7679 7680 final ContentValues values = new ContentValues(initialValues); 7681 switch (match) { 7682 case AUDIO_MEDIA_ID: 7683 case AUDIO_PLAYLISTS_ID: 7684 case VIDEO_MEDIA_ID: 7685 case IMAGES_MEDIA_ID: 7686 case FILES_ID: 7687 case DOWNLOADS_ID: { 7688 FileUtils.computeValuesFromData(values, isFuseThread()); 7689 break; 7690 } 7691 } 7692 7693 if (initialValues.containsKey(FileColumns.MEDIA_TYPE)) { 7694 final int mediaType = initialValues.getAsInteger(FileColumns.MEDIA_TYPE); 7695 switch (mediaType) { 7696 case FileColumns.MEDIA_TYPE_AUDIO: { 7697 computeAudioLocalizedValues(values); 7698 computeAudioKeyValues(values); 7699 break; 7700 } 7701 } 7702 } 7703 7704 boolean deferScan = false; 7705 if (triggerScan) { 7706 if (SdkLevel.isAtLeastS() && 7707 CompatChanges.isChangeEnabled(ENABLE_DEFERRED_SCAN, Binder.getCallingUid())) { 7708 if (extras.containsKey(QUERY_ARG_DO_ASYNC_SCAN)) { 7709 throw new IllegalArgumentException("Unsupported argument " + 7710 QUERY_ARG_DO_ASYNC_SCAN + " used in extras"); 7711 } 7712 deferScan = extras.getBoolean(QUERY_ARG_DEFER_SCAN, false); 7713 if (deferScan && initialValues.containsKey(MediaColumns.IS_PENDING) && 7714 (initialValues.getAsInteger(MediaColumns.IS_PENDING) == 1)) { 7715 // if the scan runs in async, ensure that the database row is excluded in 7716 // default query until the metadata is updated by deferred scan. 7717 // Apps will still be able to see this database row when queried with 7718 // QUERY_ARG_MATCH_PENDING=MATCH_INCLUDE 7719 values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_CR_PENDING_METADATA); 7720 qb.allowColumn(FileColumns._MODIFIER); 7721 } 7722 } else { 7723 // Allow apps to use QUERY_ARG_DO_ASYNC_SCAN if the device is R or app is targeting 7724 // targetSDK<=R. 7725 deferScan = extras.getBoolean(QUERY_ARG_DO_ASYNC_SCAN, false); 7726 } 7727 } 7728 7729 count = updateAllowingReplace(qb, helper, values, userWhere, userWhereArgs); 7730 7731 // If the caller tried (and failed) to update metadata, the file on disk 7732 // might have changed, to scan it to collect the latest metadata. 7733 if (triggerInvalidate || triggerScan) { 7734 Trace.beginSection("MP.invalidate"); 7735 final LocalCallingIdentity token = clearLocalCallingIdentity(); 7736 try { 7737 for (int i = 0; i < updatedIds.size(); i++) { 7738 final long updatedId = updatedIds.get(i); 7739 final Uri updatedUri = Files.getContentUri(volumeName, updatedId); 7740 helper.postBackground(() -> { 7741 invalidateThumbnails(updatedUri); 7742 }); 7743 7744 if (triggerScan) { 7745 try (Cursor c = queryForSingleItem(updatedUri, 7746 new String[] { FileColumns.DATA }, null, null, null)) { 7747 final File file = new File(c.getString(0)); 7748 final boolean notifyTranscodeHelper = isUriPublished; 7749 if (deferScan) { 7750 helper.postBackground(() -> { 7751 scanFileAsMediaProvider(file, REASON_DEMAND); 7752 if (notifyTranscodeHelper) { 7753 notifyTranscodeHelperOnUriPublished(updatedUri, file); 7754 } 7755 }); 7756 } else { 7757 helper.postBlocking(() -> { 7758 scanFileAsMediaProvider(file, REASON_DEMAND); 7759 if (notifyTranscodeHelper) { 7760 notifyTranscodeHelperOnUriPublished(updatedUri, file); 7761 } 7762 }); 7763 } 7764 } catch (Exception e) { 7765 Log.w(TAG, "Failed to update metadata for " + updatedUri, e); 7766 } 7767 } 7768 } 7769 } finally { 7770 restoreLocalCallingIdentity(token); 7771 Trace.endSection(); 7772 } 7773 } 7774 7775 return count; 7776 } 7777 7778 private boolean isUpdateAllowedForOwnedPath(@Nullable String srcOwner, 7779 @Nullable String destOwner, @NonNull String srcPath, @NonNull String destPath) { 7780 // 1. Allow if the update is within owned path 7781 // update() from /sdcard/Android/media/com.foo/ABC/image.jpeg to 7782 // /sdcard/Android/media/com.foo/XYZ/image.jpeg - Allowed 7783 if(Objects.equals(srcOwner, destOwner)) { 7784 return true; 7785 } 7786 7787 // 2. Check if the calling package is a special app which has global access 7788 if (isCallingPackageManager() || (canSystemGalleryAccessTheFile(srcPath) && 7789 (canSystemGalleryAccessTheFile(destPath)))) { 7790 return true; 7791 } 7792 7793 // 3. Allow update from srcPath if the source is not a owned path or calling package is the 7794 // owner of the source path or calling package shares the UID with the owner of the source 7795 // path 7796 // update() from /sdcard/DCIM/Foo.jpeg - Allowed 7797 // update() from /sdcard/Android/media/com.foo/image.jpeg - Allowed for 7798 // callingPackage=com.foo, not allowed for callingPackage=com.bar 7799 final boolean isSrcUpdateAllowed = srcOwner == null 7800 || isCallingIdentitySharedPackageName(srcOwner); 7801 7802 // 4. Allow update to dstPath if the destination is not a owned path or calling package is 7803 // the owner of the destination path or calling package shares the UID with the owner of the 7804 // destination path 7805 // update() to /sdcard/Pictures/image.jpeg - Allowed 7806 // update() to /sdcard/Android/media/com.foo/image.jpeg - Allowed for 7807 // callingPackage=com.foo, not allowed for callingPackage=com.bar 7808 final boolean isDestUpdateAllowed = destOwner == null 7809 || isCallingIdentitySharedPackageName(destOwner); 7810 7811 return isSrcUpdateAllowed && isDestUpdateAllowed; 7812 } 7813 7814 private void notifyTranscodeHelperOnUriPublished(Uri uri, File file) { 7815 if (!mTranscodeHelper.supportsTranscode(file.getPath())) { 7816 return; 7817 } 7818 7819 BackgroundThread.getExecutor().execute(() -> { 7820 final LocalCallingIdentity token = clearLocalCallingIdentity(); 7821 try { 7822 mTranscodeHelper.onUriPublished(uri); 7823 } finally { 7824 restoreLocalCallingIdentity(token); 7825 } 7826 }); 7827 } 7828 7829 private void notifyTranscodeHelperOnFileOpen(String path, String ioPath, int uid, 7830 int transformsReason) { 7831 if (!mTranscodeHelper.supportsTranscode(path)) { 7832 return; 7833 } 7834 7835 BackgroundThread.getExecutor().execute(() -> { 7836 final LocalCallingIdentity token = clearLocalCallingIdentity(); 7837 try { 7838 mTranscodeHelper.onFileOpen(path, ioPath, uid, transformsReason); 7839 } finally { 7840 restoreLocalCallingIdentity(token); 7841 } 7842 }); 7843 } 7844 7845 /** 7846 * Update row(s) that match {@code userWhere} in MediaProvider database with {@code values}. 7847 * Treats update as replace for updates with conflicts. 7848 */ 7849 private int updateAllowingReplace(@NonNull SQLiteQueryBuilder qb, 7850 @NonNull DatabaseHelper helper, @NonNull ContentValues values, String userWhere, 7851 String[] userWhereArgs) throws SQLiteConstraintException { 7852 return helper.runWithTransaction((db) -> { 7853 try { 7854 return qb.update(helper, values, userWhere, userWhereArgs); 7855 } catch (SQLiteConstraintException e) { 7856 // b/155320967 Apps sometimes create a file via file path and then update another 7857 // explicitly inserted db row to this file. We have to resolve this update with a 7858 // replace. 7859 7860 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) { 7861 // We don't support replace for non-legacy apps. Non legacy apps should have 7862 // clearer interactions with MediaProvider. 7863 throw e; 7864 } 7865 7866 final String path = values.getAsString(FileColumns.DATA); 7867 7868 // We will only handle UNIQUE constraint error for FileColumns.DATA. We will not try 7869 // update and replace if no file exists for conflicting db row. 7870 if (path == null || !new File(path).exists()) { 7871 throw e; 7872 } 7873 7874 final Uri uri = FileUtils.getContentUriForPath(path); 7875 final boolean allowHidden = isCallingPackageAllowedHidden(); 7876 // The db row which caused UNIQUE constraint error may not match all column values 7877 // of the given queryBuilder, hence using a generic queryBuilder with Files uri. 7878 Bundle extras = new Bundle(); 7879 extras.putInt(QUERY_ARG_MATCH_PENDING, MATCH_INCLUDE); 7880 extras.putInt(QUERY_ARG_MATCH_TRASHED, MATCH_INCLUDE); 7881 final SQLiteQueryBuilder qbForReplace = getQueryBuilder(TYPE_DELETE, 7882 matchUri(uri, allowHidden), uri, extras, null); 7883 final long rowId = getIdIfPathOwnedByPackages(qbForReplace, helper, path, 7884 mCallingIdentity.get().getSharedPackagesAsString()); 7885 7886 if (rowId != -1 && qbForReplace.delete(helper, "_id=?", 7887 new String[] {Long.toString(rowId)}) == 1) { 7888 Log.i(TAG, "Retrying database update after deleting conflicting entry"); 7889 return qb.update(helper, values, userWhere, userWhereArgs); 7890 } 7891 // Rethrow SQLiteConstraintException if app doesn't own the conflicting db row. 7892 throw e; 7893 } 7894 }); 7895 } 7896 7897 /** 7898 * Update the internal table of {@link MediaStore.Audio.Playlists.Members} 7899 * by parsing the playlist file on disk and resolving it against scanned 7900 * audio items. 7901 * <p> 7902 * When a playlist references a missing audio item, the associated 7903 * {@link Playlists.Members#PLAY_ORDER} is skipped, leaving a gap to ensure 7904 * that the playlist entry is retained to avoid user data loss. 7905 */ 7906 private void resolvePlaylistMembers(@NonNull Uri playlistUri) { 7907 Trace.beginSection("MP.resolvePlaylistMembers"); 7908 try { 7909 final DatabaseHelper helper; 7910 try { 7911 helper = getDatabaseForUri(playlistUri); 7912 } catch (VolumeNotFoundException e) { 7913 throw e.rethrowAsIllegalArgumentException(); 7914 } 7915 7916 helper.runWithTransaction((db) -> { 7917 resolvePlaylistMembersInternal(playlistUri, db); 7918 return null; 7919 }); 7920 } finally { 7921 Trace.endSection(); 7922 } 7923 } 7924 7925 private void resolvePlaylistMembersInternal(@NonNull Uri playlistUri, 7926 @NonNull SQLiteDatabase db) { 7927 try { 7928 // Refresh playlist members based on what we parse from disk 7929 final long playlistId = ContentUris.parseId(playlistUri); 7930 final Map<String, Long> membersMap = getAllPlaylistMembers(playlistId); 7931 db.delete("audio_playlists_map", "playlist_id=" + playlistId, null); 7932 7933 final Path playlistPath = queryForDataFile(playlistUri, null).toPath(); 7934 final Playlist playlist = new Playlist(); 7935 playlist.read(playlistPath.toFile()); 7936 7937 final List<Path> members = playlist.asList(); 7938 for (int i = 0; i < members.size(); i++) { 7939 try { 7940 final Path audioPath = playlistPath.getParent().resolve(members.get(i)); 7941 final long audioId = queryForPlaylistMember(audioPath, membersMap); 7942 7943 final ContentValues values = new ContentValues(); 7944 values.put(Playlists.Members.PLAY_ORDER, i + 1); 7945 values.put(Playlists.Members.PLAYLIST_ID, playlistId); 7946 values.put(Playlists.Members.AUDIO_ID, audioId); 7947 db.insert("audio_playlists_map", null, values); 7948 } catch (IOException e) { 7949 Log.w(TAG, "Failed to resolve playlist member", e); 7950 } 7951 } 7952 } catch (IOException e) { 7953 Log.w(TAG, "Failed to refresh playlist", e); 7954 } 7955 } 7956 7957 private Map<String, Long> getAllPlaylistMembers(long playlistId) { 7958 final Map<String, Long> membersMap = new ArrayMap<>(); 7959 7960 final Uri uri = Playlists.Members.getContentUri(MediaStore.VOLUME_EXTERNAL, playlistId); 7961 final String[] projection = new String[] { 7962 Playlists.Members.DATA, 7963 Playlists.Members.AUDIO_ID 7964 }; 7965 try (Cursor c = query(uri, projection, null, null)) { 7966 if (c == null) { 7967 Log.e(TAG, "Cursor is null, failed to create cached playlist member info."); 7968 return membersMap; 7969 } 7970 while (c.moveToNext()) { 7971 membersMap.put(c.getString(0), c.getLong(1)); 7972 } 7973 } 7974 return membersMap; 7975 } 7976 7977 /** 7978 * Make two attempts to query this playlist member: first based on the exact 7979 * path, and if that fails, fall back to picking a single item matching the 7980 * display name. When there are multiple items with the same display name, 7981 * we can't resolve between them, and leave this member unresolved. 7982 */ 7983 private long queryForPlaylistMember(@NonNull Path path, @NonNull Map<String, Long> membersMap) 7984 throws IOException { 7985 final String data = path.toFile().getCanonicalPath(); 7986 if (membersMap.containsKey(data)) { 7987 return membersMap.get(data); 7988 } 7989 final Uri audioUri = Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL); 7990 try (Cursor c = queryForSingleItem(audioUri, 7991 new String[] { BaseColumns._ID }, MediaColumns.DATA + "=?", 7992 new String[] { data }, null)) { 7993 return c.getLong(0); 7994 } catch (FileNotFoundException ignored) { 7995 } 7996 try (Cursor c = queryForSingleItem(audioUri, 7997 new String[] { BaseColumns._ID }, MediaColumns.DISPLAY_NAME + "=?", 7998 new String[] { path.toFile().getName() }, null)) { 7999 return c.getLong(0); 8000 } catch (FileNotFoundException ignored) { 8001 } 8002 throw new FileNotFoundException(); 8003 } 8004 8005 /** 8006 * Add the given audio item to the given playlist. Defaults to adding at the 8007 * end of the playlist when no {@link Playlists.Members#PLAY_ORDER} is 8008 * defined. 8009 */ 8010 private long addPlaylistMembers(@NonNull Uri playlistUri, @NonNull ContentValues values) 8011 throws FallbackException { 8012 final long audioId = values.getAsLong(Audio.Playlists.Members.AUDIO_ID); 8013 final String volumeName = MediaStore.VOLUME_INTERNAL.equals(getVolumeName(playlistUri)) 8014 ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL; 8015 final Uri audioUri = Audio.Media.getContentUri(volumeName, audioId); 8016 8017 Integer playOrder = values.getAsInteger(Playlists.Members.PLAY_ORDER); 8018 playOrder = (playOrder != null) ? (playOrder - 1) : Integer.MAX_VALUE; 8019 8020 try { 8021 final File playlistFile = queryForDataFile(playlistUri, null); 8022 final File audioFile = queryForDataFile(audioUri, null); 8023 8024 final Playlist playlist = new Playlist(); 8025 playlist.read(playlistFile); 8026 playOrder = playlist.add(playOrder, 8027 playlistFile.toPath().getParent().relativize(audioFile.toPath())); 8028 playlist.write(playlistFile); 8029 invalidateFuseDentry(playlistFile); 8030 8031 resolvePlaylistMembers(playlistUri); 8032 8033 // Callers are interested in the actual ID we generated 8034 final Uri membersUri = Playlists.Members.getContentUri(volumeName, 8035 ContentUris.parseId(playlistUri)); 8036 try (Cursor c = query(membersUri, new String[] { BaseColumns._ID }, 8037 Playlists.Members.PLAY_ORDER + "=" + (playOrder + 1), null, null)) { 8038 c.moveToFirst(); 8039 return c.getLong(0); 8040 } 8041 } catch (IOException e) { 8042 throw new FallbackException("Failed to update playlist", e, 8043 android.os.Build.VERSION_CODES.R); 8044 } 8045 } 8046 8047 private int addPlaylistMembers(@NonNull Uri playlistUri, @NonNull ContentValues[] initialValues) 8048 throws FallbackException { 8049 final String volumeName = getVolumeName(playlistUri); 8050 final String audioVolumeName = 8051 MediaStore.VOLUME_INTERNAL.equals(volumeName) 8052 ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL; 8053 8054 try { 8055 final File playlistFile = queryForDataFile(playlistUri, null); 8056 final Playlist playlist = new Playlist(); 8057 playlist.read(playlistFile); 8058 8059 for (ContentValues values : initialValues) { 8060 final long audioId = values.getAsLong(Audio.Playlists.Members.AUDIO_ID); 8061 final Uri audioUri = Audio.Media.getContentUri(audioVolumeName, audioId); 8062 final File audioFile = queryForDataFile(audioUri, null); 8063 8064 Integer playOrder = values.getAsInteger(Playlists.Members.PLAY_ORDER); 8065 playOrder = (playOrder != null) ? (playOrder - 1) : Integer.MAX_VALUE; 8066 playlist.add(playOrder, 8067 playlistFile.toPath().getParent().relativize(audioFile.toPath())); 8068 } 8069 playlist.write(playlistFile); 8070 8071 resolvePlaylistMembers(playlistUri); 8072 } catch (IOException e) { 8073 throw new FallbackException("Failed to update playlist", e, 8074 android.os.Build.VERSION_CODES.R); 8075 } 8076 8077 return initialValues.length; 8078 } 8079 8080 /** 8081 * Move an audio item within the given playlist. 8082 */ 8083 private int movePlaylistMembers(@NonNull Uri playlistUri, @NonNull ContentValues values, 8084 @NonNull Bundle queryArgs) throws FallbackException { 8085 final int fromIndex = resolvePlaylistIndex(playlistUri, queryArgs); 8086 final int toIndex = values.getAsInteger(Playlists.Members.PLAY_ORDER) - 1; 8087 if (fromIndex == -1) { 8088 throw new FallbackException("Failed to resolve playlist member " + queryArgs, 8089 android.os.Build.VERSION_CODES.R); 8090 } 8091 try { 8092 final File playlistFile = queryForDataFile(playlistUri, null); 8093 8094 final Playlist playlist = new Playlist(); 8095 playlist.read(playlistFile); 8096 final int finalIndex = playlist.move(fromIndex, toIndex); 8097 playlist.write(playlistFile); 8098 invalidateFuseDentry(playlistFile); 8099 8100 resolvePlaylistMembers(playlistUri); 8101 return finalIndex; 8102 } catch (IOException e) { 8103 throw new FallbackException("Failed to update playlist", e, 8104 android.os.Build.VERSION_CODES.R); 8105 } 8106 } 8107 8108 /** 8109 * Removes an audio item or multiple audio items(if targetSDK<R) from the given playlist. 8110 */ 8111 private int removePlaylistMembers(@NonNull Uri playlistUri, @NonNull Bundle queryArgs) 8112 throws FallbackException { 8113 final int[] indexes = resolvePlaylistIndexes(playlistUri, queryArgs); 8114 try { 8115 final File playlistFile = queryForDataFile(playlistUri, null); 8116 8117 final Playlist playlist = new Playlist(); 8118 playlist.read(playlistFile); 8119 final int count; 8120 if (indexes.length == 0) { 8121 // This means either no playlist members match the query or VolumeNotFoundException 8122 // was thrown. So we don't have anything to delete. 8123 count = 0; 8124 } else { 8125 count = playlist.removeMultiple(indexes); 8126 } 8127 playlist.write(playlistFile); 8128 invalidateFuseDentry(playlistFile); 8129 8130 resolvePlaylistMembers(playlistUri); 8131 return count; 8132 } catch (IOException e) { 8133 throw new FallbackException("Failed to update playlist", e, 8134 android.os.Build.VERSION_CODES.R); 8135 } 8136 } 8137 8138 /** 8139 * Remove an audio item from the given playlist since the playlist file or the audio file is 8140 * already removed. 8141 */ 8142 private void removePlaylistMembers(int mediaType, long id) { 8143 final DatabaseHelper helper; 8144 try { 8145 helper = getDatabaseForUri(Audio.Media.EXTERNAL_CONTENT_URI); 8146 } catch (VolumeNotFoundException e) { 8147 Log.w(TAG, e); 8148 return; 8149 } 8150 8151 helper.runWithTransaction((db) -> { 8152 final String where; 8153 if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 8154 where = "playlist_id=?"; 8155 } else { 8156 where = "audio_id=?"; 8157 } 8158 db.delete("audio_playlists_map", where, new String[] { "" + id }); 8159 return null; 8160 }); 8161 } 8162 8163 /** 8164 * Resolve query arguments that are designed to select specific playlist 8165 * items using the playlist's {@link Playlists.Members#PLAY_ORDER}. 8166 * 8167 * @return an array of the indexes that match the query. 8168 */ 8169 private int[] resolvePlaylistIndexes(@NonNull Uri playlistUri, @NonNull Bundle queryArgs) { 8170 final Uri membersUri = Playlists.Members.getContentUri( 8171 getVolumeName(playlistUri), ContentUris.parseId(playlistUri)); 8172 8173 final DatabaseHelper helper; 8174 final SQLiteQueryBuilder qb; 8175 try { 8176 helper = getDatabaseForUri(membersUri); 8177 qb = getQueryBuilder(TYPE_DELETE, AUDIO_PLAYLISTS_ID_MEMBERS, 8178 membersUri, queryArgs, null); 8179 } catch (VolumeNotFoundException ignored) { 8180 return new int[0]; 8181 } 8182 8183 try (Cursor c = qb.query(helper, 8184 new String[] { Playlists.Members.PLAY_ORDER }, queryArgs, null)) { 8185 if ((c.getCount() >= 1) && c.moveToFirst()) { 8186 int size = c.getCount(); 8187 int[] res = new int[size]; 8188 for (int i = 0; i < size; ++i) { 8189 res[i] = c.getInt(0) - 1; 8190 c.moveToNext(); 8191 } 8192 return res; 8193 } else { 8194 // Cursor size is 0 8195 return new int[0]; 8196 } 8197 } 8198 } 8199 8200 /** 8201 * Resolve query arguments that are designed to select a specific playlist 8202 * item using its {@link Playlists.Members#PLAY_ORDER}. 8203 * 8204 * @return if there's only 1 item that matches the query, returns its index. Returns -1 8205 * otherwise. 8206 */ 8207 private int resolvePlaylistIndex(@NonNull Uri playlistUri, @NonNull Bundle queryArgs) { 8208 int[] indexes = resolvePlaylistIndexes(playlistUri, queryArgs); 8209 if (indexes.length == 1) { 8210 return indexes[0]; 8211 } 8212 return -1; 8213 } 8214 8215 private boolean isPickerUri(Uri uri) { 8216 final int match = matchUri(uri, /* allowHidden */ isCallingPackageAllowedHidden()); 8217 return match == PICKER_ID; 8218 } 8219 8220 @Override 8221 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 8222 return openFileCommon(uri, mode, /*signal*/ null, /*opts*/ null); 8223 } 8224 8225 @Override 8226 public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) 8227 throws FileNotFoundException { 8228 return openFileCommon(uri, mode, signal, /*opts*/ null); 8229 } 8230 8231 private ParcelFileDescriptor openFileCommon(Uri uri, String mode, CancellationSignal signal, 8232 @Nullable Bundle opts) 8233 throws FileNotFoundException { 8234 opts = opts == null ? new Bundle() : opts; 8235 // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider. 8236 opts.remove(QUERY_ARG_REDACTED_URI); 8237 if (isRedactedUri(uri)) { 8238 opts.putParcelable(QUERY_ARG_REDACTED_URI, uri); 8239 uri = getUriForRedactedUri(uri); 8240 } 8241 uri = safeUncanonicalize(uri); 8242 8243 if (isPickerUri(uri)) { 8244 final int callingPid = mCallingIdentity.get().pid; 8245 final int callingUid = mCallingIdentity.get().uid; 8246 return mPickerUriResolver.openFile(uri, mode, signal, callingPid, callingUid); 8247 } 8248 8249 final boolean allowHidden = isCallingPackageAllowedHidden(); 8250 final int match = matchUri(uri, allowHidden); 8251 final String volumeName = getVolumeName(uri); 8252 8253 // Handle some legacy cases where we need to redirect thumbnails 8254 try { 8255 switch (match) { 8256 case AUDIO_ALBUMART_ID: { 8257 final long albumId = Long.parseLong(uri.getPathSegments().get(3)); 8258 final Uri targetUri = ContentUris 8259 .withAppendedId(Audio.Albums.getContentUri(volumeName), albumId); 8260 return ensureThumbnail(targetUri, signal); 8261 } 8262 case AUDIO_ALBUMART_FILE_ID: { 8263 final long audioId = Long.parseLong(uri.getPathSegments().get(3)); 8264 final Uri targetUri = ContentUris 8265 .withAppendedId(Audio.Media.getContentUri(volumeName), audioId); 8266 return ensureThumbnail(targetUri, signal); 8267 } 8268 case VIDEO_MEDIA_ID_THUMBNAIL: { 8269 final long videoId = Long.parseLong(uri.getPathSegments().get(3)); 8270 final Uri targetUri = ContentUris 8271 .withAppendedId(Video.Media.getContentUri(volumeName), videoId); 8272 return ensureThumbnail(targetUri, signal); 8273 } 8274 case IMAGES_MEDIA_ID_THUMBNAIL: { 8275 final long imageId = Long.parseLong(uri.getPathSegments().get(3)); 8276 final Uri targetUri = ContentUris 8277 .withAppendedId(Images.Media.getContentUri(volumeName), imageId); 8278 return ensureThumbnail(targetUri, signal); 8279 } 8280 case CLI: { 8281 // Command Line Interface "file" - content://media/cli - may be opened only by 8282 // Shell (or Root). 8283 if (!isCallingPackageShell()) { 8284 throw new SecurityException("Only shell (or root) is allowed to open " 8285 + "MediaProvider's CLI file (" + uri + ')'); 8286 } 8287 8288 // We expect the uri's query to hold a single parameter - "cmd" - which contains 8289 // the command name followed by the arguments (if any), all joined with '+' 8290 // symbols: 8291 // ?cmd=command[+arg1+[arg2+[arg3...]]] 8292 // 8293 // For example: 8294 // (1) ?cmd=version 8295 // (2) ?cmd=set-cloud-provider=com.my.cloud.provider.authority 8296 // 8297 // We retrieve the command name and the argument (if any) with 8298 // uri.getQueryParameter("cmd") call, which will replace all '+' delimiters with 8299 // spaces. 8300 8301 final String[] cmdAndArgs = uri.getQueryParameter("cmd").split("\\s+"); 8302 Log.d(TAG, "MediaProvider CLI command: " + Arrays.toString(cmdAndArgs)); 8303 try { 8304 // Create a UNIX pipe. 8305 final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); 8306 // Pass the write end - pipe[1] - to our shell command. 8307 final var cmd = new MediaProviderShellCommand(getContext(), 8308 mConfigStore, 8309 mPickerSyncController, 8310 /* out */ pipe[1]); 8311 cmd.exec(cmdAndArgs); 8312 // Return the read end - pipe[0] - to the caller. 8313 return pipe[0]; 8314 } catch (IOException e) { 8315 Log.e(TAG, "Could not create a pipe", e); 8316 return null; 8317 } 8318 } 8319 } 8320 } finally { 8321 // We have to log separately here because openFileAndEnforcePathPermissionsHelper calls 8322 // a public MediaProvider API and so logs the access there. 8323 PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName); 8324 } 8325 8326 return openFileAndEnforcePathPermissionsHelper(uri, match, mode, signal, opts); 8327 } 8328 8329 @Override 8330 public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) 8331 throws FileNotFoundException { 8332 return openTypedAssetFileCommon(uri, mimeTypeFilter, opts, null); 8333 } 8334 8335 @Override 8336 public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts, 8337 CancellationSignal signal) throws FileNotFoundException { 8338 return openTypedAssetFileCommon(uri, mimeTypeFilter, opts, signal); 8339 } 8340 8341 private AssetFileDescriptor openTypedAssetFileCommon(Uri uri, String mimeTypeFilter, 8342 Bundle opts, CancellationSignal signal) throws FileNotFoundException { 8343 final boolean wantsThumb = (opts != null) && opts.containsKey(ContentResolver.EXTRA_SIZE) 8344 && StringUtils.startsWithIgnoreCase(mimeTypeFilter, "image/"); 8345 String mode = "r"; 8346 8347 // If request is not for thumbnail and arising from MediaProvider, then check for EXTRA_MODE 8348 if (opts != null && !wantsThumb && isCallingPackageSelf()) { 8349 mode = opts.getString(MediaStore.EXTRA_MODE, "r"); 8350 } else if (opts != null) { 8351 opts.remove(MediaStore.EXTRA_MODE); 8352 } 8353 8354 if (opts != null && opts.containsKey(MediaStore.EXTRA_FILE_DESCRIPTOR)) { 8355 // This is called as part of MediaStore#getOriginalMediaFormatFileDescriptor 8356 // We don't need to use the |uri| because the input fd already identifies the file and 8357 // we actually don't have a valid URI, we are going to identify the file via the fd. 8358 // While identifying the file, we also perform the following security checks. 8359 // 1. Find the FUSE file with the associated inode 8360 // 2. Verify that the binder caller opened it 8361 // 3. Verify the access level the fd is opened with (r/w) 8362 // 4. Open the original (non-transcoded) file *with* redaction enabled and the access 8363 // level from #3 8364 // 5. Return the fd from #4 to the app or throw an exception if any of the conditions 8365 // are not met 8366 try { 8367 return getOriginalMediaFormatFileDescriptor(opts); 8368 } finally { 8369 // Clearing the Bundle closes the underlying Parcel, ensuring that the input fd 8370 // owned by the Parcel is closed immediately and not at the next GC. 8371 // This works around a change in behavior introduced by: 8372 // aosp/Icfe8880cad00c3cd2afcbe4b92400ad4579e680e 8373 opts.clear(); 8374 } 8375 } 8376 8377 // This is needed for thumbnail resolution as it doesn't go through openFileCommon 8378 if (isPickerUri(uri)) { 8379 final int callingPid = mCallingIdentity.get().pid; 8380 final int callingUid = mCallingIdentity.get().uid; 8381 return mPickerUriResolver.openTypedAssetFile(uri, mimeTypeFilter, opts, signal, 8382 callingPid, callingUid); 8383 } 8384 8385 // TODO: enforce that caller has access to this uri 8386 8387 // Offer thumbnail of media, when requested 8388 if (wantsThumb) { 8389 final ParcelFileDescriptor pfd = ensureThumbnail(uri, signal); 8390 return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH); 8391 } 8392 8393 // Worst case, return the underlying file 8394 return new AssetFileDescriptor(openFileCommon(uri, mode, signal, opts), 0, 8395 AssetFileDescriptor.UNKNOWN_LENGTH); 8396 } 8397 8398 private ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal) 8399 throws FileNotFoundException { 8400 final boolean allowHidden = isCallingPackageAllowedHidden(); 8401 final int match = matchUri(uri, allowHidden); 8402 8403 Trace.beginSection("MP.ensureThumbnail"); 8404 final LocalCallingIdentity token = clearLocalCallingIdentity(); 8405 try { 8406 switch (match) { 8407 case AUDIO_ALBUMS_ID: { 8408 final String volumeName = MediaStore.getVolumeName(uri); 8409 final Uri baseUri = MediaStore.Audio.Media.getContentUri(volumeName); 8410 final long albumId = ContentUris.parseId(uri); 8411 try (Cursor c = query(baseUri, new String[] { MediaStore.Audio.Media._ID }, 8412 MediaStore.Audio.Media.ALBUM_ID + "=" + albumId, null, null, signal)) { 8413 if (c.moveToFirst()) { 8414 final long audioId = c.getLong(0); 8415 final Uri targetUri = ContentUris.withAppendedId(baseUri, audioId); 8416 return mAudioThumbnailer.ensureThumbnail(targetUri, signal); 8417 } else { 8418 throw new FileNotFoundException("No media for album " + uri); 8419 } 8420 } 8421 } 8422 case AUDIO_MEDIA_ID: 8423 return mAudioThumbnailer.ensureThumbnail(uri, signal); 8424 case VIDEO_MEDIA_ID: 8425 return mVideoThumbnailer.ensureThumbnail(uri, signal); 8426 case IMAGES_MEDIA_ID: 8427 return mImageThumbnailer.ensureThumbnail(uri, signal); 8428 case FILES_ID: 8429 case DOWNLOADS_ID: { 8430 // When item is referenced in a generic way, resolve to actual type 8431 final int mediaType = MimeUtils.resolveMediaType(getType(uri)); 8432 switch (mediaType) { 8433 case FileColumns.MEDIA_TYPE_AUDIO: 8434 return mAudioThumbnailer.ensureThumbnail(uri, signal); 8435 case FileColumns.MEDIA_TYPE_VIDEO: 8436 return mVideoThumbnailer.ensureThumbnail(uri, signal); 8437 case FileColumns.MEDIA_TYPE_IMAGE: 8438 return mImageThumbnailer.ensureThumbnail(uri, signal); 8439 default: 8440 throw new FileNotFoundException(); 8441 } 8442 } 8443 default: 8444 throw new FileNotFoundException(); 8445 } 8446 } catch (IOException e) { 8447 Log.w(TAG, e); 8448 throw new FileNotFoundException(e.getMessage()); 8449 } finally { 8450 restoreLocalCallingIdentity(token); 8451 Trace.endSection(); 8452 } 8453 } 8454 8455 /** 8456 * Update the metadata columns for the image residing at given {@link Uri} 8457 * by reading data from the underlying image. 8458 */ 8459 private void updateImageMetadata(ContentValues values, File file) { 8460 final BitmapFactory.Options bitmapOpts = new BitmapFactory.Options(); 8461 bitmapOpts.inJustDecodeBounds = true; 8462 BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOpts); 8463 8464 values.put(MediaColumns.WIDTH, bitmapOpts.outWidth); 8465 values.put(MediaColumns.HEIGHT, bitmapOpts.outHeight); 8466 } 8467 8468 private void handleInsertedRowForFuse(long rowId) { 8469 if (isFuseThread()) { 8470 // Removes restored row ID saved list. 8471 mCallingIdentity.get().removeDeletedRowId(rowId); 8472 } 8473 } 8474 8475 private void handleUpdatedRowForFuse(@NonNull String oldPath, @NonNull String ownerPackage, 8476 long oldRowId, long newRowId) { 8477 if (oldRowId == newRowId) { 8478 // Update didn't delete or add row ID. We don't need to save row ID or remove saved 8479 // deleted ID. 8480 return; 8481 } 8482 8483 handleDeletedRowForFuse(oldPath, ownerPackage, oldRowId); 8484 handleInsertedRowForFuse(newRowId); 8485 } 8486 8487 private void handleDeletedRowForFuse(@NonNull String path, @NonNull String ownerPackage, 8488 long rowId) { 8489 if (!isFuseThread()) { 8490 return; 8491 } 8492 8493 // Invalidate saved owned ID's of the previous owner of the deleted path, this prevents old 8494 // owner from gaining access to newly created file with restored row ID. 8495 if (!ownerPackage.equals("null") && !ownerPackage.equals(getCallingPackageOrSelf())) { 8496 invalidateLocalCallingIdentityCache(ownerPackage, "owned_database_row_deleted:" 8497 + path); 8498 } 8499 // Saves row ID corresponding to deleted path. Saved row ID will be restored on subsequent 8500 // create or rename. 8501 mCallingIdentity.get().addDeletedRowId(path, rowId); 8502 } 8503 8504 private void handleOwnerPackageNameChange(@NonNull String oldPath, 8505 @NonNull String oldOwnerPackage, @NonNull String newOwnerPackage) { 8506 if (Objects.equals(oldOwnerPackage, newOwnerPackage)) { 8507 return; 8508 } 8509 // Invalidate saved owned ID's of the previous owner of the renamed path, this prevents old 8510 // owner from gaining access to replaced file. 8511 invalidateLocalCallingIdentityCache(oldOwnerPackage, "owner_package_changed:" + oldPath); 8512 } 8513 8514 /** 8515 * Return the {@link MediaColumns#DATA} field for the given {@code Uri}. 8516 */ 8517 File queryForDataFile(Uri uri, CancellationSignal signal) 8518 throws FileNotFoundException { 8519 return queryForDataFile(uri, null, null, signal); 8520 } 8521 8522 /** 8523 * Return the {@link MediaColumns#DATA} field for the given {@code Uri}. 8524 */ 8525 File queryForDataFile(Uri uri, String selection, String[] selectionArgs, 8526 CancellationSignal signal) throws FileNotFoundException { 8527 try (Cursor cursor = queryForSingleItem(uri, new String[] { MediaColumns.DATA }, 8528 selection, selectionArgs, signal)) { 8529 final String data = cursor.getString(0); 8530 if (TextUtils.isEmpty(data)) { 8531 throw new FileNotFoundException("Missing path for " + uri); 8532 } else { 8533 return new File(data); 8534 } 8535 } 8536 } 8537 8538 /** 8539 * Return the {@link Uri} for the given {@code File}. 8540 */ 8541 Uri queryForMediaUri(File file, CancellationSignal signal) throws FileNotFoundException { 8542 final String volumeName = FileUtils.getVolumeName(getContext(), file); 8543 final Uri uri = Files.getContentUri(volumeName); 8544 try (Cursor cursor = queryForSingleItem(uri, new String[] { MediaColumns._ID }, 8545 MediaColumns.DATA + "=?", new String[] { file.getAbsolutePath() }, signal)) { 8546 return ContentUris.withAppendedId(uri, cursor.getLong(0)); 8547 } 8548 } 8549 8550 /** 8551 * Query the given {@link Uri} as MediaProvider, expecting only a single item to be found. 8552 * 8553 * @throws FileNotFoundException if no items were found, or multiple items 8554 * were found, or there was trouble reading the data. 8555 */ 8556 Cursor queryForSingleItemAsMediaProvider(Uri uri, String[] projection, String selection, 8557 String[] selectionArgs, CancellationSignal signal) 8558 throws FileNotFoundException { 8559 final LocalCallingIdentity tokenInner = clearLocalCallingIdentity(); 8560 try { 8561 return queryForSingleItem(uri, projection, selection, selectionArgs, signal); 8562 } finally { 8563 restoreLocalCallingIdentity(tokenInner); 8564 } 8565 } 8566 8567 /** 8568 * Query the given {@link Uri}, expecting only a single item to be found. 8569 * 8570 * @throws FileNotFoundException if no items were found, or multiple items 8571 * were found, or there was trouble reading the data. 8572 */ 8573 Cursor queryForSingleItem(Uri uri, String[] projection, String selection, 8574 String[] selectionArgs, CancellationSignal signal) throws FileNotFoundException { 8575 Cursor c = null; 8576 try { 8577 c = query(uri, projection, 8578 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null), 8579 signal, true); 8580 } catch (IllegalArgumentException e) { 8581 throw new FileNotFoundException("Volume not found for " + uri); 8582 } 8583 if (c == null) { 8584 throw new FileNotFoundException("Missing cursor for " + uri); 8585 } else if (c.getCount() < 1) { 8586 FileUtils.closeQuietly(c); 8587 throw new FileNotFoundException("No item at " + uri); 8588 } else if (c.getCount() > 1) { 8589 FileUtils.closeQuietly(c); 8590 throw new FileNotFoundException("Multiple items at " + uri); 8591 } 8592 8593 if (c.moveToFirst()) { 8594 return c; 8595 } else { 8596 FileUtils.closeQuietly(c); 8597 throw new FileNotFoundException("Failed to read row from " + uri); 8598 } 8599 } 8600 8601 /** 8602 * Compares {@code itemOwner} with package name of {@link LocalCallingIdentity} and throws 8603 * {@link IllegalStateException} if it doesn't match. 8604 * Make sure to set calling identity properly before calling. 8605 */ 8606 private void requireOwnershipForItem(@Nullable String itemOwner, Uri item) { 8607 final boolean hasOwner = (itemOwner != null); 8608 final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), itemOwner); 8609 if (hasOwner && !callerIsOwner) { 8610 throw new IllegalStateException( 8611 "Only owner is able to interact with pending/trashed item " + item); 8612 } 8613 } 8614 8615 private ParcelFileDescriptor openWithFuse(String filePath, int uid, int mediaCapabilitiesUid, 8616 int modeBits, boolean shouldRedact, boolean shouldTranscode, int transcodeReason) 8617 throws FileNotFoundException { 8618 Log.d(TAG, "Open with FUSE. FilePath: " + filePath 8619 + ". Uid: " + uid 8620 + ". Media Capabilities Uid: " + mediaCapabilitiesUid 8621 + ". ShouldRedact: " + shouldRedact 8622 + ". ShouldTranscode: " + shouldTranscode); 8623 8624 int tid = android.os.Process.myTid(); 8625 synchronized (mPendingOpenInfo) { 8626 mPendingOpenInfo.put(tid, 8627 new PendingOpenInfo(uid, mediaCapabilitiesUid, shouldRedact, transcodeReason)); 8628 } 8629 8630 try { 8631 return FileUtils.openSafely(toFuseFile(new File(filePath)), modeBits); 8632 } finally { 8633 synchronized (mPendingOpenInfo) { 8634 mPendingOpenInfo.remove(tid); 8635 } 8636 } 8637 } 8638 8639 /** 8640 * @return {@link FuseDaemon} corresponding to a given file 8641 */ 8642 @NonNull 8643 public static FuseDaemon getFuseDaemonForFile(@NonNull File file, VolumeCache volumeCache) 8644 throws FileNotFoundException { 8645 final FuseDaemon daemon = ExternalStorageServiceImpl.getFuseDaemon( 8646 volumeCache.getVolumeId(file)); 8647 if (daemon == null) { 8648 throw new FileNotFoundException("Missing FUSE daemon for " + file); 8649 } else { 8650 return daemon; 8651 } 8652 } 8653 8654 @NonNull 8655 public static FuseDaemon getFuseDaemonForFileWithWait(@NonNull File file, 8656 VolumeCache volumeCache, long waitTimeInMilliseconds) throws FileNotFoundException { 8657 FuseDaemon fuseDaemon = null; 8658 long time = 0; 8659 while (time < waitTimeInMilliseconds) { 8660 fuseDaemon = ExternalStorageServiceImpl.getFuseDaemon( 8661 volumeCache.getVolumeId(file)); 8662 if (fuseDaemon != null) { 8663 break; 8664 } 8665 SystemClock.sleep(POLLING_TIME_IN_MILLIS); 8666 time += POLLING_TIME_IN_MILLIS; 8667 } 8668 8669 if (fuseDaemon == null) { 8670 throw new FileNotFoundException("Missing FUSE daemon for " + file); 8671 } else { 8672 return fuseDaemon; 8673 } 8674 } 8675 8676 private void invalidateFuseDentry(@NonNull File file) { 8677 invalidateFuseDentry(file.getAbsolutePath()); 8678 } 8679 8680 private void invalidateFuseDentry(@NonNull String path) { 8681 try { 8682 final FuseDaemon daemon = getFuseDaemonForFile(new File(path), mVolumeCache); 8683 if (isFuseThread()) { 8684 // If we are on a FUSE thread, we don't need to invalidate, 8685 // (and *must* not, otherwise we'd crash) because the invalidation 8686 // is already reflected in the lower filesystem 8687 return; 8688 } else { 8689 daemon.invalidateFuseDentryCache(path); 8690 } 8691 } catch (FileNotFoundException e) { 8692 Log.w(TAG, "Failed to invalidate FUSE dentry", e); 8693 } 8694 } 8695 8696 /** 8697 * Replacement for {@link #openFileHelper(Uri, String)} which enforces any 8698 * permissions applicable to the path before returning. 8699 * 8700 * <p>This function should never be called from the fuse thread since it tries to open 8701 * a "/mnt/user" path. 8702 */ 8703 private ParcelFileDescriptor openFileAndEnforcePathPermissionsHelper(Uri uri, int match, 8704 String mode, CancellationSignal signal, @NonNull Bundle opts) 8705 throws FileNotFoundException { 8706 int modeBits = ParcelFileDescriptor.parseMode(mode); 8707 boolean forWrite = (modeBits & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0; 8708 final Uri redactedUri = opts.getParcelable(QUERY_ARG_REDACTED_URI); 8709 if (forWrite) { 8710 if (redactedUri != null) { 8711 throw new UnsupportedOperationException( 8712 "Write is not supported on " + redactedUri.toString()); 8713 } 8714 // Upgrade 'w' only to 'rw'. This allows us acquire a WR_LOCK when calling 8715 // #shouldOpenWithFuse 8716 modeBits |= ParcelFileDescriptor.MODE_READ_WRITE; 8717 } 8718 8719 final boolean hasOwnerPackageName = hasOwnerPackageName(uri); 8720 final String[] projection = new String[] { 8721 MediaColumns.DATA, 8722 hasOwnerPackageName ? MediaColumns.OWNER_PACKAGE_NAME : "NULL", 8723 hasOwnerPackageName ? MediaColumns.IS_PENDING : "0", 8724 }; 8725 8726 final File file; 8727 final String ownerPackageName; 8728 final boolean isPending; 8729 final LocalCallingIdentity token = clearLocalCallingIdentity(); 8730 try (Cursor c = queryForSingleItem(uri, projection, null, null, signal)) { 8731 final String data = c.getString(0); 8732 if (TextUtils.isEmpty(data)) { 8733 throw new FileNotFoundException("Missing path for " + uri); 8734 } else { 8735 file = new File(data).getCanonicalFile(); 8736 } 8737 ownerPackageName = c.getString(1); 8738 isPending = c.getInt(2) != 0; 8739 } catch (IOException e) { 8740 throw new FileNotFoundException(e.toString()); 8741 } finally { 8742 restoreLocalCallingIdentity(token); 8743 } 8744 8745 if (redactedUri == null) { 8746 checkAccess(uri, Bundle.EMPTY, file, forWrite); 8747 } else { 8748 checkAccess(redactedUri, Bundle.EMPTY, file, false); 8749 } 8750 8751 // We don't check ownership for files with IS_PENDING set by FUSE 8752 if (isPending && !isPendingFromFuse(file)) { 8753 requireOwnershipForItem(ownerPackageName, uri); 8754 } 8755 8756 // Figure out if we need to redact contents 8757 final boolean redactionNeeded = isRedactionNeededForOpenViaContentResolver(redactedUri, 8758 ownerPackageName, file); 8759 final RedactionInfo redactionInfo; 8760 try { 8761 redactionInfo = redactionNeeded ? getRedactionRanges(file) 8762 : new RedactionInfo(new long[0], new long[0]); 8763 } catch (IOException e) { 8764 throw new IllegalStateException(e); 8765 } 8766 8767 // Yell if caller requires original, since we can't give it to them 8768 // unless they have access granted above 8769 if (redactionNeeded && MediaStore.getRequireOriginal(uri)) { 8770 throw new UnsupportedOperationException( 8771 "Caller must hold ACCESS_MEDIA_LOCATION permission to access original"); 8772 } 8773 8774 // Kick off metadata update when writing is finished 8775 final OnCloseListener listener = (e) -> { 8776 // We always update metadata to reflect the state on disk, even when 8777 // the remote writer tried claiming an exception 8778 invalidateThumbnails(uri); 8779 8780 // Invalidate so subsequent stat(2) on the upper fs is eventually consistent 8781 invalidateFuseDentry(file); 8782 try { 8783 switch (match) { 8784 case IMAGES_THUMBNAILS_ID: 8785 case VIDEO_THUMBNAILS_ID: 8786 final ContentValues values = new ContentValues(); 8787 updateImageMetadata(values, file); 8788 update(uri, values, null, null); 8789 break; 8790 default: 8791 scanFileAsMediaProvider(file, REASON_DEMAND); 8792 break; 8793 } 8794 } catch (Exception e2) { 8795 Log.w(TAG, "Failed to update metadata for " + uri, e2); 8796 } 8797 }; 8798 8799 try { 8800 // First, handle any redaction that is needed for caller 8801 final ParcelFileDescriptor pfd; 8802 final String filePath = file.getPath(); 8803 final int uid = Binder.getCallingUid(); 8804 final int transcodeReason = mTranscodeHelper.shouldTranscode(filePath, uid, opts); 8805 final boolean shouldTranscode = transcodeReason > 0; 8806 int mediaCapabilitiesUid = opts.getInt(MediaStore.EXTRA_MEDIA_CAPABILITIES_UID); 8807 if (!shouldTranscode || mediaCapabilitiesUid < Process.FIRST_APPLICATION_UID) { 8808 // Although 0 is a valid UID, it's not a valid app uid. 8809 // So, we use it to signify that mediaCapabilitiesUid is not set. 8810 mediaCapabilitiesUid = 0; 8811 } 8812 if (redactionInfo.redactionRanges.length > 0) { 8813 // If fuse is enabled, we can provide an fd that points to the fuse 8814 // file system and handle redaction in the fuse handler when the caller reads. 8815 pfd = openWithFuse(filePath, uid, mediaCapabilitiesUid, modeBits, 8816 true /* shouldRedact */, shouldTranscode, transcodeReason); 8817 } else if (shouldTranscode) { 8818 pfd = openWithFuse(filePath, uid, mediaCapabilitiesUid, modeBits, 8819 false /* shouldRedact */, shouldTranscode, transcodeReason); 8820 } else { 8821 FuseDaemon daemon = null; 8822 try { 8823 daemon = getFuseDaemonForFile(file, mVolumeCache); 8824 } catch (FileNotFoundException ignored) { 8825 } 8826 ParcelFileDescriptor lowerFsFd = FileUtils.openSafely(file, modeBits); 8827 // Always acquire a readLock. This allows us make multiple opens via lower 8828 // filesystem 8829 boolean shouldOpenWithFuse = daemon != null 8830 && daemon.shouldOpenWithFuse(filePath, true /* forRead */, 8831 lowerFsFd.getFd()); 8832 8833 if (shouldOpenWithFuse) { 8834 // If the file is already opened on the FUSE mount with VFS caching enabled 8835 // we return an upper filesystem fd (via FUSE) to avoid file corruption 8836 // resulting from cache inconsistencies between the upper and lower 8837 // filesystem caches 8838 pfd = openWithFuse(filePath, uid, mediaCapabilitiesUid, modeBits, 8839 false /* shouldRedact */, shouldTranscode, transcodeReason); 8840 try { 8841 lowerFsFd.close(); 8842 } catch (IOException e) { 8843 Log.w(TAG, "Failed to close lower filesystem fd " + file.getPath(), e); 8844 } 8845 } else { 8846 Log.i(TAG, "Open with lower FS for " + filePath + ". Uid: " + uid); 8847 if (forWrite) { 8848 // When opening for write on the lower filesystem, invalidate the VFS dentry 8849 // so subsequent open/getattr calls will return correctly. 8850 // 8851 // A 'dirty' dentry with write back cache enabled can cause the kernel to 8852 // ignore file attributes or even see stale page cache data when the lower 8853 // filesystem has been modified outside of the FUSE driver 8854 invalidateFuseDentry(file); 8855 } 8856 8857 pfd = lowerFsFd; 8858 } 8859 } 8860 8861 // Second, wrap in any listener that we've requested 8862 if (!isPending && forWrite) { 8863 return ParcelFileDescriptor.wrap(pfd, BackgroundThread.getHandler(), listener); 8864 } else { 8865 return pfd; 8866 } 8867 } catch (IOException e) { 8868 if (e instanceof FileNotFoundException) { 8869 throw (FileNotFoundException) e; 8870 } else { 8871 throw new IllegalStateException(e); 8872 } 8873 } 8874 } 8875 8876 private boolean isRedactionNeededForOpenViaContentResolver(Uri redactedUri, 8877 String ownerPackageName, File file) { 8878 // Redacted Uris should always redact information 8879 if (redactedUri != null) { 8880 return true; 8881 } 8882 8883 final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), ownerPackageName); 8884 if (callerIsOwner) { 8885 return false; 8886 } 8887 8888 // To be consistent with FUSE redaction checks we allow similar access for File Manager 8889 // and System Gallery apps. 8890 if (isCallingPackageManager() || canSystemGalleryAccessTheFile(file.getPath())) { 8891 return false; 8892 } 8893 8894 return isRedactionNeeded(); 8895 } 8896 8897 private void deleteAndInvalidate(@NonNull Path path) { 8898 deleteAndInvalidate(path.toFile()); 8899 } 8900 8901 private void deleteAndInvalidate(@NonNull File file) { 8902 file.delete(); 8903 invalidateFuseDentry(file); 8904 } 8905 8906 private void deleteIfAllowed(Uri uri, Bundle extras, String path) { 8907 try { 8908 final File file = new File(path).getCanonicalFile(); 8909 checkAccess(uri, extras, file, true); 8910 deleteAndInvalidate(file); 8911 } catch (Exception e) { 8912 Log.e(TAG, "Couldn't delete " + path, e); 8913 } 8914 } 8915 8916 @Deprecated 8917 private boolean isPending(Uri uri) { 8918 final int match = matchUri(uri, true); 8919 switch (match) { 8920 case AUDIO_MEDIA_ID: 8921 case VIDEO_MEDIA_ID: 8922 case IMAGES_MEDIA_ID: 8923 try (Cursor c = queryForSingleItem(uri, 8924 new String[] { MediaColumns.IS_PENDING }, null, null, null)) { 8925 return (c.getInt(0) != 0); 8926 } catch (FileNotFoundException e) { 8927 throw new IllegalStateException(e); 8928 } 8929 default: 8930 return false; 8931 } 8932 } 8933 8934 @Deprecated 8935 private boolean isRedactionNeeded(Uri uri) { 8936 return mCallingIdentity.get().hasPermission(PERMISSION_IS_REDACTION_NEEDED); 8937 } 8938 8939 private boolean isRedactionNeeded() { 8940 return mCallingIdentity.get().hasPermission(PERMISSION_IS_REDACTION_NEEDED); 8941 } 8942 8943 private boolean isCallingPackageRequestingLegacy() { 8944 return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_GRANTED); 8945 } 8946 8947 private boolean shouldBypassDatabase(int uid) { 8948 if (uid != android.os.Process.SHELL_UID && isCallingPackageManager()) { 8949 return mCallingIdentity.get().shouldBypassDatabase(false /*isSystemGallery*/); 8950 } else if (isCallingPackageSystemGallery()) { 8951 if (isCallingPackageLegacyWrite()) { 8952 // We bypass db operations for legacy system galleries with W_E_S (see b/167307393). 8953 // Tracking a longer term solution in b/168784136. 8954 return true; 8955 } else if (!SdkLevel.isAtLeastS()) { 8956 // We don't parse manifest flags for SdkLevel<=R yet. Hence, we don't bypass 8957 // database updates for SystemGallery targeting R or above on R OS. 8958 return false; 8959 } 8960 return mCallingIdentity.get().shouldBypassDatabase(true /*isSystemGallery*/); 8961 } 8962 return false; 8963 } 8964 8965 private static int getFileMediaType(String path) { 8966 final File file = new File(path); 8967 final String mimeType = MimeUtils.resolveMimeType(file); 8968 return MimeUtils.resolveMediaType(mimeType); 8969 } 8970 8971 private boolean canSystemGalleryAccessTheFile(String filePath) { 8972 8973 if (!isCallingPackageSystemGallery()) { 8974 return false; 8975 } 8976 8977 final int mediaType = getFileMediaType(filePath); 8978 8979 return mediaType == FileColumns.MEDIA_TYPE_IMAGE || 8980 mediaType == FileColumns.MEDIA_TYPE_VIDEO; 8981 } 8982 8983 /** 8984 * Returns true if: 8985 * <ul> 8986 * <li>the calling identity is an app targeting Q or older versions AND is requesting legacy 8987 * storage and has the corresponding legacy access (read/write) permissions 8988 * <li>the calling identity holds {@code MANAGE_EXTERNAL_STORAGE} 8989 * <li>the calling identity owns or has access to the filePath (eg /Android/data/com.foo) 8990 * <li>the calling identity has permission to write images and the given file is an image file 8991 * <li>the calling identity has permission to write video and the given file is an video file 8992 * </ul> 8993 */ 8994 private boolean shouldBypassFuseRestrictions(boolean forWrite, String filePath) { 8995 boolean isRequestingLegacyStorage = forWrite ? isCallingPackageLegacyWrite() 8996 : isCallingPackageLegacyRead(); 8997 if (isRequestingLegacyStorage) { 8998 return true; 8999 } 9000 9001 if (isCallingPackageManager()) { 9002 return true; 9003 } 9004 9005 // Check if the caller has access to private app directories. 9006 if (isUidAllowedAccessToDataOrObbPathForFuse(mCallingIdentity.get().uid, filePath)) { 9007 return true; 9008 } 9009 9010 // Apps with write access to images and/or videos can bypass our restrictions if all of the 9011 // the files they're accessing are of the compatible media type. 9012 return canSystemGalleryAccessTheFile(filePath); 9013 } 9014 9015 /** 9016 * Returns true if the passed in path is an application-private data directory 9017 * (such as Android/data/com.foo or Android/obb/com.foo) that does not belong to the caller and 9018 * the caller does not have special access. 9019 */ 9020 private boolean isPrivatePackagePathNotAccessibleByCaller(String path) { 9021 // Files under the apps own private directory 9022 final String appSpecificDir = extractPathOwnerPackageName(path); 9023 9024 if (appSpecificDir == null) { 9025 return false; 9026 } 9027 9028 // Android/media is not considered private, because it contains media that is explicitly 9029 // scanned and shared by other apps 9030 if (isExternalMediaDirectory(path)) { 9031 return false; 9032 } 9033 return !isUidAllowedAccessToDataOrObbPathForFuse(mCallingIdentity.get().uid, path); 9034 } 9035 9036 private boolean shouldBypassDatabaseAndSetDirtyForFuse(int uid, String path) { 9037 if (shouldBypassDatabase(uid)) { 9038 synchronized (mNonHiddenPaths) { 9039 File file = new File(path); 9040 String key = file.getParent(); 9041 boolean maybeHidden = !mNonHiddenPaths.containsKey(key); 9042 9043 if (maybeHidden) { 9044 File topNoMediaDir = FileUtils.getTopLevelNoMedia(new File(path)); 9045 if (topNoMediaDir == null) { 9046 mNonHiddenPaths.put(key, 0); 9047 } else { 9048 mMediaScanner.onDirectoryDirty(topNoMediaDir); 9049 } 9050 } 9051 } 9052 return true; 9053 } 9054 return false; 9055 } 9056 9057 /** 9058 * Set of Exif tags that should be considered for redaction. 9059 */ 9060 private static final String[] REDACTED_EXIF_TAGS = new String[] { 9061 ExifInterface.TAG_GPS_ALTITUDE, 9062 ExifInterface.TAG_GPS_ALTITUDE_REF, 9063 ExifInterface.TAG_GPS_AREA_INFORMATION, 9064 ExifInterface.TAG_GPS_DOP, 9065 ExifInterface.TAG_GPS_DATESTAMP, 9066 ExifInterface.TAG_GPS_DEST_BEARING, 9067 ExifInterface.TAG_GPS_DEST_BEARING_REF, 9068 ExifInterface.TAG_GPS_DEST_DISTANCE, 9069 ExifInterface.TAG_GPS_DEST_DISTANCE_REF, 9070 ExifInterface.TAG_GPS_DEST_LATITUDE, 9071 ExifInterface.TAG_GPS_DEST_LATITUDE_REF, 9072 ExifInterface.TAG_GPS_DEST_LONGITUDE, 9073 ExifInterface.TAG_GPS_DEST_LONGITUDE_REF, 9074 ExifInterface.TAG_GPS_DIFFERENTIAL, 9075 ExifInterface.TAG_GPS_IMG_DIRECTION, 9076 ExifInterface.TAG_GPS_IMG_DIRECTION_REF, 9077 ExifInterface.TAG_GPS_LATITUDE, 9078 ExifInterface.TAG_GPS_LATITUDE_REF, 9079 ExifInterface.TAG_GPS_LONGITUDE, 9080 ExifInterface.TAG_GPS_LONGITUDE_REF, 9081 ExifInterface.TAG_GPS_MAP_DATUM, 9082 ExifInterface.TAG_GPS_MEASURE_MODE, 9083 ExifInterface.TAG_GPS_PROCESSING_METHOD, 9084 ExifInterface.TAG_GPS_SATELLITES, 9085 ExifInterface.TAG_GPS_SPEED, 9086 ExifInterface.TAG_GPS_SPEED_REF, 9087 ExifInterface.TAG_GPS_STATUS, 9088 ExifInterface.TAG_GPS_TIMESTAMP, 9089 ExifInterface.TAG_GPS_TRACK, 9090 ExifInterface.TAG_GPS_TRACK_REF, 9091 ExifInterface.TAG_GPS_VERSION_ID, 9092 }; 9093 9094 /** 9095 * Set of ISO boxes that should be considered for redaction. 9096 */ 9097 private static final int[] REDACTED_ISO_BOXES = new int[] { 9098 IsoInterface.BOX_LOCI, 9099 IsoInterface.BOX_XYZ, 9100 IsoInterface.BOX_GPS, 9101 IsoInterface.BOX_GPS0, 9102 }; 9103 9104 public static final Set<String> sRedactedExifTags = new ArraySet<>( 9105 Arrays.asList(REDACTED_EXIF_TAGS)); 9106 9107 private static final class RedactionInfo { 9108 public final long[] redactionRanges; 9109 public final long[] freeOffsets; 9110 9111 public RedactionInfo() { 9112 this.redactionRanges = new long[0]; 9113 this.freeOffsets = new long[0]; 9114 } 9115 9116 public RedactionInfo(long[] redactionRanges, long[] freeOffsets) { 9117 this.redactionRanges = redactionRanges; 9118 this.freeOffsets = freeOffsets; 9119 } 9120 } 9121 9122 private static class LRUCache<K, V> extends LinkedHashMap<K, V> { 9123 private final int mMaxSize; 9124 9125 public LRUCache(int maxSize) { 9126 this.mMaxSize = maxSize; 9127 } 9128 9129 @Override 9130 protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { 9131 return size() > mMaxSize; 9132 } 9133 } 9134 9135 private static final class PendingOpenInfo { 9136 public final int uid; 9137 public final int mediaCapabilitiesUid; 9138 public final boolean shouldRedact; 9139 public final int transcodeReason; 9140 9141 public PendingOpenInfo(int uid, int mediaCapabilitiesUid, boolean shouldRedact, 9142 int transcodeReason) { 9143 this.uid = uid; 9144 this.mediaCapabilitiesUid = mediaCapabilitiesUid; 9145 this.shouldRedact = shouldRedact; 9146 this.transcodeReason = transcodeReason; 9147 } 9148 } 9149 9150 /** 9151 * Calculates the ranges that need to be redacted for the given file and user that wants to 9152 * access the file. 9153 * Note: This method assumes that the caller of this function has already done permission checks 9154 * for the uid to access this path. 9155 * 9156 * @param uid UID of the package wanting to access the file 9157 * @param path File path 9158 * @param tid thread id making IO on the FUSE filesystem 9159 * @return Ranges that should be redacted. 9160 * 9161 * @throws IOException if an error occurs while calculating the redaction ranges 9162 */ 9163 @NonNull 9164 private long[] getRedactionRangesForFuse(String path, String ioPath, int original_uid, int uid, 9165 int tid, boolean forceRedaction) throws IOException { 9166 // |ioPath| might refer to a transcoded file path (which is not indexed in the db) 9167 // |path| will always refer to a valid _data column 9168 // We use |ioPath| for the filesystem access because in the case of transcoding, 9169 // we want to get redaction ranges from the transcoded file and *not* the original file 9170 final File file = new File(ioPath); 9171 9172 if (forceRedaction) { 9173 return getRedactionRanges(file).redactionRanges; 9174 } 9175 9176 // When calculating redaction ranges initiated from MediaProvider, the redaction policy 9177 // is slightly different from the FUSE initiated opens redaction policy. targetSdk=29 from 9178 // MediaProvider requires redaction, but targetSdk=29 apps from FUSE don't require redaction 9179 // Hence, we check the mPendingOpenInfo object (populated when opens are initiated from 9180 // MediaProvider) if there's a pending open from MediaProvider with matching tid and uid and 9181 // use the shouldRedact decision there if there's one. 9182 synchronized (mPendingOpenInfo) { 9183 PendingOpenInfo info = mPendingOpenInfo.get(tid); 9184 if (info != null && info.uid == original_uid) { 9185 boolean shouldRedact = info.shouldRedact; 9186 if (shouldRedact) { 9187 return getRedactionRanges(file).redactionRanges; 9188 } else { 9189 return new long[0]; 9190 } 9191 } 9192 } 9193 9194 final LocalCallingIdentity token = 9195 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 9196 try { 9197 if (!isRedactionNeeded() 9198 || shouldBypassFuseRestrictions(/* forWrite */ false, path)) { 9199 return new long[0]; 9200 } 9201 9202 final Uri contentUri = FileUtils.getContentUriForPath(path); 9203 final String[] projection = new String[]{ 9204 MediaColumns.OWNER_PACKAGE_NAME, MediaColumns._ID , FileColumns.MEDIA_TYPE}; 9205 final String selection = MediaColumns.DATA + "=?"; 9206 final String[] selectionArgs = new String[]{path}; 9207 final String ownerPackageName; 9208 final int id; 9209 final int mediaType; 9210 // Query as MediaProvider as non-RES apps will result in FileNotFoundException. 9211 // Note: The caller uid already has passed permission checks to access this file. 9212 try (final Cursor c = queryForSingleItemAsMediaProvider(contentUri, projection, 9213 selection, selectionArgs, null)) { 9214 c.moveToFirst(); 9215 ownerPackageName = c.getString(0); 9216 id = c.getInt(1); 9217 mediaType = c.getInt(2); 9218 } catch (FileNotFoundException e) { 9219 // Ideally, this shouldn't happen unless the file was deleted after we checked its 9220 // existence and before we get to the redaction logic here. In this case we throw 9221 // and fail the operation and FuseDaemon should handle this and fail the whole open 9222 // operation gracefully. 9223 throw new FileNotFoundException( 9224 path + " not found while calculating redaction ranges: " + e.getMessage()); 9225 } 9226 9227 final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), 9228 ownerPackageName); 9229 9230 // Do not redact if the caller is the owner 9231 if (callerIsOwner) { 9232 return new long[0]; 9233 } 9234 9235 // Do not redact if the caller has write uri permission granted on the file. 9236 final Uri fileUri = ContentUris.withAppendedId(contentUri, id); 9237 boolean callerHasWriteUriPermission = getContext().checkUriPermission( 9238 fileUri, mCallingIdentity.get().pid, mCallingIdentity.get().uid, 9239 Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == PERMISSION_GRANTED; 9240 if (callerHasWriteUriPermission) { 9241 return new long[0]; 9242 } 9243 // Check if the caller has write access to other uri formats for the same file. 9244 callerHasWriteUriPermission = getOtherUriGrantsForPath(path, mediaType, 9245 Long.toString(id), /* forWrite */ true) != null; 9246 if (callerHasWriteUriPermission) { 9247 return new long[0]; 9248 } 9249 9250 return getRedactionRanges(file).redactionRanges; 9251 } finally { 9252 restoreLocalCallingIdentity(token); 9253 } 9254 } 9255 9256 /** 9257 * Calculates the ranges containing sensitive metadata that should be redacted if the caller 9258 * doesn't have the required permissions. 9259 * 9260 * @param file file to be redacted 9261 * @return the ranges to be redacted in a RedactionInfo object, could be empty redaction ranges 9262 * if there's sensitive metadata 9263 * @throws IOException if an IOException happens while calculating the redaction ranges 9264 */ 9265 @VisibleForTesting 9266 public static RedactionInfo getRedactionRanges(File file) throws IOException { 9267 try (FileInputStream is = new FileInputStream(file)) { 9268 return getRedactionRanges(is, MimeUtils.resolveMimeType(file)); 9269 } catch (FileNotFoundException ignored) { 9270 // If file not found, then there's nothing to redact 9271 return new RedactionInfo(); 9272 } catch (IOException e) { 9273 throw new IOException("Failed to redact " + file, e); 9274 } 9275 } 9276 9277 /** 9278 * Calculates the ranges containing sensitive metadata that should be redacted if the caller 9279 * doesn't have the required permissions. 9280 * 9281 * @param fis {@link FileInputStream} to be redacted 9282 * @return the ranges to be redacted in a RedactionInfo object, could be empty redaction ranges 9283 * if there's sensitive metadata 9284 * @throws IOException if an IOException happens while calculating the redaction ranges 9285 */ 9286 @VisibleForTesting 9287 public static RedactionInfo getRedactionRanges(FileInputStream fis, String mimeType) 9288 throws IOException { 9289 final LongArray res = new LongArray(); 9290 final LongArray freeOffsets = new LongArray(); 9291 9292 Trace.beginSection("MP.getRedactionRanges"); 9293 try { 9294 if (ExifInterface.isSupportedMimeType(mimeType)) { 9295 final ExifInterface exif = new ExifInterface(fis.getFD()); 9296 for (String tag : REDACTED_EXIF_TAGS) { 9297 final long[] range = exif.getAttributeRange(tag); 9298 if (range != null) { 9299 res.add(range[0]); 9300 res.add(range[0] + range[1]); 9301 } 9302 } 9303 // Redact xmp where present 9304 final XmpInterface exifXmp = XmpInterface.fromContainer(exif); 9305 res.addAll(exifXmp.getRedactionRanges()); 9306 } 9307 9308 if (IsoInterface.isSupportedMimeType(mimeType)) { 9309 final IsoInterface iso = IsoInterface.fromFileDescriptor(fis.getFD()); 9310 for (int box : REDACTED_ISO_BOXES) { 9311 final long[] ranges = iso.getBoxRanges(box); 9312 for (int i = 0; i < ranges.length; i += 2) { 9313 long boxTypeOffset = ranges[i] - 4; 9314 freeOffsets.add(boxTypeOffset); 9315 res.add(boxTypeOffset); 9316 res.add(ranges[i + 1]); 9317 } 9318 } 9319 // Redact xmp where present 9320 final XmpInterface isoXmp = XmpInterface.fromContainer(iso); 9321 res.addAll(isoXmp.getRedactionRanges()); 9322 } 9323 9324 return new RedactionInfo(res.toArray(), freeOffsets.toArray()); 9325 } finally { 9326 Trace.endSection(); 9327 } 9328 } 9329 9330 /** 9331 * @return {@code true} if {@code file} is pending from FUSE, {@code false} otherwise. 9332 * Files pending from FUSE will not have pending file pattern. 9333 */ 9334 private static boolean isPendingFromFuse(@NonNull File file) { 9335 final Matcher matcher = 9336 FileUtils.PATTERN_EXPIRES_FILE.matcher(extractDisplayName(file.getName())); 9337 return !matcher.matches(); 9338 } 9339 9340 private FileAccessAttributes queryForFileAttributes(final String path) 9341 throws FileNotFoundException { 9342 Trace.beginSection("MP.queryFileAttr"); 9343 final Uri contentUri = FileUtils.getContentUriForPath(path); 9344 final String[] projection = new String[]{ 9345 MediaColumns._ID, 9346 MediaColumns.OWNER_PACKAGE_NAME, 9347 MediaColumns.IS_PENDING, 9348 FileColumns.MEDIA_TYPE, 9349 MediaColumns.IS_TRASHED 9350 }; 9351 final String selection = MediaColumns.DATA + "=?"; 9352 final String[] selectionArgs = new String[]{path}; 9353 FileAccessAttributes fileAccessAttributes; 9354 try (final Cursor c = queryForSingleItemAsMediaProvider(contentUri, projection, 9355 selection, 9356 selectionArgs, null)) { 9357 fileAccessAttributes = FileAccessAttributes.fromCursor(c); 9358 } 9359 Trace.endSection(); 9360 return fileAccessAttributes; 9361 } 9362 9363 private void checkIfFileOpenIsPermitted(String path, 9364 FileAccessAttributes fileAccessAttributes, String redactedUriId, 9365 boolean forWrite) throws FileNotFoundException { 9366 final File file = new File(path); 9367 Uri fileUri = MediaStore.Files.getContentUri(extractVolumeName(path), 9368 fileAccessAttributes.getId()); 9369 // We don't check ownership for files with IS_PENDING set by FUSE 9370 // Please note that even if ownerPackageName is null, the check below will throw an 9371 // IllegalStateException 9372 if (fileAccessAttributes.isTrashed() || (fileAccessAttributes.isPending() 9373 && !isPendingFromFuse(new File(path)))) { 9374 requireOwnershipForItem(fileAccessAttributes.getOwnerPackageName(), fileUri); 9375 } 9376 9377 // Check that path looks consistent before uri checks 9378 if (!FileUtils.contains(Environment.getStorageDirectory(), file)) { 9379 checkWorldReadAccess(file.getAbsolutePath()); 9380 } 9381 9382 try { 9383 // checkAccess throws FileNotFoundException only from checkWorldReadAccess(), 9384 // which we already check above. Hence, handling only SecurityException. 9385 if (redactedUriId != null) { 9386 fileUri = ContentUris.removeId(fileUri).buildUpon().appendPath( 9387 redactedUriId).build(); 9388 } 9389 checkAccess(fileUri, Bundle.EMPTY, file, forWrite); 9390 } catch (SecurityException e) { 9391 // Check for other Uri formats only when the single uri check flow fails. 9392 // Throw the previous exception if the multi-uri checks failed. 9393 final String uriId = redactedUriId == null 9394 ? Long.toString(fileAccessAttributes.getId()) : redactedUriId; 9395 if (getOtherUriGrantsForPath(path, fileAccessAttributes.getMediaType(), 9396 uriId, forWrite) == null) { 9397 throw e; 9398 } 9399 } 9400 } 9401 9402 9403 /** 9404 * Checks if the app identified by the given UID is allowed to open the given file for the given 9405 * access mode. 9406 * 9407 * @param path the path of the file to be opened 9408 * @param uid UID of the app requesting to open the file 9409 * @param forWrite specifies if the file is to be opened for write 9410 * @return {@link FileOpenResult} with {@code status} {@code 0} upon success and 9411 * {@link FileOpenResult} with {@code status} {@link OsConstants#EACCES} if the operation is 9412 * illegal or not permitted for the given {@code uid} or if the calling package is a legacy app 9413 * that doesn't have right storage permission. 9414 * 9415 * Called from JNI in jni/MediaProviderWrapper.cpp 9416 */ 9417 @Keep 9418 public FileOpenResult onFileOpenForFuse(String path, String ioPath, int uid, int tid, 9419 int transformsReason, boolean forWrite, boolean redact, boolean logTransformsMetrics) { 9420 final LocalCallingIdentity token = 9421 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 9422 9423 PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path); 9424 9425 boolean isSuccess = false; 9426 9427 final int originalUid = getBinderUidForFuse(uid, tid); 9428 final int callingUserId = uidToUserId(uid); 9429 int mediaCapabilitiesUid = 0; 9430 final PendingOpenInfo pendingOpenInfo; 9431 synchronized (mPendingOpenInfo) { 9432 pendingOpenInfo = mPendingOpenInfo.get(tid); 9433 } 9434 9435 if (pendingOpenInfo != null && pendingOpenInfo.uid == originalUid) { 9436 mediaCapabilitiesUid = pendingOpenInfo.mediaCapabilitiesUid; 9437 } 9438 9439 try { 9440 boolean forceRedaction = false; 9441 String redactedUriId = null; 9442 if (isSyntheticPath(path, callingUserId)) { 9443 if (forWrite) { 9444 // Synthetic URIs are not allowed to update EXIF headers. 9445 return new FileOpenResult(OsConstants.EACCES /* status */, originalUid, 9446 mediaCapabilitiesUid, new long[0]); 9447 } 9448 9449 if (isRedactedPath(path, callingUserId)) { 9450 redactedUriId = extractFileName(path); 9451 9452 // If path is redacted Uris' path, ioPath must be the real path, ioPath must 9453 // haven been updated to the real path during onFileLookupForFuse. 9454 path = ioPath; 9455 9456 // Irrespective of the permissions we want to redact in this case. 9457 redact = true; 9458 forceRedaction = true; 9459 } else if (isPickerPath(path, callingUserId)) { 9460 return handlePickerFileOpen(path, originalUid); 9461 } else { 9462 // we don't support any other transformations under .transforms/synthetic dir 9463 return new FileOpenResult(OsConstants.ENOENT /* status */, originalUid, 9464 mediaCapabilitiesUid, new long[0]); 9465 } 9466 } 9467 9468 if (isPrivatePackagePathNotAccessibleByCaller(path)) { 9469 Log.e(TAG, "Can't open a file in another app's external directory!"); 9470 return new FileOpenResult(OsConstants.ENOENT, originalUid, mediaCapabilitiesUid, 9471 new long[0]); 9472 } 9473 9474 if (shouldBypassFuseRestrictions(forWrite, path)) { 9475 isSuccess = true; 9476 return new FileOpenResult(0 /* status */, originalUid, mediaCapabilitiesUid, 9477 redact ? getRedactionRangesForFuse(path, ioPath, originalUid, uid, tid, 9478 forceRedaction) : new long[0]); 9479 } 9480 // Legacy apps that made is this far don't have the right storage permission and hence 9481 // are not allowed to access anything other than their external app directory 9482 if (isCallingPackageRequestingLegacy()) { 9483 return new FileOpenResult(OsConstants.EACCES /* status */, originalUid, 9484 mediaCapabilitiesUid, new long[0]); 9485 } 9486 // TODO: Fetch owner id from Android/media directory and check if caller is owner 9487 FileAccessAttributes fileAttributes = null; 9488 if (XAttrUtils.ENABLE_XATTR_METADATA_FOR_FUSE) { 9489 Optional<FileAccessAttributes> fileAttributesThroughXattr = 9490 XAttrUtils.getFileAttributesFromXAttr(path, 9491 XAttrUtils.FILE_ACCESS_XATTR_KEY); 9492 if (fileAttributesThroughXattr.isPresent()) { 9493 fileAttributes = fileAttributesThroughXattr.get(); 9494 } 9495 } 9496 9497 // FileAttributes will be null if the xattr call failed or the flag to enable xattr 9498 // metadata support is not set 9499 if (fileAttributes == null) { 9500 fileAttributes = queryForFileAttributes(path); 9501 } 9502 checkIfFileOpenIsPermitted(path, fileAttributes, redactedUriId, forWrite); 9503 isSuccess = true; 9504 return new FileOpenResult(0 /* status */, originalUid, mediaCapabilitiesUid, 9505 redact ? getRedactionRangesForFuse(path, ioPath, originalUid, uid, tid, 9506 forceRedaction) : new long[0]); 9507 } catch (IOException e) { 9508 // We are here because 9509 // * There is no db row corresponding to the requested path, which is more unlikely. 9510 // * getRedactionRangesForFuse couldn't fetch the redaction info correctly 9511 // In all of these cases, it means that app doesn't have access permission to the file. 9512 Log.e(TAG, "Couldn't find file: " + path, e); 9513 return new FileOpenResult(OsConstants.EACCES /* status */, originalUid, 9514 mediaCapabilitiesUid, new long[0]); 9515 } catch (IllegalStateException | SecurityException e) { 9516 Log.e(TAG, "Permission to access file: " + path + " is denied"); 9517 return new FileOpenResult(OsConstants.EACCES /* status */, originalUid, 9518 mediaCapabilitiesUid, new long[0]); 9519 } finally { 9520 if (isSuccess && logTransformsMetrics) { 9521 notifyTranscodeHelperOnFileOpen(path, ioPath, originalUid, transformsReason); 9522 } 9523 restoreLocalCallingIdentity(token); 9524 } 9525 } 9526 9527 @Nullable 9528 private Uri getOtherUriGrantsForPath(String path, boolean forWrite) { 9529 final Uri contentUri = FileUtils.getContentUriForPath(path); 9530 final String[] projection = new String[]{ 9531 MediaColumns._ID, 9532 FileColumns.MEDIA_TYPE}; 9533 final String selection = MediaColumns.DATA + "=?"; 9534 final String[] selectionArgs = new String[]{path}; 9535 final String id; 9536 final int mediaType; 9537 try (final Cursor c = queryForSingleItemAsMediaProvider(contentUri, projection, selection, 9538 selectionArgs, null)) { 9539 id = c.getString(0); 9540 mediaType = c.getInt(1); 9541 return getOtherUriGrantsForPath(path, mediaType, id, forWrite); 9542 } catch (FileNotFoundException ignored) { 9543 } 9544 return null; 9545 } 9546 9547 @Nullable 9548 private Uri getOtherUriGrantsForPath(String path, int mediaType, String id, boolean forWrite) { 9549 List<Uri> otherUris = new ArrayList<>(); 9550 final Uri mediaUri = getMediaUriForFuse(extractVolumeName(path), mediaType, id); 9551 otherUris.add(mediaUri); 9552 final Uri externalMediaUri = getMediaUriForFuse(MediaStore.VOLUME_EXTERNAL, mediaType, id); 9553 otherUris.add(externalMediaUri); 9554 return getPermissionGrantedUri(otherUris, forWrite); 9555 } 9556 9557 @NonNull 9558 private Uri getMediaUriForFuse(@NonNull String volumeName, int mediaType, String id) { 9559 Uri uri = MediaStore.Files.getContentUri(volumeName); 9560 switch (mediaType) { 9561 case FileColumns.MEDIA_TYPE_IMAGE: 9562 uri = MediaStore.Images.Media.getContentUri(volumeName); 9563 break; 9564 case FileColumns.MEDIA_TYPE_VIDEO: 9565 uri = MediaStore.Video.Media.getContentUri(volumeName); 9566 break; 9567 case FileColumns.MEDIA_TYPE_AUDIO: 9568 uri = MediaStore.Audio.Media.getContentUri(volumeName); 9569 break; 9570 case FileColumns.MEDIA_TYPE_PLAYLIST: 9571 uri = MediaStore.Audio.Playlists.getContentUri(volumeName); 9572 break; 9573 } 9574 9575 return uri.buildUpon().appendPath(id).build(); 9576 } 9577 9578 /** 9579 * Returns {@code true} if {@link #mCallingIdentity#getSharedPackageNamesList(String)} contains 9580 * the given package name, {@code false} otherwise. 9581 * <p> Assumes that {@code mCallingIdentity} has been properly set to reflect the calling 9582 * package. 9583 */ 9584 private boolean isCallingIdentitySharedPackageName(@NonNull String packageName) { 9585 for (String sharedPkgName : mCallingIdentity.get().getSharedPackageNamesArray()) { 9586 if (packageName.toLowerCase(Locale.ROOT) 9587 .equals(sharedPkgName.toLowerCase(Locale.ROOT))) { 9588 return true; 9589 } 9590 } 9591 return false; 9592 } 9593 9594 /** 9595 * @throws IllegalStateException if path is invalid or doesn't match a volume. 9596 */ 9597 @NonNull 9598 private Uri getContentUriForFile(@NonNull String filePath, @NonNull String mimeType) { 9599 final String volName; 9600 try { 9601 volName = FileUtils.getVolumeName(getContext(), new File(filePath)); 9602 } catch (FileNotFoundException e) { 9603 throw new IllegalStateException("Couldn't get volume name for " + filePath); 9604 } 9605 Uri uri = Files.getContentUri(volName); 9606 String topLevelDir = extractTopLevelDir(filePath); 9607 if (topLevelDir == null) { 9608 // If the file path doesn't match the external storage directory, we use the files URI 9609 // as default and let #insert enforce the restrictions 9610 return uri; 9611 } 9612 topLevelDir = topLevelDir.toLowerCase(Locale.ROOT); 9613 9614 switch (topLevelDir) { 9615 case DIRECTORY_PODCASTS_LOWER_CASE: 9616 case DIRECTORY_RINGTONES_LOWER_CASE: 9617 case DIRECTORY_ALARMS_LOWER_CASE: 9618 case DIRECTORY_NOTIFICATIONS_LOWER_CASE: 9619 case DIRECTORY_AUDIOBOOKS_LOWER_CASE: 9620 case DIRECTORY_RECORDINGS_LOWER_CASE: 9621 uri = Audio.Media.getContentUri(volName); 9622 break; 9623 case DIRECTORY_MUSIC_LOWER_CASE: 9624 if (MimeUtils.isPlaylistMimeType(mimeType)) { 9625 uri = Audio.Playlists.getContentUri(volName); 9626 } else if (!MimeUtils.isSubtitleMimeType(mimeType)) { 9627 // Send Files uri for media type subtitle 9628 uri = Audio.Media.getContentUri(volName); 9629 } 9630 break; 9631 case DIRECTORY_MOVIES_LOWER_CASE: 9632 if (MimeUtils.isPlaylistMimeType(mimeType)) { 9633 uri = Audio.Playlists.getContentUri(volName); 9634 } else if (!MimeUtils.isSubtitleMimeType(mimeType)) { 9635 // Send Files uri for media type subtitle 9636 uri = Video.Media.getContentUri(volName); 9637 } 9638 break; 9639 case DIRECTORY_DCIM_LOWER_CASE: 9640 case DIRECTORY_PICTURES_LOWER_CASE: 9641 if (MimeUtils.isImageMimeType(mimeType)) { 9642 uri = Images.Media.getContentUri(volName); 9643 } else { 9644 uri = Video.Media.getContentUri(volName); 9645 } 9646 break; 9647 case DIRECTORY_DOWNLOADS_LOWER_CASE: 9648 case DIRECTORY_DOCUMENTS_LOWER_CASE: 9649 break; 9650 default: 9651 Log.w(TAG, "Forgot to handle a top level directory in getContentUriForFile?"); 9652 } 9653 return uri; 9654 } 9655 9656 private boolean containsIgnoreCase(@Nullable List<String> stringsList, @Nullable String item) { 9657 if (item == null || stringsList == null) return false; 9658 9659 for (String current : stringsList) { 9660 if (item.equalsIgnoreCase(current)) return true; 9661 } 9662 return false; 9663 } 9664 9665 private boolean fileExists(@NonNull String absolutePath) { 9666 // We don't care about specific columns in the match, 9667 // we just want to check IF there's a match 9668 final String[] projection = {}; 9669 final String selection = FileColumns.DATA + " = ?"; 9670 final String[] selectionArgs = {absolutePath}; 9671 final Uri uri = FileUtils.getContentUriForPath(absolutePath); 9672 9673 final LocalCallingIdentity token = clearLocalCallingIdentity(); 9674 try { 9675 try (final Cursor c = query(uri, projection, selection, selectionArgs, null)) { 9676 // Shouldn't return null 9677 return c.getCount() > 0; 9678 } 9679 } finally { 9680 clearLocalCallingIdentity(token); 9681 } 9682 } 9683 9684 private Uri insertFileForFuse(@NonNull String path, @NonNull Uri uri, @NonNull String mimeType, 9685 boolean useData) { 9686 ContentValues values = new ContentValues(); 9687 values.put(FileColumns.OWNER_PACKAGE_NAME, getCallingPackageOrSelf()); 9688 values.put(MediaColumns.MIME_TYPE, mimeType); 9689 values.put(FileColumns.IS_PENDING, 1); 9690 9691 if (useData) { 9692 values.put(FileColumns.DATA, path); 9693 } else { 9694 values.put(FileColumns.VOLUME_NAME, extractVolumeName(path)); 9695 values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path)); 9696 values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path)); 9697 } 9698 return insert(uri, values, Bundle.EMPTY); 9699 } 9700 9701 /** 9702 * Enforces file creation restrictions (see return values) for the given file on behalf of the 9703 * app with the given {@code uid}. If the file is added to the shared storage, creates a 9704 * database entry for it. 9705 * <p> Does NOT create file. 9706 * 9707 * @param path the path of the file 9708 * @param uid UID of the app requesting to create the file 9709 * @return In case of success, 0. If the operation is illegal or not permitted, returns the 9710 * appropriate {@code errno} value: 9711 * <ul> 9712 * <li>{@link OsConstants#ENOENT} if the app tries to create file in other app's external dir 9713 * <li>{@link OsConstants#EEXIST} if the file already exists 9714 * <li>{@link OsConstants#EPERM} if the file type doesn't match the relative path, or if the 9715 * calling package is a legacy app that doesn't have WRITE_EXTERNAL_STORAGE permission. 9716 * <li>{@link OsConstants#EIO} in case of any other I/O exception 9717 * </ul> 9718 * 9719 * @throws IllegalStateException if given path is invalid. 9720 * 9721 * Called from JNI in jni/MediaProviderWrapper.cpp 9722 */ 9723 @Keep 9724 public int insertFileIfNecessaryForFuse(@NonNull String path, int uid) { 9725 final LocalCallingIdentity token = 9726 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 9727 PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path); 9728 9729 try { 9730 if (isPrivatePackagePathNotAccessibleByCaller(path)) { 9731 Log.e(TAG, "Can't create a file in another app's external directory"); 9732 return OsConstants.ENOENT; 9733 } 9734 9735 if (!path.equals(getAbsoluteSanitizedPath(path))) { 9736 Log.e(TAG, "File name contains invalid characters"); 9737 return OsConstants.EPERM; 9738 } 9739 9740 if (shouldBypassDatabaseAndSetDirtyForFuse(uid, path)) { 9741 if (path.endsWith("/.nomedia")) { 9742 File parent = new File(path).getParentFile(); 9743 synchronized (mNonHiddenPaths) { 9744 mNonHiddenPaths.keySet().removeIf( 9745 k -> FileUtils.contains(parent, new File(k))); 9746 } 9747 } 9748 return 0; 9749 } 9750 9751 final String mimeType = MimeUtils.resolveMimeType(new File(path)); 9752 9753 if (shouldBypassFuseRestrictions(/* forWrite */ true, path)) { 9754 final boolean callerRequestingLegacy = isCallingPackageRequestingLegacy(); 9755 if (!fileExists(path)) { 9756 // If app has already inserted the db row, inserting the row again might set 9757 // IS_PENDING=1. We shouldn't overwrite existing entry as part of FUSE 9758 // operation, hence, insert the db row only when it doesn't exist. 9759 try { 9760 insertFileForFuse(path, FileUtils.getContentUriForPath(path), 9761 mimeType, /* useData */ callerRequestingLegacy); 9762 } catch (Exception ignored) { 9763 } 9764 } else { 9765 // Upon creating a file via FUSE, if a row matching the path already exists 9766 // but a file doesn't exist on the filesystem, we transfer ownership to the 9767 // app attempting to create the file. If we don't update ownership, then the 9768 // app that inserted the original row may be able to observe the contents of 9769 // written file even though they don't hold the right permissions to do so. 9770 if (callerRequestingLegacy) { 9771 final String owner = getCallingPackageOrSelf(); 9772 if (owner != null && !updateOwnerForPath(path, owner)) { 9773 return OsConstants.EPERM; 9774 } 9775 } 9776 } 9777 9778 return 0; 9779 } 9780 9781 // Legacy apps that made is this far don't have the right storage permission and hence 9782 // are not allowed to access anything other than their external app directory 9783 if (isCallingPackageRequestingLegacy()) { 9784 return OsConstants.EPERM; 9785 } 9786 9787 if (fileExists(path)) { 9788 // If the file already exists in the db, we shouldn't allow the file creation. 9789 return OsConstants.EEXIST; 9790 } 9791 9792 final Uri contentUri = getContentUriForFile(path, mimeType); 9793 final Uri item = insertFileForFuse(path, contentUri, mimeType, /* useData */ false); 9794 if (item == null) { 9795 return OsConstants.EPERM; 9796 } 9797 return 0; 9798 } catch (IllegalArgumentException e) { 9799 Log.e(TAG, "insertFileIfNecessary failed", e); 9800 return OsConstants.EPERM; 9801 } finally { 9802 restoreLocalCallingIdentity(token); 9803 } 9804 } 9805 9806 private boolean updateOwnerForPath(@NonNull String path, @NonNull String newOwner) { 9807 final DatabaseHelper helper; 9808 try { 9809 helper = getDatabaseForUri(FileUtils.getContentUriForPath(path)); 9810 } catch (VolumeNotFoundException e) { 9811 // Cannot happen, as this is a path that we already resolved. 9812 throw new AssertionError("Path must already be resolved", e); 9813 } 9814 9815 ContentValues values = new ContentValues(1); 9816 values.put(FileColumns.OWNER_PACKAGE_NAME, newOwner); 9817 9818 return helper.runWithoutTransaction( 9819 (db) -> db.update("files", values, "_data=?", new String[] { path })) == 1; 9820 } 9821 9822 private int deleteFileUnchecked(@NonNull String path, 9823 LocalCallingIdentity localCallingIdentity) { 9824 final File toDelete = new File(path); 9825 if (toDelete.delete()) { 9826 localCallingIdentity.incrementDeletedFileCountBypassingDatabase(); 9827 return 0; 9828 } else { 9829 return OsConstants.ENOENT; 9830 } 9831 } 9832 9833 /** 9834 * Deletes file with the given {@code path} on behalf of the app with the given {@code uid}. 9835 * <p>Before deleting, checks if app has permissions to delete this file. 9836 * 9837 * @param path the path of the file 9838 * @param uid UID of the app requesting to delete the file 9839 * @return 0 upon success. 9840 * In case of error, return the appropriate negated {@code errno} value: 9841 * <ul> 9842 * <li>{@link OsConstants#ENOENT} if the file does not exist or if the app tries to delete file 9843 * in another app's external dir 9844 * <li>{@link OsConstants#EPERM} a security exception was thrown by {@link #delete}, or if the 9845 * calling package is a legacy app that doesn't have WRITE_EXTERNAL_STORAGE permission. 9846 * </ul> 9847 * 9848 * Called from JNI in jni/MediaProviderWrapper.cpp 9849 */ 9850 @Keep 9851 public int deleteFileForFuse(@NonNull String path, int uid) throws IOException { 9852 final LocalCallingIdentity localCallingIdentity = getCachedCallingIdentityForFuse(uid); 9853 final LocalCallingIdentity token = clearLocalCallingIdentity(localCallingIdentity); 9854 PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path); 9855 9856 try { 9857 if (isPrivatePackagePathNotAccessibleByCaller(path)) { 9858 Log.e(TAG, "Can't delete a file in another app's external directory!"); 9859 return OsConstants.ENOENT; 9860 } 9861 9862 if (shouldBypassDatabaseAndSetDirtyForFuse(uid, path)) { 9863 return deleteFileUnchecked(path, localCallingIdentity); 9864 } 9865 9866 final boolean shouldBypass = shouldBypassFuseRestrictions(/*forWrite*/ true, path); 9867 9868 // Legacy apps that made is this far don't have the right storage permission and hence 9869 // are not allowed to access anything other than their external app directory 9870 if (!shouldBypass && isCallingPackageRequestingLegacy()) { 9871 return OsConstants.EPERM; 9872 } 9873 9874 final Uri contentUri = FileUtils.getContentUriForPath(path); 9875 final String where = FileColumns.DATA + " = ?"; 9876 final String[] whereArgs = {path}; 9877 9878 if (delete(contentUri, where, whereArgs) == 0) { 9879 if (shouldBypass) { 9880 return deleteFileUnchecked(path, localCallingIdentity); 9881 } 9882 return OsConstants.ENOENT; 9883 } else { 9884 // success - 1 file was deleted 9885 return 0; 9886 } 9887 9888 } catch (SecurityException e) { 9889 Log.e(TAG, "File deletion not allowed", e); 9890 return OsConstants.EPERM; 9891 } finally { 9892 restoreLocalCallingIdentity(token); 9893 } 9894 } 9895 9896 // These need to stay in sync with MediaProviderWrapper.cpp's DirectoryAccessRequestType enum 9897 @IntDef(flag = true, prefix = { "DIRECTORY_ACCESS_FOR_" }, value = { 9898 DIRECTORY_ACCESS_FOR_READ, 9899 DIRECTORY_ACCESS_FOR_WRITE, 9900 DIRECTORY_ACCESS_FOR_CREATE, 9901 DIRECTORY_ACCESS_FOR_DELETE, 9902 }) 9903 @Retention(RetentionPolicy.SOURCE) 9904 @VisibleForTesting 9905 @interface DirectoryAccessType {} 9906 9907 @VisibleForTesting 9908 static final int DIRECTORY_ACCESS_FOR_READ = 1; 9909 9910 @VisibleForTesting 9911 static final int DIRECTORY_ACCESS_FOR_WRITE = 2; 9912 9913 @VisibleForTesting 9914 static final int DIRECTORY_ACCESS_FOR_CREATE = 3; 9915 9916 @VisibleForTesting 9917 static final int DIRECTORY_ACCESS_FOR_DELETE = 4; 9918 9919 /** 9920 * Checks whether the app with the given UID is allowed to access the directory denoted by the 9921 * given path. 9922 * 9923 * @param path directory's path 9924 * @param uid UID of the requesting app 9925 * @param accessType type of access being requested - eg {@link 9926 * MediaProvider#DIRECTORY_ACCESS_FOR_READ} 9927 * @return 0 if it's allowed to access the directory, {@link OsConstants#ENOENT} for attempts 9928 * to access a private package path in Android/data or Android/obb the caller doesn't have 9929 * access to, and otherwise {@link OsConstants#EACCES} if the calling package is a legacy app 9930 * that doesn't have READ_EXTERNAL_STORAGE permission or for other invalid attempts to access 9931 * Android/data or Android/obb dirs. 9932 * 9933 * Called from JNI in jni/MediaProviderWrapper.cpp 9934 */ 9935 @Keep 9936 public int isDirAccessAllowedForFuse(@NonNull String path, int uid, 9937 @DirectoryAccessType int accessType) { 9938 Preconditions.checkArgumentInRange(accessType, 1, DIRECTORY_ACCESS_FOR_DELETE, 9939 "accessType"); 9940 9941 final boolean forRead = accessType == DIRECTORY_ACCESS_FOR_READ; 9942 final LocalCallingIdentity token = 9943 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 9944 PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path); 9945 try { 9946 if ("/storage/emulated".equals(path)) { 9947 return OsConstants.EPERM; 9948 } 9949 if (isPrivatePackagePathNotAccessibleByCaller(path)) { 9950 Log.e(TAG, "Can't access another app's external directory!"); 9951 return OsConstants.ENOENT; 9952 } 9953 9954 if (shouldBypassFuseRestrictions(/* forWrite= */ !forRead, path)) { 9955 return 0; 9956 } 9957 9958 // Do not allow apps that reach this point to access Android/data or Android/obb dirs. 9959 // Creation should be via getContext().getExternalFilesDir() etc methods. 9960 // Reads and writes on primary volumes should be via mount views of lowerfs for apps 9961 // that get special access to these directories. 9962 // Reads and writes on secondary volumes would be provided via an early return from 9963 // shouldBypassFuseRestrictions above (again just for apps with special access). 9964 if (isDataOrObbPath(path)) { 9965 return OsConstants.EACCES; 9966 } 9967 9968 // Legacy apps that made is this far don't have the right storage permission and hence 9969 // are not allowed to access anything other than their external app directory 9970 if (isCallingPackageRequestingLegacy()) { 9971 return OsConstants.EACCES; 9972 } 9973 // This is a non-legacy app. Rest of the directories are generally writable 9974 // except for non-default top-level directories. 9975 if (!forRead) { 9976 final String[] relativePath = sanitizePath(extractRelativePath(path)); 9977 if (relativePath.length == 0) { 9978 Log.e(TAG, 9979 "Directory update not allowed on invalid relative path for " + path); 9980 return OsConstants.EPERM; 9981 } 9982 final boolean isTopLevelDir = 9983 relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]); 9984 if (isTopLevelDir) { 9985 // We don't allow deletion of any top-level folders 9986 if (accessType == DIRECTORY_ACCESS_FOR_DELETE) { 9987 Log.e(TAG, "Deleting top level directories are not allowed!"); 9988 return OsConstants.EACCES; 9989 } 9990 9991 // We allow creating or writing to default top-level folders, but we don't 9992 // allow creation or writing to non-default top-level folders. 9993 if ((accessType == DIRECTORY_ACCESS_FOR_CREATE 9994 || accessType == DIRECTORY_ACCESS_FOR_WRITE) 9995 && FileUtils.isDefaultDirectoryName(extractDisplayName(path))) { 9996 return 0; 9997 } 9998 9999 Log.e(TAG, 10000 "Creating or writing to a non-default top level directory is not " 10001 + "allowed!"); 10002 return OsConstants.EACCES; 10003 } 10004 } 10005 10006 return 0; 10007 } finally { 10008 restoreLocalCallingIdentity(token); 10009 } 10010 } 10011 10012 @Keep 10013 public boolean isUidAllowedAccessToDataOrObbPathForFuse(int uid, String path) { 10014 final LocalCallingIdentity token = 10015 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 10016 try { 10017 return isCallingIdentityAllowedAccessToDataOrObbPath( 10018 extractRelativePathWithDisplayName(path)); 10019 } finally { 10020 restoreLocalCallingIdentity(token); 10021 } 10022 } 10023 10024 private boolean isCallingIdentityAllowedAccessToDataOrObbPath(String relativePath) { 10025 // Files under the apps own private directory 10026 final String appSpecificDir = extractOwnerPackageNameFromRelativePath(relativePath); 10027 10028 if (appSpecificDir != null && isCallingIdentitySharedPackageName(appSpecificDir)) { 10029 return true; 10030 } 10031 // This is a private-package relativePath; return true if accessible by the caller 10032 return isCallingIdentityAllowedSpecialPrivatePathAccess(relativePath); 10033 } 10034 10035 /** 10036 * @return true iff the caller has installer privileges which gives write access to obb dirs. 10037 * 10038 * @deprecated This method should only be called for Android R. For Android S+, please use 10039 * {@link StorageManager#getExternalStorageMountMode} to check if the caller has 10040 * {@link StorageManager#MOUNT_MODE_EXTERNAL_INSTALLER} access. 10041 * 10042 * Note: WRITE_EXTERNAL_STORAGE permission should ideally not be requested by non-legacy apps. 10043 * But to be consistent with {@link StorageManager} check for Installer apps access for primary 10044 * volumes in Android R, we do not add non-legacy apps check here as well. 10045 */ 10046 @Deprecated 10047 private boolean isCallingIdentityAllowedInstallerAccess() { 10048 final boolean hasWrite = mCallingIdentity.get(). 10049 hasPermission(PERMISSION_WRITE_EXTERNAL_STORAGE); 10050 10051 if (!hasWrite) { 10052 return false; 10053 } 10054 10055 // We're only willing to give out installer access if they also hold 10056 // runtime permission; this is a firm CDD requirement 10057 final boolean hasInstall = mCallingIdentity.get(). 10058 hasPermission(PERMISSION_INSTALL_PACKAGES); 10059 10060 if (hasInstall) { 10061 return true; 10062 } 10063 // OPSTR_REQUEST_INSTALL_PACKAGES is granted/denied per package but vold can't 10064 // update mountpoints of a specific package. So, check the appop for all packages 10065 // sharing the uid and allow same level of storage access for all packages even if 10066 // one of the packages has the appop granted. 10067 // To maintain consistency of access in primary volume and secondary volumes use the same 10068 // logic as we do for Zygote.MOUNT_EXTERNAL_INSTALLER view. 10069 return mCallingIdentity.get().hasPermission(APPOP_REQUEST_INSTALL_PACKAGES_FOR_SHARED_UID); 10070 } 10071 10072 private String getExternalStorageProviderAuthority() { 10073 if (SdkLevel.isAtLeastS()) { 10074 return getExternalStorageProviderAuthorityFromDocumentsContract(); 10075 } 10076 return MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY; 10077 } 10078 10079 @RequiresApi(Build.VERSION_CODES.S) 10080 private String getExternalStorageProviderAuthorityFromDocumentsContract() { 10081 return DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY; 10082 } 10083 10084 private String getDownloadsProviderAuthority() { 10085 if (SdkLevel.isAtLeastS()) { 10086 return getDownloadsProviderAuthorityFromDocumentsContract(); 10087 } 10088 return DOWNLOADS_PROVIDER_AUTHORITY; 10089 } 10090 10091 @RequiresApi(Build.VERSION_CODES.S) 10092 private String getDownloadsProviderAuthorityFromDocumentsContract() { 10093 return DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY; 10094 } 10095 10096 private boolean isCallingIdentityDownloadProvider() { 10097 return getCallingUidOrSelf() == mDownloadsAuthorityAppId; 10098 } 10099 10100 private boolean isCallingIdentityExternalStorageProvider() { 10101 return getCallingUidOrSelf() == mExternalStorageAuthorityAppId; 10102 } 10103 10104 private boolean isCallingIdentityMtp() { 10105 return mCallingIdentity.get().hasPermission(PERMISSION_ACCESS_MTP); 10106 } 10107 10108 /** 10109 * The following apps have access to all private-app directories on secondary volumes: 10110 * * ExternalStorageProvider 10111 * * DownloadProvider 10112 * * Signature apps with ACCESS_MTP permission granted 10113 * (Note: For Android R we also allow privileged apps with ACCESS_MTP to access all 10114 * private-app directories, this additional access is removed for Android S+). 10115 * 10116 * Installer apps can only access private-app directories on Android/obb. 10117 * 10118 * @param relativePath the relative path of the file to access 10119 */ 10120 private boolean isCallingIdentityAllowedSpecialPrivatePathAccess(String relativePath) { 10121 if (SdkLevel.isAtLeastS()) { 10122 return isMountModeAllowedPrivatePathAccess(getCallingUidOrSelf(), getCallingPackage(), 10123 relativePath); 10124 } else { 10125 if (isCallingIdentityDownloadProvider() || 10126 isCallingIdentityExternalStorageProvider() || isCallingIdentityMtp()) { 10127 return true; 10128 } 10129 return (isObbOrChildRelativePath(relativePath) && 10130 isCallingIdentityAllowedInstallerAccess()); 10131 } 10132 } 10133 10134 @RequiresApi(Build.VERSION_CODES.S) 10135 private boolean isMountModeAllowedPrivatePathAccess(int uid, String packageName, 10136 String relativePath) { 10137 // This is required as only MediaProvider (package with WRITE_MEDIA_STORAGE) can access 10138 // mount modes. 10139 final CallingIdentity token = clearCallingIdentity(); 10140 try { 10141 final int mountMode = mStorageManager.getExternalStorageMountMode(uid, packageName); 10142 switch (mountMode) { 10143 case StorageManager.MOUNT_MODE_EXTERNAL_ANDROID_WRITABLE: 10144 case StorageManager.MOUNT_MODE_EXTERNAL_PASS_THROUGH: 10145 return true; 10146 case StorageManager.MOUNT_MODE_EXTERNAL_INSTALLER: 10147 return isObbOrChildRelativePath(relativePath); 10148 } 10149 } catch (Exception e) { 10150 Log.w(TAG, "Caller does not have the permissions to access mount modes: ", e); 10151 } finally { 10152 restoreCallingIdentity(token); 10153 } 10154 return false; 10155 } 10156 10157 private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) { 10158 // System internals can work with all media 10159 if (isCallingPackageSelf() || isCallingPackageShell()) { 10160 return true; 10161 } 10162 10163 // Apps that have permission to manage external storage can work with all files 10164 if (isCallingPackageManager()) { 10165 return true; 10166 } 10167 10168 // Check if caller is known to be owner of this item, to speed up 10169 // performance of our permission checks 10170 final int table = matchUri(uri, true); 10171 switch (table) { 10172 case AUDIO_MEDIA_ID: 10173 case VIDEO_MEDIA_ID: 10174 case IMAGES_MEDIA_ID: 10175 case FILES_ID: 10176 case DOWNLOADS_ID: 10177 final long id = ContentUris.parseId(uri); 10178 if (mCallingIdentity.get().isOwned(id)) { 10179 return true; 10180 } 10181 break; 10182 default: 10183 // continue below 10184 } 10185 10186 // Check whether the uri is a specific table or not. Don't allow the global access to these 10187 // table uris 10188 switch (table) { 10189 case AUDIO_MEDIA: 10190 case IMAGES_MEDIA: 10191 case VIDEO_MEDIA: 10192 case DOWNLOADS: 10193 case FILES: 10194 case AUDIO_ALBUMS: 10195 case AUDIO_ARTISTS: 10196 case AUDIO_GENRES: 10197 case AUDIO_PLAYLISTS: 10198 return false; 10199 default: 10200 // continue below 10201 } 10202 10203 // Outstanding grant means they get access 10204 return isUriPermissionGranted(uri, forWrite); 10205 } 10206 10207 /** 10208 * Returns any uri that is granted from the set of Uris passed. 10209 */ 10210 @Nullable 10211 private Uri getPermissionGrantedUri(@NonNull List<Uri> uris, boolean forWrite) { 10212 for (Uri uri : uris) { 10213 if (isUriPermissionGranted(uri, forWrite)) { 10214 return uri; 10215 } 10216 } 10217 return null; 10218 } 10219 10220 private boolean isUriPermissionGranted(Uri uri, boolean forWrite) { 10221 final int modeFlags = forWrite 10222 ? Intent.FLAG_GRANT_WRITE_URI_PERMISSION 10223 : Intent.FLAG_GRANT_READ_URI_PERMISSION; 10224 int uriPermission = getContext().checkUriPermission(uri, mCallingIdentity.get().pid, 10225 mCallingIdentity.get().uid, modeFlags); 10226 return uriPermission == PERMISSION_GRANTED; 10227 } 10228 10229 @VisibleForTesting 10230 public boolean isFuseThread() { 10231 return FuseDaemon.native_is_fuse_thread(); 10232 } 10233 10234 10235 /** 10236 * Enforce that caller has access to the given {@link Uri}. 10237 * 10238 * @throws SecurityException if access isn't allowed. 10239 */ 10240 private void enforceCallingPermission(@NonNull Uri uri, @NonNull Bundle extras, 10241 boolean forWrite) { 10242 Trace.beginSection("MP.enforceCallingPermission"); 10243 try { 10244 enforceCallingPermissionInternal(uri, extras, forWrite); 10245 } finally { 10246 Trace.endSection(); 10247 } 10248 } 10249 10250 private void enforceCallingPermission(@NonNull Collection<Uri> uris, boolean forWrite) { 10251 for (Uri uri : uris) { 10252 enforceCallingPermission(uri, Bundle.EMPTY, forWrite); 10253 } 10254 } 10255 10256 private void enforceCallingPermissionInternal(@NonNull Uri uri, @NonNull Bundle extras, 10257 boolean forWrite) { 10258 Objects.requireNonNull(uri); 10259 Objects.requireNonNull(extras); 10260 10261 // Try a simple global check first before falling back to performing a 10262 // simple query to probe for access. 10263 if (checkCallingPermissionGlobal(uri, forWrite)) { 10264 // Access allowed, yay! 10265 return; 10266 } 10267 10268 // For redacted URI proceed with its corresponding URI as query builder doesn't support 10269 // redacted URIs for fetching a database row 10270 // NOTE: The grants (if any) must have been on redacted URI hence global check requires 10271 // redacted URI 10272 Uri redactedUri = null; 10273 if (isRedactedUri(uri)) { 10274 redactedUri = uri; 10275 uri = getUriForRedactedUri(uri); 10276 } 10277 10278 final DatabaseHelper helper; 10279 try { 10280 helper = getDatabaseForUri(uri); 10281 } catch (VolumeNotFoundException e) { 10282 throw e.rethrowAsIllegalArgumentException(); 10283 } 10284 10285 final boolean allowHidden = isCallingPackageAllowedHidden(); 10286 final int table = matchUri(uri, allowHidden); 10287 10288 final String selection = extras.getString(QUERY_ARG_SQL_SELECTION); 10289 final String[] selectionArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS); 10290 10291 // First, check to see if caller has direct write access 10292 if (forWrite) { 10293 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, table, uri, extras, null); 10294 qb.allowColumn(SQLiteQueryBuilder.ROWID_COLUMN); 10295 try (Cursor c = qb.query(helper, new String[] { SQLiteQueryBuilder.ROWID_COLUMN }, 10296 selection, selectionArgs, null, null, null, null, null)) { 10297 if (c.moveToFirst()) { 10298 // Direct write access granted, yay! 10299 return; 10300 } 10301 } 10302 } 10303 10304 // We only allow the user to grant access to specific media items in 10305 // strongly typed collections; never to broad collections 10306 boolean allowUserGrant = false; 10307 final int matchUri = matchUri(uri, true); 10308 switch (matchUri) { 10309 case IMAGES_MEDIA_ID: 10310 case AUDIO_MEDIA_ID: 10311 case VIDEO_MEDIA_ID: 10312 allowUserGrant = true; 10313 break; 10314 } 10315 10316 // Second, check to see if caller has direct read access 10317 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, extras, null); 10318 qb.allowColumn(SQLiteQueryBuilder.ROWID_COLUMN); 10319 try (Cursor c = qb.query(helper, new String[] { SQLiteQueryBuilder.ROWID_COLUMN }, 10320 selection, selectionArgs, null, null, null, null, null)) { 10321 if (c.moveToFirst()) { 10322 if (!forWrite) { 10323 // Direct read access granted, yay! 10324 return; 10325 } else if (allowUserGrant) { 10326 // Caller has read access, but they wanted to write, and 10327 // they'll need to get the user to grant that access 10328 final Context context = getContext(); 10329 final Collection<Uri> uris = Collections.singletonList(uri); 10330 final PendingIntent intent = MediaStore 10331 .createWriteRequest(ContentResolver.wrap(this), uris); 10332 10333 final Icon icon = getCollectionIcon(uri); 10334 final RemoteAction action = new RemoteAction(icon, 10335 context.getText(R.string.permission_required_action), 10336 context.getText(R.string.permission_required_action), 10337 intent); 10338 10339 throw new RecoverableSecurityException(new SecurityException( 10340 getCallingPackageOrSelf() + " has no access to " + uri), 10341 context.getText(R.string.permission_required), action); 10342 } 10343 } 10344 } 10345 10346 if (redactedUri != null) uri = redactedUri; 10347 throw new SecurityException(getCallingPackageOrSelf() + " has no access to " + uri); 10348 } 10349 10350 private Icon getCollectionIcon(Uri uri) { 10351 final PackageManager pm = getContext().getPackageManager(); 10352 final String type = uri.getPathSegments().get(1); 10353 final String groupName; 10354 switch (type) { 10355 default: groupName = android.Manifest.permission_group.STORAGE; break; 10356 } 10357 try { 10358 final PermissionGroupInfo perm = pm.getPermissionGroupInfo(groupName, 0); 10359 return Icon.createWithResource(perm.packageName, perm.icon); 10360 } catch (NameNotFoundException e) { 10361 throw new RuntimeException(e); 10362 } 10363 } 10364 10365 private void checkAccess(@NonNull Uri uri, @NonNull Bundle extras, @NonNull File file, 10366 boolean isWrite) throws FileNotFoundException { 10367 // First, does caller have the needed row-level access? 10368 enforceCallingPermission(uri, extras, isWrite); 10369 10370 // Second, does the path look consistent? 10371 if (!FileUtils.contains(Environment.getStorageDirectory(), file)) { 10372 checkWorldReadAccess(file.getAbsolutePath()); 10373 } 10374 } 10375 10376 /** 10377 * Check whether the path is a world-readable file 10378 */ 10379 @VisibleForTesting 10380 public static void checkWorldReadAccess(String path) throws FileNotFoundException { 10381 // Path has already been canonicalized, and we relax the check to look 10382 // at groups to support runtime storage permissions. 10383 final int accessBits = path.startsWith("/storage/") ? OsConstants.S_IRGRP 10384 : OsConstants.S_IROTH; 10385 try { 10386 StructStat stat = Os.stat(path); 10387 if (OsConstants.S_ISREG(stat.st_mode) && 10388 ((stat.st_mode & accessBits) == accessBits)) { 10389 checkLeadingPathComponentsWorldExecutable(path); 10390 return; 10391 } 10392 } catch (ErrnoException e) { 10393 // couldn't stat the file, either it doesn't exist or isn't 10394 // accessible to us 10395 } 10396 10397 throw new FileNotFoundException("Can't access " + path); 10398 } 10399 10400 private static void checkLeadingPathComponentsWorldExecutable(String filePath) 10401 throws FileNotFoundException { 10402 File parent = new File(filePath).getParentFile(); 10403 10404 // Path has already been canonicalized, and we relax the check to look 10405 // at groups to support runtime storage permissions. 10406 final int accessBits = filePath.startsWith("/storage/") ? OsConstants.S_IXGRP 10407 : OsConstants.S_IXOTH; 10408 10409 while (parent != null) { 10410 if (! parent.exists()) { 10411 // parent dir doesn't exist, give up 10412 throw new FileNotFoundException("access denied"); 10413 } 10414 try { 10415 StructStat stat = Os.stat(parent.getPath()); 10416 if ((stat.st_mode & accessBits) != accessBits) { 10417 // the parent dir doesn't have the appropriate access 10418 throw new FileNotFoundException("Can't access " + filePath); 10419 } 10420 } catch (ErrnoException e1) { 10421 // couldn't stat() parent 10422 throw new FileNotFoundException("Can't access " + filePath); 10423 } 10424 parent = parent.getParentFile(); 10425 } 10426 } 10427 10428 @VisibleForTesting 10429 static class FallbackException extends Exception { 10430 private final int mThrowSdkVersion; 10431 10432 public FallbackException(String message, int throwSdkVersion) { 10433 super(message); 10434 mThrowSdkVersion = throwSdkVersion; 10435 } 10436 10437 public FallbackException(String message, Throwable cause, int throwSdkVersion) { 10438 super(message, cause); 10439 mThrowSdkVersion = throwSdkVersion; 10440 } 10441 10442 @Override 10443 public String getMessage() { 10444 if (getCause() != null) { 10445 return super.getMessage() + ": " + getCause().getMessage(); 10446 } else { 10447 return super.getMessage(); 10448 } 10449 } 10450 10451 public IllegalArgumentException rethrowAsIllegalArgumentException() { 10452 throw new IllegalArgumentException(getMessage()); 10453 } 10454 10455 public Cursor translateForQuery(int targetSdkVersion) { 10456 if (targetSdkVersion >= mThrowSdkVersion) { 10457 throw new IllegalArgumentException(getMessage()); 10458 } else { 10459 Log.w(TAG, getMessage()); 10460 return null; 10461 } 10462 } 10463 10464 public Uri translateForInsert(int targetSdkVersion) { 10465 if (targetSdkVersion >= mThrowSdkVersion) { 10466 throw new IllegalArgumentException(getMessage()); 10467 } else { 10468 Log.w(TAG, getMessage()); 10469 return null; 10470 } 10471 } 10472 10473 public int translateForBulkInsert(int targetSdkVersion) { 10474 if (targetSdkVersion >= mThrowSdkVersion) { 10475 throw new IllegalArgumentException(getMessage()); 10476 } else { 10477 Log.w(TAG, getMessage()); 10478 return 0; 10479 } 10480 } 10481 10482 public int translateForUpdateDelete(int targetSdkVersion) { 10483 if (targetSdkVersion >= mThrowSdkVersion) { 10484 throw new IllegalArgumentException(getMessage()); 10485 } else { 10486 Log.w(TAG, getMessage()); 10487 return 0; 10488 } 10489 } 10490 } 10491 10492 @VisibleForTesting 10493 static class VolumeNotFoundException extends FallbackException { 10494 public VolumeNotFoundException(String volumeName) { 10495 super("Volume " + volumeName + " not found", Build.VERSION_CODES.Q); 10496 } 10497 } 10498 10499 @VisibleForTesting 10500 static class VolumeArgumentException extends FallbackException { 10501 public VolumeArgumentException(File actual, Collection<File> allowed) { 10502 super("Requested path " + actual + " doesn't appear under " + allowed, 10503 Build.VERSION_CODES.Q); 10504 } 10505 } 10506 10507 public List<String> getSupportedTranscodingRelativePaths() { 10508 return mTranscodeHelper.getSupportedRelativePaths(); 10509 } 10510 10511 public List<String> getSupportedUncachedRelativePaths() { 10512 return StringUtils.verifySupportedUncachedRelativePaths( 10513 StringUtils.getStringArrayConfig(getContext(), 10514 R.array.config_supported_uncached_relative_paths)); 10515 } 10516 10517 /** 10518 * Creating a new method for Transcoding to avoid any merge conflicts. 10519 * TODO(b/170465810): Remove this when the code is refactored. 10520 */ 10521 @NonNull DatabaseHelper getDatabaseForUriForTranscoding(Uri uri) 10522 throws VolumeNotFoundException { 10523 return getDatabaseForUri(uri); 10524 } 10525 10526 @NonNull 10527 private DatabaseHelper getDatabaseForUri(Uri uri) throws VolumeNotFoundException { 10528 final String volumeName = resolveVolumeName(uri); 10529 synchronized (mAttachedVolumes) { 10530 boolean volumeAttached = false; 10531 UserHandle user = mCallingIdentity.get().getUser(); 10532 for (MediaVolume vol : mAttachedVolumes) { 10533 if (vol.getName().equals(volumeName) 10534 && (vol.isVisibleToUser(user) || vol.isPublicVolume()) ) { 10535 volumeAttached = true; 10536 break; 10537 } 10538 } 10539 if (!volumeAttached) { 10540 // Dump some more debug info 10541 Log.e(TAG, "Volume " + volumeName + " not found, calling identity: " 10542 + user + ", attached volumes: " + mAttachedVolumes); 10543 throw new VolumeNotFoundException(volumeName); 10544 } 10545 } 10546 if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) { 10547 return mInternalDatabase; 10548 } else { 10549 return mExternalDatabase; 10550 } 10551 } 10552 10553 static boolean isMediaDatabaseName(String name) { 10554 if (INTERNAL_DATABASE_NAME.equals(name)) { 10555 return true; 10556 } 10557 if (EXTERNAL_DATABASE_NAME.equals(name)) { 10558 return true; 10559 } 10560 return name.startsWith("external-") && name.endsWith(".db"); 10561 } 10562 10563 @NonNull 10564 private Uri getBaseContentUri(@NonNull String volumeName) { 10565 return MediaStore.AUTHORITY_URI.buildUpon().appendPath(volumeName).build(); 10566 } 10567 10568 public Uri attachVolume(MediaVolume volume, boolean validate) { 10569 if (mCallingIdentity.get().pid != android.os.Process.myPid()) { 10570 throw new SecurityException( 10571 "Opening and closing databases not allowed."); 10572 } 10573 10574 final String volumeName = volume.getName(); 10575 10576 // Quick check for shady volume names 10577 MediaStore.checkArgumentVolumeName(volumeName); 10578 10579 // Quick check that volume actually exists 10580 if (!MediaStore.VOLUME_INTERNAL.equals(volumeName) && validate) { 10581 try { 10582 getVolumePath(volumeName); 10583 } catch (IOException e) { 10584 throw new IllegalArgumentException( 10585 "Volume " + volume + " currently unavailable", e); 10586 } 10587 } 10588 10589 synchronized (mAttachedVolumes) { 10590 mAttachedVolumes.add(volume); 10591 } 10592 10593 mDatabaseBackupAndRecovery.setupVolumeDbBackupAndRecovery(volume.getName(), 10594 volume.getPath()); 10595 10596 final ContentResolver resolver = getContext().getContentResolver(); 10597 final Uri uri = getBaseContentUri(volumeName); 10598 // TODO(b/182396009) we probably also want to notify clone profile (and vice versa) 10599 resolver.notifyChange(getBaseContentUri(volumeName), null); 10600 10601 if (LOGV) Log.v(TAG, "Attached volume: " + volume); 10602 if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)) { 10603 // Also notify on synthetic view of all devices 10604 resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null); 10605 10606 ForegroundThread.getExecutor().execute(() -> { 10607 mExternalDatabase.runWithTransaction((db) -> { 10608 ensureDefaultFolders(volume, db); 10609 ensureThumbnailsValid(volume, db); 10610 return null; 10611 }); 10612 10613 // We just finished the database operation above, we know that 10614 // it's ready to answer queries, so notify our DocumentProvider 10615 // so it can answer queries without risking ANR 10616 MediaDocumentsProvider.onMediaStoreReady(getContext()); 10617 }); 10618 } 10619 return uri; 10620 } 10621 10622 private void detachVolume(Uri uri) { 10623 final String volumeName = MediaStore.getVolumeName(uri); 10624 try { 10625 detachVolume(getVolume(volumeName)); 10626 } catch (FileNotFoundException e) { 10627 Log.e(TAG, "Couldn't find volume for URI " + uri, e) ; 10628 } 10629 } 10630 10631 public boolean isVolumeAttached(MediaVolume volume) { 10632 synchronized (mAttachedVolumes) { 10633 return mAttachedVolumes.contains(volume); 10634 } 10635 } 10636 10637 public void detachVolume(MediaVolume volume) { 10638 if (mCallingIdentity.get().pid != android.os.Process.myPid()) { 10639 throw new SecurityException( 10640 "Opening and closing databases not allowed."); 10641 } 10642 10643 final String volumeName = volume.getName(); 10644 10645 // Quick check for shady volume names 10646 MediaStore.checkArgumentVolumeName(volumeName); 10647 10648 if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) { 10649 throw new UnsupportedOperationException( 10650 "Deleting the internal volume is not allowed"); 10651 } 10652 10653 // Signal any scanning to shut down 10654 mMediaScanner.onDetachVolume(volume); 10655 10656 synchronized (mAttachedVolumes) { 10657 mAttachedVolumes.remove(volume); 10658 } 10659 10660 final ContentResolver resolver = getContext().getContentResolver(); 10661 resolver.notifyChange(getBaseContentUri(volumeName), null); 10662 10663 if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)) { 10664 // Also notify on synthetic view of all devices 10665 resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null); 10666 } 10667 10668 if (LOGV) Log.v(TAG, "Detached volume: " + volumeName); 10669 } 10670 10671 @GuardedBy("mAttachedVolumes") 10672 private final ArraySet<MediaVolume> mAttachedVolumes = new ArraySet<>(); 10673 @GuardedBy("mCustomCollators") 10674 private final ArraySet<String> mCustomCollators = new ArraySet<>(); 10675 10676 private MediaScanner mMediaScanner; 10677 10678 private ProjectionHelper mProjectionHelper; 10679 private DatabaseHelper mInternalDatabase; 10680 private DatabaseHelper mExternalDatabase; 10681 private PickerDbFacade mPickerDbFacade; 10682 private ExternalDbFacade mExternalDbFacade; 10683 private PickerDataLayer mPickerDataLayer; 10684 private ConfigStore mConfigStore; 10685 private PickerSyncController mPickerSyncController; 10686 private TranscodeHelper mTranscodeHelper; 10687 private MediaGrants mMediaGrants; 10688 private DatabaseBackupAndRecovery mDatabaseBackupAndRecovery; 10689 10690 // name of the volume currently being scanned by the media scanner (or null) 10691 private String mMediaScannerVolume; 10692 10693 10694 private static final HashSet<Integer> REDACTED_URI_SUPPORTED_TYPES = new HashSet<>( 10695 Arrays.asList(AUDIO_MEDIA_ID, IMAGES_MEDIA_ID, VIDEO_MEDIA_ID, FILES_ID, DOWNLOADS_ID)); 10696 10697 private LocalUriMatcher mUriMatcher; 10698 10699 private int matchUri(Uri uri, boolean allowHidden) { 10700 return mUriMatcher.matchUri(uri, allowHidden); 10701 } 10702 10703 10704 10705 /** 10706 * Set of columns that can be safely mutated by external callers; all other 10707 * columns are treated as read-only, since they reflect what the media 10708 * scanner found on disk, and any mutations would be overwritten the next 10709 * time the media was scanned. 10710 */ 10711 private static final ArraySet<String> sMutableColumns = new ArraySet<>(); 10712 10713 static { 10714 sMutableColumns.add(MediaStore.MediaColumns.DATA); 10715 sMutableColumns.add(MediaStore.MediaColumns.RELATIVE_PATH); 10716 sMutableColumns.add(MediaStore.MediaColumns.DISPLAY_NAME); 10717 sMutableColumns.add(MediaStore.MediaColumns.IS_PENDING); 10718 sMutableColumns.add(MediaStore.MediaColumns.IS_TRASHED); 10719 sMutableColumns.add(MediaStore.MediaColumns.IS_FAVORITE); 10720 sMutableColumns.add(MediaStore.MediaColumns.OWNER_PACKAGE_NAME); 10721 10722 sMutableColumns.add(MediaStore.Audio.AudioColumns.BOOKMARK); 10723 10724 sMutableColumns.add(MediaStore.Video.VideoColumns.TAGS); 10725 sMutableColumns.add(MediaStore.Video.VideoColumns.CATEGORY); 10726 sMutableColumns.add(MediaStore.Video.VideoColumns.BOOKMARK); 10727 10728 sMutableColumns.add(MediaStore.Audio.Playlists.NAME); 10729 sMutableColumns.add(MediaStore.Audio.Playlists.Members.AUDIO_ID); 10730 sMutableColumns.add(MediaStore.Audio.Playlists.Members.PLAY_ORDER); 10731 10732 sMutableColumns.add(MediaStore.DownloadColumns.DOWNLOAD_URI); 10733 sMutableColumns.add(MediaStore.DownloadColumns.REFERER_URI); 10734 10735 sMutableColumns.add(MediaStore.Files.FileColumns.MIME_TYPE); 10736 sMutableColumns.add(MediaStore.Files.FileColumns.MEDIA_TYPE); 10737 } 10738 10739 /** 10740 * Set of columns that affect placement of files on disk. 10741 */ 10742 private static final ArraySet<String> sPlacementColumns = new ArraySet<>(); 10743 10744 static { 10745 sPlacementColumns.add(MediaStore.MediaColumns.DATA); 10746 sPlacementColumns.add(MediaStore.MediaColumns.RELATIVE_PATH); 10747 sPlacementColumns.add(MediaStore.MediaColumns.DISPLAY_NAME); 10748 sPlacementColumns.add(MediaStore.MediaColumns.MIME_TYPE); 10749 sPlacementColumns.add(MediaStore.MediaColumns.IS_PENDING); 10750 sPlacementColumns.add(MediaStore.MediaColumns.IS_TRASHED); 10751 sPlacementColumns.add(MediaStore.MediaColumns.DATE_EXPIRES); 10752 } 10753 10754 /** 10755 * List of abusive custom columns that we're willing to allow via 10756 * {@link SQLiteQueryBuilder#setProjectionGreylist(List)}. 10757 */ 10758 static final ArrayList<Pattern> sGreylist = new ArrayList<>(); 10759 10760 private static void addGreylistPattern(String pattern) { 10761 sGreylist.add(Pattern.compile(" *" + pattern + " *")); 10762 } 10763 10764 static { 10765 final String maybeAs = "( (as )?[_a-z0-9]+)?"; 10766 addGreylistPattern("(?i)[_a-z0-9]+" + maybeAs); 10767 addGreylistPattern("audio\\._id AS _id"); 10768 addGreylistPattern( 10769 "(?i)(min|max|sum|avg|total|count|cast)\\(([_a-z0-9]+" 10770 + maybeAs 10771 + "|\\*)\\)" 10772 + maybeAs); 10773 addGreylistPattern( 10774 "case when case when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added" 10775 + " \\* \\d+ when \\(date_added >= \\d+ and date_added < \\d+\\) then" 10776 + " date_added when \\(date_added >= \\d+ and date_added < \\d+\\) then" 10777 + " date_added / \\d+ else \\d+ end > case when \\(date_modified >= \\d+ and" 10778 + " date_modified < \\d+\\) then date_modified \\* \\d+ when \\(date_modified" 10779 + " >= \\d+ and date_modified < \\d+\\) then date_modified when" 10780 + " \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified /" 10781 + " \\d+ else \\d+ end then case when \\(date_added >= \\d+ and date_added <" 10782 + " \\d+\\) then date_added \\* \\d+ when \\(date_added >= \\d+ and date_added" 10783 + " < \\d+\\) then date_added when \\(date_added >= \\d+ and date_added <" 10784 + " \\d+\\) then date_added / \\d+ else \\d+ end else case when" 10785 + " \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified \\*" 10786 + " \\d+ when \\(date_modified >= \\d+ and date_modified < \\d+\\) then" 10787 + " date_modified when \\(date_modified >= \\d+ and date_modified < \\d+\\)" 10788 + " then date_modified / \\d+ else \\d+ end end as corrected_added_modified"); 10789 addGreylistPattern( 10790 "MAX\\(case when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken \\*" 10791 + " \\d+ when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken when" 10792 + " \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken / \\d+ else" 10793 + " \\d+ end\\)"); 10794 addGreylistPattern( 10795 "MAX\\(case when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added \\*" 10796 + " \\d+ when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added" 10797 + " when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added / \\d+" 10798 + " else \\d+ end\\)"); 10799 addGreylistPattern( 10800 "MAX\\(case when \\(date_modified >= \\d+ and date_modified < \\d+\\) then" 10801 + " date_modified \\* \\d+ when \\(date_modified >= \\d+ and date_modified <" 10802 + " \\d+\\) then date_modified when \\(date_modified >= \\d+ and date_modified" 10803 + " < \\d+\\) then date_modified / \\d+ else \\d+ end\\)"); 10804 addGreylistPattern("\"content://media/[a-z]+/audio/media\""); 10805 addGreylistPattern( 10806 "substr\\(_data, length\\(_data\\)-length\\(_display_name\\), 1\\) as" 10807 + " filename_prevchar"); 10808 addGreylistPattern("\\*" + maybeAs); 10809 addGreylistPattern( 10810 "case when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken \\* \\d+" 10811 + " when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken when" 10812 + " \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken / \\d+ else" 10813 + " \\d+ end"); 10814 } 10815 10816 public ArrayMap<String, String> getProjectionMap(Class<?>... clazzes) { 10817 return mProjectionHelper.getProjectionMap(clazzes); 10818 } 10819 10820 static <T> boolean containsAny(Set<T> a, Set<T> b) { 10821 for (T i : b) { 10822 if (a.contains(i)) { 10823 return true; 10824 } 10825 } 10826 return false; 10827 } 10828 10829 @VisibleForTesting 10830 @Nullable 10831 static Uri computeCommonPrefix(@NonNull List<Uri> uris) { 10832 if (uris.isEmpty()) return null; 10833 10834 final Uri base = uris.get(0); 10835 final List<String> basePath = new ArrayList<>(base.getPathSegments()); 10836 for (int i = 1; i < uris.size(); i++) { 10837 final List<String> probePath = uris.get(i).getPathSegments(); 10838 for (int j = 0; j < basePath.size() && j < probePath.size(); j++) { 10839 if (!Objects.equals(basePath.get(j), probePath.get(j))) { 10840 // Trim away all remaining common elements 10841 while (basePath.size() > j) { 10842 basePath.remove(j); 10843 } 10844 } 10845 } 10846 10847 final int probeSize = probePath.size(); 10848 while (basePath.size() > probeSize) { 10849 basePath.remove(probeSize); 10850 } 10851 } 10852 10853 final Uri.Builder builder = base.buildUpon().path(null); 10854 for (String s : basePath) { 10855 builder.appendPath(s); 10856 } 10857 return builder.build(); 10858 } 10859 10860 public ExternalDbFacade getExternalDbFacade() { 10861 return mExternalDbFacade; 10862 } 10863 10864 public PickerSyncController getPickerSyncController() { 10865 return mPickerSyncController; 10866 } 10867 10868 private boolean isCallingPackageSystemGallery() { 10869 if (mCallingIdentity.get().hasPermission(PERMISSION_IS_SYSTEM_GALLERY)) { 10870 if (isCallingPackageRequestingLegacy()) { 10871 return isCallingPackageLegacyWrite(); 10872 } 10873 return true; 10874 } 10875 return false; 10876 } 10877 10878 private int getCallingUidOrSelf() { 10879 return mCallingIdentity.get().uid; 10880 } 10881 10882 @Deprecated 10883 private String getCallingPackageOrSelf() { 10884 return mCallingIdentity.get().getPackageName(); 10885 } 10886 10887 @Deprecated 10888 @VisibleForTesting 10889 public int getCallingPackageTargetSdkVersion() { 10890 return mCallingIdentity.get().getTargetSdkVersion(); 10891 } 10892 10893 @Deprecated 10894 private boolean isCallingPackageAllowedHidden() { 10895 return isCallingPackageSelf(); 10896 } 10897 10898 @Deprecated 10899 private boolean isCallingPackageSelf() { 10900 return mCallingIdentity.get().hasPermission(PERMISSION_IS_SELF); 10901 } 10902 10903 @Deprecated 10904 private boolean isCallingPackageShell() { 10905 return mCallingIdentity.get().hasPermission(PERMISSION_IS_SHELL); 10906 } 10907 10908 @Deprecated 10909 private boolean isCallingPackageManager() { 10910 return mCallingIdentity.get().hasPermission(PERMISSION_IS_MANAGER); 10911 } 10912 10913 @Deprecated 10914 private boolean isCallingPackageDelegator() { 10915 return mCallingIdentity.get().hasPermission(PERMISSION_IS_DELEGATOR); 10916 } 10917 10918 @Deprecated 10919 private boolean isCallingPackageLegacyRead() { 10920 return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_READ); 10921 } 10922 10923 @Deprecated 10924 private boolean isCallingPackageLegacyWrite() { 10925 return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_WRITE); 10926 } 10927 10928 @Override 10929 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 10930 writer.println("mThumbSize=" + mThumbSize); 10931 synchronized (mAttachedVolumes) { 10932 writer.println("mAttachedVolumes=" + mAttachedVolumes); 10933 } 10934 writer.println(); 10935 10936 mVolumeCache.dump(writer); 10937 writer.println(); 10938 10939 mUserCache.dump(writer); 10940 writer.println(); 10941 10942 mTranscodeHelper.dump(writer); 10943 writer.println(); 10944 10945 dumpAccessLogs(writer); 10946 writer.println(); 10947 10948 Logging.dumpPersistent(writer); 10949 } 10950 10951 private void dumpAccessLogs(PrintWriter writer) { 10952 synchronized (mCachedCallingIdentityForFuse) { 10953 for (int key = 0; key <= mCachedCallingIdentityForFuse.size(); key++) { 10954 if (mCachedCallingIdentityForFuse.contains(key)) { 10955 mCachedCallingIdentityForFuse.get(key).dump(writer); 10956 } 10957 } 10958 } 10959 } 10960 10961 /** 10962 * Called once - from {@link #onCreate()}. 10963 */ 10964 @NonNull 10965 private ConfigStore createConfigStore() { 10966 // Tests may want override provideConfigStore() in order to inject a mock object. 10967 ConfigStore configStore = provideConfigStore(); 10968 if (configStore == null) { 10969 // Tests did not provide an alternative implementation: create our regular "production" 10970 // ConfigStore. 10971 configStore = MediaApplication.getConfigStore(); 10972 } 10973 return configStore; 10974 } 10975 10976 /** 10977 * <b>FOT TESTING PURPOSES ONLY</b> 10978 * <p> 10979 * Allows injecting alternative {@link ConfigStore} implementation. 10980 */ 10981 @VisibleForTesting 10982 @Nullable 10983 protected ConfigStore provideConfigStore() { 10984 return null; 10985 } 10986 10987 protected VolumeCache getVolumeCache() { 10988 return mVolumeCache; 10989 } 10990 10991 protected DatabaseBackupAndRecovery createDatabaseBackupAndRecovery() { 10992 return new DatabaseBackupAndRecovery(mConfigStore, mVolumeCache); 10993 } 10994 } 10995