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