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