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