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