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.provider.MediaStore.MATCH_DEFAULT; 28 import static android.provider.MediaStore.MATCH_EXCLUDE; 29 import static android.provider.MediaStore.MATCH_INCLUDE; 30 import static android.provider.MediaStore.MATCH_ONLY; 31 import static android.provider.MediaStore.QUERY_ARG_MATCH_FAVORITE; 32 import static android.provider.MediaStore.QUERY_ARG_MATCH_PENDING; 33 import static android.provider.MediaStore.QUERY_ARG_MATCH_TRASHED; 34 import static android.provider.MediaStore.QUERY_ARG_RELATED_URI; 35 import static android.provider.MediaStore.getVolumeName; 36 37 import static com.android.providers.media.DatabaseHelper.EXTERNAL_DATABASE_NAME; 38 import static com.android.providers.media.DatabaseHelper.INTERNAL_DATABASE_NAME; 39 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_DELEGATOR; 40 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_GRANTED; 41 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_READ; 42 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE; 43 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_MANAGER; 44 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED; 45 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SELF; 46 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SHELL; 47 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_AUDIO; 48 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_IMAGES; 49 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_VIDEO; 50 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_AUDIO; 51 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_IMAGES; 52 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_VIDEO; 53 import static com.android.providers.media.scan.MediaScanner.REASON_DEMAND; 54 import static com.android.providers.media.scan.MediaScanner.REASON_IDLE; 55 import static com.android.providers.media.util.DatabaseUtils.bindList; 56 import static com.android.providers.media.util.FileUtils.PATTERN_PENDING_FILEPATH_FOR_SQL; 57 import static com.android.providers.media.util.FileUtils.extractDisplayName; 58 import static com.android.providers.media.util.FileUtils.extractFileName; 59 import static com.android.providers.media.util.FileUtils.extractPathOwnerPackageName; 60 import static com.android.providers.media.util.FileUtils.extractRelativePath; 61 import static com.android.providers.media.util.FileUtils.extractRelativePathForDirectory; 62 import static com.android.providers.media.util.FileUtils.extractTopLevelDir; 63 import static com.android.providers.media.util.FileUtils.extractVolumeName; 64 import static com.android.providers.media.util.FileUtils.getAbsoluteSanitizedPath; 65 import static com.android.providers.media.util.FileUtils.isDataOrObbPath; 66 import static com.android.providers.media.util.FileUtils.isDownload; 67 import static com.android.providers.media.util.FileUtils.sanitizePath; 68 import static com.android.providers.media.util.Logging.LOGV; 69 import static com.android.providers.media.util.Logging.TAG; 70 71 import android.app.AppOpsManager; 72 import android.app.AppOpsManager.OnOpActiveChangedListener; 73 import android.app.AppOpsManager.OnOpChangedListener; 74 import android.app.DownloadManager; 75 import android.app.PendingIntent; 76 import android.app.RecoverableSecurityException; 77 import android.app.RemoteAction; 78 import android.content.BroadcastReceiver; 79 import android.content.ClipData; 80 import android.content.ClipDescription; 81 import android.content.ContentProvider; 82 import android.content.ContentProviderClient; 83 import android.content.ContentProviderOperation; 84 import android.content.ContentProviderResult; 85 import android.content.ContentResolver; 86 import android.content.ContentUris; 87 import android.content.ContentValues; 88 import android.content.Context; 89 import android.content.Intent; 90 import android.content.IntentFilter; 91 import android.content.OperationApplicationException; 92 import android.content.SharedPreferences; 93 import android.content.UriMatcher; 94 import android.content.pm.ApplicationInfo; 95 import android.content.pm.PackageInstaller.SessionInfo; 96 import android.content.pm.PackageManager; 97 import android.content.pm.PackageManager.NameNotFoundException; 98 import android.content.pm.PermissionGroupInfo; 99 import android.content.pm.ProviderInfo; 100 import android.content.res.AssetFileDescriptor; 101 import android.content.res.Configuration; 102 import android.content.res.Resources; 103 import android.database.Cursor; 104 import android.database.MatrixCursor; 105 import android.database.sqlite.SQLiteConstraintException; 106 import android.database.sqlite.SQLiteDatabase; 107 import android.graphics.Bitmap; 108 import android.graphics.BitmapFactory; 109 import android.graphics.drawable.Icon; 110 import android.icu.util.ULocale; 111 import android.media.ExifInterface; 112 import android.media.ThumbnailUtils; 113 import android.mtp.MtpConstants; 114 import android.net.Uri; 115 import android.os.Binder; 116 import android.os.Binder.ProxyTransactListener; 117 import android.os.Build; 118 import android.os.Bundle; 119 import android.os.CancellationSignal; 120 import android.os.Environment; 121 import android.os.IBinder; 122 import android.os.ParcelFileDescriptor; 123 import android.os.ParcelFileDescriptor.OnCloseListener; 124 import android.os.RemoteException; 125 import android.os.SystemClock; 126 import android.os.SystemProperties; 127 import android.os.Trace; 128 import android.os.UserHandle; 129 import android.os.storage.StorageManager; 130 import android.os.storage.StorageManager.StorageVolumeCallback; 131 import android.os.storage.StorageVolume; 132 import android.preference.PreferenceManager; 133 import android.provider.BaseColumns; 134 import android.provider.Column; 135 import android.provider.MediaStore; 136 import android.provider.MediaStore.Audio; 137 import android.provider.MediaStore.Audio.AudioColumns; 138 import android.provider.MediaStore.Audio.Playlists; 139 import android.provider.MediaStore.Downloads; 140 import android.provider.MediaStore.Files; 141 import android.provider.MediaStore.Files.FileColumns; 142 import android.provider.MediaStore.Images; 143 import android.provider.MediaStore.Images.ImageColumns; 144 import android.provider.MediaStore.MediaColumns; 145 import android.provider.MediaStore.Video; 146 import android.system.ErrnoException; 147 import android.system.Os; 148 import android.system.OsConstants; 149 import android.system.StructStat; 150 import android.text.TextUtils; 151 import android.text.format.DateUtils; 152 import android.util.ArrayMap; 153 import android.util.ArraySet; 154 import android.util.DisplayMetrics; 155 import android.util.Log; 156 import android.util.LongSparseArray; 157 import android.util.Size; 158 import android.util.SparseArray; 159 import android.webkit.MimeTypeMap; 160 161 import androidx.annotation.GuardedBy; 162 import androidx.annotation.Keep; 163 import androidx.annotation.NonNull; 164 import androidx.annotation.Nullable; 165 import androidx.annotation.VisibleForTesting; 166 167 import com.android.providers.media.DatabaseHelper.OnFilesChangeListener; 168 import com.android.providers.media.DatabaseHelper.OnLegacyMigrationListener; 169 import com.android.providers.media.fuse.ExternalStorageServiceImpl; 170 import com.android.providers.media.fuse.FuseDaemon; 171 import com.android.providers.media.playlist.Playlist; 172 import com.android.providers.media.scan.MediaScanner; 173 import com.android.providers.media.scan.ModernMediaScanner; 174 import com.android.providers.media.util.BackgroundThread; 175 import com.android.providers.media.util.CachedSupplier; 176 import com.android.providers.media.util.DatabaseUtils; 177 import com.android.providers.media.util.FileUtils; 178 import com.android.providers.media.util.ForegroundThread; 179 import com.android.providers.media.util.IsoInterface; 180 import com.android.providers.media.util.Logging; 181 import com.android.providers.media.util.LongArray; 182 import com.android.providers.media.util.Metrics; 183 import com.android.providers.media.util.MimeUtils; 184 import com.android.providers.media.util.PermissionUtils; 185 import com.android.providers.media.util.RedactingFileDescriptor; 186 import com.android.providers.media.util.SQLiteQueryBuilder; 187 import com.android.providers.media.util.XmpInterface; 188 189 import com.google.common.hash.Hashing; 190 191 import java.io.File; 192 import java.io.FileDescriptor; 193 import java.io.FileInputStream; 194 import java.io.FileNotFoundException; 195 import java.io.FileOutputStream; 196 import java.io.IOException; 197 import java.io.OutputStream; 198 import java.io.PrintWriter; 199 import java.nio.charset.StandardCharsets; 200 import java.nio.file.Path; 201 import java.util.ArrayList; 202 import java.util.Arrays; 203 import java.util.Collection; 204 import java.util.List; 205 import java.util.Locale; 206 import java.util.Map; 207 import java.util.Objects; 208 import java.util.Optional; 209 import java.util.Set; 210 import java.util.function.Consumer; 211 import java.util.function.Supplier; 212 import java.util.function.UnaryOperator; 213 import java.util.regex.Matcher; 214 import java.util.regex.Pattern; 215 216 /** 217 * Media content provider. See {@link android.provider.MediaStore} for details. 218 * Separate databases are kept for each external storage card we see (using the 219 * card's ID as an index). The content visible at content://media/external/... 220 * changes with the card. 221 */ 222 public class MediaProvider extends ContentProvider { 223 /** 224 * Regex of a selection string that matches a specific ID. 225 */ 226 static final Pattern PATTERN_SELECTION_ID = Pattern.compile( 227 "(?:image_id|video_id)\\s*=\\s*(\\d+)"); 228 229 /** 230 * Property that indicates whether fuse is enabled. 231 */ 232 private static final String PROP_FUSE = "persist.sys.fuse"; 233 234 /** 235 * These directory names aren't declared in Environment as final variables, and so we need to 236 * have the same values in separate final variables in order to have them considered constant 237 * expressions. 238 */ 239 private static final String DIRECTORY_MUSIC = "Music"; 240 private static final String DIRECTORY_PODCASTS = "Podcasts"; 241 private static final String DIRECTORY_RINGTONES = "Ringtones"; 242 private static final String DIRECTORY_ALARMS = "Alarms"; 243 private static final String DIRECTORY_NOTIFICATIONS = "Notifications"; 244 private static final String DIRECTORY_PICTURES = "Pictures"; 245 private static final String DIRECTORY_MOVIES = "Movies"; 246 private static final String DIRECTORY_DOWNLOADS = "Download"; 247 private static final String DIRECTORY_DCIM = "DCIM"; 248 private static final String DIRECTORY_DOCUMENTS = "Documents"; 249 private static final String DIRECTORY_AUDIOBOOKS = "Audiobooks"; 250 private static final String DIRECTORY_ANDROID = "Android"; 251 252 private static final String DIRECTORY_MEDIA = "media"; 253 private static final String DIRECTORY_THUMBNAILS = ".thumbnails"; 254 255 /** 256 * Hard-coded filename where the current value of 257 * {@link DatabaseHelper#getOrCreateUuid} is persisted on a physical SD card 258 * to help identify stale thumbnail collections. 259 */ 260 private static final String FILE_DATABASE_UUID = ".database_uuid"; 261 262 /** 263 * Specify what default directories the caller gets full access to. By default, the caller 264 * shouldn't get full access to any default dirs. 265 * But for example, we do an exception for System Gallery apps and allow them full access to: 266 * DCIM, Pictures, Movies. 267 */ 268 private static final String INCLUDED_DEFAULT_DIRECTORIES = 269 "android:included-default-directories"; 270 271 /** 272 * Value indicating that operations should include database rows matching the criteria defined 273 * by this key only when calling package has write permission to the database row or column is 274 * {@column MediaColumns#IS_PENDING} and is set by FUSE. 275 * <p> 276 * Note that items <em>not</em> matching the criteria will also be included, and as part of this 277 * match no additional write permission checks are carried out for those items. 278 */ 279 private static final int MATCH_VISIBLE_FOR_FILEPATH = 32; 280 281 /** 282 * Where clause to match pending files from FUSE. Pending files from FUSE will not have 283 * PATTERN_PENDING_FILEPATH_FOR_SQL pattern. 284 */ 285 private static final String MATCH_PENDING_FROM_FUSE = String.format("lower(%s) NOT REGEXP '%s'", 286 MediaColumns.DATA, PATTERN_PENDING_FILEPATH_FOR_SQL); 287 288 /** 289 * Set of {@link Cursor} columns that refer to raw filesystem paths. 290 */ 291 private static final ArrayMap<String, Object> sDataColumns = new ArrayMap<>(); 292 293 { sDataColumns.put(MediaStore.MediaColumns.DATA, null)294 sDataColumns.put(MediaStore.MediaColumns.DATA, null); sDataColumns.put(MediaStore.Images.Thumbnails.DATA, null)295 sDataColumns.put(MediaStore.Images.Thumbnails.DATA, null); sDataColumns.put(MediaStore.Video.Thumbnails.DATA, null)296 sDataColumns.put(MediaStore.Video.Thumbnails.DATA, null); sDataColumns.put(MediaStore.Audio.PlaylistsColumns.DATA, null)297 sDataColumns.put(MediaStore.Audio.PlaylistsColumns.DATA, null); sDataColumns.put(MediaStore.Audio.AlbumColumns.ALBUM_ART, null)298 sDataColumns.put(MediaStore.Audio.AlbumColumns.ALBUM_ART, null); 299 } 300 301 private static final Object sCacheLock = new Object(); 302 303 @GuardedBy("sCacheLock") 304 private static final Set<String> sCachedExternalVolumeNames = new ArraySet<>(); 305 @GuardedBy("sCacheLock") 306 private static final Map<String, File> sCachedVolumePaths = new ArrayMap<>(); 307 @GuardedBy("sCacheLock") 308 private static final Map<String, Collection<File>> sCachedVolumeScanPaths = new ArrayMap<>(); 309 @GuardedBy("sCacheLock") 310 private static final ArrayMap<File, String> sCachedVolumePathToId = new ArrayMap<>(); 311 312 @GuardedBy("mShouldRedactThreadIds") 313 private final LongArray mShouldRedactThreadIds = new LongArray(); 314 updateVolumes()315 public void updateVolumes() { 316 synchronized (sCacheLock) { 317 sCachedExternalVolumeNames.clear(); 318 sCachedExternalVolumeNames.addAll(MediaStore.getExternalVolumeNames(getContext())); 319 Log.v(TAG, "Updated external volumes to: " + sCachedExternalVolumeNames.toString()); 320 321 sCachedVolumePaths.clear(); 322 sCachedVolumeScanPaths.clear(); 323 sCachedVolumePathToId.clear(); 324 try { 325 sCachedVolumeScanPaths.put(MediaStore.VOLUME_INTERNAL, 326 FileUtils.getVolumeScanPaths(getContext(), MediaStore.VOLUME_INTERNAL)); 327 } catch (FileNotFoundException e) { 328 Log.wtf(TAG, "Failed to update volume " + MediaStore.VOLUME_INTERNAL, e); 329 } 330 331 for (String volumeName : sCachedExternalVolumeNames) { 332 try { 333 final Uri uri = MediaStore.Files.getContentUri(volumeName); 334 final StorageVolume volume = mStorageManager.getStorageVolume(uri); 335 sCachedVolumePaths.put(volumeName, volume.getDirectory()); 336 sCachedVolumeScanPaths.put(volumeName, 337 FileUtils.getVolumeScanPaths(getContext(), volumeName)); 338 sCachedVolumePathToId.put(volume.getDirectory(), volume.getId()); 339 } catch (IllegalStateException | FileNotFoundException e) { 340 Log.wtf(TAG, "Failed to update volume " + volumeName, e); 341 } 342 } 343 } 344 345 // Update filters to reflect mounted volumes so users don't get 346 // confused by metadata from ejected volumes 347 ForegroundThread.getExecutor().execute(() -> { 348 mExternalDatabase.setFilterVolumeNames(getExternalVolumeNames()); 349 }); 350 } 351 getVolumePath(@onNull String volumeName)352 public @NonNull File getVolumePath(@NonNull String volumeName) throws FileNotFoundException { 353 // Ugly hack to keep unit tests passing, where we don't always have a 354 // Context to discover volumes with 355 if (getContext() == null) { 356 return Environment.getExternalStorageDirectory(); 357 } 358 359 synchronized (sCacheLock) { 360 if (sCachedVolumePaths.containsKey(volumeName)) { 361 return sCachedVolumePaths.get(volumeName); 362 } 363 364 // Nothing found above; let's ask directly and cache the answer 365 final File res = FileUtils.getVolumePath(getContext(), volumeName); 366 sCachedVolumePaths.put(volumeName, res); 367 return res; 368 } 369 } 370 getVolumeId(@onNull File file)371 public @NonNull String getVolumeId(@NonNull File file) throws FileNotFoundException { 372 synchronized (sCacheLock) { 373 for (int i = 0; i < sCachedVolumePathToId.size(); i++) { 374 if (FileUtils.contains(sCachedVolumePathToId.keyAt(i), file)) { 375 return sCachedVolumePathToId.valueAt(i); 376 } 377 } 378 379 // Nothing found above; let's ask directly and cache the answer 380 final StorageVolume volume = mStorageManager.getStorageVolume(file); 381 if (volume == null) { 382 throw new FileNotFoundException("Missing volume for " + file); 383 } 384 sCachedVolumePathToId.put(volume.getDirectory(), volume.getId()); 385 return volume.getId(); 386 } 387 } 388 getExternalVolumeNames()389 public @NonNull Set<String> getExternalVolumeNames() { 390 synchronized (sCacheLock) { 391 return new ArraySet<>(sCachedExternalVolumeNames); 392 } 393 } 394 getVolumeScanPaths(String volumeName)395 public @NonNull Collection<File> getVolumeScanPaths(String volumeName) 396 throws FileNotFoundException { 397 synchronized (sCacheLock) { 398 if (sCachedVolumeScanPaths.containsKey(volumeName)) { 399 return new ArrayList<>(sCachedVolumeScanPaths.get(volumeName)); 400 } 401 402 // Nothing found above; let's ask directly and cache the answer 403 final Collection<File> res = FileUtils.getVolumeScanPaths(getContext(), volumeName); 404 sCachedVolumeScanPaths.put(volumeName, res); 405 return res; 406 } 407 } 408 409 private StorageManager mStorageManager; 410 private AppOpsManager mAppOpsManager; 411 private PackageManager mPackageManager; 412 413 private Size mThumbSize; 414 415 /** 416 * Map from UID to cached {@link LocalCallingIdentity}. Values are only 417 * maintained in this map while the UID is actively working with a 418 * performance-critical component, such as camera. 419 */ 420 @GuardedBy("mCachedCallingIdentity") 421 private final SparseArray<LocalCallingIdentity> mCachedCallingIdentity = new SparseArray<>(); 422 423 private final OnOpActiveChangedListener mActiveListener = (code, uid, packageName, active) -> { 424 synchronized (mCachedCallingIdentity) { 425 if (active) { 426 // TODO moltmann: Set correct featureId 427 mCachedCallingIdentity.put(uid, 428 LocalCallingIdentity.fromExternal(getContext(), uid, packageName, 429 null)); 430 } else { 431 mCachedCallingIdentity.remove(uid); 432 } 433 } 434 }; 435 436 /** 437 * Map from UID to cached {@link LocalCallingIdentity}. Values are only 438 * maintained in this map until there's any change in the appops needed or packages 439 * used in the {@link LocalCallingIdentity}. 440 */ 441 @GuardedBy("mCachedCallingIdentityForFuse") 442 private final SparseArray<LocalCallingIdentity> mCachedCallingIdentityForFuse = 443 new SparseArray<>(); 444 445 private OnOpChangedListener mModeListener = 446 (op, packageName) -> invalidateLocalCallingIdentityCache(packageName, "op " + op); 447 448 /** 449 * Retrieves a cached calling identity or creates a new one. Also, always sets the app-op 450 * description for the calling identity. 451 */ getCachedCallingIdentityForFuse(int uid)452 private LocalCallingIdentity getCachedCallingIdentityForFuse(int uid) { 453 synchronized (mCachedCallingIdentityForFuse) { 454 PermissionUtils.setOpDescription("via FUSE"); 455 LocalCallingIdentity ident = mCachedCallingIdentityForFuse.get(uid); 456 if (ident == null) { 457 ident = LocalCallingIdentity.fromExternal(getContext(), uid); 458 mCachedCallingIdentityForFuse.put(uid, ident); 459 } 460 return ident; 461 } 462 } 463 464 /** 465 * Calling identity state about on the current thread. Populated on demand, 466 * and invalidated by {@link #onCallingPackageChanged()} when each remote 467 * call is finished. 468 */ 469 private final ThreadLocal<LocalCallingIdentity> mCallingIdentity = ThreadLocal 470 .withInitial(() -> { 471 PermissionUtils.setOpDescription("via MediaProvider"); 472 synchronized (mCachedCallingIdentity) { 473 final LocalCallingIdentity cached = mCachedCallingIdentity 474 .get(Binder.getCallingUid()); 475 return (cached != null) ? cached 476 : LocalCallingIdentity.fromBinder(getContext(), this); 477 } 478 }); 479 480 /** 481 * We simply propagate the UID that is being tracked by 482 * {@link LocalCallingIdentity}, which means we accurately blame both 483 * incoming Binder calls and FUSE calls. 484 */ 485 private final ProxyTransactListener mTransactListener = new ProxyTransactListener() { 486 @Override 487 public Object onTransactStarted(IBinder binder, int transactionCode) { 488 if (LOGV) Trace.beginSection(Thread.currentThread().getStackTrace()[5].getMethodName()); 489 return Binder.setCallingWorkSourceUid(mCallingIdentity.get().uid); 490 } 491 492 @Override 493 public void onTransactEnded(Object session) { 494 final long token = (long) session; 495 Binder.restoreCallingWorkSource(token); 496 if (LOGV) Trace.endSection(); 497 } 498 }; 499 500 // In memory cache of path<->id mappings, to speed up inserts during media scan 501 @GuardedBy("mDirectoryCache") 502 private final ArrayMap<String, Long> mDirectoryCache = new ArrayMap<>(); 503 504 private static final String[] sDataOnlyColumn = new String[] { 505 FileColumns.DATA 506 }; 507 508 private static final String ID_NOT_PARENT_CLAUSE = 509 "_id NOT IN (SELECT parent FROM files WHERE parent IS NOT NULL)"; 510 511 private static final String CANONICAL = "canonical"; 512 513 private BroadcastReceiver mPackageReceiver = new BroadcastReceiver() { 514 @Override 515 public void onReceive(Context context, Intent intent) { 516 switch (intent.getAction()) { 517 case Intent.ACTION_PACKAGE_REMOVED: 518 case Intent.ACTION_PACKAGE_ADDED: 519 Uri uri = intent.getData(); 520 String pkg = uri != null ? uri.getSchemeSpecificPart() : null; 521 if (pkg != null) { 522 invalidateLocalCallingIdentityCache(pkg, "package " + intent.getAction()); 523 } else { 524 Log.w(TAG, "Failed to retrieve package from intent: " + intent.getAction()); 525 } 526 break; 527 } 528 } 529 }; 530 invalidateLocalCallingIdentityCache(String packageName, String reason)531 private void invalidateLocalCallingIdentityCache(String packageName, String reason) { 532 synchronized (mCachedCallingIdentityForFuse) { 533 try { 534 Log.i(TAG, "Invalidating LocalCallingIdentity cache for package " + packageName 535 + ". Reason: " + reason); 536 mCachedCallingIdentityForFuse.remove( 537 getContext().getPackageManager().getPackageUid(packageName, 0)); 538 } catch (NameNotFoundException ignored) { 539 } 540 } 541 } 542 updateQuotaTypeForUri(@onNull Uri uri, int mediaType)543 private final void updateQuotaTypeForUri(@NonNull Uri uri, int mediaType) { 544 Trace.beginSection("updateQuotaTypeForUri"); 545 File file; 546 try { 547 file = queryForDataFile(uri, null); 548 if (!file.exists()) { 549 // This can happen if an item is inserted in MediaStore before it is created 550 return; 551 } 552 553 if (mediaType == FileColumns.MEDIA_TYPE_NONE) { 554 // This might be because the file is hidden; but we still want to 555 // attribute its quota to the correct type, so get the type from 556 // the extension instead. 557 mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(file)); 558 } 559 560 updateQuotaTypeForFileInternal(file, mediaType); 561 } catch (FileNotFoundException e) { 562 // Ignore 563 return; 564 } finally { 565 Trace.endSection(); 566 } 567 } 568 updateQuotaTypeForFileInternal(File file, int mediaType)569 private final void updateQuotaTypeForFileInternal(File file, int mediaType) { 570 try { 571 switch (mediaType) { 572 case FileColumns.MEDIA_TYPE_AUDIO: 573 mStorageManager.updateExternalStorageFileQuotaType(file, 574 StorageManager.QUOTA_TYPE_MEDIA_AUDIO); 575 break; 576 case FileColumns.MEDIA_TYPE_VIDEO: 577 mStorageManager.updateExternalStorageFileQuotaType(file, 578 StorageManager.QUOTA_TYPE_MEDIA_VIDEO); 579 break; 580 case FileColumns.MEDIA_TYPE_IMAGE: 581 mStorageManager.updateExternalStorageFileQuotaType(file, 582 StorageManager.QUOTA_TYPE_MEDIA_IMAGE); 583 break; 584 default: 585 mStorageManager.updateExternalStorageFileQuotaType(file, 586 StorageManager.QUOTA_TYPE_MEDIA_NONE); 587 break; 588 } 589 } catch (IOException e) { 590 Log.w(TAG, "Failed to update quota type for " + file.getPath(), e); 591 } 592 } 593 594 /** 595 * Since these operations are in the critical path of apps working with 596 * media, we only collect the {@link Uri} that need to be notified, and all 597 * other side-effect operations are delegated to {@link BackgroundThread} so 598 * that we return as quickly as possible. 599 */ 600 private final OnFilesChangeListener mFilesListener = new OnFilesChangeListener() { 601 @Override 602 public void onInsert(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id, 603 int mediaType, boolean isDownload) { 604 handleInsertedRowForFuse(id); 605 acceptWithExpansion(helper::notifyInsert, volumeName, id, mediaType, isDownload); 606 607 helper.postBackground(() -> { 608 if (helper.isExternal()) { 609 // Update the quota type on the filesystem 610 Uri fileUri = MediaStore.Files.getContentUri(volumeName, id); 611 updateQuotaTypeForUri(fileUri, mediaType); 612 } 613 614 // Tell our SAF provider so it knows when views are no longer empty 615 MediaDocumentsProvider.onMediaStoreInsert(getContext(), volumeName, mediaType, id); 616 }); 617 } 618 619 @Override 620 public void onUpdate(@NonNull DatabaseHelper helper, @NonNull String volumeName, 621 long oldId, int oldMediaType, boolean oldIsDownload, 622 long newId, int newMediaType, boolean newIsDownload, 623 String oldOwnerPackage, String newOwnerPackage, String oldPath) { 624 final boolean isDownload = oldIsDownload || newIsDownload; 625 final Uri fileUri = MediaStore.Files.getContentUri(volumeName, oldId); 626 handleUpdatedRowForFuse(oldPath, oldOwnerPackage, oldId, newId); 627 handleOwnerPackageNameChange(oldPath, oldOwnerPackage, newOwnerPackage); 628 acceptWithExpansion(helper::notifyUpdate, volumeName, oldId, oldMediaType, isDownload); 629 630 helper.postBackground(() -> { 631 if (helper.isExternal()) { 632 // Update the quota type on the filesystem 633 updateQuotaTypeForUri(fileUri, newMediaType); 634 } 635 }); 636 637 if (newMediaType != oldMediaType) { 638 acceptWithExpansion(helper::notifyUpdate, volumeName, oldId, newMediaType, 639 isDownload); 640 641 helper.postBackground(() -> { 642 // Invalidate any thumbnails when the media type changes 643 invalidateThumbnails(fileUri); 644 }); 645 } 646 } 647 648 @Override 649 public void onDelete(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id, 650 int mediaType, boolean isDownload, String ownerPackageName, String path) { 651 handleDeletedRowForFuse(path, ownerPackageName, id); 652 acceptWithExpansion(helper::notifyDelete, volumeName, id, mediaType, isDownload); 653 654 helper.postBackground(() -> { 655 // Item no longer exists, so revoke all access to it 656 Trace.beginSection("revokeUriPermission"); 657 try { 658 acceptWithExpansion((uri) -> { 659 getContext().revokeUriPermission(uri, ~0); 660 }, volumeName, id, mediaType, isDownload); 661 } finally { 662 Trace.endSection(); 663 } 664 665 // Invalidate any thumbnails now that media is gone 666 invalidateThumbnails(MediaStore.Files.getContentUri(volumeName, id)); 667 668 // Tell our SAF provider so it can revoke too 669 MediaDocumentsProvider.onMediaStoreDelete(getContext(), volumeName, mediaType, id); 670 }); 671 } 672 }; 673 674 private final UnaryOperator<String> mIdGenerator = path -> { 675 final long rowId = mCallingIdentity.get().getDeletedRowId(path); 676 if (rowId != -1 && isFuseThread()) { 677 return String.valueOf(rowId); 678 } 679 return null; 680 }; 681 682 private final OnLegacyMigrationListener mMigrationListener = new OnLegacyMigrationListener() { 683 @Override 684 public void onStarted(ContentProviderClient client, String volumeName) { 685 MediaStore.startLegacyMigration(ContentResolver.wrap(client), volumeName); 686 } 687 688 @Override 689 public void onProgress(ContentProviderClient client, String volumeName, 690 long progress, long total) { 691 // TODO: notify blocked threads of progress once we can change APIs 692 } 693 694 @Override 695 public void onFinished(ContentProviderClient client, String volumeName) { 696 MediaStore.finishLegacyMigration(ContentResolver.wrap(client), volumeName); 697 } 698 }; 699 700 /** 701 * Apply {@link Consumer#accept} to the given item. 702 * <p> 703 * Since media items can be exposed through multiple collections or views, 704 * this method expands the single item being accepted to also accept all 705 * relevant views. 706 */ acceptWithExpansion(@onNull Consumer<Uri> consumer, @NonNull String volumeName, long id, int mediaType, boolean isDownload)707 private void acceptWithExpansion(@NonNull Consumer<Uri> consumer, @NonNull String volumeName, 708 long id, int mediaType, boolean isDownload) { 709 switch (mediaType) { 710 case FileColumns.MEDIA_TYPE_AUDIO: 711 consumer.accept(MediaStore.Audio.Media.getContentUri(volumeName, id)); 712 713 // Any changing audio items mean we probably need to invalidate all 714 // indexed views built from that media 715 consumer.accept(Audio.Genres.getContentUri(volumeName)); 716 consumer.accept(Audio.Playlists.getContentUri(volumeName)); 717 consumer.accept(Audio.Artists.getContentUri(volumeName)); 718 consumer.accept(Audio.Albums.getContentUri(volumeName)); 719 break; 720 721 case FileColumns.MEDIA_TYPE_VIDEO: 722 consumer.accept(MediaStore.Video.Media.getContentUri(volumeName, id)); 723 break; 724 725 case FileColumns.MEDIA_TYPE_IMAGE: 726 consumer.accept(MediaStore.Images.Media.getContentUri(volumeName, id)); 727 break; 728 } 729 730 // Also notify through any generic views 731 consumer.accept(MediaStore.Files.getContentUri(volumeName, id)); 732 if (isDownload) { 733 consumer.accept(MediaStore.Downloads.getContentUri(volumeName, id)); 734 } 735 736 // Rinse and repeat through any synthetic views 737 switch (volumeName) { 738 case MediaStore.VOLUME_INTERNAL: 739 case MediaStore.VOLUME_EXTERNAL: 740 // Already a top-level view, no need to expand 741 break; 742 default: 743 acceptWithExpansion(consumer, MediaStore.VOLUME_EXTERNAL, 744 id, mediaType, isDownload); 745 break; 746 } 747 } 748 749 private static final String[] sDefaultFolderNames = { 750 Environment.DIRECTORY_MUSIC, 751 Environment.DIRECTORY_PODCASTS, 752 Environment.DIRECTORY_RINGTONES, 753 Environment.DIRECTORY_ALARMS, 754 Environment.DIRECTORY_NOTIFICATIONS, 755 Environment.DIRECTORY_PICTURES, 756 Environment.DIRECTORY_MOVIES, 757 Environment.DIRECTORY_DOWNLOADS, 758 Environment.DIRECTORY_DCIM, 759 Environment.DIRECTORY_AUDIOBOOKS, 760 Environment.DIRECTORY_DOCUMENTS, 761 }; 762 isDefaultDirectoryName(@ullable String dirName)763 private static boolean isDefaultDirectoryName(@Nullable String dirName) { 764 for (String defaultDirName : sDefaultFolderNames) { 765 if (defaultDirName.equals(dirName)) { 766 return true; 767 } 768 } 769 return false; 770 } 771 772 /** 773 * Ensure that default folders are created on mounted primary storage 774 * devices. We only do this once per volume so we don't annoy the user if 775 * deleted manually. 776 */ ensureDefaultFolders(@onNull String volumeName, @NonNull SQLiteDatabase db)777 private void ensureDefaultFolders(@NonNull String volumeName, @NonNull SQLiteDatabase db) { 778 try { 779 final File path = getVolumePath(volumeName); 780 final StorageVolume vol = mStorageManager.getStorageVolume(path); 781 final String key; 782 if (vol == null) { 783 Log.w(TAG, "Failed to ensure default folders for " + volumeName); 784 return; 785 } 786 787 if (vol.isPrimary()) { 788 key = "created_default_folders"; 789 } else { 790 key = "created_default_folders_" + vol.getMediaStoreVolumeName(); 791 } 792 793 final SharedPreferences prefs = PreferenceManager 794 .getDefaultSharedPreferences(getContext()); 795 if (prefs.getInt(key, 0) == 0) { 796 for (String folderName : sDefaultFolderNames) { 797 final File folder = new File(vol.getDirectory(), folderName); 798 if (!folder.exists()) { 799 folder.mkdirs(); 800 insertDirectory(db, folder.getAbsolutePath()); 801 } 802 } 803 804 SharedPreferences.Editor editor = prefs.edit(); 805 editor.putInt(key, 1); 806 editor.commit(); 807 } 808 } catch (IOException e) { 809 Log.w(TAG, "Failed to ensure default folders for " + volumeName, e); 810 } 811 } 812 813 /** 814 * Ensure that any thumbnail collections on the given storage volume can be 815 * used with the given {@link DatabaseHelper}. If the 816 * {@link DatabaseHelper#getOrCreateUuid} doesn't match the UUID found on 817 * disk, then all thumbnails will be considered stable and will be deleted. 818 */ ensureThumbnailsValid(@onNull String volumeName, @NonNull SQLiteDatabase db)819 private void ensureThumbnailsValid(@NonNull String volumeName, @NonNull SQLiteDatabase db) { 820 final String uuidFromDatabase = DatabaseHelper.getOrCreateUuid(db); 821 try { 822 for (File dir : getThumbnailDirectories(volumeName)) { 823 if (!dir.exists()) { 824 dir.mkdirs(); 825 } 826 827 final File file = new File(dir, FILE_DATABASE_UUID); 828 final Optional<String> uuidFromDisk = FileUtils.readString(file); 829 830 final boolean updateUuid; 831 if (!uuidFromDisk.isPresent()) { 832 // For newly inserted volumes or upgrading of existing volumes, 833 // assume that our current UUID is valid 834 updateUuid = true; 835 } else if (!Objects.equals(uuidFromDatabase, uuidFromDisk.get())) { 836 // The UUID of database disagrees with the one on disk, 837 // which means we can't trust any thumbnails 838 Log.d(TAG, "Invalidating all thumbnails under " + dir); 839 FileUtils.walkFileTreeContents(dir.toPath(), this::deleteAndInvalidate); 840 updateUuid = true; 841 } else { 842 updateUuid = false; 843 } 844 845 if (updateUuid) { 846 FileUtils.writeString(file, Optional.of(uuidFromDatabase)); 847 } 848 } 849 } catch (IOException e) { 850 Log.w(TAG, "Failed to ensure thumbnails valid for " + volumeName, e); 851 } 852 } 853 854 @Override attachInfo(Context context, ProviderInfo info)855 public void attachInfo(Context context, ProviderInfo info) { 856 Log.v(TAG, "Attached " + info.authority + " from " + info.applicationInfo.packageName); 857 858 mUriMatcher = new LocalUriMatcher(info.authority); 859 860 super.attachInfo(context, info); 861 } 862 863 @Override onCreate()864 public boolean onCreate() { 865 final Context context = getContext(); 866 867 // Shift call statistics back to the original caller 868 Binder.setProxyTransactListener(mTransactListener); 869 870 mStorageManager = context.getSystemService(StorageManager.class); 871 mAppOpsManager = context.getSystemService(AppOpsManager.class); 872 mPackageManager = context.getPackageManager(); 873 874 // Reasonable thumbnail size is half of the smallest screen edge width 875 final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); 876 final int thumbSize = Math.min(metrics.widthPixels, metrics.heightPixels) / 2; 877 mThumbSize = new Size(thumbSize, thumbSize); 878 879 mMediaScanner = new ModernMediaScanner(context); 880 881 mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, 882 true, false, false, Column.class, 883 Metrics::logSchemaChange, mFilesListener, mMigrationListener, mIdGenerator); 884 mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME, 885 false, false, false, Column.class, 886 Metrics::logSchemaChange, mFilesListener, mMigrationListener, mIdGenerator); 887 888 final IntentFilter packageFilter = new IntentFilter(); 889 packageFilter.setPriority(10); 890 packageFilter.addDataScheme("package"); 891 packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 892 packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); 893 context.registerReceiver(mPackageReceiver, packageFilter); 894 895 // Watch for invalidation of cached volumes 896 mStorageManager.registerStorageVolumeCallback(context.getMainExecutor(), 897 new StorageVolumeCallback() { 898 @Override 899 public void onStateChanged(@NonNull StorageVolume volume) { 900 updateVolumes(); 901 } 902 }); 903 904 updateVolumes(); 905 attachVolume(MediaStore.VOLUME_INTERNAL, /* validate */ false); 906 for (String volumeName : getExternalVolumeNames()) { 907 attachVolume(volumeName, /* validate */ false); 908 } 909 910 // Watch for performance-sensitive activity 911 mAppOpsManager.startWatchingActive(new String[] { 912 AppOpsManager.OPSTR_CAMERA 913 }, context.getMainExecutor(), mActiveListener); 914 915 mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE, 916 null /* all packages */, mModeListener); 917 mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_EXTERNAL_STORAGE, 918 null /* all packages */, mModeListener); 919 mAppOpsManager.startWatchingMode(permissionToOp(ACCESS_MEDIA_LOCATION), 920 null /* all packages */, mModeListener); 921 // Legacy apps 922 mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_LEGACY_STORAGE, 923 null /* all packages */, mModeListener); 924 // File managers 925 mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_MANAGE_EXTERNAL_STORAGE, 926 null /* all packages */, mModeListener); 927 // Default gallery changes 928 mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES, 929 null /* all packages */, mModeListener); 930 mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO, 931 null /* all packages */, mModeListener); 932 try { 933 // Here we are forced to depend on the non-public API of AppOpsManager. If 934 // OPSTR_NO_ISOLATED_STORAGE app op is not defined in AppOpsManager, then this call will 935 // throw an IllegalArgumentException during MediaProvider startup. In combination with 936 // MediaProvider's CTS tests it should give us guarantees that OPSTR_NO_ISOLATED_STORAGE 937 // is defined. 938 mAppOpsManager.startWatchingMode(PermissionUtils.OPSTR_NO_ISOLATED_STORAGE, 939 null /* all packages */, mModeListener); 940 } catch (IllegalArgumentException e) { 941 Log.w(TAG, "Failed to start watching " + PermissionUtils.OPSTR_NO_ISOLATED_STORAGE, e); 942 } 943 return true; 944 } 945 946 @Override onCallingPackageChanged()947 public void onCallingPackageChanged() { 948 // Identity of the current thread has changed, so invalidate caches 949 mCallingIdentity.remove(); 950 } 951 clearLocalCallingIdentity()952 public LocalCallingIdentity clearLocalCallingIdentity() { 953 return clearLocalCallingIdentity(LocalCallingIdentity.fromSelf(getContext())); 954 } 955 clearLocalCallingIdentity(LocalCallingIdentity replacement)956 public LocalCallingIdentity clearLocalCallingIdentity(LocalCallingIdentity replacement) { 957 final LocalCallingIdentity token = mCallingIdentity.get(); 958 mCallingIdentity.set(replacement); 959 return token; 960 } 961 restoreLocalCallingIdentity(LocalCallingIdentity token)962 public void restoreLocalCallingIdentity(LocalCallingIdentity token) { 963 mCallingIdentity.set(token); 964 } 965 isPackageKnown(@onNull String packageName)966 private boolean isPackageKnown(@NonNull String packageName) { 967 final PackageManager pm = getContext().getPackageManager(); 968 969 // First, is the app actually installed? 970 try { 971 pm.getPackageInfo(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES); 972 return true; 973 } catch (NameNotFoundException ignored) { 974 } 975 976 // Second, is the app pending, probably from a backup/restore operation? 977 for (SessionInfo si : pm.getPackageInstaller().getAllSessions()) { 978 if (Objects.equals(packageName, si.getAppPackageName())) { 979 return true; 980 } 981 } 982 983 // I've never met this package in my life 984 return false; 985 } 986 onIdleMaintenance(@onNull CancellationSignal signal)987 public void onIdleMaintenance(@NonNull CancellationSignal signal) { 988 final long startTime = SystemClock.elapsedRealtime(); 989 990 // Trim any stale log files before we emit new events below 991 Logging.trimPersistent(); 992 993 // Scan all volumes to resolve any staleness 994 for (String volumeName : getExternalVolumeNames()) { 995 // Possibly bail before digging into each volume 996 signal.throwIfCanceled(); 997 998 try { 999 MediaService.onScanVolume(getContext(), volumeName, REASON_IDLE); 1000 } catch (IOException e) { 1001 Log.w(TAG, e); 1002 } 1003 1004 // Ensure that our thumbnails are valid 1005 mExternalDatabase.runWithTransaction((db) -> { 1006 ensureThumbnailsValid(volumeName, db); 1007 return null; 1008 }); 1009 } 1010 1011 // Delete any stale thumbnails 1012 final int staleThumbnails = mExternalDatabase.runWithTransaction((db) -> { 1013 return pruneThumbnails(db, signal); 1014 }); 1015 Log.d(TAG, "Pruned " + staleThumbnails + " unknown thumbnails"); 1016 1017 // Finished orphaning any content whose package no longer exists 1018 final int stalePackages = mExternalDatabase.runWithTransaction((db) -> { 1019 final ArraySet<String> unknownPackages = new ArraySet<>(); 1020 try (Cursor c = db.query(true, "files", new String[] { "owner_package_name" }, 1021 null, null, null, null, null, null, signal)) { 1022 while (c.moveToNext()) { 1023 final String packageName = c.getString(0); 1024 if (TextUtils.isEmpty(packageName)) continue; 1025 1026 if (!isPackageKnown(packageName)) { 1027 unknownPackages.add(packageName); 1028 } 1029 } 1030 } 1031 for (String packageName : unknownPackages) { 1032 onPackageOrphaned(db, packageName); 1033 } 1034 return unknownPackages.size(); 1035 }); 1036 Log.d(TAG, "Pruned " + stalePackages + " unknown packages"); 1037 1038 // Delete any expired content; we're paranoid about wildly changing 1039 // clocks, so only delete items within the last week 1040 final long from = ((System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS) / 1000); 1041 final long to = (System.currentTimeMillis() / 1000); 1042 final int expiredMedia = mExternalDatabase.runWithTransaction((db) -> { 1043 try (Cursor c = db.query(true, "files", new String[] { "volume_name", "_id" }, 1044 FileColumns.DATE_EXPIRES + " BETWEEN " + from + " AND " + to, null, 1045 null, null, null, null, signal)) { 1046 while (c.moveToNext()) { 1047 final String volumeName = c.getString(0); 1048 final long id = c.getLong(1); 1049 delete(Files.getContentUri(volumeName, id), null, null); 1050 } 1051 return c.getCount(); 1052 } 1053 }); 1054 Log.d(TAG, "Deleted " + expiredMedia + " expired items"); 1055 1056 // Forget any stale volumes 1057 mExternalDatabase.runWithTransaction((db) -> { 1058 final Set<String> recentVolumeNames = MediaStore 1059 .getRecentExternalVolumeNames(getContext()); 1060 final Set<String> knownVolumeNames = new ArraySet<>(); 1061 try (Cursor c = db.query(true, "files", new String[] { MediaColumns.VOLUME_NAME }, 1062 null, null, null, null, null, null, signal)) { 1063 while (c.moveToNext()) { 1064 knownVolumeNames.add(c.getString(0)); 1065 } 1066 } 1067 final Set<String> staleVolumeNames = new ArraySet<>(); 1068 staleVolumeNames.addAll(knownVolumeNames); 1069 staleVolumeNames.removeAll(recentVolumeNames); 1070 for (String staleVolumeName : staleVolumeNames) { 1071 final int num = db.delete("files", FileColumns.VOLUME_NAME + "=?", 1072 new String[] { staleVolumeName }); 1073 Log.d(TAG, "Forgot " + num + " stale items from " + staleVolumeName); 1074 } 1075 return null; 1076 }); 1077 1078 synchronized (mDirectoryCache) { 1079 mDirectoryCache.clear(); 1080 } 1081 1082 final long itemCount = mExternalDatabase.runWithTransaction((db) -> { 1083 return DatabaseHelper.getItemCount(db); 1084 }); 1085 1086 final long durationMillis = (SystemClock.elapsedRealtime() - startTime); 1087 Metrics.logIdleMaintenance(MediaStore.VOLUME_EXTERNAL, itemCount, 1088 durationMillis, staleThumbnails, expiredMedia); 1089 } 1090 onPackageOrphaned(String packageName)1091 public void onPackageOrphaned(String packageName) { 1092 mExternalDatabase.runWithTransaction((db) -> { 1093 onPackageOrphaned(db, packageName); 1094 return null; 1095 }); 1096 } 1097 onPackageOrphaned(@onNull SQLiteDatabase db, @NonNull String packageName)1098 public void onPackageOrphaned(@NonNull SQLiteDatabase db, @NonNull String packageName) { 1099 final ContentValues values = new ContentValues(); 1100 values.putNull(FileColumns.OWNER_PACKAGE_NAME); 1101 1102 final int count = db.update("files", values, 1103 "owner_package_name=?", new String[] { packageName }); 1104 if (count > 0) { 1105 Log.d(TAG, "Orphaned " + count + " items belonging to " 1106 + packageName + " on " + db.getPath()); 1107 } 1108 } 1109 scanDirectory(File file, int reason)1110 public void scanDirectory(File file, int reason) { 1111 mMediaScanner.scanDirectory(file, reason); 1112 } 1113 scanFile(File file, int reason)1114 public Uri scanFile(File file, int reason) { 1115 return mMediaScanner.scanFile(file, reason); 1116 } 1117 scanFile(File file, int reason, String ownerPackage)1118 public Uri scanFile(File file, int reason, String ownerPackage) { 1119 return mMediaScanner.scanFile(file, reason, ownerPackage); 1120 } 1121 1122 /** 1123 * Makes MediaScanner scan the given file. 1124 * @param file path of the file to be scanned 1125 * 1126 * Called from JNI in jni/MediaProviderWrapper.cpp 1127 */ 1128 @Keep scanFileForFuse(String file)1129 public void scanFileForFuse(String file) { 1130 scanFile(new File(file), REASON_DEMAND); 1131 } 1132 1133 /** 1134 * Called when a new file is created through FUSE 1135 * 1136 * @param file path of the file that was created 1137 * 1138 * Called from JNI in jni/MediaProviderWrapper.cpp 1139 */ 1140 @Keep onFileCreatedForFuse(String path)1141 public void onFileCreatedForFuse(String path) { 1142 // Make sure we update the quota type of the file 1143 BackgroundThread.getExecutor().execute(() -> { 1144 File file = new File(path); 1145 int mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(file)); 1146 updateQuotaTypeForFileInternal(file, mediaType); 1147 }); 1148 } 1149 1150 /** 1151 * Returns true if the app denoted by the given {@code uid} and {@code packageName} is allowed 1152 * to clear other apps' cache directories. 1153 */ hasPermissionToClearCaches(Context context, ApplicationInfo ai)1154 static boolean hasPermissionToClearCaches(Context context, ApplicationInfo ai) { 1155 PermissionUtils.setOpDescription("clear app cache"); 1156 try { 1157 return PermissionUtils.checkPermissionManager(context, /* pid */ -1, ai.uid, 1158 ai.packageName, /* attributionTag */ null); 1159 } finally { 1160 PermissionUtils.clearOpDescription(); 1161 } 1162 } 1163 1164 @VisibleForTesting computeAudioLocalizedValues(ContentValues values)1165 void computeAudioLocalizedValues(ContentValues values) { 1166 try { 1167 final String title = values.getAsString(AudioColumns.TITLE); 1168 final String titleRes = values.getAsString(AudioColumns.TITLE_RESOURCE_URI); 1169 1170 if (!TextUtils.isEmpty(titleRes)) { 1171 final String localized = getLocalizedTitle(titleRes); 1172 if (!TextUtils.isEmpty(localized)) { 1173 values.put(AudioColumns.TITLE, localized); 1174 } 1175 } else { 1176 final String localized = getLocalizedTitle(title); 1177 if (!TextUtils.isEmpty(localized)) { 1178 values.put(AudioColumns.TITLE, localized); 1179 values.put(AudioColumns.TITLE_RESOURCE_URI, title); 1180 } 1181 } 1182 } catch (Exception e) { 1183 Log.w(TAG, "Failed to localize title", e); 1184 } 1185 } 1186 1187 @VisibleForTesting computeAudioKeyValues(ContentValues values)1188 static void computeAudioKeyValues(ContentValues values) { 1189 computeAudioKeyValue(values, 1190 AudioColumns.TITLE, AudioColumns.TITLE_KEY, null); 1191 computeAudioKeyValue(values, 1192 AudioColumns.ALBUM, AudioColumns.ALBUM_KEY, AudioColumns.ALBUM_ID); 1193 computeAudioKeyValue(values, 1194 AudioColumns.ARTIST, AudioColumns.ARTIST_KEY, AudioColumns.ARTIST_ID); 1195 computeAudioKeyValue(values, 1196 AudioColumns.GENRE, AudioColumns.GENRE_KEY, AudioColumns.GENRE_ID); 1197 } 1198 computeAudioKeyValue(@onNull ContentValues values, @NonNull String focus, @Nullable String focusKey, @Nullable String focusId)1199 private static void computeAudioKeyValue(@NonNull ContentValues values, @NonNull String focus, 1200 @Nullable String focusKey, @Nullable String focusId) { 1201 if (focusKey != null) values.remove(focusKey); 1202 if (focusId != null) values.remove(focusId); 1203 1204 final String value = values.getAsString(focus); 1205 if (TextUtils.isEmpty(value)) return; 1206 1207 final String key = Audio.keyFor(value); 1208 if (key == null) return; 1209 1210 if (focusKey != null) { 1211 values.put(focusKey, key); 1212 } 1213 if (focusId != null) { 1214 // Many apps break if we generate negative IDs, so trim off the 1215 // highest bit to ensure we're always unsigned 1216 final long id = Hashing.farmHashFingerprint64() 1217 .hashString(key, StandardCharsets.UTF_8).asLong() & ~(1L << 63); 1218 values.put(focusId, id); 1219 } 1220 } 1221 1222 @Override canonicalize(Uri uri)1223 public Uri canonicalize(Uri uri) { 1224 final boolean allowHidden = isCallingPackageAllowedHidden(); 1225 final int match = matchUri(uri, allowHidden); 1226 1227 // Skip when we have nothing to canonicalize 1228 if ("1".equals(uri.getQueryParameter(CANONICAL))) { 1229 return uri; 1230 } 1231 1232 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) { 1233 switch (match) { 1234 case AUDIO_MEDIA_ID: { 1235 final String title = getDefaultTitleFromCursor(c); 1236 if (!TextUtils.isEmpty(title)) { 1237 final Uri.Builder builder = uri.buildUpon(); 1238 builder.appendQueryParameter(AudioColumns.TITLE, title); 1239 builder.appendQueryParameter(CANONICAL, "1"); 1240 return builder.build(); 1241 } 1242 break; 1243 } 1244 case VIDEO_MEDIA_ID: 1245 case IMAGES_MEDIA_ID: { 1246 final String documentId = c 1247 .getString(c.getColumnIndexOrThrow(MediaColumns.DOCUMENT_ID)); 1248 if (!TextUtils.isEmpty(documentId)) { 1249 final Uri.Builder builder = uri.buildUpon(); 1250 builder.appendQueryParameter(MediaColumns.DOCUMENT_ID, documentId); 1251 builder.appendQueryParameter(CANONICAL, "1"); 1252 return builder.build(); 1253 } 1254 break; 1255 } 1256 } 1257 } catch (FileNotFoundException e) { 1258 Log.w(TAG, e.getMessage()); 1259 } 1260 return null; 1261 } 1262 1263 @Override uncanonicalize(Uri uri)1264 public Uri uncanonicalize(Uri uri) { 1265 final boolean allowHidden = isCallingPackageAllowedHidden(); 1266 final int match = matchUri(uri, allowHidden); 1267 1268 // Skip when we have nothing to uncanonicalize 1269 if (!"1".equals(uri.getQueryParameter(CANONICAL))) { 1270 return uri; 1271 } 1272 1273 // Extract values and then clear to avoid recursive lookups 1274 final String title = uri.getQueryParameter(AudioColumns.TITLE); 1275 final String documentId = uri.getQueryParameter(MediaColumns.DOCUMENT_ID); 1276 uri = uri.buildUpon().clearQuery().build(); 1277 1278 switch (match) { 1279 case AUDIO_MEDIA_ID: { 1280 // First check for an exact match 1281 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) { 1282 if (Objects.equals(title, getDefaultTitleFromCursor(c))) { 1283 return uri; 1284 } 1285 } catch (FileNotFoundException e) { 1286 Log.w(TAG, "Trouble resolving " + uri + "; falling back to search: " + e); 1287 } 1288 1289 // Otherwise fallback to searching 1290 final Uri baseUri = ContentUris.removeId(uri); 1291 try (Cursor c = queryForSingleItem(baseUri, 1292 new String[] { BaseColumns._ID }, 1293 AudioColumns.TITLE + "=?", new String[] { title }, null)) { 1294 return ContentUris.withAppendedId(baseUri, c.getLong(0)); 1295 } catch (FileNotFoundException e) { 1296 Log.w(TAG, "Failed to resolve " + uri + ": " + e); 1297 return null; 1298 } 1299 } 1300 case VIDEO_MEDIA_ID: 1301 case IMAGES_MEDIA_ID: { 1302 // First check for an exact match 1303 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) { 1304 if (Objects.equals(title, getDefaultTitleFromCursor(c))) { 1305 return uri; 1306 } 1307 } catch (FileNotFoundException e) { 1308 Log.w(TAG, "Trouble resolving " + uri + "; falling back to search: " + e); 1309 } 1310 1311 // Otherwise fallback to searching 1312 final Uri baseUri = ContentUris.removeId(uri); 1313 try (Cursor c = queryForSingleItem(baseUri, 1314 new String[] { BaseColumns._ID }, 1315 MediaColumns.DOCUMENT_ID + "=?", new String[] { documentId }, null)) { 1316 return ContentUris.withAppendedId(baseUri, c.getLong(0)); 1317 } catch (FileNotFoundException e) { 1318 Log.w(TAG, "Failed to resolve " + uri + ": " + e); 1319 return null; 1320 } 1321 } 1322 } 1323 1324 return uri; 1325 } 1326 safeUncanonicalize(Uri uri)1327 private Uri safeUncanonicalize(Uri uri) { 1328 Uri newUri = uncanonicalize(uri); 1329 if (newUri != null) { 1330 return newUri; 1331 } 1332 return uri; 1333 } 1334 1335 /** 1336 * @return where clause to exclude database rows where 1337 * <ul> 1338 * <li> {@code column} is set or 1339 * <li> {@code column} is {@link MediaColumns#IS_PENDING} and is set by FUSE and not owned by 1340 * calling package. 1341 * </ul> 1342 */ getWhereClauseForMatchExclude(@onNull String column)1343 private String getWhereClauseForMatchExclude(@NonNull String column) { 1344 if (column.equalsIgnoreCase(MediaColumns.IS_PENDING)) { 1345 final String callingPackage = getCallingPackageOrSelf(); 1346 final String matchSharedPackagesClause = FileColumns.OWNER_PACKAGE_NAME + " IN " 1347 + getSharedPackages(); 1348 // Include owned pending files from Fuse 1349 return String.format("%s=0 OR (%s=1 AND %s AND %s)", column, column, 1350 MATCH_PENDING_FROM_FUSE, matchSharedPackagesClause); 1351 } 1352 return column + "=0"; 1353 } 1354 1355 /** 1356 * @return where clause to include database rows where 1357 * <ul> 1358 * <li> {@code column} is not set or 1359 * <li> {@code column} is set and calling package has write permission to corresponding db row 1360 * or {@code column} is {@link MediaColumns#IS_PENDING} and is set by FUSE. 1361 * </ul> 1362 * The method is used to match db rows corresponding to writable pending and trashed files. 1363 */ 1364 @Nullable getWhereClauseForMatchableVisibleFromFilePath(@onNull Uri uri, @NonNull String column)1365 private String getWhereClauseForMatchableVisibleFromFilePath(@NonNull Uri uri, 1366 @NonNull String column) { 1367 if (isCallingPackageLegacyWrite() || checkCallingPermissionGlobal(uri, /*forWrite*/ true)) { 1368 // No special filtering needed 1369 return null; 1370 } 1371 1372 final String callingPackage = getCallingPackageOrSelf(); 1373 1374 final ArrayList<String> options = new ArrayList<>(); 1375 switch(matchUri(uri, isCallingPackageAllowedHidden())) { 1376 case IMAGES_MEDIA_ID: 1377 case IMAGES_MEDIA: 1378 case IMAGES_THUMBNAILS_ID: 1379 case IMAGES_THUMBNAILS: 1380 if (checkCallingPermissionImages(/*forWrite*/ true, callingPackage)) { 1381 // No special filtering needed 1382 return null; 1383 } 1384 break; 1385 case AUDIO_MEDIA_ID: 1386 case AUDIO_MEDIA: 1387 case AUDIO_PLAYLISTS_ID: 1388 case AUDIO_PLAYLISTS: 1389 if (checkCallingPermissionAudio(/*forWrite*/ true, callingPackage)) { 1390 // No special filtering needed 1391 return null; 1392 } 1393 break; 1394 case VIDEO_MEDIA_ID: 1395 case VIDEO_MEDIA: 1396 case VIDEO_THUMBNAILS_ID: 1397 case VIDEO_THUMBNAILS: 1398 if (checkCallingPermissionVideo(/*firWrite*/ true, callingPackage)) { 1399 // No special filtering needed 1400 return null; 1401 } 1402 break; 1403 case DOWNLOADS_ID: 1404 case DOWNLOADS: 1405 // No app has special permissions for downloads. 1406 break; 1407 case FILES_ID: 1408 case FILES: 1409 if (checkCallingPermissionAudio(/*forWrite*/ true, callingPackage)) { 1410 // Allow apps with audio permission to include audio* media types. 1411 options.add(DatabaseUtils.bindSelection("media_type=?", 1412 FileColumns.MEDIA_TYPE_AUDIO)); 1413 options.add(DatabaseUtils.bindSelection("media_type=?", 1414 FileColumns.MEDIA_TYPE_PLAYLIST)); 1415 options.add(DatabaseUtils.bindSelection("media_type=?", 1416 FileColumns.MEDIA_TYPE_SUBTITLE)); 1417 } 1418 if (checkCallingPermissionVideo(/*forWrite*/ true, callingPackage)) { 1419 // Allow apps with video permission to include video* media types. 1420 options.add(DatabaseUtils.bindSelection("media_type=?", 1421 FileColumns.MEDIA_TYPE_VIDEO)); 1422 options.add(DatabaseUtils.bindSelection("media_type=?", 1423 FileColumns.MEDIA_TYPE_SUBTITLE)); 1424 } 1425 if (checkCallingPermissionImages(/*forWrite*/ true, callingPackage)) { 1426 // Allow apps with images permission to include images* media types. 1427 options.add(DatabaseUtils.bindSelection("media_type=?", 1428 FileColumns.MEDIA_TYPE_IMAGE)); 1429 } 1430 break; 1431 default: 1432 // is_pending, is_trashed are not applicable for rest of the media tables. 1433 return null; 1434 } 1435 1436 final String matchSharedPackagesClause = FileColumns.OWNER_PACKAGE_NAME + " IN " 1437 + getSharedPackages(); 1438 options.add(DatabaseUtils.bindSelection(matchSharedPackagesClause)); 1439 1440 if (column.equalsIgnoreCase(MediaColumns.IS_PENDING)) { 1441 // Include all pending files from Fuse 1442 options.add(MATCH_PENDING_FROM_FUSE); 1443 } 1444 1445 final String matchWritableRowsClause = String.format("%s=0 OR (%s=1 AND %s)", column, 1446 column, TextUtils.join(" OR ", options)); 1447 return matchWritableRowsClause; 1448 } 1449 1450 /** 1451 * Gets list of files in {@code path} from media provider database. 1452 * 1453 * @param path path of the directory. 1454 * @param uid UID of the calling process. 1455 * @return a list of file names in the given directory path. 1456 * An empty list is returned if no files are visible to the calling app or the given directory 1457 * does not have any files. 1458 * A list with ["/"] is returned if the path is not indexed by MediaProvider database or 1459 * calling package is a legacy app and has appropriate storage permissions for the given path. 1460 * In both scenarios file names should be obtained from lower file system. 1461 * A list with empty string[""] is returned if the calling package doesn't have access to the 1462 * given path. 1463 * 1464 * <p>Directory names are always obtained from lower file system. 1465 * 1466 * Called from JNI in jni/MediaProviderWrapper.cpp 1467 */ 1468 @Keep getFilesInDirectoryForFuse(String path, int uid)1469 public String[] getFilesInDirectoryForFuse(String path, int uid) { 1470 final LocalCallingIdentity token = 1471 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 1472 1473 try { 1474 if (isPrivatePackagePathNotOwnedByCaller(path)) { 1475 return new String[] {""}; 1476 } 1477 1478 // Do not allow apps to list Android/data or Android/obb dirs. Installer and 1479 // MOUNT_EXTERNAL_ANDROID_WRITABLE apps won't be blocked by this, as their OBB dirs 1480 // are mounted to lowerfs directly. 1481 if (isDataOrObbPath(path)) { 1482 return new String[] {""}; 1483 } 1484 1485 if (shouldBypassFuseRestrictions(/*forWrite*/ false, path)) { 1486 return new String[] {"/"}; 1487 } 1488 // Legacy apps that made is this far don't have the right storage permission and hence 1489 // are not allowed to access anything other than their external app directory 1490 if (isCallingPackageRequestingLegacy()) { 1491 return new String[] {""}; 1492 } 1493 1494 // Get relative path for the contents of given directory. 1495 String relativePath = extractRelativePathForDirectory(path); 1496 1497 if (relativePath == null) { 1498 // Path is /storage/emulated/, if relativePath is null, MediaProvider doesn't 1499 // have any details about the given directory. Use lower file system to obtain 1500 // files and directories in the given directory. 1501 return new String[] {"/"}; 1502 } 1503 1504 // For all other paths, get file names from media provider database. 1505 // Return media and non-media files visible to the calling package. 1506 ArrayList<String> fileNamesList = new ArrayList<>(); 1507 1508 // Only FileColumns.DATA contains actual name of the file. 1509 String[] projection = {MediaColumns.DATA}; 1510 1511 Bundle queryArgs = new Bundle(); 1512 queryArgs.putString(QUERY_ARG_SQL_SELECTION, MediaColumns.RELATIVE_PATH + 1513 " =? and mime_type not like 'null'"); 1514 queryArgs.putStringArray(QUERY_ARG_SQL_SELECTION_ARGS, new String[] {relativePath}); 1515 // Get database entries for files from MediaProvider database with 1516 // MediaColumns.RELATIVE_PATH as the given path. 1517 try (final Cursor cursor = query(FileUtils.getContentUriForPath(path), projection, 1518 queryArgs, null)) { 1519 while(cursor.moveToNext()) { 1520 fileNamesList.add(extractDisplayName(cursor.getString(0))); 1521 } 1522 } 1523 return fileNamesList.toArray(new String[fileNamesList.size()]); 1524 } finally { 1525 restoreLocalCallingIdentity(token); 1526 } 1527 } 1528 1529 /** 1530 * Scan files during directory renames for the following reasons: 1531 * <ul> 1532 * <li>Because we don't update db rows for directories, we scan the oldPath to discard stale 1533 * directory db rows. This prevents conflicts during subsequent db operations with oldPath. 1534 * <li>We need to scan newPath as well, because the new directory may have become hidden 1535 * or unhidden, in which case we need to update the media types of the contained files 1536 * </ul> 1537 */ scanRenamedDirectoryForFuse(@onNull String oldPath, @NonNull String newPath)1538 private void scanRenamedDirectoryForFuse(@NonNull String oldPath, @NonNull String newPath) { 1539 final LocalCallingIdentity token = clearLocalCallingIdentity(); 1540 try { 1541 scanFile(new File(oldPath), REASON_DEMAND); 1542 scanFile(new File(newPath), REASON_DEMAND); 1543 } finally { 1544 restoreLocalCallingIdentity(token); 1545 } 1546 } 1547 1548 /** 1549 * Checks if given {@code mimeType} is supported in {@code path}. 1550 */ isMimeTypeSupportedInPath(String path, String mimeType)1551 private boolean isMimeTypeSupportedInPath(String path, String mimeType) { 1552 final String supportedPrimaryMimeType; 1553 final int match = matchUri(getContentUriForFile(path, mimeType), true); 1554 switch (match) { 1555 case AUDIO_MEDIA: 1556 supportedPrimaryMimeType = "audio"; 1557 break; 1558 case VIDEO_MEDIA: 1559 supportedPrimaryMimeType = "video"; 1560 break; 1561 case IMAGES_MEDIA: 1562 supportedPrimaryMimeType = "image"; 1563 break; 1564 default: 1565 supportedPrimaryMimeType = ClipDescription.MIMETYPE_UNKNOWN; 1566 } 1567 return (supportedPrimaryMimeType.equalsIgnoreCase(ClipDescription.MIMETYPE_UNKNOWN) || 1568 MimeUtils.startsWithIgnoreCase(mimeType, supportedPrimaryMimeType)); 1569 } 1570 1571 /** 1572 * Removes owner package for the renamed path if the calling package doesn't own the db row 1573 * 1574 * When oldPath is renamed to newPath, if newPath exists in the database, and caller is not the 1575 * owner of the file, owner package is set to 'null'. This prevents previous owner of newPath 1576 * from accessing renamed file. 1577 * @return {@code true} if 1578 * <ul> 1579 * <li> there is no corresponding database row for given {@code path} 1580 * <li> shared calling package is the owner of the database row 1581 * <li> owner package name is already set to 'null' 1582 * <li> updating owner package name to 'null' was successful. 1583 * </ul> 1584 * Returns {@code false} otherwise. 1585 */ maybeRemoveOwnerPackageForFuseRename(@onNull DatabaseHelper helper, @NonNull String path)1586 private boolean maybeRemoveOwnerPackageForFuseRename(@NonNull DatabaseHelper helper, 1587 @NonNull String path) { 1588 1589 final Uri uri = FileUtils.getContentUriForPath(path); 1590 final int match = matchUri(uri, isCallingPackageAllowedHidden()); 1591 final String ownerPackageName; 1592 final String selection = MediaColumns.DATA + " =? AND " 1593 + MediaColumns.OWNER_PACKAGE_NAME + " != 'null'"; 1594 final String[] selectionArgs = new String[] {path}; 1595 1596 final SQLiteQueryBuilder qbForQuery = 1597 getQueryBuilder(TYPE_QUERY, match, uri, Bundle.EMPTY, null); 1598 try (Cursor c = qbForQuery.query(helper, new String[] {FileColumns.OWNER_PACKAGE_NAME}, 1599 selection, selectionArgs, null, null, null, null, null)) { 1600 if (!c.moveToFirst()) { 1601 // We don't need to remove owner_package from db row if path doesn't exist in 1602 // database or owner_package is already set to 'null' 1603 return true; 1604 } 1605 ownerPackageName = c.getString(0); 1606 if (isCallingIdentitySharedPackageName(ownerPackageName)) { 1607 // We don't need to remove owner_package from db row if calling package is the owner 1608 // of the database row 1609 return true; 1610 } 1611 } 1612 1613 final SQLiteQueryBuilder qbForUpdate = 1614 getQueryBuilder(TYPE_UPDATE, match, uri, Bundle.EMPTY, null); 1615 ContentValues values = new ContentValues(); 1616 values.put(FileColumns.OWNER_PACKAGE_NAME, "null"); 1617 return qbForUpdate.update(helper, values, selection, selectionArgs) == 1; 1618 } 1619 updateDatabaseForFuseRename(@onNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values)1620 private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper, 1621 @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values) { 1622 return updateDatabaseForFuseRename(helper, oldPath, newPath, values, Bundle.EMPTY); 1623 } 1624 1625 /** 1626 * Updates database entry for given {@code path} with {@code values} 1627 */ updateDatabaseForFuseRename(@onNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values, @NonNull Bundle qbExtras)1628 private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper, 1629 @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values, 1630 @NonNull Bundle qbExtras) { 1631 final Uri uriOldPath = FileUtils.getContentUriForPath(oldPath); 1632 boolean allowHidden = isCallingPackageAllowedHidden(); 1633 final SQLiteQueryBuilder qbForUpdate = getQueryBuilder(TYPE_UPDATE, 1634 matchUri(uriOldPath, allowHidden), uriOldPath, qbExtras, null); 1635 final String selection = MediaColumns.DATA + " =? "; 1636 int count = 0; 1637 boolean retryUpdateWithReplace = false; 1638 1639 try { 1640 // TODO(b/146777893): System gallery apps can rename a media directory containing 1641 // non-media files. This update doesn't support updating non-media files that are not 1642 // owned by system gallery app. 1643 count = qbForUpdate.update(helper, values, selection, new String[]{oldPath}); 1644 } catch (SQLiteConstraintException e) { 1645 Log.w(TAG, "Database update failed while renaming " + oldPath, e); 1646 retryUpdateWithReplace = true; 1647 } 1648 1649 if (retryUpdateWithReplace) { 1650 // We are replacing file in newPath with file in oldPath. If calling package has 1651 // write permission for newPath, delete existing database entry and retry update. 1652 final Uri uriNewPath = FileUtils.getContentUriForPath(oldPath); 1653 final SQLiteQueryBuilder qbForDelete = getQueryBuilder(TYPE_DELETE, 1654 matchUri(uriNewPath, allowHidden), uriNewPath, qbExtras, null); 1655 if (qbForDelete.delete(helper, selection, new String[] {newPath}) == 1) { 1656 Log.i(TAG, "Retrying database update after deleting conflicting entry"); 1657 count = qbForUpdate.update(helper, values, selection, new String[]{oldPath}); 1658 } else { 1659 return false; 1660 } 1661 } 1662 return count == 1; 1663 } 1664 1665 /** 1666 * Gets {@link ContentValues} for updating database entry to {@code path}. 1667 */ getContentValuesForFuseRename(String path, String newMimeType, boolean checkHidden)1668 private ContentValues getContentValuesForFuseRename(String path, String newMimeType, 1669 boolean checkHidden) { 1670 ContentValues values = new ContentValues(); 1671 values.put(MediaColumns.MIME_TYPE, newMimeType); 1672 values.put(MediaColumns.DATA, path); 1673 1674 if (checkHidden && shouldFileBeHidden(new File(path))) { 1675 values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE); 1676 } else { 1677 int mediaType = MimeUtils.resolveMediaType(newMimeType); 1678 values.put(FileColumns.MEDIA_TYPE, mediaType); 1679 } 1680 final boolean allowHidden = isCallingPackageAllowedHidden(); 1681 if (!newMimeType.equalsIgnoreCase("null") && 1682 matchUri(getContentUriForFile(path, newMimeType), allowHidden) == AUDIO_MEDIA) { 1683 computeAudioLocalizedValues(values); 1684 computeAudioKeyValues(values); 1685 } 1686 FileUtils.computeValuesFromData(values, isFuseThread()); 1687 return values; 1688 } 1689 getIncludedDefaultDirectories()1690 private ArrayList<String> getIncludedDefaultDirectories() { 1691 final ArrayList<String> includedDefaultDirs = new ArrayList<>(); 1692 if (checkCallingPermissionVideo(/*forWrite*/ true, null)) { 1693 includedDefaultDirs.add(DIRECTORY_DCIM); 1694 includedDefaultDirs.add(DIRECTORY_PICTURES); 1695 includedDefaultDirs.add(DIRECTORY_MOVIES); 1696 } else if (checkCallingPermissionImages(/*forWrite*/ true, null)) { 1697 includedDefaultDirs.add(DIRECTORY_DCIM); 1698 includedDefaultDirs.add(DIRECTORY_PICTURES); 1699 } 1700 return includedDefaultDirs; 1701 } 1702 1703 /** 1704 * Gets all files in the given {@code path} and subdirectories of the given {@code path}. 1705 */ getAllFilesForRenameDirectory(String oldPath)1706 private ArrayList<String> getAllFilesForRenameDirectory(String oldPath) { 1707 final String selection = MediaColumns.RELATIVE_PATH + " REGEXP '^" + 1708 extractRelativePathForDirectory(oldPath) + "/?.*' and mime_type not like 'null'"; 1709 ArrayList<String> fileList = new ArrayList<>(); 1710 1711 final LocalCallingIdentity token = clearLocalCallingIdentity(); 1712 try (final Cursor c = query(FileUtils.getContentUriForPath(oldPath), 1713 new String[] {MediaColumns.DATA}, selection, null, null)) { 1714 while (c.moveToNext()) { 1715 final String filePath = c.getString(0).replaceFirst("^" + oldPath + "/(.*)", "$1"); 1716 fileList.add(filePath); 1717 } 1718 } finally { 1719 restoreLocalCallingIdentity(token); 1720 } 1721 return fileList; 1722 } 1723 1724 /** 1725 * Gets files in the given {@code path} and subdirectories of the given {@code path} for which 1726 * calling package has write permissions. 1727 * 1728 * This method throws {@code IllegalArgumentException} if the directory has one or more 1729 * files for which calling package doesn't have write permission or if file type is not 1730 * supported in {@code newPath} 1731 */ getWritableFilesForRenameDirectory(String oldPath, String newPath)1732 private ArrayList<String> getWritableFilesForRenameDirectory(String oldPath, String newPath) 1733 throws IllegalArgumentException { 1734 // Try a simple check to see if the caller has full access to the given collections first 1735 // before falling back to performing a query to probe for access. 1736 final String oldRelativePath = extractRelativePathForDirectory(oldPath); 1737 final String newRelativePath = extractRelativePathForDirectory(newPath); 1738 boolean hasFullAccessToOldPath = false; 1739 boolean hasFullAccessToNewPath = false; 1740 for (String defaultDir : getIncludedDefaultDirectories()) { 1741 if (oldRelativePath.startsWith(defaultDir)) hasFullAccessToOldPath = true; 1742 if (newRelativePath.startsWith(defaultDir)) hasFullAccessToNewPath = true; 1743 } 1744 if (hasFullAccessToNewPath && hasFullAccessToOldPath) { 1745 return getAllFilesForRenameDirectory(oldPath); 1746 } 1747 1748 final int countAllFilesInDirectory; 1749 final String selection = MediaColumns.RELATIVE_PATH + " REGEXP '^" + 1750 extractRelativePathForDirectory(oldPath) + "/?.*' and mime_type not like 'null'"; 1751 final Uri uriOldPath = FileUtils.getContentUriForPath(oldPath); 1752 1753 final LocalCallingIdentity token = clearLocalCallingIdentity(); 1754 try (final Cursor c = query(uriOldPath, new String[] {MediaColumns._ID}, selection, null, 1755 null)) { 1756 // get actual number of files in the given directory. 1757 countAllFilesInDirectory = c.getCount(); 1758 } finally { 1759 restoreLocalCallingIdentity(token); 1760 } 1761 1762 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, 1763 matchUri(uriOldPath, isCallingPackageAllowedHidden()), uriOldPath, Bundle.EMPTY, 1764 null); 1765 final DatabaseHelper helper; 1766 try { 1767 helper = getDatabaseForUri(uriOldPath); 1768 } catch (VolumeNotFoundException e) { 1769 throw new IllegalStateException("Volume not found while querying files for renaming " 1770 + oldPath); 1771 } 1772 1773 ArrayList<String> fileList = new ArrayList<>(); 1774 final String[] projection = {MediaColumns.DATA, MediaColumns.MIME_TYPE}; 1775 try (Cursor c = qb.query(helper, projection, selection, null, 1776 null, null, null, null, null)) { 1777 // Check if the calling package has write permission to all files in the given 1778 // directory. If calling package has write permission to all files in the directory, the 1779 // query with update uri should return same number of files as previous query. 1780 if (c.getCount() != countAllFilesInDirectory) { 1781 throw new IllegalArgumentException("Calling package doesn't have write permission " 1782 + " to rename one or more files in " + oldPath); 1783 } 1784 while(c.moveToNext()) { 1785 final String filePath = c.getString(0).replaceFirst("^" + oldPath + "/(.*)", "$1"); 1786 final String mimeType = c.getString(1); 1787 if (!isMimeTypeSupportedInPath(newPath + "/" + filePath, mimeType)) { 1788 throw new IllegalArgumentException("Can't rename " + oldPath + "/" + filePath 1789 + ". Mime type " + mimeType + " not supported in " + newPath); 1790 } 1791 fileList.add(filePath); 1792 } 1793 } 1794 return fileList; 1795 } 1796 renameInLowerFs(String oldPath, String newPath)1797 private int renameInLowerFs(String oldPath, String newPath) { 1798 try { 1799 Os.rename(oldPath, newPath); 1800 return 0; 1801 } catch (ErrnoException e) { 1802 final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed."; 1803 Log.e(TAG, errorMessage, e); 1804 return e.errno; 1805 } 1806 } 1807 1808 /** 1809 * Rename directory from {@code oldPath} to {@code newPath}. 1810 * 1811 * Renaming a directory is only allowed if calling package has write permission to all files in 1812 * the given directory tree and all file types in the given directory tree are supported by the 1813 * top level directory of new path. Renaming a directory is split into three steps: 1814 * 1. Check calling package's permissions for all files in the given directory tree. Also check 1815 * file type support for all files in the {@code newPath}. 1816 * 2. Try updating database for all files in the directory. 1817 * 3. Rename the directory in lower file system. If rename in the lower file system is 1818 * successful, commit database update. 1819 * 1820 * @param oldPath path of the directory to be renamed. 1821 * @param newPath new path of directory to be renamed. 1822 * @return 0 on successful rename, appropriate negated errno value if the rename is not allowed. 1823 * <ul> 1824 * <li>{@link OsConstants#EPERM} Renaming a directory with file types not supported by 1825 * {@code newPath} or renaming a directory with files for which calling package doesn't have 1826 * write permission. 1827 * This method can also return errno returned from {@code Os.rename} function. 1828 */ renameDirectoryCheckedForFuse(String oldPath, String newPath)1829 private int renameDirectoryCheckedForFuse(String oldPath, String newPath) { 1830 final ArrayList<String> fileList; 1831 try { 1832 fileList = getWritableFilesForRenameDirectory(oldPath, newPath); 1833 } catch (IllegalArgumentException e) { 1834 final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed. "; 1835 Log.e(TAG, errorMessage, e); 1836 return OsConstants.EPERM; 1837 } 1838 1839 return renameDirectoryUncheckedForFuse(oldPath, newPath, fileList); 1840 } 1841 renameDirectoryUncheckedForFuse(String oldPath, String newPath, ArrayList<String> fileList)1842 private int renameDirectoryUncheckedForFuse(String oldPath, String newPath, 1843 ArrayList<String> fileList) { 1844 final DatabaseHelper helper; 1845 try { 1846 helper = getDatabaseForUri(FileUtils.getContentUriForPath(oldPath)); 1847 } catch (VolumeNotFoundException e) { 1848 throw new IllegalStateException("Volume not found while trying to update database for " 1849 + oldPath, e); 1850 } 1851 1852 helper.beginTransaction(); 1853 try { 1854 final Bundle qbExtras = new Bundle(); 1855 qbExtras.putStringArrayList(INCLUDED_DEFAULT_DIRECTORIES, 1856 getIncludedDefaultDirectories()); 1857 for (String filePath : fileList) { 1858 final String newFilePath = newPath + "/" + filePath; 1859 final String mimeType = MimeUtils.resolveMimeType(new File(newFilePath)); 1860 if(!updateDatabaseForFuseRename(helper, oldPath + "/" + filePath, newFilePath, 1861 getContentValuesForFuseRename(newFilePath, mimeType, 1862 false /* checkHidden - will be fixed up below */), qbExtras)) { 1863 Log.e(TAG, "Calling package doesn't have write permission to rename file."); 1864 return OsConstants.EPERM; 1865 } 1866 } 1867 1868 // Rename the directory in lower file system. 1869 int errno = renameInLowerFs(oldPath, newPath); 1870 if (errno == 0) { 1871 helper.setTransactionSuccessful(); 1872 } else { 1873 return errno; 1874 } 1875 } finally { 1876 helper.endTransaction(); 1877 } 1878 // Directory movement might have made new/old path hidden. 1879 scanRenamedDirectoryForFuse(oldPath, newPath); 1880 return 0; 1881 } 1882 1883 /** 1884 * Rename a file from {@code oldPath} to {@code newPath}. 1885 * 1886 * Renaming a file is split into three parts: 1887 * 1. Check if {@code newPath} supports new file type. 1888 * 2. Try updating database entry from {@code oldPath} to {@code newPath}. This update may fail 1889 * if calling package doesn't have write permission for {@code oldPath} and {@code newPath}. 1890 * 3. Rename the file in lower file system. If Rename in lower file system succeeds, commit 1891 * database update. 1892 * @param oldPath path of the file to be renamed. 1893 * @param newPath new path of the file to be renamed. 1894 * @return 0 on successful rename, appropriate negated errno value if the rename is not allowed. 1895 * <ul> 1896 * <li>{@link OsConstants#EPERM} Calling package doesn't have write permission for 1897 * {@code oldPath} or {@code newPath}, or file type is not supported by {@code newPath}. 1898 * This method can also return errno returned from {@code Os.rename} function. 1899 */ renameFileCheckedForFuse(String oldPath, String newPath)1900 private int renameFileCheckedForFuse(String oldPath, String newPath) { 1901 // Check if new mime type is supported in new path. 1902 final String newMimeType = MimeUtils.resolveMimeType(new File(newPath)); 1903 if (!isMimeTypeSupportedInPath(newPath, newMimeType)) { 1904 return OsConstants.EPERM; 1905 } 1906 return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ false) ; 1907 } 1908 renameFileUncheckedForFuse(String oldPath, String newPath)1909 private int renameFileUncheckedForFuse(String oldPath, String newPath) { 1910 return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ true) ; 1911 } 1912 shouldFileBeHidden(@onNull File file)1913 private static boolean shouldFileBeHidden(@NonNull File file) { 1914 if (FileUtils.isFileHidden(file)) { 1915 return true; 1916 } 1917 File parent = file.getParentFile(); 1918 while (parent != null) { 1919 if (FileUtils.isDirectoryHidden(parent)) { 1920 return true; 1921 } 1922 parent = parent.getParentFile(); 1923 } 1924 1925 return false; 1926 } 1927 renameFileForFuse(String oldPath, String newPath, boolean bypassRestrictions)1928 private int renameFileForFuse(String oldPath, String newPath, boolean bypassRestrictions) { 1929 final DatabaseHelper helper; 1930 try { 1931 helper = getDatabaseForUri(FileUtils.getContentUriForPath(oldPath)); 1932 } catch (VolumeNotFoundException e) { 1933 throw new IllegalStateException("Failed to update database row with " + oldPath, e); 1934 } 1935 1936 helper.beginTransaction(); 1937 try { 1938 final String newMimeType = MimeUtils.resolveMimeType(new File(newPath)); 1939 if (!updateDatabaseForFuseRename(helper, oldPath, newPath, 1940 getContentValuesForFuseRename(newPath, newMimeType, true /* checkHidden */))) { 1941 if (!bypassRestrictions) { 1942 Log.e(TAG, "Calling package doesn't have write permission to rename file."); 1943 return OsConstants.EPERM; 1944 } else if (!maybeRemoveOwnerPackageForFuseRename(helper, newPath)) { 1945 Log.wtf(TAG, "Couldn't clear owner package name for " + newPath); 1946 return OsConstants.EPERM; 1947 } 1948 } 1949 1950 // Try renaming oldPath to newPath in lower file system. 1951 int errno = renameInLowerFs(oldPath, newPath); 1952 if (errno == 0) { 1953 helper.setTransactionSuccessful(); 1954 } else { 1955 return errno; 1956 } 1957 } finally { 1958 helper.endTransaction(); 1959 } 1960 // The above code should have taken are of the mime/media type of the new file, 1961 // even if it was moved to/from a hidden directory. 1962 // This leaves cases where the source/dest of the move is a .nomedia file itself. Eg: 1963 // 1) /sdcard/foo/.nomedia => /sdcard/foo/bar.mp3 1964 // in this case, the code above has given bar.mp3 the correct mime type, but we should 1965 // still can /sdcard/foo, because it's now no longer hidden 1966 // 2) /sdcard/foo/.nomedia => /sdcard/bar/.nomedia 1967 // in this case, we need to scan both /sdcard/foo and /sdcard/bar/ 1968 // 3) /sdcard/foo/bar.mp3 => /sdcard/foo/.nomedia 1969 // in this case, we need to scan all of /sdcard/foo 1970 if (extractDisplayName(oldPath).equals(".nomedia")) { 1971 scanFile(new File(oldPath).getParentFile(), REASON_DEMAND); 1972 } 1973 if (extractDisplayName(newPath).equals(".nomedia")) { 1974 scanFile(new File(newPath).getParentFile(), REASON_DEMAND); 1975 } 1976 return 0; 1977 } 1978 1979 /** 1980 * Rename file/directory without imposing any restrictions. 1981 * 1982 * We don't impose any rename restrictions for apps that bypass scoped storage restrictions. 1983 * However, we update database entries for renamed files to keep the database consistent. 1984 */ renameUncheckedForFuse(String oldPath, String newPath)1985 private int renameUncheckedForFuse(String oldPath, String newPath) { 1986 if (new File(oldPath).isFile()) { 1987 return renameFileUncheckedForFuse(oldPath, newPath); 1988 } else { 1989 return renameDirectoryUncheckedForFuse(oldPath, newPath, 1990 getAllFilesForRenameDirectory(oldPath)); 1991 } 1992 } 1993 1994 /** 1995 * Rename file or directory from {@code oldPath} to {@code newPath}. 1996 * 1997 * @param oldPath path of the file or directory to be renamed. 1998 * @param newPath new path of the file or directory to be renamed. 1999 * @param uid UID of the calling package. 2000 * @return 0 on successful rename, appropriate errno value if the rename is not allowed. 2001 * <ul> 2002 * <li>{@link OsConstants#ENOENT} Renaming a non-existing file or renaming a file from path that 2003 * is not indexed by MediaProvider database. 2004 * <li>{@link OsConstants#EPERM} Renaming a default directory or renaming a file to a file type 2005 * not supported by new path. 2006 * This method can also return errno returned from {@code Os.rename} function. 2007 * 2008 * Called from JNI in jni/MediaProviderWrapper.cpp 2009 */ 2010 @Keep renameForFuse(String oldPath, String newPath, int uid)2011 public int renameForFuse(String oldPath, String newPath, int uid) { 2012 final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed. "; 2013 final LocalCallingIdentity token = 2014 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 2015 2016 try { 2017 if (isPrivatePackagePathNotOwnedByCaller(oldPath) 2018 || isPrivatePackagePathNotOwnedByCaller(newPath)) { 2019 return OsConstants.EACCES; 2020 } 2021 2022 if (!newPath.equals(getAbsoluteSanitizedPath(newPath))) { 2023 Log.e(TAG, "New path name contains invalid characters."); 2024 return OsConstants.EPERM; 2025 } 2026 2027 if (shouldBypassFuseRestrictions(/*forWrite*/ true, oldPath) 2028 && shouldBypassFuseRestrictions(/*forWrite*/ true, newPath)) { 2029 return renameUncheckedForFuse(oldPath, newPath); 2030 } 2031 // Legacy apps that made is this far don't have the right storage permission and hence 2032 // are not allowed to access anything other than their external app directory 2033 if (isCallingPackageRequestingLegacy()) { 2034 return OsConstants.EACCES; 2035 } 2036 2037 final String[] oldRelativePath = sanitizePath(extractRelativePath(oldPath)); 2038 final String[] newRelativePath = sanitizePath(extractRelativePath(newPath)); 2039 if (oldRelativePath.length == 0 || newRelativePath.length == 0) { 2040 // Rename not allowed on paths that can't be translated to RELATIVE_PATH. 2041 Log.e(TAG, errorMessage + "Invalid path."); 2042 return OsConstants.EPERM; 2043 } else if (oldRelativePath.length == 1 && TextUtils.isEmpty(oldRelativePath[0])) { 2044 // Allow rename of files/folders other than default directories. 2045 final String displayName = extractDisplayName(oldPath); 2046 for (String defaultFolder : sDefaultFolderNames) { 2047 if (displayName.equals(defaultFolder)) { 2048 Log.e(TAG, errorMessage + oldPath + " is a default folder." 2049 + " Renaming a default folder is not allowed."); 2050 return OsConstants.EPERM; 2051 } 2052 } 2053 } else if (newRelativePath.length == 1 && TextUtils.isEmpty(newRelativePath[0])) { 2054 Log.e(TAG, errorMessage + newPath + " is in root folder." 2055 + " Renaming a file/directory to root folder is not allowed"); 2056 return OsConstants.EPERM; 2057 } 2058 2059 final File directoryAndroid = new File(Environment.getExternalStorageDirectory(), 2060 DIRECTORY_ANDROID); 2061 final File directoryAndroidMedia = new File(directoryAndroid, DIRECTORY_MEDIA); 2062 if (directoryAndroidMedia.getAbsolutePath().equals(oldPath)) { 2063 // Don't allow renaming 'Android/media' directory. 2064 // Android/[data|obb] are bind mounted and these paths don't go through FUSE. 2065 Log.e(TAG, errorMessage + oldPath + " is a default folder in app external " 2066 + "directory. Renaming a default folder is not allowed."); 2067 return OsConstants.EPERM; 2068 } else if (FileUtils.contains(directoryAndroid, new File(newPath))) { 2069 if (newRelativePath.length == 1) { 2070 // New path is Android/*. Path is directly under Android. Don't allow moving 2071 // files and directories to Android/. 2072 Log.e(TAG, errorMessage + newPath + " is in app external directory. " 2073 + "Renaming a file/directory to app external directory is not " 2074 + "allowed."); 2075 return OsConstants.EPERM; 2076 } else if(!FileUtils.contains(directoryAndroidMedia, new File(newPath))) { 2077 // New path is Android/*/*. Don't allow moving of files or directories 2078 // to app external directory other than media directory. 2079 Log.e(TAG, errorMessage + newPath + " is not in external media directory." 2080 + "File/directory can only be renamed to a path in external media " 2081 + "directory. Renaming file/directory to path in other external " 2082 + "directories is not allowed"); 2083 return OsConstants.EPERM; 2084 } 2085 } 2086 2087 // Continue renaming files/directories if rename of oldPath to newPath is allowed. 2088 if (new File(oldPath).isFile()) { 2089 return renameFileCheckedForFuse(oldPath, newPath); 2090 } else { 2091 return renameDirectoryCheckedForFuse(oldPath, newPath); 2092 } 2093 } finally { 2094 restoreLocalCallingIdentity(token); 2095 } 2096 } 2097 2098 @Override checkUriPermission(@onNull Uri uri, int uid, int modeFlags)2099 public int checkUriPermission(@NonNull Uri uri, int uid, 2100 /* @Intent.AccessUriMode */ int modeFlags) { 2101 final LocalCallingIdentity token = clearLocalCallingIdentity( 2102 LocalCallingIdentity.fromExternal(getContext(), uid)); 2103 2104 try { 2105 final boolean allowHidden = isCallingPackageAllowedHidden(); 2106 final int table = matchUri(uri, allowHidden); 2107 2108 final DatabaseHelper helper; 2109 try { 2110 helper = getDatabaseForUri(uri); 2111 } catch (VolumeNotFoundException e) { 2112 return PackageManager.PERMISSION_DENIED; 2113 } 2114 2115 final int type; 2116 if ((modeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) { 2117 type = TYPE_UPDATE; 2118 } else { 2119 type = TYPE_QUERY; 2120 } 2121 2122 final SQLiteQueryBuilder qb = getQueryBuilder(type, table, uri, Bundle.EMPTY, null); 2123 try (Cursor c = qb.query(helper, 2124 new String[] { BaseColumns._ID }, null, null, null, null, null, null, null)) { 2125 if (c.getCount() == 1) { 2126 return PackageManager.PERMISSION_GRANTED; 2127 } 2128 } 2129 } finally { 2130 restoreLocalCallingIdentity(token); 2131 } 2132 return PackageManager.PERMISSION_DENIED; 2133 } 2134 2135 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)2136 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 2137 String sortOrder) { 2138 return query(uri, projection, 2139 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, sortOrder), null); 2140 } 2141 2142 @Override query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal)2143 public Cursor query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal) { 2144 Trace.beginSection("query"); 2145 try { 2146 return queryInternal(uri, projection, queryArgs, signal); 2147 } catch (FallbackException e) { 2148 return e.translateForQuery(getCallingPackageTargetSdkVersion()); 2149 } finally { 2150 Trace.endSection(); 2151 } 2152 } 2153 queryInternal(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal)2154 private Cursor queryInternal(Uri uri, String[] projection, Bundle queryArgs, 2155 CancellationSignal signal) throws FallbackException { 2156 queryArgs = (queryArgs != null) ? queryArgs : new Bundle(); 2157 2158 // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. 2159 queryArgs.remove(INCLUDED_DEFAULT_DIRECTORIES); 2160 2161 final ArraySet<String> honoredArgs = new ArraySet<>(); 2162 DatabaseUtils.resolveQueryArgs(queryArgs, honoredArgs::add, this::ensureCustomCollator); 2163 2164 uri = safeUncanonicalize(uri); 2165 2166 final String volumeName = getVolumeName(uri); 2167 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 2168 final boolean allowHidden = isCallingPackageAllowedHidden(); 2169 final int table = matchUri(uri, allowHidden); 2170 2171 //Log.v(TAG, "query: uri="+uri+", selection="+selection); 2172 // handle MEDIA_SCANNER before calling getDatabaseForUri() 2173 if (table == MEDIA_SCANNER) { 2174 // create a cursor to return volume currently being scanned by the media scanner 2175 MatrixCursor c = new MatrixCursor(new String[] {MediaStore.MEDIA_SCANNER_VOLUME}); 2176 c.addRow(new String[] {mMediaScannerVolume}); 2177 return c; 2178 } 2179 2180 // Used temporarily (until we have unique media IDs) to get an identifier 2181 // for the current sd card, so that the music app doesn't have to use the 2182 // non-public getFatVolumeId method 2183 if (table == FS_ID) { 2184 MatrixCursor c = new MatrixCursor(new String[] {"fsid"}); 2185 c.addRow(new Integer[] {mVolumeId}); 2186 return c; 2187 } 2188 2189 if (table == VERSION) { 2190 MatrixCursor c = new MatrixCursor(new String[] {"version"}); 2191 c.addRow(new Integer[] {DatabaseHelper.getDatabaseVersion(getContext())}); 2192 return c; 2193 } 2194 2195 final DatabaseHelper helper = getDatabaseForUri(uri); 2196 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, queryArgs, 2197 honoredArgs::add); 2198 2199 if (targetSdkVersion < Build.VERSION_CODES.R) { 2200 // Some apps are abusing "ORDER BY" clauses to inject "LIMIT" 2201 // clauses; gracefully lift them out. 2202 DatabaseUtils.recoverAbusiveSortOrder(queryArgs); 2203 2204 // Some apps are abusing the Uri query parameters to inject LIMIT 2205 // clauses; gracefully lift them out. 2206 DatabaseUtils.recoverAbusiveLimit(uri, queryArgs); 2207 } 2208 2209 if (targetSdkVersion < Build.VERSION_CODES.Q) { 2210 // Some apps are abusing the "WHERE" clause by injecting "GROUP BY" 2211 // clauses; gracefully lift them out. 2212 DatabaseUtils.recoverAbusiveSelection(queryArgs); 2213 2214 // Some apps are abusing the first column to inject "DISTINCT"; 2215 // gracefully lift them out. 2216 if ((projection != null) && (projection.length > 0) 2217 && projection[0].startsWith("DISTINCT ")) { 2218 projection[0] = projection[0].substring("DISTINCT ".length()); 2219 qb.setDistinct(true); 2220 } 2221 2222 // Some apps are generating thumbnails with getThumbnail(), but then 2223 // ignoring the returned Bitmap and querying the raw table; give 2224 // them a row with enough information to find the original image. 2225 final String selection = queryArgs.getString(QUERY_ARG_SQL_SELECTION); 2226 if ((table == IMAGES_THUMBNAILS || table == VIDEO_THUMBNAILS) 2227 && !TextUtils.isEmpty(selection)) { 2228 final Matcher matcher = PATTERN_SELECTION_ID.matcher(selection); 2229 if (matcher.matches()) { 2230 final long id = Long.parseLong(matcher.group(1)); 2231 2232 final Uri fullUri; 2233 if (table == IMAGES_THUMBNAILS) { 2234 fullUri = ContentUris.withAppendedId( 2235 Images.Media.getContentUri(volumeName), id); 2236 } else if (table == VIDEO_THUMBNAILS) { 2237 fullUri = ContentUris.withAppendedId( 2238 Video.Media.getContentUri(volumeName), id); 2239 } else { 2240 throw new IllegalArgumentException(); 2241 } 2242 2243 final MatrixCursor cursor = new MatrixCursor(projection); 2244 final File file = ContentResolver.encodeToFile( 2245 fullUri.buildUpon().appendPath("thumbnail").build()); 2246 final String data = file.getAbsolutePath(); 2247 cursor.newRow().add(MediaColumns._ID, null) 2248 .add(Images.Thumbnails.IMAGE_ID, id) 2249 .add(Video.Thumbnails.VIDEO_ID, id) 2250 .add(MediaColumns.DATA, data); 2251 return cursor; 2252 } 2253 } 2254 } 2255 2256 final Cursor c = qb.query(helper, projection, queryArgs, signal); 2257 if (c != null) { 2258 // As a performance optimization, only configure notifications when 2259 // resulting cursor will leave our process 2260 final boolean callerIsRemote = mCallingIdentity.get().pid != android.os.Process.myPid(); 2261 if (callerIsRemote && !isFuseThread()) { 2262 c.setNotificationUri(getContext().getContentResolver(), uri); 2263 } 2264 2265 final Bundle extras = new Bundle(); 2266 extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, 2267 honoredArgs.toArray(new String[honoredArgs.size()])); 2268 c.setExtras(extras); 2269 } 2270 return c; 2271 } 2272 2273 @Override getType(Uri url)2274 public String getType(Uri url) { 2275 final int match = matchUri(url, true); 2276 switch (match) { 2277 case IMAGES_MEDIA_ID: 2278 case AUDIO_MEDIA_ID: 2279 case AUDIO_PLAYLISTS_ID: 2280 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 2281 case VIDEO_MEDIA_ID: 2282 case DOWNLOADS_ID: 2283 case FILES_ID: 2284 final LocalCallingIdentity token = clearLocalCallingIdentity(); 2285 try (Cursor cursor = queryForSingleItem(url, 2286 new String[] { MediaColumns.MIME_TYPE }, null, null, null)) { 2287 return cursor.getString(0); 2288 } catch (FileNotFoundException e) { 2289 throw new IllegalArgumentException(e.getMessage()); 2290 } finally { 2291 restoreLocalCallingIdentity(token); 2292 } 2293 2294 case IMAGES_MEDIA: 2295 case IMAGES_THUMBNAILS: 2296 return Images.Media.CONTENT_TYPE; 2297 2298 case AUDIO_ALBUMART_ID: 2299 case AUDIO_ALBUMART_FILE_ID: 2300 case IMAGES_THUMBNAILS_ID: 2301 case VIDEO_THUMBNAILS_ID: 2302 return "image/jpeg"; 2303 2304 case AUDIO_MEDIA: 2305 case AUDIO_GENRES_ID_MEMBERS: 2306 case AUDIO_PLAYLISTS_ID_MEMBERS: 2307 return Audio.Media.CONTENT_TYPE; 2308 2309 case AUDIO_GENRES: 2310 case AUDIO_MEDIA_ID_GENRES: 2311 return Audio.Genres.CONTENT_TYPE; 2312 case AUDIO_GENRES_ID: 2313 case AUDIO_MEDIA_ID_GENRES_ID: 2314 return Audio.Genres.ENTRY_CONTENT_TYPE; 2315 case AUDIO_PLAYLISTS: 2316 return Audio.Playlists.CONTENT_TYPE; 2317 2318 case VIDEO_MEDIA: 2319 return Video.Media.CONTENT_TYPE; 2320 case DOWNLOADS: 2321 return Downloads.CONTENT_TYPE; 2322 } 2323 throw new IllegalStateException("Unknown URL : " + url); 2324 } 2325 2326 @VisibleForTesting ensureFileColumns(@onNull Uri uri, @NonNull ContentValues values)2327 void ensureFileColumns(@NonNull Uri uri, @NonNull ContentValues values) 2328 throws VolumeArgumentException, VolumeNotFoundException { 2329 final LocalUriMatcher matcher = new LocalUriMatcher(MediaStore.AUTHORITY); 2330 final int match = matcher.matchUri(uri, true); 2331 ensureNonUniqueFileColumns(match, uri, Bundle.EMPTY, values, null /* currentPath */); 2332 } 2333 ensureUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath)2334 private void ensureUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, 2335 @NonNull ContentValues values, @Nullable String currentPath) 2336 throws VolumeArgumentException, VolumeNotFoundException { 2337 ensureFileColumns(match, uri, extras, values, true, currentPath); 2338 } 2339 ensureNonUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath)2340 private void ensureNonUniqueFileColumns(int match, @NonNull Uri uri, 2341 @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath) 2342 throws VolumeArgumentException, VolumeNotFoundException { 2343 ensureFileColumns(match, uri, extras, values, false, currentPath); 2344 } 2345 2346 /** 2347 * Get the various file-related {@link MediaColumns} in the given 2348 * {@link ContentValues} into sane condition. Also validates that defined 2349 * columns are valid for the given {@link Uri}, such as ensuring that only 2350 * {@code image/*} can be inserted into 2351 * {@link android.provider.MediaStore.Images}. 2352 */ ensureFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath)2353 private void ensureFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, 2354 @NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath) 2355 throws VolumeArgumentException, VolumeNotFoundException { 2356 Trace.beginSection("ensureFileColumns"); 2357 2358 Objects.requireNonNull(uri); 2359 Objects.requireNonNull(extras); 2360 Objects.requireNonNull(values); 2361 2362 // Figure out defaults based on Uri being modified 2363 String defaultMimeType = ClipDescription.MIMETYPE_UNKNOWN; 2364 int defaultMediaType = FileColumns.MEDIA_TYPE_NONE; 2365 String defaultPrimary = Environment.DIRECTORY_DOWNLOADS; 2366 String defaultSecondary = null; 2367 List<String> allowedPrimary = Arrays.asList( 2368 Environment.DIRECTORY_DOWNLOADS, 2369 Environment.DIRECTORY_DOCUMENTS); 2370 switch (match) { 2371 case AUDIO_MEDIA: 2372 case AUDIO_MEDIA_ID: 2373 defaultMimeType = "audio/mpeg"; 2374 defaultMediaType = FileColumns.MEDIA_TYPE_AUDIO; 2375 defaultPrimary = Environment.DIRECTORY_MUSIC; 2376 allowedPrimary = Arrays.asList( 2377 Environment.DIRECTORY_ALARMS, 2378 Environment.DIRECTORY_AUDIOBOOKS, 2379 Environment.DIRECTORY_MUSIC, 2380 Environment.DIRECTORY_NOTIFICATIONS, 2381 Environment.DIRECTORY_PODCASTS, 2382 Environment.DIRECTORY_RINGTONES); 2383 break; 2384 case VIDEO_MEDIA: 2385 case VIDEO_MEDIA_ID: 2386 defaultMimeType = "video/mp4"; 2387 defaultMediaType = FileColumns.MEDIA_TYPE_VIDEO; 2388 defaultPrimary = Environment.DIRECTORY_MOVIES; 2389 allowedPrimary = Arrays.asList( 2390 Environment.DIRECTORY_DCIM, 2391 Environment.DIRECTORY_MOVIES, 2392 Environment.DIRECTORY_PICTURES); 2393 break; 2394 case IMAGES_MEDIA: 2395 case IMAGES_MEDIA_ID: 2396 defaultMimeType = "image/jpeg"; 2397 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE; 2398 defaultPrimary = Environment.DIRECTORY_PICTURES; 2399 allowedPrimary = Arrays.asList( 2400 Environment.DIRECTORY_DCIM, 2401 Environment.DIRECTORY_PICTURES); 2402 break; 2403 case AUDIO_ALBUMART: 2404 case AUDIO_ALBUMART_ID: 2405 defaultMimeType = "image/jpeg"; 2406 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE; 2407 defaultPrimary = Environment.DIRECTORY_MUSIC; 2408 allowedPrimary = Arrays.asList(defaultPrimary); 2409 defaultSecondary = DIRECTORY_THUMBNAILS; 2410 break; 2411 case VIDEO_THUMBNAILS: 2412 case VIDEO_THUMBNAILS_ID: 2413 defaultMimeType = "image/jpeg"; 2414 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE; 2415 defaultPrimary = Environment.DIRECTORY_MOVIES; 2416 allowedPrimary = Arrays.asList(defaultPrimary); 2417 defaultSecondary = DIRECTORY_THUMBNAILS; 2418 break; 2419 case IMAGES_THUMBNAILS: 2420 case IMAGES_THUMBNAILS_ID: 2421 defaultMimeType = "image/jpeg"; 2422 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE; 2423 defaultPrimary = Environment.DIRECTORY_PICTURES; 2424 allowedPrimary = Arrays.asList(defaultPrimary); 2425 defaultSecondary = DIRECTORY_THUMBNAILS; 2426 break; 2427 case AUDIO_PLAYLISTS: 2428 case AUDIO_PLAYLISTS_ID: 2429 defaultMimeType = "audio/mpegurl"; 2430 defaultMediaType = FileColumns.MEDIA_TYPE_PLAYLIST; 2431 defaultPrimary = Environment.DIRECTORY_MUSIC; 2432 allowedPrimary = Arrays.asList( 2433 Environment.DIRECTORY_MUSIC, 2434 Environment.DIRECTORY_MOVIES); 2435 break; 2436 case DOWNLOADS: 2437 case DOWNLOADS_ID: 2438 defaultPrimary = Environment.DIRECTORY_DOWNLOADS; 2439 allowedPrimary = Arrays.asList(defaultPrimary); 2440 break; 2441 case FILES: 2442 case FILES_ID: 2443 // Use defaults above 2444 break; 2445 default: 2446 Log.w(TAG, "Unhandled location " + uri + "; assuming generic files"); 2447 break; 2448 } 2449 2450 final String resolvedVolumeName = resolveVolumeName(uri); 2451 2452 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA)) 2453 && MediaStore.VOLUME_INTERNAL.equals(resolvedVolumeName)) { 2454 // TODO: promote this to top-level check 2455 throw new UnsupportedOperationException( 2456 "Writing to internal storage is not supported."); 2457 } 2458 2459 // Force values when raw path provided 2460 if (!TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) { 2461 FileUtils.computeValuesFromData(values, isFuseThread()); 2462 } 2463 2464 final boolean isTargetSdkROrHigher = 2465 getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R; 2466 final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME); 2467 final String mimeTypeFromExt = TextUtils.isEmpty(displayName) ? null : 2468 MimeUtils.resolveMimeType(new File(displayName)); 2469 2470 if (TextUtils.isEmpty(values.getAsString(MediaColumns.MIME_TYPE))) { 2471 if (isTargetSdkROrHigher) { 2472 // Extract the MIME type from the display name if we couldn't resolve it from the 2473 // raw path 2474 if (mimeTypeFromExt != null) { 2475 values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt); 2476 } else { 2477 // We couldn't resolve mimeType, it means that both display name and MIME type 2478 // were missing in values, so we use defaultMimeType. 2479 values.put(MediaColumns.MIME_TYPE, defaultMimeType); 2480 } 2481 } else if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) { 2482 values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt); 2483 } else { 2484 // We don't use mimeTypeFromExt to preserve legacy behavior. 2485 values.put(MediaColumns.MIME_TYPE, defaultMimeType); 2486 } 2487 } 2488 2489 String mimeType = values.getAsString(MediaColumns.MIME_TYPE); 2490 if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) { 2491 // We allow any mimeType for generic uri with default media type as MEDIA_TYPE_NONE. 2492 } else if (mimeType != null && 2493 MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) == null) { 2494 if (mimeTypeFromExt != null && 2495 defaultMediaType == MimeUtils.resolveMediaType(mimeTypeFromExt)) { 2496 // If mimeType from extension matches the defaultMediaType of uri, we use mimeType 2497 // from file extension as mimeType. This is an effort to guess the mimeType when we 2498 // get unsupported mimeType. 2499 // Note: We can't force defaultMimeType because when we force defaultMimeType, we 2500 // will force the file extension as well. For example, if DISPLAY_NAME=Foo.png and 2501 // mimeType="image/*". If we force mimeType to be "image/jpeg", we append the file 2502 // name with the new file extension i.e., "Foo.png.jpg" where as the expected file 2503 // name was "Foo.png" 2504 values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt); 2505 } else if (isTargetSdkROrHigher) { 2506 // We are here because given mimeType is unsupported also we couldn't guess valid 2507 // mimeType from file extension. 2508 throw new IllegalArgumentException("Unsupported MIME type " + mimeType); 2509 } else { 2510 // We can't throw error for legacy apps, so we try to use defaultMimeType. 2511 values.put(MediaColumns.MIME_TYPE, defaultMimeType); 2512 } 2513 } 2514 2515 // Give ourselves sane defaults when missing 2516 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DISPLAY_NAME))) { 2517 values.put(MediaColumns.DISPLAY_NAME, 2518 String.valueOf(System.currentTimeMillis())); 2519 } 2520 final Integer formatObject = values.getAsInteger(FileColumns.FORMAT); 2521 final int format = formatObject == null ? 0 : formatObject.intValue(); 2522 if (format == MtpConstants.FORMAT_ASSOCIATION) { 2523 values.putNull(MediaColumns.MIME_TYPE); 2524 } 2525 2526 mimeType = values.getAsString(MediaColumns.MIME_TYPE); 2527 // Sanity check MIME type against table 2528 if (mimeType != null) { 2529 final int actualMediaType = MimeUtils.resolveMediaType(mimeType); 2530 if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) { 2531 // Give callers an opportunity to work with playlists and 2532 // subtitles using the generic files table 2533 switch (actualMediaType) { 2534 case FileColumns.MEDIA_TYPE_PLAYLIST: 2535 defaultMimeType = "audio/mpegurl"; 2536 defaultMediaType = FileColumns.MEDIA_TYPE_PLAYLIST; 2537 defaultPrimary = Environment.DIRECTORY_MUSIC; 2538 allowedPrimary = Arrays.asList( 2539 Environment.DIRECTORY_MUSIC, 2540 Environment.DIRECTORY_MOVIES); 2541 break; 2542 case FileColumns.MEDIA_TYPE_SUBTITLE: 2543 defaultMimeType = "application/x-subrip"; 2544 defaultMediaType = FileColumns.MEDIA_TYPE_SUBTITLE; 2545 defaultPrimary = Environment.DIRECTORY_MOVIES; 2546 allowedPrimary = Arrays.asList( 2547 Environment.DIRECTORY_MUSIC, 2548 Environment.DIRECTORY_MOVIES); 2549 break; 2550 } 2551 } else if (defaultMediaType != actualMediaType) { 2552 final String[] split = defaultMimeType.split("/"); 2553 throw new IllegalArgumentException( 2554 "MIME type " + mimeType + " cannot be inserted into " + uri 2555 + "; expected MIME type under " + split[0] + "/*"); 2556 } 2557 } 2558 2559 // Use default directories when missing 2560 if (TextUtils.isEmpty(values.getAsString(MediaColumns.RELATIVE_PATH))) { 2561 if (defaultSecondary != null) { 2562 values.put(MediaColumns.RELATIVE_PATH, 2563 defaultPrimary + '/' + defaultSecondary + '/'); 2564 } else { 2565 values.put(MediaColumns.RELATIVE_PATH, 2566 defaultPrimary + '/'); 2567 } 2568 } 2569 2570 // Generate path when undefined 2571 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) { 2572 File volumePath; 2573 try { 2574 volumePath = getVolumePath(resolvedVolumeName); 2575 } catch (FileNotFoundException e) { 2576 throw new IllegalArgumentException(e); 2577 } 2578 2579 FileUtils.sanitizeValues(values, /*rewriteHiddenFileName*/ !isFuseThread()); 2580 FileUtils.computeDataFromValues(values, volumePath, isFuseThread()); 2581 2582 // Create result file 2583 File res = new File(values.getAsString(MediaColumns.DATA)); 2584 try { 2585 if (makeUnique) { 2586 res = FileUtils.buildUniqueFile(res.getParentFile(), 2587 mimeType, res.getName()); 2588 } else { 2589 res = FileUtils.buildNonUniqueFile(res.getParentFile(), 2590 mimeType, res.getName()); 2591 } 2592 } catch (FileNotFoundException e) { 2593 throw new IllegalStateException( 2594 "Failed to build unique file: " + res + " " + values); 2595 } 2596 2597 // Require that content lives under well-defined directories to help 2598 // keep the user's content organized 2599 2600 // Start by saying unchanged directories are valid 2601 final String currentDir = (currentPath != null) 2602 ? new File(currentPath).getParent() : null; 2603 boolean validPath = res.getParent().equals(currentDir); 2604 2605 // Next, consider allowing based on allowed primary directory 2606 final String[] relativePath = values.getAsString(MediaColumns.RELATIVE_PATH).split("/"); 2607 final String primary = (relativePath.length > 0) ? relativePath[0] : null; 2608 if (!validPath) { 2609 validPath = allowedPrimary.contains(primary); 2610 } 2611 2612 // Next, consider allowing paths when referencing a related item 2613 final Uri relatedUri = extras.getParcelable(QUERY_ARG_RELATED_URI); 2614 if (!validPath && relatedUri != null) { 2615 try (Cursor c = queryForSingleItem(relatedUri, new String[] { 2616 MediaColumns.MIME_TYPE, 2617 MediaColumns.RELATIVE_PATH, 2618 }, null, null, null)) { 2619 // If top-level MIME type matches, and relative path 2620 // matches, then allow caller to place things here 2621 2622 final String expectedType = MimeUtils.extractPrimaryType( 2623 c.getString(0)); 2624 final String actualType = MimeUtils.extractPrimaryType( 2625 values.getAsString(MediaColumns.MIME_TYPE)); 2626 if (!Objects.equals(expectedType, actualType)) { 2627 throw new IllegalArgumentException("Placement of " + actualType 2628 + " item not allowed in relation to " + expectedType + " item"); 2629 } 2630 2631 final String expectedPath = c.getString(1); 2632 final String actualPath = values.getAsString(MediaColumns.RELATIVE_PATH); 2633 if (!Objects.equals(expectedPath, actualPath)) { 2634 throw new IllegalArgumentException("Placement of " + actualPath 2635 + " item not allowed in relation to " + expectedPath + " item"); 2636 } 2637 2638 // If we didn't see any trouble above, then we'll allow it 2639 validPath = true; 2640 } catch (FileNotFoundException e) { 2641 Log.w(TAG, "Failed to find related item " + relatedUri + ": " + e); 2642 } 2643 } 2644 2645 // Consider allowing external media directory of calling package 2646 if (!validPath) { 2647 final String pathOwnerPackage = extractPathOwnerPackageName(res.getAbsolutePath()); 2648 if (pathOwnerPackage != null) { 2649 validPath = isExternalMediaDirectory(res.getAbsolutePath()) && 2650 isCallingIdentitySharedPackageName(pathOwnerPackage); 2651 } 2652 } 2653 2654 // Allow apps with MANAGE_EXTERNAL_STORAGE to create files anywhere 2655 if (!validPath) { 2656 validPath = isCallingPackageManager(); 2657 } 2658 2659 // Allow system gallery to create image/video files. 2660 if (!validPath) { 2661 // System gallery can create image/video files in any existing directory, it can 2662 // also create subdirectories in any existing top-level directory. However, system 2663 // gallery is not allowed to create non-default top level directory. 2664 final boolean createNonDefaultTopLevelDir = primary != null && 2665 !FileUtils.buildPath(volumePath, primary).exists(); 2666 validPath = !createNonDefaultTopLevelDir && 2667 canAccessMediaFile(res.getAbsolutePath(), /*allowLegacy*/ false); 2668 } 2669 2670 // Nothing left to check; caller can't use this path 2671 if (!validPath) { 2672 throw new IllegalArgumentException( 2673 "Primary directory " + primary + " not allowed for " + uri 2674 + "; allowed directories are " + allowedPrimary); 2675 } 2676 2677 // Ensure all parent folders of result file exist 2678 res.getParentFile().mkdirs(); 2679 if (!res.getParentFile().exists()) { 2680 throw new IllegalStateException("Failed to create directory: " + res); 2681 } 2682 values.put(MediaColumns.DATA, res.getAbsolutePath()); 2683 // buildFile may have changed the file name, compute values to extract new DISPLAY_NAME. 2684 // Note: We can't extract displayName from res.getPath() because for pending & trashed 2685 // files DISPLAY_NAME will not be same as file name. 2686 FileUtils.computeValuesFromData(values, isFuseThread()); 2687 } else { 2688 assertFileColumnsSane(match, uri, values); 2689 } 2690 2691 // Drop columns that aren't relevant for special tables 2692 switch (match) { 2693 case AUDIO_ALBUMART: 2694 case VIDEO_THUMBNAILS: 2695 case IMAGES_THUMBNAILS: 2696 final Set<String> valid = getProjectionMap(MediaStore.Images.Thumbnails.class) 2697 .keySet(); 2698 for (String key : new ArraySet<>(values.keySet())) { 2699 if (!valid.contains(key)) { 2700 values.remove(key); 2701 } 2702 } 2703 break; 2704 } 2705 2706 Trace.endSection(); 2707 } 2708 2709 /** 2710 * Sanity check that any requested {@link MediaColumns#DATA} paths actually 2711 * live on the storage volume being targeted. 2712 */ assertFileColumnsSane(int match, Uri uri, ContentValues values)2713 private void assertFileColumnsSane(int match, Uri uri, ContentValues values) 2714 throws VolumeArgumentException, VolumeNotFoundException { 2715 if (!values.containsKey(MediaColumns.DATA)) return; 2716 2717 final String volumeName = resolveVolumeName(uri); 2718 try { 2719 // Sanity check that the requested path actually lives on volume 2720 final Collection<File> allowed = getVolumeScanPaths(volumeName); 2721 final File actual = new File(values.getAsString(MediaColumns.DATA)) 2722 .getCanonicalFile(); 2723 if (!FileUtils.contains(allowed, actual)) { 2724 throw new VolumeArgumentException(actual, allowed); 2725 } 2726 } catch (IOException e) { 2727 throw new VolumeNotFoundException(volumeName); 2728 } 2729 } 2730 2731 @Override bulkInsert(Uri uri, ContentValues values[])2732 public int bulkInsert(Uri uri, ContentValues values[]) { 2733 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 2734 final boolean allowHidden = isCallingPackageAllowedHidden(); 2735 final int match = matchUri(uri, allowHidden); 2736 2737 if (match == VOLUMES) { 2738 return super.bulkInsert(uri, values); 2739 } 2740 2741 final DatabaseHelper helper; 2742 try { 2743 helper = getDatabaseForUri(uri); 2744 } catch (VolumeNotFoundException e) { 2745 return e.translateForUpdateDelete(targetSdkVersion); 2746 } 2747 2748 helper.beginTransaction(); 2749 try { 2750 final int result = super.bulkInsert(uri, values); 2751 helper.setTransactionSuccessful(); 2752 return result; 2753 } finally { 2754 helper.endTransaction(); 2755 } 2756 } 2757 insertDirectory(@onNull SQLiteDatabase db, @NonNull String path)2758 private long insertDirectory(@NonNull SQLiteDatabase db, @NonNull String path) { 2759 if (LOGV) Log.v(TAG, "inserting directory " + path); 2760 ContentValues values = new ContentValues(); 2761 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION); 2762 values.put(FileColumns.DATA, path); 2763 values.put(FileColumns.PARENT, getParent(db, path)); 2764 values.put(FileColumns.OWNER_PACKAGE_NAME, extractPathOwnerPackageName(path)); 2765 values.put(FileColumns.VOLUME_NAME, extractVolumeName(path)); 2766 values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path)); 2767 values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path)); 2768 values.put(FileColumns.IS_DOWNLOAD, isDownload(path) ? 1 : 0); 2769 File file = new File(path); 2770 if (file.exists()) { 2771 values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000); 2772 } 2773 return db.insert("files", FileColumns.DATE_MODIFIED, values); 2774 } 2775 getParent(@onNull SQLiteDatabase db, @NonNull String path)2776 private long getParent(@NonNull SQLiteDatabase db, @NonNull String path) { 2777 final String parentPath = new File(path).getParent(); 2778 if (Objects.equals("/", parentPath)) { 2779 return -1; 2780 } else { 2781 synchronized (mDirectoryCache) { 2782 Long id = mDirectoryCache.get(parentPath); 2783 if (id != null) { 2784 return id; 2785 } 2786 } 2787 2788 final long id; 2789 try (Cursor c = db.query("files", new String[] { FileColumns._ID }, 2790 FileColumns.DATA + "=?", new String[] { parentPath }, null, null, null)) { 2791 if (c.moveToFirst()) { 2792 id = c.getLong(0); 2793 } else { 2794 id = insertDirectory(db, parentPath); 2795 } 2796 } 2797 2798 synchronized (mDirectoryCache) { 2799 mDirectoryCache.put(parentPath, id); 2800 } 2801 return id; 2802 } 2803 } 2804 2805 /** 2806 * @param c the Cursor whose title to retrieve 2807 * @return the result of {@link #getDefaultTitle(String)} if the result is valid; otherwise 2808 * the value of the {@code MediaStore.Audio.Media.TITLE} column 2809 */ getDefaultTitleFromCursor(Cursor c)2810 private String getDefaultTitleFromCursor(Cursor c) { 2811 String title = null; 2812 final int columnIndex = c.getColumnIndex("title_resource_uri"); 2813 // Necessary to check for existence because we may be reading from an old DB version 2814 if (columnIndex > -1) { 2815 final String titleResourceUri = c.getString(columnIndex); 2816 if (titleResourceUri != null) { 2817 try { 2818 title = getDefaultTitle(titleResourceUri); 2819 } catch (Exception e) { 2820 // Best attempt only 2821 } 2822 } 2823 } 2824 if (title == null) { 2825 title = c.getString(c.getColumnIndex(MediaStore.Audio.Media.TITLE)); 2826 } 2827 return title; 2828 } 2829 2830 /** 2831 * @param title_resource_uri The title resource for which to retrieve the default localization 2832 * @return The title localized to {@code Locale.US}, or {@code null} if unlocalizable 2833 * @throws Exception Thrown if the title appears to be localizable, but the localization failed 2834 * for any reason. For example, the application from which the localized title is fetched is not 2835 * installed, or it does not have the resource which needs to be localized 2836 */ getDefaultTitle(String title_resource_uri)2837 private String getDefaultTitle(String title_resource_uri) throws Exception{ 2838 try { 2839 return getTitleFromResourceUri(title_resource_uri, false); 2840 } catch (Exception e) { 2841 Log.e(TAG, "Error getting default title for " + title_resource_uri, e); 2842 throw e; 2843 } 2844 } 2845 2846 /** 2847 * @param title_resource_uri The title resource to localize 2848 * @return The localized title, or {@code null} if unlocalizable 2849 * @throws Exception Thrown if the title appears to be localizable, but the localization failed 2850 * for any reason. For example, the application from which the localized title is fetched is not 2851 * installed, or it does not have the resource which needs to be localized 2852 */ getLocalizedTitle(String title_resource_uri)2853 private String getLocalizedTitle(String title_resource_uri) throws Exception { 2854 try { 2855 return getTitleFromResourceUri(title_resource_uri, true); 2856 } catch (Exception e) { 2857 Log.e(TAG, "Error getting localized title for " + title_resource_uri, e); 2858 throw e; 2859 } 2860 } 2861 2862 /** 2863 * Localizable titles conform to this URI pattern: 2864 * Scheme: {@link ContentResolver.SCHEME_ANDROID_RESOURCE} 2865 * Authority: Package Name of ringtone title provider 2866 * First Path Segment: Type of resource (must be "string") 2867 * Second Path Segment: Resource name of title 2868 * 2869 * @param title_resource_uri The title resource to retrieve 2870 * @param localize Whether or not to localize the title 2871 * @return The title, or {@code null} if unlocalizable 2872 * @throws Exception Thrown if the title appears to be localizable, but the localization failed 2873 * for any reason. For example, the application from which the localized title is fetched is not 2874 * installed, or it does not have the resource which needs to be localized 2875 */ getTitleFromResourceUri(String title_resource_uri, boolean localize)2876 private String getTitleFromResourceUri(String title_resource_uri, boolean localize) 2877 throws Exception { 2878 if (TextUtils.isEmpty(title_resource_uri)) { 2879 return null; 2880 } 2881 final Uri titleUri = Uri.parse(title_resource_uri); 2882 final String scheme = titleUri.getScheme(); 2883 if (!ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) { 2884 return null; 2885 } 2886 final List<String> pathSegments = titleUri.getPathSegments(); 2887 if (pathSegments.size() != 2) { 2888 Log.e(TAG, "Error getting localized title for " + title_resource_uri 2889 + ", must have 2 path segments"); 2890 return null; 2891 } 2892 final String type = pathSegments.get(0); 2893 if (!"string".equals(type)) { 2894 Log.e(TAG, "Error getting localized title for " + title_resource_uri 2895 + ", first path segment must be \"string\""); 2896 return null; 2897 } 2898 final String packageName = titleUri.getAuthority(); 2899 final Resources resources; 2900 if (localize) { 2901 resources = mPackageManager.getResourcesForApplication(packageName); 2902 } else { 2903 final Context packageContext = getContext().createPackageContext(packageName, 0); 2904 final Configuration configuration = packageContext.getResources().getConfiguration(); 2905 configuration.setLocale(Locale.US); 2906 resources = packageContext.createConfigurationContext(configuration).getResources(); 2907 } 2908 final String resourceIdentifier = pathSegments.get(1); 2909 final int id = resources.getIdentifier(resourceIdentifier, type, packageName); 2910 return resources.getString(id); 2911 } 2912 onLocaleChanged()2913 public void onLocaleChanged() { 2914 mInternalDatabase.runWithTransaction((db) -> { 2915 localizeTitles(db); 2916 return null; 2917 }); 2918 } 2919 localizeTitles(@onNull SQLiteDatabase db)2920 private void localizeTitles(@NonNull SQLiteDatabase db) { 2921 try (Cursor c = db.query("files", new String[]{"_id", "title_resource_uri"}, 2922 "title_resource_uri IS NOT NULL", null, null, null, null)) { 2923 while (c.moveToNext()) { 2924 final String id = c.getString(0); 2925 final String titleResourceUri = c.getString(1); 2926 final ContentValues values = new ContentValues(); 2927 try { 2928 values.put(AudioColumns.TITLE_RESOURCE_URI, titleResourceUri); 2929 computeAudioLocalizedValues(values); 2930 computeAudioKeyValues(values); 2931 db.update("files", values, "_id=?", new String[]{id}); 2932 } catch (Exception e) { 2933 Log.e(TAG, "Error updating localized title for " + titleResourceUri 2934 + ", keeping old localization"); 2935 } 2936 } 2937 } 2938 } 2939 insertFile(@onNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper, int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, int mediaType)2940 private Uri insertFile(@NonNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper, 2941 int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, 2942 int mediaType) throws VolumeArgumentException, VolumeNotFoundException { 2943 boolean wasPathEmpty = !values.containsKey(MediaStore.MediaColumns.DATA) 2944 || TextUtils.isEmpty(values.getAsString(MediaStore.MediaColumns.DATA)); 2945 2946 // Make sure all file-related columns are defined 2947 ensureUniqueFileColumns(match, uri, extras, values, null); 2948 2949 switch (mediaType) { 2950 case FileColumns.MEDIA_TYPE_AUDIO: { 2951 computeAudioLocalizedValues(values); 2952 computeAudioKeyValues(values); 2953 break; 2954 } 2955 } 2956 2957 // compute bucket_id and bucket_display_name for all files 2958 String path = values.getAsString(MediaStore.MediaColumns.DATA); 2959 FileUtils.computeValuesFromData(values, isFuseThread()); 2960 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 2961 2962 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 2963 if (title == null && path != null) { 2964 title = extractFileName(path); 2965 } 2966 values.put(FileColumns.TITLE, title); 2967 2968 String mimeType = null; 2969 int format = MtpConstants.FORMAT_ASSOCIATION; 2970 if (path != null && new File(path).isDirectory()) { 2971 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION); 2972 values.putNull(MediaStore.MediaColumns.MIME_TYPE); 2973 } else { 2974 mimeType = values.getAsString(MediaStore.MediaColumns.MIME_TYPE); 2975 final Integer formatObject = values.getAsInteger(FileColumns.FORMAT); 2976 format = (formatObject == null ? 0 : formatObject.intValue()); 2977 } 2978 2979 if (format == 0) { 2980 format = MimeUtils.resolveFormatCode(mimeType); 2981 } 2982 if (path != null && path.endsWith("/")) { 2983 // TODO: convert to using FallbackException once VERSION_CODES.S is defined 2984 Log.e(TAG, "directory has trailing slash: " + path); 2985 return null; 2986 } 2987 if (format != 0) { 2988 values.put(FileColumns.FORMAT, format); 2989 } 2990 2991 if (mimeType == null && path != null && format != MtpConstants.FORMAT_ASSOCIATION) { 2992 mimeType = MimeUtils.resolveMimeType(new File(path)); 2993 } 2994 2995 if (mimeType != null) { 2996 values.put(FileColumns.MIME_TYPE, mimeType); 2997 if (isCallingPackageSelf() && values.containsKey(FileColumns.MEDIA_TYPE)) { 2998 // Leave FileColumns.MEDIA_TYPE untouched if the caller is ModernMediaScanner and 2999 // FileColumns.MEDIA_TYPE is already populated. 3000 } else if (path != null && shouldFileBeHidden(new File(path))) { 3001 values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE); 3002 } else { 3003 values.put(FileColumns.MEDIA_TYPE, MimeUtils.resolveMediaType(mimeType)); 3004 } 3005 } else { 3006 values.put(FileColumns.MEDIA_TYPE, mediaType); 3007 } 3008 3009 final long rowId; 3010 { 3011 if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 3012 String name = values.getAsString(Audio.Playlists.NAME); 3013 if (name == null && path == null) { 3014 // MediaScanner will compute the name from the path if we have one 3015 throw new IllegalArgumentException( 3016 "no name was provided when inserting abstract playlist"); 3017 } 3018 } else { 3019 if (path == null) { 3020 // path might be null for playlists created on the device 3021 // or transfered via MTP 3022 throw new IllegalArgumentException( 3023 "no path was provided when inserting new file"); 3024 } 3025 } 3026 3027 // make sure modification date and size are set 3028 if (path != null) { 3029 File file = new File(path); 3030 if (file.exists()) { 3031 values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000); 3032 if (!values.containsKey(FileColumns.SIZE)) { 3033 values.put(FileColumns.SIZE, file.length()); 3034 } 3035 } 3036 } 3037 3038 rowId = insertAllowingUpsert(qb, helper, values, path); 3039 } 3040 if (format == MtpConstants.FORMAT_ASSOCIATION) { 3041 synchronized (mDirectoryCache) { 3042 mDirectoryCache.put(path, rowId); 3043 } 3044 } 3045 3046 return ContentUris.withAppendedId(uri, rowId); 3047 } 3048 3049 /** 3050 * Inserts a new row in MediaProvider database with {@code values}. Treats insert as upsert for 3051 * double inserts from same package. 3052 */ insertAllowingUpsert(@onNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper, @NonNull ContentValues values, String path)3053 private long insertAllowingUpsert(@NonNull SQLiteQueryBuilder qb, 3054 @NonNull DatabaseHelper helper, @NonNull ContentValues values, String path) 3055 throws SQLiteConstraintException { 3056 return helper.runWithTransaction((db) -> { 3057 Long parent = values.getAsLong(FileColumns.PARENT); 3058 if (parent == null) { 3059 if (path != null) { 3060 final long parentId = getParent(db, path); 3061 values.put(FileColumns.PARENT, parentId); 3062 } 3063 } 3064 3065 try { 3066 return qb.insert(helper, values); 3067 } catch (SQLiteConstraintException e) { 3068 final String packages = getAllowedPackagesForUpsert( 3069 values.getAsString(MediaColumns.OWNER_PACKAGE_NAME)); 3070 SQLiteQueryBuilder qbForUpsert = getQueryBuilderForUpsert(path); 3071 final long rowId = getIdIfPathOwnedByPackages(qbForUpsert, helper, path, packages); 3072 // Apps sometimes create a file via direct path and then insert it into 3073 // MediaStore via ContentResolver. The former should create a database entry, 3074 // so we have to treat the latter as an upsert. 3075 // TODO(b/149917493) Perform all INSERT operations as UPSERT. 3076 if (rowId != -1 && qbForUpsert.update(helper, values, "_id=?", 3077 new String[]{Long.toString(rowId)}) == 1) { 3078 return rowId; 3079 } 3080 // Rethrow SQLiteConstraintException on failed upsert. 3081 throw e; 3082 } 3083 }); 3084 } 3085 3086 /** 3087 * @return row id of the entry with path {@code path} if the owner is one of {@code packages}. 3088 */ getIdIfPathOwnedByPackages(@onNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper, String path, String packages)3089 private long getIdIfPathOwnedByPackages(@NonNull SQLiteQueryBuilder qb, 3090 @NonNull DatabaseHelper helper, String path, String packages) { 3091 final String[] projection = new String[] {FileColumns._ID}; 3092 final String ownerPackageMatchClause = DatabaseUtils.bindSelection( 3093 MediaColumns.OWNER_PACKAGE_NAME + " IN " + packages); 3094 final String selection = FileColumns.DATA + " =? AND " + ownerPackageMatchClause; 3095 3096 try (Cursor c = qb.query(helper, projection, selection, new String[] {path}, null, null, 3097 null, null, null)) { 3098 if (c.moveToFirst()) { 3099 return c.getLong(0); 3100 } 3101 } 3102 return -1; 3103 } 3104 3105 /** 3106 * Gets packages that should match to upsert a db row. 3107 * 3108 * A database row can be upserted if 3109 * <ul> 3110 * <li> Calling package or one of the shared packages owns the db row. 3111 * <li> {@code givenOwnerPackage} owns the db row. This is useful when DownloadProvider 3112 * requests upsert on behalf of another app 3113 * </ul> 3114 */ getAllowedPackagesForUpsert(@ullable String givenOwnerPackage)3115 private String getAllowedPackagesForUpsert(@Nullable String givenOwnerPackage) { 3116 ArrayList<String> packages = new ArrayList<>(); 3117 packages.addAll(Arrays.asList(mCallingIdentity.get().getSharedPackageNames())); 3118 3119 // If givenOwnerPackage is CallingIdentity, packages list would already have shared package 3120 // names of givenOwnerPackage. If givenOwnerPackage is not CallingIdentity, since 3121 // DownloadProvider can upsert a row on behalf of app, we should include all shared packages 3122 // of givenOwnerPackage. 3123 if (givenOwnerPackage != null && isCallingPackageDelegator() && 3124 !isCallingIdentitySharedPackageName(givenOwnerPackage)) { 3125 // Allow DownloadProvider to Upsert if givenOwnerPackage is owner of the db row. 3126 packages.addAll(Arrays.asList(getSharedPackagesForPackage(givenOwnerPackage))); 3127 } 3128 return bindList((Object[]) packages.toArray()); 3129 } 3130 3131 /** 3132 * @return {@link SQLiteQueryBuilder} for upsert with Files uri. This disables strict columns 3133 * check to allow upsert to update any column with Files uri. 3134 */ getQueryBuilderForUpsert(@onNull String path)3135 private SQLiteQueryBuilder getQueryBuilderForUpsert(@NonNull String path) { 3136 final boolean allowHidden = isCallingPackageAllowedHidden(); 3137 Bundle extras = new Bundle(); 3138 extras.putInt(QUERY_ARG_MATCH_PENDING, MATCH_INCLUDE); 3139 extras.putInt(QUERY_ARG_MATCH_TRASHED, MATCH_INCLUDE); 3140 3141 // When Fuse inserts a file to database it doesn't set is_download column. When app tries 3142 // insert with Downloads uri, upsert fails because getIdIfPathExistsForCallingPackage can't 3143 // find a row ID with is_download=1. Use Files uri to get queryBuilder & update any existing 3144 // row irrespective of is_download=1. 3145 final Uri uri = FileUtils.getContentUriForPath(path); 3146 SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, matchUri(uri, allowHidden), uri, 3147 extras, null); 3148 3149 // We won't be able to update columns that are not part of projection map of Files table. We 3150 // have already checked strict columns in previous insert operation which failed with 3151 // exception. Any malicious column usage would have got caught in insert operation, hence we 3152 // can safely disable strict column check for upsert. 3153 qb.setStrictColumns(false); 3154 return qb; 3155 } 3156 maybePut(@onNull ContentValues values, @NonNull String key, @Nullable String value)3157 private void maybePut(@NonNull ContentValues values, @NonNull String key, 3158 @Nullable String value) { 3159 if (value != null) { 3160 values.put(key, value); 3161 } 3162 } 3163 maybeMarkAsDownload(@onNull ContentValues values)3164 private boolean maybeMarkAsDownload(@NonNull ContentValues values) { 3165 final String path = values.getAsString(MediaColumns.DATA); 3166 if (path != null && isDownload(path)) { 3167 values.put(FileColumns.IS_DOWNLOAD, 1); 3168 return true; 3169 } 3170 return false; 3171 } 3172 resolveVolumeName(@onNull Uri uri)3173 private static @NonNull String resolveVolumeName(@NonNull Uri uri) { 3174 final String volumeName = getVolumeName(uri); 3175 if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) { 3176 return MediaStore.VOLUME_EXTERNAL_PRIMARY; 3177 } else { 3178 return volumeName; 3179 } 3180 } 3181 3182 /** 3183 * @deprecated all operations should be routed through the overload that 3184 * accepts a {@link Bundle} of extras. 3185 */ 3186 @Override 3187 @Deprecated insert(Uri uri, ContentValues values)3188 public Uri insert(Uri uri, ContentValues values) { 3189 return insert(uri, values, null); 3190 } 3191 3192 @Override insert(@onNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras)3193 public @Nullable Uri insert(@NonNull Uri uri, @Nullable ContentValues values, 3194 @Nullable Bundle extras) { 3195 Trace.beginSection("insert"); 3196 try { 3197 try { 3198 return insertInternal(uri, values, extras); 3199 } catch (SQLiteConstraintException e) { 3200 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) { 3201 throw e; 3202 } else { 3203 return null; 3204 } 3205 } 3206 } catch (FallbackException e) { 3207 return e.translateForInsert(getCallingPackageTargetSdkVersion()); 3208 } finally { 3209 Trace.endSection(); 3210 } 3211 } 3212 insertInternal(@onNull Uri uri, @Nullable ContentValues initialValues, @Nullable Bundle extras)3213 private @Nullable Uri insertInternal(@NonNull Uri uri, @Nullable ContentValues initialValues, 3214 @Nullable Bundle extras) throws FallbackException { 3215 extras = (extras != null) ? extras : new Bundle(); 3216 3217 // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. 3218 extras.remove(INCLUDED_DEFAULT_DIRECTORIES); 3219 3220 final boolean allowHidden = isCallingPackageAllowedHidden(); 3221 final int match = matchUri(uri, allowHidden); 3222 3223 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 3224 final String originalVolumeName = getVolumeName(uri); 3225 final String resolvedVolumeName = resolveVolumeName(uri); 3226 3227 // handle MEDIA_SCANNER before calling getDatabaseForUri() 3228 if (match == MEDIA_SCANNER) { 3229 mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME); 3230 3231 final DatabaseHelper helper = getDatabaseForUri( 3232 MediaStore.Files.getContentUri(mMediaScannerVolume)); 3233 3234 helper.mScanStartTime = SystemClock.elapsedRealtime(); 3235 return MediaStore.getMediaScannerUri(); 3236 } 3237 3238 if (match == VOLUMES) { 3239 String name = initialValues.getAsString("name"); 3240 Uri attachedVolume = attachVolume(name, /* validate */ true); 3241 if (mMediaScannerVolume != null && mMediaScannerVolume.equals(name)) { 3242 final DatabaseHelper helper = getDatabaseForUri( 3243 MediaStore.Files.getContentUri(mMediaScannerVolume)); 3244 helper.mScanStartTime = SystemClock.elapsedRealtime(); 3245 } 3246 return attachedVolume; 3247 } 3248 3249 switch (match) { 3250 case AUDIO_PLAYLISTS_ID: 3251 case AUDIO_PLAYLISTS_ID_MEMBERS: { 3252 final long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 3253 final Uri playlistUri = ContentUris.withAppendedId( 3254 MediaStore.Audio.Playlists.getContentUri(resolvedVolumeName), playlistId); 3255 3256 final long audioId = initialValues 3257 .getAsLong(MediaStore.Audio.Playlists.Members.AUDIO_ID); 3258 final Uri audioUri = ContentUris.withAppendedId( 3259 MediaStore.Audio.Media.getContentUri(resolvedVolumeName), audioId); 3260 3261 // Require that caller has write access to underlying media 3262 enforceCallingPermission(playlistUri, Bundle.EMPTY, true); 3263 enforceCallingPermission(audioUri, Bundle.EMPTY, false); 3264 3265 // Playlist contents are always persisted directly into playlist 3266 // files on disk to ensure that we can reliably migrate between 3267 // devices and recover from database corruption 3268 final long id = addPlaylistMembers(playlistUri, initialValues); 3269 return ContentUris.withAppendedId(MediaStore.Audio.Playlists.Members 3270 .getContentUri(originalVolumeName, playlistId), id); 3271 } 3272 } 3273 3274 String path = null; 3275 String ownerPackageName = null; 3276 if (initialValues != null) { 3277 // IDs are forever; nobody should be editing them 3278 initialValues.remove(MediaColumns._ID); 3279 3280 // Expiration times are hard-coded; let's derive them 3281 FileUtils.computeDateExpires(initialValues); 3282 3283 // Ignore or augment incoming raw filesystem paths 3284 for (String column : sDataColumns.keySet()) { 3285 if (!initialValues.containsKey(column)) continue; 3286 3287 if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) { 3288 // Mutation allowed 3289 } else { 3290 Log.w(TAG, "Ignoring mutation of " + column + " from " 3291 + getCallingPackageOrSelf()); 3292 initialValues.remove(column); 3293 } 3294 } 3295 3296 path = initialValues.getAsString(MediaStore.MediaColumns.DATA); 3297 3298 if (!isCallingPackageSelf()) { 3299 initialValues.remove(FileColumns.IS_DOWNLOAD); 3300 } 3301 3302 // We no longer track location metadata 3303 if (initialValues.containsKey(ImageColumns.LATITUDE)) { 3304 initialValues.putNull(ImageColumns.LATITUDE); 3305 } 3306 if (initialValues.containsKey(ImageColumns.LONGITUDE)) { 3307 initialValues.putNull(ImageColumns.LONGITUDE); 3308 } 3309 3310 if (isCallingPackageSelf() || isCallingPackageShell()) { 3311 // When media inserted by ourselves during a scan, or by the 3312 // shell, the best we can do is guess ownership based on path 3313 // when it's not explicitly provided 3314 ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME); 3315 if (TextUtils.isEmpty(ownerPackageName)) { 3316 ownerPackageName = extractPathOwnerPackageName(path); 3317 } 3318 } else if (isCallingPackageDelegator()) { 3319 // When caller is a delegator, we handle ownership as a hybrid 3320 // of the two other cases: we're willing to accept any ownership 3321 // transfer attempted during insert, but we fall back to using 3322 // the Binder identity if they don't request a specific owner 3323 ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME); 3324 if (TextUtils.isEmpty(ownerPackageName)) { 3325 ownerPackageName = getCallingPackageOrSelf(); 3326 } 3327 } else { 3328 // Remote callers have no direct control over owner column; we force 3329 // it be whoever is creating the content. 3330 initialValues.remove(FileColumns.OWNER_PACKAGE_NAME); 3331 ownerPackageName = getCallingPackageOrSelf(); 3332 } 3333 } 3334 3335 long rowId = -1; 3336 Uri newUri = null; 3337 3338 final DatabaseHelper helper = getDatabaseForUri(uri); 3339 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_INSERT, match, uri, extras, null); 3340 3341 switch (match) { 3342 case IMAGES_MEDIA: { 3343 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 3344 final boolean isDownload = maybeMarkAsDownload(initialValues); 3345 newUri = insertFile(qb, helper, match, uri, extras, initialValues, 3346 FileColumns.MEDIA_TYPE_IMAGE); 3347 break; 3348 } 3349 3350 case IMAGES_THUMBNAILS: { 3351 if (helper.mInternal) { 3352 throw new UnsupportedOperationException( 3353 "Writing to internal storage is not supported."); 3354 } 3355 3356 // Require that caller has write access to underlying media 3357 final long imageId = initialValues.getAsLong(MediaStore.Images.Thumbnails.IMAGE_ID); 3358 enforceCallingPermission(ContentUris.withAppendedId( 3359 MediaStore.Images.Media.getContentUri(resolvedVolumeName), imageId), 3360 extras, true); 3361 3362 ensureUniqueFileColumns(match, uri, extras, initialValues, null); 3363 3364 rowId = qb.insert(helper, initialValues); 3365 if (rowId > 0) { 3366 newUri = ContentUris.withAppendedId(Images.Thumbnails. 3367 getContentUri(originalVolumeName), rowId); 3368 } 3369 break; 3370 } 3371 3372 case VIDEO_THUMBNAILS: { 3373 if (helper.mInternal) { 3374 throw new UnsupportedOperationException( 3375 "Writing to internal storage is not supported."); 3376 } 3377 3378 // Require that caller has write access to underlying media 3379 final long videoId = initialValues.getAsLong(MediaStore.Video.Thumbnails.VIDEO_ID); 3380 enforceCallingPermission(ContentUris.withAppendedId( 3381 MediaStore.Video.Media.getContentUri(resolvedVolumeName), videoId), 3382 Bundle.EMPTY, true); 3383 3384 ensureUniqueFileColumns(match, uri, extras, initialValues, null); 3385 3386 rowId = qb.insert(helper, initialValues); 3387 if (rowId > 0) { 3388 newUri = ContentUris.withAppendedId(Video.Thumbnails. 3389 getContentUri(originalVolumeName), rowId); 3390 } 3391 break; 3392 } 3393 3394 case AUDIO_MEDIA: { 3395 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 3396 final boolean isDownload = maybeMarkAsDownload(initialValues); 3397 newUri = insertFile(qb, helper, match, uri, extras, initialValues, 3398 FileColumns.MEDIA_TYPE_AUDIO); 3399 break; 3400 } 3401 3402 case AUDIO_MEDIA_ID_GENRES: { 3403 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R); 3404 } 3405 3406 case AUDIO_GENRES: { 3407 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R); 3408 } 3409 3410 case AUDIO_GENRES_ID_MEMBERS: { 3411 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R); 3412 } 3413 3414 case AUDIO_PLAYLISTS: { 3415 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 3416 final boolean isDownload = maybeMarkAsDownload(initialValues); 3417 ContentValues values = new ContentValues(initialValues); 3418 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000); 3419 // Playlist names are stored as display names, but leave 3420 // values untouched if the caller is ModernMediaScanner 3421 if (!isCallingPackageSelf()) { 3422 if (values.containsKey(Playlists.NAME)) { 3423 values.put(MediaColumns.DISPLAY_NAME, values.getAsString(Playlists.NAME)); 3424 } 3425 if (!values.containsKey(MediaColumns.MIME_TYPE)) { 3426 values.put(MediaColumns.MIME_TYPE, "audio/mpegurl"); 3427 } 3428 } 3429 newUri = insertFile(qb, helper, match, uri, extras, values, 3430 FileColumns.MEDIA_TYPE_PLAYLIST); 3431 if (newUri != null) { 3432 // Touch empty playlist file on disk so its ready for renames 3433 if (Binder.getCallingUid() != android.os.Process.myUid()) { 3434 try (OutputStream out = ContentResolver.wrap(this) 3435 .openOutputStream(newUri)) { 3436 } catch (IOException ignored) { 3437 } 3438 } 3439 } 3440 break; 3441 } 3442 3443 case VIDEO_MEDIA: { 3444 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 3445 final boolean isDownload = maybeMarkAsDownload(initialValues); 3446 newUri = insertFile(qb, helper, match, uri, extras, initialValues, 3447 FileColumns.MEDIA_TYPE_VIDEO); 3448 break; 3449 } 3450 3451 case AUDIO_ALBUMART: { 3452 if (helper.mInternal) { 3453 throw new UnsupportedOperationException("no internal album art allowed"); 3454 } 3455 3456 ensureUniqueFileColumns(match, uri, extras, initialValues, null); 3457 3458 rowId = qb.insert(helper, initialValues); 3459 if (rowId > 0) { 3460 newUri = ContentUris.withAppendedId(uri, rowId); 3461 } 3462 break; 3463 } 3464 3465 case FILES: { 3466 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 3467 final boolean isDownload = maybeMarkAsDownload(initialValues); 3468 final String mimeType = initialValues.getAsString(MediaColumns.MIME_TYPE); 3469 final int mediaType = MimeUtils.resolveMediaType(mimeType); 3470 newUri = insertFile(qb, helper, match, uri, extras, initialValues, 3471 mediaType); 3472 break; 3473 } 3474 3475 case DOWNLOADS: 3476 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 3477 initialValues.put(FileColumns.IS_DOWNLOAD, 1); 3478 newUri = insertFile(qb, helper, match, uri, extras, initialValues, 3479 FileColumns.MEDIA_TYPE_NONE); 3480 break; 3481 3482 default: 3483 throw new UnsupportedOperationException("Invalid URI " + uri); 3484 } 3485 3486 // Remember that caller is owner of this item, to speed up future 3487 // permission checks for this caller 3488 mCallingIdentity.get().setOwned(rowId, true); 3489 3490 if (path != null && path.toLowerCase(Locale.ROOT).endsWith("/.nomedia")) { 3491 mMediaScanner.scanFile(new File(path).getParentFile(), REASON_DEMAND); 3492 } 3493 3494 return newUri; 3495 } 3496 3497 @Override applyBatch(ArrayList<ContentProviderOperation> operations)3498 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 3499 throws OperationApplicationException { 3500 // Open transactions on databases for requested volumes 3501 final Set<DatabaseHelper> transactions = new ArraySet<>(); 3502 try { 3503 for (ContentProviderOperation op : operations) { 3504 final DatabaseHelper helper = getDatabaseForUri(op.getUri()); 3505 if (transactions.contains(helper)) continue; 3506 3507 if (!helper.isTransactionActive()) { 3508 helper.beginTransaction(); 3509 transactions.add(helper); 3510 } else { 3511 // We normally don't allow nested transactions (since we 3512 // don't have a good way to selectively roll them back) but 3513 // if the incoming operation is ignoring exceptions, then we 3514 // don't need to worry about partial rollback and can 3515 // piggyback on the larger active transaction 3516 if (!op.isExceptionAllowed()) { 3517 throw new IllegalStateException("Nested transactions not supported"); 3518 } 3519 } 3520 } 3521 3522 final ContentProviderResult[] result = super.applyBatch(operations); 3523 for (DatabaseHelper helper : transactions) { 3524 helper.setTransactionSuccessful(); 3525 } 3526 return result; 3527 } catch (VolumeNotFoundException e) { 3528 throw e.rethrowAsIllegalArgumentException(); 3529 } finally { 3530 for (DatabaseHelper helper : transactions) { 3531 helper.endTransaction(); 3532 } 3533 } 3534 } 3535 appendWhereStandaloneMatch(@onNull SQLiteQueryBuilder qb, @NonNull String column, int match, Uri uri)3536 private void appendWhereStandaloneMatch(@NonNull SQLiteQueryBuilder qb, 3537 @NonNull String column, /* @Match */ int match, Uri uri) { 3538 switch (match) { 3539 case MATCH_INCLUDE: 3540 // No special filtering needed 3541 break; 3542 case MATCH_EXCLUDE: 3543 appendWhereStandalone(qb, getWhereClauseForMatchExclude(column)); 3544 break; 3545 case MATCH_ONLY: 3546 appendWhereStandalone(qb, column + "=?", 1); 3547 break; 3548 case MATCH_VISIBLE_FOR_FILEPATH: 3549 final String whereClause = 3550 getWhereClauseForMatchableVisibleFromFilePath(uri, column); 3551 if (whereClause != null) { 3552 appendWhereStandalone(qb, whereClause); 3553 } 3554 break; 3555 default: 3556 throw new IllegalArgumentException(); 3557 } 3558 } 3559 appendWhereStandalone(@onNull SQLiteQueryBuilder qb, @Nullable String selection, @Nullable Object... selectionArgs)3560 private static void appendWhereStandalone(@NonNull SQLiteQueryBuilder qb, 3561 @Nullable String selection, @Nullable Object... selectionArgs) { 3562 qb.appendWhereStandalone(DatabaseUtils.bindSelection(selection, selectionArgs)); 3563 } 3564 appendWhereStandaloneFilter(@onNull SQLiteQueryBuilder qb, @NonNull String[] columns, @Nullable String filter)3565 private static void appendWhereStandaloneFilter(@NonNull SQLiteQueryBuilder qb, 3566 @NonNull String[] columns, @Nullable String filter) { 3567 if (TextUtils.isEmpty(filter)) return; 3568 for (String filterWord : filter.split("\\s+")) { 3569 appendWhereStandalone(qb, String.join("||", columns) + " LIKE ? ESCAPE '\\'", 3570 "%" + DatabaseUtils.escapeForLike(Audio.keyFor(filterWord)) + "%"); 3571 } 3572 } 3573 3574 @Deprecated getSharedPackages()3575 private String getSharedPackages() { 3576 final String[] sharedPackageNames = mCallingIdentity.get().getSharedPackageNames(); 3577 return bindList((Object[]) sharedPackageNames); 3578 } 3579 3580 /** 3581 * Gets shared packages names for given {@code packageName} 3582 */ getSharedPackagesForPackage(String packageName)3583 private String[] getSharedPackagesForPackage(String packageName) { 3584 try { 3585 final int packageUid = getContext().getPackageManager() 3586 .getPackageUid(packageName, 0); 3587 return getContext().getPackageManager().getPackagesForUid(packageUid); 3588 } catch (NameNotFoundException ignored) { 3589 return new String[] {packageName}; 3590 } 3591 } 3592 3593 private static final int TYPE_QUERY = 0; 3594 private static final int TYPE_INSERT = 1; 3595 private static final int TYPE_UPDATE = 2; 3596 private static final int TYPE_DELETE = 3; 3597 3598 /** 3599 * Generate a {@link SQLiteQueryBuilder} that is filtered based on the 3600 * runtime permissions and/or {@link Uri} grants held by the caller. 3601 * <ul> 3602 * <li>If caller holds a {@link Uri} grant, access is allowed according to 3603 * that grant. 3604 * <li>If caller holds the write permission for a collection, they can 3605 * read/write all contents of that collection. 3606 * <li>If caller holds the read permission for a collection, they can read 3607 * all contents of that collection, but writes are limited to content they 3608 * own. 3609 * <li>If caller holds no permissions for a collection, all reads/write are 3610 * limited to content they own. 3611 * </ul> 3612 */ getQueryBuilder(int type, int match, @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored)3613 private @NonNull SQLiteQueryBuilder getQueryBuilder(int type, int match, 3614 @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) { 3615 Trace.beginSection("getQueryBuilder"); 3616 try { 3617 return getQueryBuilderInternal(type, match, uri, extras, honored); 3618 } finally { 3619 Trace.endSection(); 3620 } 3621 } 3622 getQueryBuilderInternal(int type, int match, @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored)3623 private @NonNull SQLiteQueryBuilder getQueryBuilderInternal(int type, int match, 3624 @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) { 3625 final boolean forWrite; 3626 switch (type) { 3627 case TYPE_QUERY: forWrite = false; break; 3628 case TYPE_INSERT: forWrite = true; break; 3629 case TYPE_UPDATE: forWrite = true; break; 3630 case TYPE_DELETE: forWrite = true; break; 3631 default: throw new IllegalStateException(); 3632 } 3633 3634 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 3635 if (uri.getBooleanQueryParameter("distinct", false)) { 3636 qb.setDistinct(true); 3637 } 3638 qb.setStrict(true); 3639 if (isCallingPackageSelf()) { 3640 // When caller is system, such as the media scanner, we're willing 3641 // to let them access any columns they want 3642 } else { 3643 qb.setTargetSdkVersion(getCallingPackageTargetSdkVersion()); 3644 qb.setStrictColumns(true); 3645 qb.setStrictGrammar(true); 3646 } 3647 3648 // TODO: throw when requesting a currently unmounted volume 3649 final String volumeName = MediaStore.getVolumeName(uri); 3650 final String includeVolumes; 3651 if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) { 3652 includeVolumes = bindList(getExternalVolumeNames().toArray()); 3653 } else { 3654 includeVolumes = bindList(volumeName); 3655 } 3656 final String sharedPackages = getSharedPackages(); 3657 final String matchSharedPackagesClause = FileColumns.OWNER_PACKAGE_NAME + " IN " 3658 + sharedPackages; 3659 3660 final boolean allowGlobal = checkCallingPermissionGlobal(uri, forWrite); 3661 final boolean allowLegacy = 3662 forWrite ? isCallingPackageLegacyWrite() : isCallingPackageLegacyRead(); 3663 final boolean allowLegacyRead = allowLegacy && !forWrite; 3664 3665 int matchPending = extras.getInt(QUERY_ARG_MATCH_PENDING, MATCH_DEFAULT); 3666 int matchTrashed = extras.getInt(QUERY_ARG_MATCH_TRASHED, MATCH_DEFAULT); 3667 int matchFavorite = extras.getInt(QUERY_ARG_MATCH_FAVORITE, MATCH_DEFAULT); 3668 3669 final ArrayList<String> includedDefaultDirs = extras.getStringArrayList( 3670 INCLUDED_DEFAULT_DIRECTORIES); 3671 3672 // Handle callers using legacy arguments 3673 if (MediaStore.getIncludePending(uri)) matchPending = MATCH_INCLUDE; 3674 3675 // Resolve any remaining default options 3676 final int defaultMatchForPendingAndTrashed; 3677 if (isFuseThread()) { 3678 // Write operations always check for file ownership, we don't need additional write 3679 // permission check for is_pending and is_trashed. 3680 defaultMatchForPendingAndTrashed = 3681 forWrite ? MATCH_INCLUDE : MATCH_VISIBLE_FOR_FILEPATH; 3682 } else { 3683 defaultMatchForPendingAndTrashed = MATCH_EXCLUDE; 3684 } 3685 if (matchPending == MATCH_DEFAULT) matchPending = defaultMatchForPendingAndTrashed; 3686 if (matchTrashed == MATCH_DEFAULT) matchTrashed = defaultMatchForPendingAndTrashed; 3687 if (matchFavorite == MATCH_DEFAULT) matchFavorite = MATCH_INCLUDE; 3688 3689 // Handle callers using legacy filtering 3690 final String filter = uri.getQueryParameter("filter"); 3691 3692 boolean includeAllVolumes = false; 3693 final String callingPackage = getCallingPackageOrSelf(); 3694 3695 switch (match) { 3696 case IMAGES_MEDIA_ID: 3697 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3698 matchPending = MATCH_INCLUDE; 3699 matchTrashed = MATCH_INCLUDE; 3700 // fall-through 3701 case IMAGES_MEDIA: { 3702 if (type == TYPE_QUERY) { 3703 qb.setTables("images"); 3704 qb.setProjectionMap( 3705 getProjectionMap(Images.Media.class)); 3706 } else { 3707 qb.setTables("files"); 3708 qb.setProjectionMap( 3709 getProjectionMap(Images.Media.class, Files.FileColumns.class)); 3710 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 3711 FileColumns.MEDIA_TYPE_IMAGE); 3712 } 3713 if (!allowGlobal && !checkCallingPermissionImages(forWrite, callingPackage)) { 3714 appendWhereStandalone(qb, matchSharedPackagesClause); 3715 } 3716 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); 3717 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); 3718 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 3719 if (honored != null) { 3720 honored.accept(QUERY_ARG_MATCH_PENDING); 3721 honored.accept(QUERY_ARG_MATCH_TRASHED); 3722 honored.accept(QUERY_ARG_MATCH_FAVORITE); 3723 } 3724 if (!includeAllVolumes) { 3725 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 3726 } 3727 break; 3728 } 3729 case IMAGES_THUMBNAILS_ID: 3730 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3731 // fall-through 3732 case IMAGES_THUMBNAILS: { 3733 qb.setTables("thumbnails"); 3734 3735 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 3736 getProjectionMap(Images.Thumbnails.class)); 3737 projectionMap.put(Images.Thumbnails.THUMB_DATA, 3738 "NULL AS " + Images.Thumbnails.THUMB_DATA); 3739 qb.setProjectionMap(projectionMap); 3740 3741 if (!allowGlobal && !checkCallingPermissionImages(forWrite, callingPackage)) { 3742 appendWhereStandalone(qb, 3743 "image_id IN (SELECT _id FROM images WHERE " 3744 + matchSharedPackagesClause + ")"); 3745 } 3746 break; 3747 } 3748 case AUDIO_MEDIA_ID: 3749 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3750 matchPending = MATCH_INCLUDE; 3751 matchTrashed = MATCH_INCLUDE; 3752 // fall-through 3753 case AUDIO_MEDIA: { 3754 if (type == TYPE_QUERY) { 3755 qb.setTables("audio"); 3756 qb.setProjectionMap( 3757 getProjectionMap(Audio.Media.class)); 3758 } else { 3759 qb.setTables("files"); 3760 qb.setProjectionMap( 3761 getProjectionMap(Audio.Media.class, Files.FileColumns.class)); 3762 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 3763 FileColumns.MEDIA_TYPE_AUDIO); 3764 } 3765 if (!allowGlobal && !checkCallingPermissionAudio(forWrite, callingPackage)) { 3766 // Apps without Audio permission can only see their own 3767 // media, but we also let them see ringtone-style media to 3768 // support legacy use-cases. 3769 appendWhereStandalone(qb, 3770 DatabaseUtils.bindSelection(matchSharedPackagesClause 3771 + " OR is_ringtone=1 OR is_alarm=1 OR is_notification=1")); 3772 } 3773 appendWhereStandaloneFilter(qb, new String[] { 3774 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY 3775 }, filter); 3776 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); 3777 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); 3778 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 3779 if (honored != null) { 3780 honored.accept(QUERY_ARG_MATCH_PENDING); 3781 honored.accept(QUERY_ARG_MATCH_TRASHED); 3782 honored.accept(QUERY_ARG_MATCH_FAVORITE); 3783 } 3784 if (!includeAllVolumes) { 3785 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 3786 } 3787 break; 3788 } 3789 case AUDIO_MEDIA_ID_GENRES_ID: 3790 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(5)); 3791 // fall-through 3792 case AUDIO_MEDIA_ID_GENRES: { 3793 if (type == TYPE_QUERY) { 3794 qb.setTables("audio_genres"); 3795 qb.setProjectionMap(getProjectionMap(Audio.Genres.class)); 3796 } else { 3797 throw new UnsupportedOperationException("Genres cannot be directly modified"); 3798 } 3799 appendWhereStandalone(qb, "_id IN (SELECT genre_id FROM " + 3800 "audio WHERE _id=?)", uri.getPathSegments().get(3)); 3801 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3802 // We don't have a great way to filter parsed metadata by 3803 // owner, so callers need to hold READ_MEDIA_AUDIO 3804 appendWhereStandalone(qb, "0"); 3805 } 3806 break; 3807 } 3808 case AUDIO_GENRES_ID: 3809 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3810 // fall-through 3811 case AUDIO_GENRES: { 3812 qb.setTables("audio_genres"); 3813 qb.setProjectionMap(getProjectionMap(Audio.Genres.class)); 3814 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3815 // We don't have a great way to filter parsed metadata by 3816 // owner, so callers need to hold READ_MEDIA_AUDIO 3817 appendWhereStandalone(qb, "0"); 3818 } 3819 break; 3820 } 3821 case AUDIO_GENRES_ID_MEMBERS: 3822 appendWhereStandalone(qb, "genre_id=?", uri.getPathSegments().get(3)); 3823 // fall-through 3824 case AUDIO_GENRES_ALL_MEMBERS: { 3825 if (type == TYPE_QUERY) { 3826 qb.setTables("audio"); 3827 3828 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 3829 getProjectionMap(Audio.Genres.Members.class)); 3830 projectionMap.put(Audio.Genres.Members.AUDIO_ID, 3831 "_id AS " + Audio.Genres.Members.AUDIO_ID); 3832 qb.setProjectionMap(projectionMap); 3833 } else { 3834 throw new UnsupportedOperationException("Genres cannot be directly modified"); 3835 } 3836 appendWhereStandaloneFilter(qb, new String[] { 3837 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY 3838 }, filter); 3839 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3840 // We don't have a great way to filter parsed metadata by 3841 // owner, so callers need to hold READ_MEDIA_AUDIO 3842 appendWhereStandalone(qb, "0"); 3843 } 3844 break; 3845 } 3846 case AUDIO_PLAYLISTS_ID: 3847 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3848 matchPending = MATCH_INCLUDE; 3849 matchTrashed = MATCH_INCLUDE; 3850 // fall-through 3851 case AUDIO_PLAYLISTS: { 3852 if (type == TYPE_QUERY) { 3853 qb.setTables("audio_playlists"); 3854 qb.setProjectionMap( 3855 getProjectionMap(Audio.Playlists.class)); 3856 } else { 3857 qb.setTables("files"); 3858 qb.setProjectionMap( 3859 getProjectionMap(Audio.Playlists.class, Files.FileColumns.class)); 3860 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 3861 FileColumns.MEDIA_TYPE_PLAYLIST); 3862 } 3863 if (!allowGlobal && !checkCallingPermissionAudio(forWrite, callingPackage)) { 3864 appendWhereStandalone(qb, matchSharedPackagesClause); 3865 } 3866 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); 3867 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); 3868 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 3869 if (honored != null) { 3870 honored.accept(QUERY_ARG_MATCH_PENDING); 3871 honored.accept(QUERY_ARG_MATCH_TRASHED); 3872 honored.accept(QUERY_ARG_MATCH_FAVORITE); 3873 } 3874 if (!includeAllVolumes) { 3875 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 3876 } 3877 break; 3878 } 3879 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 3880 appendWhereStandalone(qb, "audio_playlists_map._id=?", 3881 uri.getPathSegments().get(5)); 3882 // fall-through 3883 case AUDIO_PLAYLISTS_ID_MEMBERS: { 3884 appendWhereStandalone(qb, "playlist_id=?", uri.getPathSegments().get(3)); 3885 if (type == TYPE_QUERY) { 3886 qb.setTables("audio_playlists_map, audio"); 3887 3888 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 3889 getProjectionMap(Audio.Playlists.Members.class)); 3890 projectionMap.put(Audio.Playlists.Members._ID, 3891 "audio_playlists_map._id AS " + Audio.Playlists.Members._ID); 3892 qb.setProjectionMap(projectionMap); 3893 3894 appendWhereStandalone(qb, "audio._id = audio_id"); 3895 } else { 3896 qb.setTables("audio_playlists_map"); 3897 qb.setProjectionMap(getProjectionMap(Audio.Playlists.Members.class)); 3898 } 3899 appendWhereStandaloneFilter(qb, new String[] { 3900 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY 3901 }, filter); 3902 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3903 // We don't have a great way to filter parsed metadata by 3904 // owner, so callers need to hold READ_MEDIA_AUDIO 3905 appendWhereStandalone(qb, "0"); 3906 } 3907 break; 3908 } 3909 case AUDIO_ALBUMART_ID: 3910 appendWhereStandalone(qb, "album_id=?", uri.getPathSegments().get(3)); 3911 // fall-through 3912 case AUDIO_ALBUMART: { 3913 qb.setTables("album_art"); 3914 3915 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 3916 getProjectionMap(Audio.Thumbnails.class)); 3917 projectionMap.put(Audio.Thumbnails._ID, 3918 "album_id AS " + Audio.Thumbnails._ID); 3919 qb.setProjectionMap(projectionMap); 3920 3921 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3922 // We don't have a great way to filter parsed metadata by 3923 // owner, so callers need to hold READ_MEDIA_AUDIO 3924 appendWhereStandalone(qb, "0"); 3925 } 3926 break; 3927 } 3928 case AUDIO_ARTISTS_ID_ALBUMS: { 3929 if (type == TYPE_QUERY) { 3930 qb.setTables("audio_albums"); 3931 qb.setProjectionMap(getProjectionMap(Audio.Artists.Albums.class)); 3932 3933 final String artistId = uri.getPathSegments().get(3); 3934 appendWhereStandalone(qb, "artist_id=?", artistId); 3935 } else { 3936 throw new UnsupportedOperationException("Albums cannot be directly modified"); 3937 } 3938 appendWhereStandaloneFilter(qb, new String[] { 3939 AudioColumns.ALBUM_KEY 3940 }, filter); 3941 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3942 // We don't have a great way to filter parsed metadata by 3943 // owner, so callers need to hold READ_MEDIA_AUDIO 3944 appendWhereStandalone(qb, "0"); 3945 } 3946 break; 3947 } 3948 case AUDIO_ARTISTS_ID: 3949 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3950 // fall-through 3951 case AUDIO_ARTISTS: { 3952 if (type == TYPE_QUERY) { 3953 qb.setTables("audio_artists"); 3954 qb.setProjectionMap(getProjectionMap(Audio.Artists.class)); 3955 } else { 3956 throw new UnsupportedOperationException("Artists cannot be directly modified"); 3957 } 3958 appendWhereStandaloneFilter(qb, new String[] { 3959 AudioColumns.ARTIST_KEY 3960 }, filter); 3961 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3962 // We don't have a great way to filter parsed metadata by 3963 // owner, so callers need to hold READ_MEDIA_AUDIO 3964 appendWhereStandalone(qb, "0"); 3965 } 3966 break; 3967 } 3968 case AUDIO_ALBUMS_ID: 3969 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3970 // fall-through 3971 case AUDIO_ALBUMS: { 3972 if (type == TYPE_QUERY) { 3973 qb.setTables("audio_albums"); 3974 qb.setProjectionMap(getProjectionMap(Audio.Albums.class)); 3975 } else { 3976 throw new UnsupportedOperationException("Albums cannot be directly modified"); 3977 } 3978 appendWhereStandaloneFilter(qb, new String[] { 3979 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY 3980 }, filter); 3981 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3982 // We don't have a great way to filter parsed metadata by 3983 // owner, so callers need to hold READ_MEDIA_AUDIO 3984 appendWhereStandalone(qb, "0"); 3985 } 3986 break; 3987 } 3988 case VIDEO_MEDIA_ID: 3989 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3990 matchPending = MATCH_INCLUDE; 3991 matchTrashed = MATCH_INCLUDE; 3992 // fall-through 3993 case VIDEO_MEDIA: { 3994 if (type == TYPE_QUERY) { 3995 qb.setTables("video"); 3996 qb.setProjectionMap( 3997 getProjectionMap(Video.Media.class)); 3998 } else { 3999 qb.setTables("files"); 4000 qb.setProjectionMap( 4001 getProjectionMap(Video.Media.class, Files.FileColumns.class)); 4002 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 4003 FileColumns.MEDIA_TYPE_VIDEO); 4004 } 4005 if (!allowGlobal && !checkCallingPermissionVideo(forWrite, callingPackage)) { 4006 appendWhereStandalone(qb, matchSharedPackagesClause); 4007 } 4008 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); 4009 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); 4010 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 4011 if (honored != null) { 4012 honored.accept(QUERY_ARG_MATCH_PENDING); 4013 honored.accept(QUERY_ARG_MATCH_TRASHED); 4014 honored.accept(QUERY_ARG_MATCH_FAVORITE); 4015 } 4016 if (!includeAllVolumes) { 4017 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 4018 } 4019 break; 4020 } 4021 case VIDEO_THUMBNAILS_ID: 4022 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 4023 // fall-through 4024 case VIDEO_THUMBNAILS: { 4025 qb.setTables("videothumbnails"); 4026 qb.setProjectionMap(getProjectionMap(Video.Thumbnails.class)); 4027 if (!allowGlobal && !checkCallingPermissionVideo(forWrite, callingPackage)) { 4028 appendWhereStandalone(qb, 4029 "video_id IN (SELECT _id FROM video WHERE " + 4030 matchSharedPackagesClause + ")"); 4031 } 4032 break; 4033 } 4034 case FILES_ID: 4035 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2)); 4036 matchPending = MATCH_INCLUDE; 4037 matchTrashed = MATCH_INCLUDE; 4038 // fall-through 4039 case FILES: { 4040 qb.setTables("files"); 4041 qb.setProjectionMap(getProjectionMap(Files.FileColumns.class)); 4042 4043 final ArrayList<String> options = new ArrayList<>(); 4044 if (!allowGlobal && !allowLegacyRead) { 4045 options.add(DatabaseUtils.bindSelection(matchSharedPackagesClause)); 4046 if (allowLegacy) { 4047 options.add(DatabaseUtils.bindSelection("volume_name=?", 4048 MediaStore.VOLUME_EXTERNAL_PRIMARY)); 4049 } 4050 if (checkCallingPermissionAudio(forWrite, callingPackage)) { 4051 options.add(DatabaseUtils.bindSelection("media_type=?", 4052 FileColumns.MEDIA_TYPE_AUDIO)); 4053 options.add(DatabaseUtils.bindSelection("media_type=?", 4054 FileColumns.MEDIA_TYPE_PLAYLIST)); 4055 options.add(DatabaseUtils.bindSelection("media_type=?", 4056 FileColumns.MEDIA_TYPE_SUBTITLE)); 4057 options.add(matchSharedPackagesClause 4058 + " AND media_type=0 AND mime_type LIKE 'audio/%'"); 4059 } 4060 if (checkCallingPermissionVideo(forWrite, callingPackage)) { 4061 options.add(DatabaseUtils.bindSelection("media_type=?", 4062 FileColumns.MEDIA_TYPE_VIDEO)); 4063 options.add(DatabaseUtils.bindSelection("media_type=?", 4064 FileColumns.MEDIA_TYPE_SUBTITLE)); 4065 options.add(matchSharedPackagesClause 4066 + " AND media_type=0 AND mime_type LIKE 'video/%'"); 4067 } 4068 if (checkCallingPermissionImages(forWrite, callingPackage)) { 4069 options.add(DatabaseUtils.bindSelection("media_type=?", 4070 FileColumns.MEDIA_TYPE_IMAGE)); 4071 options.add(matchSharedPackagesClause 4072 + " AND media_type=0 AND mime_type LIKE 'image/%'"); 4073 } 4074 if (includedDefaultDirs != null) { 4075 for (String defaultDir : includedDefaultDirs) { 4076 options.add(FileColumns.RELATIVE_PATH + " LIKE '" + defaultDir + "/%'"); 4077 } 4078 } 4079 } 4080 if (options.size() > 0) { 4081 appendWhereStandalone(qb, TextUtils.join(" OR ", options)); 4082 } 4083 4084 appendWhereStandaloneFilter(qb, new String[] { 4085 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY 4086 }, filter); 4087 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); 4088 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); 4089 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 4090 if (honored != null) { 4091 honored.accept(QUERY_ARG_MATCH_PENDING); 4092 honored.accept(QUERY_ARG_MATCH_TRASHED); 4093 honored.accept(QUERY_ARG_MATCH_FAVORITE); 4094 } 4095 if (!includeAllVolumes) { 4096 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 4097 } 4098 break; 4099 } 4100 case DOWNLOADS_ID: 4101 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2)); 4102 matchPending = MATCH_INCLUDE; 4103 matchTrashed = MATCH_INCLUDE; 4104 // fall-through 4105 case DOWNLOADS: { 4106 if (type == TYPE_QUERY) { 4107 qb.setTables("downloads"); 4108 qb.setProjectionMap( 4109 getProjectionMap(Downloads.class)); 4110 } else { 4111 qb.setTables("files"); 4112 qb.setProjectionMap( 4113 getProjectionMap(Downloads.class, Files.FileColumns.class)); 4114 appendWhereStandalone(qb, FileColumns.IS_DOWNLOAD + "=1"); 4115 } 4116 4117 final ArrayList<String> options = new ArrayList<>(); 4118 if (!allowGlobal && !allowLegacyRead) { 4119 options.add(DatabaseUtils.bindSelection(matchSharedPackagesClause)); 4120 if (allowLegacy) { 4121 options.add(DatabaseUtils.bindSelection("volume_name=?", 4122 MediaStore.VOLUME_EXTERNAL_PRIMARY)); 4123 } 4124 } 4125 if (options.size() > 0) { 4126 appendWhereStandalone(qb, TextUtils.join(" OR ", options)); 4127 } 4128 4129 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); 4130 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); 4131 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); 4132 if (honored != null) { 4133 honored.accept(QUERY_ARG_MATCH_PENDING); 4134 honored.accept(QUERY_ARG_MATCH_TRASHED); 4135 honored.accept(QUERY_ARG_MATCH_FAVORITE); 4136 } 4137 if (!includeAllVolumes) { 4138 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 4139 } 4140 break; 4141 } 4142 default: 4143 throw new UnsupportedOperationException( 4144 "Unknown or unsupported URL: " + uri.toString()); 4145 } 4146 4147 // To ensure we're enforcing our security model, all operations must 4148 // have a projection map configured 4149 if (qb.getProjectionMap() == null) { 4150 throw new IllegalStateException("All queries must have a projection map"); 4151 } 4152 4153 // If caller is an older app, we're willing to let through a 4154 // greylist of technically invalid columns 4155 if (getCallingPackageTargetSdkVersion() < Build.VERSION_CODES.Q) { 4156 qb.setProjectionGreylist(sGreylist); 4157 } 4158 4159 return qb; 4160 } 4161 4162 /** 4163 * Determine if given {@link Uri} has a 4164 * {@link MediaColumns#OWNER_PACKAGE_NAME} column. 4165 */ hasOwnerPackageName(Uri uri)4166 private boolean hasOwnerPackageName(Uri uri) { 4167 // It's easier to maintain this as an inverted list 4168 final int table = matchUri(uri, true); 4169 switch (table) { 4170 case IMAGES_THUMBNAILS_ID: 4171 case IMAGES_THUMBNAILS: 4172 case VIDEO_THUMBNAILS_ID: 4173 case VIDEO_THUMBNAILS: 4174 case AUDIO_ALBUMART: 4175 case AUDIO_ALBUMART_ID: 4176 case AUDIO_ALBUMART_FILE_ID: 4177 return false; 4178 default: 4179 return true; 4180 } 4181 } 4182 4183 /** 4184 * @deprecated all operations should be routed through the overload that 4185 * accepts a {@link Bundle} of extras. 4186 */ 4187 @Override 4188 @Deprecated delete(Uri uri, String selection, String[] selectionArgs)4189 public int delete(Uri uri, String selection, String[] selectionArgs) { 4190 return delete(uri, 4191 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null)); 4192 } 4193 4194 @Override delete(@onNull Uri uri, @Nullable Bundle extras)4195 public int delete(@NonNull Uri uri, @Nullable Bundle extras) { 4196 Trace.beginSection("delete"); 4197 try { 4198 return deleteInternal(uri, extras); 4199 } catch (FallbackException e) { 4200 return e.translateForUpdateDelete(getCallingPackageTargetSdkVersion()); 4201 } finally { 4202 Trace.endSection(); 4203 } 4204 } 4205 deleteInternal(@onNull Uri uri, @Nullable Bundle extras)4206 private int deleteInternal(@NonNull Uri uri, @Nullable Bundle extras) 4207 throws FallbackException { 4208 extras = (extras != null) ? extras : new Bundle(); 4209 4210 // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. 4211 extras.remove(INCLUDED_DEFAULT_DIRECTORIES); 4212 4213 uri = safeUncanonicalize(uri); 4214 final boolean allowHidden = isCallingPackageAllowedHidden(); 4215 final int match = matchUri(uri, allowHidden); 4216 4217 switch(match) { 4218 case AUDIO_MEDIA_ID: 4219 case AUDIO_PLAYLISTS_ID: 4220 case VIDEO_MEDIA_ID: 4221 case IMAGES_MEDIA_ID: 4222 case DOWNLOADS_ID: 4223 case FILES_ID: { 4224 if (!isFuseThread() && getCachedCallingIdentityForFuse(Binder.getCallingUid()). 4225 removeDeletedRowId(Long.parseLong(uri.getLastPathSegment()))) { 4226 // Apps sometimes delete the file via filePath and then try to delete the db row 4227 // using MediaProvider#delete. Since we would have already deleted the db row 4228 // during the filePath operation, the latter will result in a security 4229 // exception. Apps which don't expect an exception will break here. Since we 4230 // have already deleted the db row, silently return zero as deleted count. 4231 return 0; 4232 } 4233 } 4234 break; 4235 default: 4236 // For other match types, given uri will not correspond to a valid file. 4237 break; 4238 } 4239 4240 final String userWhere = extras.getString(QUERY_ARG_SQL_SELECTION); 4241 final String[] userWhereArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS); 4242 4243 int count = 0; 4244 4245 final String volumeName = getVolumeName(uri); 4246 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 4247 4248 // handle MEDIA_SCANNER before calling getDatabaseForUri() 4249 if (match == MEDIA_SCANNER) { 4250 if (mMediaScannerVolume == null) { 4251 return 0; 4252 } 4253 4254 final DatabaseHelper helper = getDatabaseForUri( 4255 MediaStore.Files.getContentUri(mMediaScannerVolume)); 4256 4257 helper.mScanStopTime = SystemClock.elapsedRealtime(); 4258 4259 mMediaScannerVolume = null; 4260 return 1; 4261 } 4262 4263 if (match == VOLUMES_ID) { 4264 detachVolume(uri); 4265 count = 1; 4266 } 4267 4268 switch (match) { 4269 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 4270 extras.putString(QUERY_ARG_SQL_SELECTION, 4271 BaseColumns._ID + "=" + uri.getPathSegments().get(5)); 4272 // fall-through 4273 case AUDIO_PLAYLISTS_ID_MEMBERS: { 4274 final long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 4275 final Uri playlistUri = ContentUris.withAppendedId( 4276 MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId); 4277 4278 // Playlist contents are always persisted directly into playlist 4279 // files on disk to ensure that we can reliably migrate between 4280 // devices and recover from database corruption 4281 return removePlaylistMembers(playlistUri, extras); 4282 } 4283 } 4284 4285 final DatabaseHelper helper = getDatabaseForUri(uri); 4286 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, match, uri, extras, null); 4287 4288 { 4289 // Give callers interacting with a specific media item a chance to 4290 // escalate access if they don't already have it 4291 switch (match) { 4292 case AUDIO_MEDIA_ID: 4293 case VIDEO_MEDIA_ID: 4294 case IMAGES_MEDIA_ID: 4295 enforceCallingPermission(uri, extras, true); 4296 } 4297 4298 final String[] projection = new String[] { 4299 FileColumns.MEDIA_TYPE, 4300 FileColumns.DATA, 4301 FileColumns._ID, 4302 FileColumns.IS_DOWNLOAD, 4303 FileColumns.MIME_TYPE, 4304 }; 4305 final boolean isFilesTable = qb.getTables().equals("files"); 4306 final LongSparseArray<String> deletedDownloadIds = new LongSparseArray<>(); 4307 if (isFilesTable) { 4308 String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA); 4309 if (deleteparam == null || ! deleteparam.equals("false")) { 4310 Cursor c = qb.query(helper, projection, userWhere, userWhereArgs, 4311 null, null, null, null, null); 4312 try { 4313 while (c.moveToNext()) { 4314 final int mediaType = c.getInt(0); 4315 final String data = c.getString(1); 4316 final long id = c.getLong(2); 4317 final int isDownload = c.getInt(3); 4318 final String mimeType = c.getString(4); 4319 4320 // Forget that caller is owner of this item 4321 mCallingIdentity.get().setOwned(id, false); 4322 4323 deleteIfAllowed(uri, extras, data); 4324 count += qb.delete(helper, BaseColumns._ID + "=" + id, null); 4325 4326 // Only need to inform DownloadProvider about the downloads deleted on 4327 // external volume. 4328 if (isDownload == 1) { 4329 deletedDownloadIds.put(id, mimeType); 4330 } 4331 4332 // Update any playlists that reference this item 4333 if ((mediaType == FileColumns.MEDIA_TYPE_AUDIO) 4334 && helper.isExternal()) { 4335 helper.runWithTransaction((db) -> { 4336 try (Cursor cc = db.query("audio_playlists_map", 4337 new String[] { "playlist_id" }, "audio_id=" + id, 4338 null, "playlist_id", null, null)) { 4339 while (cc.moveToNext()) { 4340 final Uri playlistUri = ContentUris.withAppendedId( 4341 Playlists.getContentUri(volumeName), 4342 cc.getLong(0)); 4343 resolvePlaylistMembers(playlistUri); 4344 } 4345 } 4346 return null; 4347 }); 4348 } 4349 } 4350 } finally { 4351 FileUtils.closeQuietly(c); 4352 } 4353 // Do not allow deletion if the file/object is referenced as parent 4354 // by some other entries. It could cause database corruption. 4355 appendWhereStandalone(qb, ID_NOT_PARENT_CLAUSE); 4356 } 4357 } 4358 4359 switch (match) { 4360 case AUDIO_GENRES_ID_MEMBERS: 4361 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R); 4362 4363 case IMAGES_THUMBNAILS_ID: 4364 case IMAGES_THUMBNAILS: 4365 case VIDEO_THUMBNAILS_ID: 4366 case VIDEO_THUMBNAILS: 4367 // Delete the referenced files first. 4368 Cursor c = qb.query(helper, sDataOnlyColumn, userWhere, userWhereArgs, null, 4369 null, null, null, null); 4370 if (c != null) { 4371 try { 4372 while (c.moveToNext()) { 4373 deleteIfAllowed(uri, extras, c.getString(0)); 4374 } 4375 } finally { 4376 FileUtils.closeQuietly(c); 4377 } 4378 } 4379 count += deleteRecursive(qb, helper, userWhere, userWhereArgs); 4380 break; 4381 4382 default: 4383 count += deleteRecursive(qb, helper, userWhere, userWhereArgs); 4384 break; 4385 } 4386 4387 if (deletedDownloadIds.size() > 0) { 4388 // Do this on a background thread, since we don't want to make binder 4389 // calls as part of a FUSE call. 4390 helper.postBackground(() -> { 4391 getContext().getSystemService(DownloadManager.class) 4392 .onMediaStoreDownloadsDeleted(deletedDownloadIds); 4393 }); 4394 } 4395 4396 if (isFilesTable && !isCallingPackageSelf()) { 4397 Metrics.logDeletion(volumeName, mCallingIdentity.get().uid, 4398 getCallingPackageOrSelf(), count); 4399 } 4400 } 4401 4402 return count; 4403 } 4404 4405 /** 4406 * Executes identical delete repeatedly within a single transaction until 4407 * stability is reached. Combined with {@link #ID_NOT_PARENT_CLAUSE}, this 4408 * can be used to recursively delete all matching entries, since it only 4409 * deletes parents when no references remaining. 4410 */ deleteRecursive(SQLiteQueryBuilder qb, DatabaseHelper helper, String userWhere, String[] userWhereArgs)4411 private int deleteRecursive(SQLiteQueryBuilder qb, DatabaseHelper helper, String userWhere, 4412 String[] userWhereArgs) { 4413 return (int) helper.runWithTransaction((db) -> { 4414 synchronized (mDirectoryCache) { 4415 mDirectoryCache.clear(); 4416 } 4417 4418 int n = 0; 4419 int total = 0; 4420 do { 4421 n = qb.delete(helper, userWhere, userWhereArgs); 4422 total += n; 4423 } while (n > 0); 4424 return total; 4425 }); 4426 } 4427 4428 @Override 4429 public Bundle call(String method, String arg, Bundle extras) { 4430 Trace.beginSection("call"); 4431 try { 4432 return callInternal(method, arg, extras); 4433 } finally { 4434 Trace.endSection(); 4435 } 4436 } 4437 4438 private Bundle callInternal(String method, String arg, Bundle extras) { 4439 switch (method) { 4440 case MediaStore.RESOLVE_PLAYLIST_MEMBERS_CALL: { 4441 final LocalCallingIdentity token = clearLocalCallingIdentity(); 4442 final CallingIdentity providerToken = clearCallingIdentity(); 4443 try { 4444 final Uri playlistUri = extras.getParcelable(MediaStore.EXTRA_URI); 4445 resolvePlaylistMembers(playlistUri); 4446 } finally { 4447 restoreCallingIdentity(providerToken); 4448 restoreLocalCallingIdentity(token); 4449 } 4450 return null; 4451 } 4452 case MediaStore.RUN_IDLE_MAINTENANCE_CALL: { 4453 // Protect ourselves from random apps by requiring a generic 4454 // permission held by common debugging components, such as shell 4455 getContext().enforceCallingOrSelfPermission( 4456 android.Manifest.permission.DUMP, TAG); 4457 final LocalCallingIdentity token = clearLocalCallingIdentity(); 4458 final CallingIdentity providerToken = clearCallingIdentity(); 4459 try { 4460 onIdleMaintenance(new CancellationSignal()); 4461 } finally { 4462 restoreCallingIdentity(providerToken); 4463 restoreLocalCallingIdentity(token); 4464 } 4465 return null; 4466 } 4467 case MediaStore.WAIT_FOR_IDLE_CALL: { 4468 ForegroundThread.waitForIdle(); 4469 BackgroundThread.waitForIdle(); 4470 return null; 4471 } 4472 case MediaStore.SCAN_FILE_CALL: 4473 case MediaStore.SCAN_VOLUME_CALL: { 4474 final LocalCallingIdentity token = clearLocalCallingIdentity(); 4475 final CallingIdentity providerToken = clearCallingIdentity(); 4476 try { 4477 final Bundle res = new Bundle(); 4478 switch (method) { 4479 case MediaStore.SCAN_FILE_CALL: { 4480 final File file = new File(arg); 4481 res.putParcelable(Intent.EXTRA_STREAM, scanFile(file, REASON_DEMAND)); 4482 break; 4483 } 4484 case MediaStore.SCAN_VOLUME_CALL: { 4485 final String volumeName = arg; 4486 MediaService.onScanVolume(getContext(), volumeName, REASON_DEMAND); 4487 break; 4488 } 4489 } 4490 return res; 4491 } catch (IOException e) { 4492 throw new RuntimeException(e); 4493 } finally { 4494 restoreCallingIdentity(providerToken); 4495 restoreLocalCallingIdentity(token); 4496 } 4497 } 4498 case MediaStore.GET_VERSION_CALL: { 4499 final String volumeName = extras.getString(Intent.EXTRA_TEXT); 4500 4501 final DatabaseHelper helper; 4502 try { 4503 helper = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName)); 4504 } catch (VolumeNotFoundException e) { 4505 throw e.rethrowAsIllegalArgumentException(); 4506 } 4507 4508 final String version = helper.runWithoutTransaction((db) -> { 4509 return db.getVersion() + ":" + DatabaseHelper.getOrCreateUuid(db); 4510 }); 4511 4512 final Bundle res = new Bundle(); 4513 res.putString(Intent.EXTRA_TEXT, version); 4514 return res; 4515 } 4516 case MediaStore.GET_GENERATION_CALL: { 4517 final String volumeName = extras.getString(Intent.EXTRA_TEXT); 4518 4519 final DatabaseHelper helper; 4520 try { 4521 helper = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName)); 4522 } catch (VolumeNotFoundException e) { 4523 throw e.rethrowAsIllegalArgumentException(); 4524 } 4525 4526 final long generation = helper.runWithoutTransaction((db) -> { 4527 return DatabaseHelper.getGeneration(db); 4528 }); 4529 4530 final Bundle res = new Bundle(); 4531 res.putLong(Intent.EXTRA_INDEX, generation); 4532 return res; 4533 } 4534 case MediaStore.GET_DOCUMENT_URI_CALL: { 4535 final Uri mediaUri = extras.getParcelable(MediaStore.EXTRA_URI); 4536 enforceCallingPermission(mediaUri, extras, false); 4537 4538 final Uri fileUri; 4539 final LocalCallingIdentity token = clearLocalCallingIdentity(); 4540 try { 4541 fileUri = Uri.fromFile(queryForDataFile(mediaUri, null)); 4542 } catch (FileNotFoundException e) { 4543 throw new IllegalArgumentException(e); 4544 } finally { 4545 restoreLocalCallingIdentity(token); 4546 } 4547 4548 try (ContentProviderClient client = getContext().getContentResolver() 4549 .acquireUnstableContentProviderClient( 4550 MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) { 4551 extras.putParcelable(MediaStore.EXTRA_URI, fileUri); 4552 return client.call(method, null, extras); 4553 } catch (RemoteException e) { 4554 throw new IllegalStateException(e); 4555 } 4556 } 4557 case MediaStore.GET_MEDIA_URI_CALL: { 4558 final Uri documentUri = extras.getParcelable(MediaStore.EXTRA_URI); 4559 getContext().enforceCallingUriPermission(documentUri, 4560 Intent.FLAG_GRANT_READ_URI_PERMISSION, TAG); 4561 4562 final Uri fileUri; 4563 try (ContentProviderClient client = getContext().getContentResolver() 4564 .acquireUnstableContentProviderClient( 4565 MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) { 4566 final Bundle res = client.call(method, null, extras); 4567 fileUri = res.getParcelable(MediaStore.EXTRA_URI); 4568 } catch (RemoteException e) { 4569 throw new IllegalStateException(e); 4570 } 4571 4572 final LocalCallingIdentity token = clearLocalCallingIdentity(); 4573 try { 4574 final Bundle res = new Bundle(); 4575 res.putParcelable(MediaStore.EXTRA_URI, 4576 queryForMediaUri(new File(fileUri.getPath()), null)); 4577 return res; 4578 } catch (FileNotFoundException e) { 4579 throw new IllegalArgumentException(e); 4580 } finally { 4581 restoreLocalCallingIdentity(token); 4582 } 4583 } 4584 case MediaStore.CREATE_WRITE_REQUEST_CALL: 4585 case MediaStore.CREATE_FAVORITE_REQUEST_CALL: 4586 case MediaStore.CREATE_TRASH_REQUEST_CALL: 4587 case MediaStore.CREATE_DELETE_REQUEST_CALL: { 4588 final PendingIntent pi = createRequest(method, extras); 4589 final Bundle res = new Bundle(); 4590 res.putParcelable(MediaStore.EXTRA_RESULT, pi); 4591 return res; 4592 } 4593 default: 4594 throw new UnsupportedOperationException("Unsupported call: " + method); 4595 } 4596 } 4597 4598 static List<Uri> collectUris(ClipData clipData) { 4599 final ArrayList<Uri> res = new ArrayList<>(); 4600 for (int i = 0; i < clipData.getItemCount(); i++) { 4601 res.add(clipData.getItemAt(i).getUri()); 4602 } 4603 return res; 4604 } 4605 4606 /** 4607 * Generate the {@link PendingIntent} for the given grant request. This 4608 * method also sanity checks the incoming arguments for security purposes 4609 * before creating the privileged {@link PendingIntent}. 4610 */ 4611 private @NonNull PendingIntent createRequest(@NonNull String method, @NonNull Bundle extras) { 4612 final ClipData clipData = extras.getParcelable(MediaStore.EXTRA_CLIP_DATA); 4613 final List<Uri> uris = collectUris(clipData); 4614 4615 for (Uri uri : uris) { 4616 final int match = matchUri(uri, false); 4617 switch (match) { 4618 case IMAGES_MEDIA_ID: 4619 case AUDIO_MEDIA_ID: 4620 case VIDEO_MEDIA_ID: 4621 // Caller is requesting a specific media item by its ID, 4622 // which means it's valid for requests 4623 break; 4624 default: 4625 throw new IllegalArgumentException( 4626 "All requested items must be referenced by specific ID"); 4627 } 4628 } 4629 4630 // Enforce that limited set of columns can be mutated 4631 final ContentValues values = extras.getParcelable(MediaStore.EXTRA_CONTENT_VALUES); 4632 final List<String> allowedColumns; 4633 switch (method) { 4634 case MediaStore.CREATE_FAVORITE_REQUEST_CALL: 4635 allowedColumns = Arrays.asList( 4636 MediaColumns.IS_FAVORITE); 4637 break; 4638 case MediaStore.CREATE_TRASH_REQUEST_CALL: 4639 allowedColumns = Arrays.asList( 4640 MediaColumns.IS_TRASHED); 4641 break; 4642 default: 4643 allowedColumns = Arrays.asList(); 4644 break; 4645 } 4646 if (values != null) { 4647 for (String key : values.keySet()) { 4648 if (!allowedColumns.contains(key)) { 4649 throw new IllegalArgumentException("Invalid column " + key); 4650 } 4651 } 4652 } 4653 4654 final Context context = getContext(); 4655 final Intent intent = new Intent(method, null, context, PermissionActivity.class); 4656 intent.putExtras(extras); 4657 return PendingIntent.getActivity(context, PermissionActivity.REQUEST_CODE, intent, 4658 FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE); 4659 } 4660 4661 /** 4662 * Ensure that all local databases have a custom collator registered for the 4663 * given {@link ULocale} locale. 4664 * 4665 * @return the corresponding custom collation name to be used in 4666 * {@code ORDER BY} clauses. 4667 */ 4668 private @NonNull String ensureCustomCollator(@NonNull String locale) { 4669 // Quick sanity check that requested locale looks sane 4670 new ULocale(locale); 4671 4672 final String collationName = "custom_" + locale.replaceAll("[^a-zA-Z]", ""); 4673 synchronized (mCustomCollators) { 4674 if (!mCustomCollators.contains(collationName)) { 4675 for (DatabaseHelper helper : new DatabaseHelper[] { 4676 mInternalDatabase, 4677 mExternalDatabase 4678 }) { 4679 helper.runWithoutTransaction((db) -> { 4680 db.execPerConnectionSQL("SELECT icu_load_collation(?, ?);", 4681 new String[] { locale, collationName }); 4682 return null; 4683 }); 4684 } 4685 mCustomCollators.add(collationName); 4686 } 4687 } 4688 return collationName; 4689 } 4690 4691 private int pruneThumbnails(@NonNull SQLiteDatabase db, @NonNull CancellationSignal signal) { 4692 int prunedCount = 0; 4693 4694 // Determine all known media items 4695 final LongArray knownIds = new LongArray(); 4696 try (Cursor c = db.query(true, "files", new String[] { BaseColumns._ID }, 4697 null, null, null, null, null, null, signal)) { 4698 while (c.moveToNext()) { 4699 knownIds.add(c.getLong(0)); 4700 } 4701 } 4702 4703 final long[] knownIdsRaw = knownIds.toArray(); 4704 Arrays.sort(knownIdsRaw); 4705 4706 for (String volumeName : getExternalVolumeNames()) { 4707 final List<File> thumbDirs; 4708 try { 4709 thumbDirs = getThumbnailDirectories(volumeName); 4710 } catch (FileNotFoundException e) { 4711 Log.w(TAG, "Failed to resolve volume " + volumeName, e); 4712 continue; 4713 } 4714 4715 // Reconcile all thumbnails, deleting stale items 4716 for (File thumbDir : thumbDirs) { 4717 // Possibly bail before digging into each directory 4718 signal.throwIfCanceled(); 4719 4720 final File[] files = thumbDir.listFiles(); 4721 for (File thumbFile : (files != null) ? files : new File[0]) { 4722 if (Objects.equals(thumbFile.getName(), FILE_DATABASE_UUID)) continue; 4723 final String name = FileUtils.extractFileName(thumbFile.getName()); 4724 try { 4725 final long id = Long.parseLong(name); 4726 if (Arrays.binarySearch(knownIdsRaw, id) >= 0) { 4727 // Thumbnail belongs to known media, keep it 4728 continue; 4729 } 4730 } catch (NumberFormatException e) { 4731 } 4732 4733 Log.v(TAG, "Deleting stale thumbnail " + thumbFile); 4734 deleteAndInvalidate(thumbFile); 4735 prunedCount++; 4736 } 4737 } 4738 } 4739 4740 // Also delete stale items from legacy tables 4741 db.execSQL("delete from thumbnails " 4742 + "where image_id not in (select _id from images)"); 4743 db.execSQL("delete from videothumbnails " 4744 + "where video_id not in (select _id from video)"); 4745 4746 return prunedCount; 4747 } 4748 4749 abstract class Thumbnailer { 4750 final String directoryName; 4751 4752 public Thumbnailer(String directoryName) { 4753 this.directoryName = directoryName; 4754 } 4755 4756 private File getThumbnailFile(Uri uri) throws IOException { 4757 final String volumeName = resolveVolumeName(uri); 4758 final File volumePath = getVolumePath(volumeName); 4759 return FileUtils.buildPath(volumePath, directoryName, 4760 DIRECTORY_THUMBNAILS, ContentUris.parseId(uri) + ".jpg"); 4761 } 4762 4763 public abstract Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) 4764 throws IOException; 4765 4766 public ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal) 4767 throws IOException { 4768 // First attempt to fast-path by opening the thumbnail; if it 4769 // doesn't exist we fall through to create it below 4770 final File thumbFile = getThumbnailFile(uri); 4771 try { 4772 return FileUtils.openSafely(thumbFile, 4773 ParcelFileDescriptor.MODE_READ_ONLY); 4774 } catch (FileNotFoundException ignored) { 4775 } 4776 4777 final File thumbDir = thumbFile.getParentFile(); 4778 thumbDir.mkdirs(); 4779 4780 // When multiple threads race for the same thumbnail, the second 4781 // thread could return a file with a thumbnail still in 4782 // progress. We could add heavy per-ID locking to mitigate this 4783 // rare race condition, but it's simpler to have both threads 4784 // generate the same thumbnail using temporary files and rename 4785 // them into place once finished. 4786 final File thumbTempFile = File.createTempFile("thumb", null, thumbDir); 4787 4788 ParcelFileDescriptor thumbWrite = null; 4789 ParcelFileDescriptor thumbRead = null; 4790 try { 4791 // Open our temporary file twice: once for local writing, and 4792 // once for remote reading. Both FDs point at the same 4793 // underlying inode on disk, so they're stable across renames 4794 // to avoid race conditions between threads. 4795 thumbWrite = FileUtils.openSafely(thumbTempFile, 4796 ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_CREATE); 4797 thumbRead = FileUtils.openSafely(thumbTempFile, 4798 ParcelFileDescriptor.MODE_READ_ONLY); 4799 4800 final Bitmap thumbnail = getThumbnailBitmap(uri, signal); 4801 thumbnail.compress(Bitmap.CompressFormat.JPEG, 90, 4802 new FileOutputStream(thumbWrite.getFileDescriptor())); 4803 4804 try { 4805 // Use direct syscall for better failure logs 4806 Os.rename(thumbTempFile.getAbsolutePath(), thumbFile.getAbsolutePath()); 4807 } catch (ErrnoException e) { 4808 e.rethrowAsIOException(); 4809 } 4810 4811 // Everything above went peachy, so return a duplicate of our 4812 // already-opened read FD to keep our finally logic below simple 4813 return thumbRead.dup(); 4814 4815 } finally { 4816 // Regardless of success or failure, try cleaning up any 4817 // remaining temporary file and close all our local FDs 4818 FileUtils.closeQuietly(thumbWrite); 4819 FileUtils.closeQuietly(thumbRead); 4820 deleteAndInvalidate(thumbTempFile); 4821 } 4822 } 4823 4824 public void invalidateThumbnail(Uri uri) throws IOException { 4825 deleteAndInvalidate(getThumbnailFile(uri)); 4826 } 4827 } 4828 4829 private Thumbnailer mAudioThumbnailer = new Thumbnailer(Environment.DIRECTORY_MUSIC) { 4830 @Override 4831 public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException { 4832 return ThumbnailUtils.createAudioThumbnail(queryForDataFile(uri, signal), 4833 mThumbSize, signal); 4834 } 4835 }; 4836 4837 private Thumbnailer mVideoThumbnailer = new Thumbnailer(Environment.DIRECTORY_MOVIES) { 4838 @Override 4839 public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException { 4840 return ThumbnailUtils.createVideoThumbnail(queryForDataFile(uri, signal), 4841 mThumbSize, signal); 4842 } 4843 }; 4844 4845 private Thumbnailer mImageThumbnailer = new Thumbnailer(Environment.DIRECTORY_PICTURES) { 4846 @Override 4847 public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException { 4848 return ThumbnailUtils.createImageThumbnail(queryForDataFile(uri, signal), 4849 mThumbSize, signal); 4850 } 4851 }; 4852 4853 private List<File> getThumbnailDirectories(String volumeName) throws FileNotFoundException { 4854 final File volumePath = getVolumePath(volumeName); 4855 return Arrays.asList( 4856 FileUtils.buildPath(volumePath, DIRECTORY_MUSIC, DIRECTORY_THUMBNAILS), 4857 FileUtils.buildPath(volumePath, DIRECTORY_MOVIES, DIRECTORY_THUMBNAILS), 4858 FileUtils.buildPath(volumePath, DIRECTORY_PICTURES, DIRECTORY_THUMBNAILS)); 4859 } 4860 4861 private void invalidateThumbnails(Uri uri) { 4862 Trace.beginSection("invalidateThumbnails"); 4863 try { 4864 invalidateThumbnailsInternal(uri); 4865 } finally { 4866 Trace.endSection(); 4867 } 4868 } 4869 4870 private void invalidateThumbnailsInternal(Uri uri) { 4871 final long id = ContentUris.parseId(uri); 4872 try { 4873 mAudioThumbnailer.invalidateThumbnail(uri); 4874 mVideoThumbnailer.invalidateThumbnail(uri); 4875 mImageThumbnailer.invalidateThumbnail(uri); 4876 } catch (IOException ignored) { 4877 } 4878 4879 final DatabaseHelper helper; 4880 try { 4881 helper = getDatabaseForUri(uri); 4882 } catch (VolumeNotFoundException e) { 4883 Log.w(TAG, e); 4884 return; 4885 } 4886 4887 helper.runWithTransaction((db) -> { 4888 final String idString = Long.toString(id); 4889 try (Cursor c = db.rawQuery("select _data from thumbnails where image_id=?" 4890 + " union all select _data from videothumbnails where video_id=?", 4891 new String[] { idString, idString })) { 4892 while (c.moveToNext()) { 4893 String path = c.getString(0); 4894 deleteIfAllowed(uri, Bundle.EMPTY, path); 4895 } 4896 } 4897 4898 db.execSQL("delete from thumbnails where image_id=?", new String[] { idString }); 4899 db.execSQL("delete from videothumbnails where video_id=?", new String[] { idString }); 4900 return null; 4901 }); 4902 } 4903 4904 /** 4905 * @deprecated all operations should be routed through the overload that 4906 * accepts a {@link Bundle} of extras. 4907 */ 4908 @Override 4909 @Deprecated 4910 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 4911 return update(uri, values, 4912 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null)); 4913 } 4914 4915 @Override 4916 public int update(@NonNull Uri uri, @Nullable ContentValues values, 4917 @Nullable Bundle extras) { 4918 Trace.beginSection("update"); 4919 try { 4920 return updateInternal(uri, values, extras); 4921 } catch (FallbackException e) { 4922 return e.translateForUpdateDelete(getCallingPackageTargetSdkVersion()); 4923 } finally { 4924 Trace.endSection(); 4925 } 4926 } 4927 4928 private int updateInternal(@NonNull Uri uri, @Nullable ContentValues initialValues, 4929 @Nullable Bundle extras) throws FallbackException { 4930 extras = (extras != null) ? extras : new Bundle(); 4931 4932 // Related items are only considered for new media creation, and they 4933 // can't be leveraged to move existing content into blocked locations 4934 extras.remove(QUERY_ARG_RELATED_URI); 4935 // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. 4936 extras.remove(INCLUDED_DEFAULT_DIRECTORIES); 4937 4938 final String userWhere = extras.getString(QUERY_ARG_SQL_SELECTION); 4939 final String[] userWhereArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS); 4940 4941 // Limit the hacky workaround to camera targeting Q and below, to allow newer versions 4942 // of camera that does the right thing to work correctly. 4943 if ("com.google.android.GoogleCamera".equals(getCallingPackageOrSelf()) 4944 && getCallingPackageTargetSdkVersion() <= Build.VERSION_CODES.Q) { 4945 if (matchUri(uri, false) == IMAGES_MEDIA_ID) { 4946 Log.w(TAG, "Working around app bug in b/111966296"); 4947 uri = MediaStore.Files.getContentUri("external", ContentUris.parseId(uri)); 4948 } else if (matchUri(uri, false) == VIDEO_MEDIA_ID) { 4949 Log.w(TAG, "Working around app bug in b/112246630"); 4950 uri = MediaStore.Files.getContentUri("external", ContentUris.parseId(uri)); 4951 } 4952 } 4953 4954 uri = safeUncanonicalize(uri); 4955 4956 int count; 4957 4958 final String volumeName = getVolumeName(uri); 4959 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 4960 final boolean allowHidden = isCallingPackageAllowedHidden(); 4961 final int match = matchUri(uri, allowHidden); 4962 4963 switch (match) { 4964 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 4965 extras.putString(QUERY_ARG_SQL_SELECTION, 4966 BaseColumns._ID + "=" + uri.getPathSegments().get(5)); 4967 // fall-through 4968 case AUDIO_PLAYLISTS_ID_MEMBERS: { 4969 final long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 4970 final Uri playlistUri = ContentUris.withAppendedId( 4971 MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId); 4972 4973 if (uri.getBooleanQueryParameter("move", false)) { 4974 // Convert explicit request into query; sigh, moveItem() 4975 // uses zero-based indexing instead of one-based indexing 4976 final int from = Integer.parseInt(uri.getPathSegments().get(5)) + 1; 4977 final int to = initialValues.getAsInteger(Playlists.Members.PLAY_ORDER) + 1; 4978 extras.putString(QUERY_ARG_SQL_SELECTION, 4979 Playlists.Members.PLAY_ORDER + "=" + from); 4980 initialValues.put(Playlists.Members.PLAY_ORDER, to); 4981 } 4982 4983 // Playlist contents are always persisted directly into playlist 4984 // files on disk to ensure that we can reliably migrate between 4985 // devices and recover from database corruption 4986 final int index; 4987 if (initialValues.containsKey(Playlists.Members.PLAY_ORDER)) { 4988 index = movePlaylistMembers(playlistUri, initialValues, extras); 4989 } else { 4990 index = resolvePlaylistIndex(playlistUri, extras); 4991 } 4992 if (initialValues.containsKey(Playlists.Members.AUDIO_ID)) { 4993 final Bundle queryArgs = new Bundle(); 4994 queryArgs.putString(QUERY_ARG_SQL_SELECTION, 4995 Playlists.Members.PLAY_ORDER + "=" + (index + 1)); 4996 removePlaylistMembers(playlistUri, queryArgs); 4997 4998 final ContentValues values = new ContentValues(); 4999 values.put(Playlists.Members.AUDIO_ID, 5000 initialValues.getAsString(Playlists.Members.AUDIO_ID)); 5001 values.put(Playlists.Members.PLAY_ORDER, (index + 1)); 5002 addPlaylistMembers(playlistUri, values); 5003 } 5004 return 1; 5005 } 5006 } 5007 5008 final DatabaseHelper helper = getDatabaseForUri(uri); 5009 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, match, uri, extras, null); 5010 5011 // Give callers interacting with a specific media item a chance to 5012 // escalate access if they don't already have it 5013 switch (match) { 5014 case AUDIO_MEDIA_ID: 5015 case VIDEO_MEDIA_ID: 5016 case IMAGES_MEDIA_ID: 5017 enforceCallingPermission(uri, extras, true); 5018 } 5019 5020 boolean triggerInvalidate = false; 5021 boolean triggerScan = false; 5022 if (initialValues != null) { 5023 // IDs are forever; nobody should be editing them 5024 initialValues.remove(MediaColumns._ID); 5025 5026 // Expiration times are hard-coded; let's derive them 5027 FileUtils.computeDateExpires(initialValues); 5028 5029 // Ignore or augment incoming raw filesystem paths 5030 for (String column : sDataColumns.keySet()) { 5031 if (!initialValues.containsKey(column)) continue; 5032 5033 if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) { 5034 // Mutation allowed 5035 } else { 5036 Log.w(TAG, "Ignoring mutation of " + column + " from " 5037 + getCallingPackageOrSelf()); 5038 initialValues.remove(column); 5039 } 5040 } 5041 5042 // Enforce allowed ownership transfers 5043 if (initialValues.containsKey(MediaColumns.OWNER_PACKAGE_NAME)) { 5044 if (isCallingPackageSelf() || isCallingPackageShell()) { 5045 // When the caller is the media scanner or the shell, we let 5046 // them change ownership however they see fit; nothing to do 5047 } else if (isCallingPackageDelegator()) { 5048 // When the caller is a delegator, allow them to shift 5049 // ownership only when current owner, or when ownerless 5050 final String currentOwner; 5051 final String proposedOwner = initialValues 5052 .getAsString(MediaColumns.OWNER_PACKAGE_NAME); 5053 final Uri genericUri = MediaStore.Files.getContentUri(volumeName, 5054 ContentUris.parseId(uri)); 5055 try (Cursor c = queryForSingleItem(genericUri, 5056 new String[] { MediaColumns.OWNER_PACKAGE_NAME }, null, null, null)) { 5057 currentOwner = c.getString(0); 5058 } catch (FileNotFoundException e) { 5059 throw new IllegalStateException(e); 5060 } 5061 final boolean transferAllowed = (currentOwner == null) 5062 || Arrays.asList(getSharedPackagesForPackage(getCallingPackageOrSelf())) 5063 .contains(currentOwner); 5064 if (transferAllowed) { 5065 Log.v(TAG, "Ownership transfer from " + currentOwner + " to " 5066 + proposedOwner + " allowed"); 5067 } else { 5068 Log.w(TAG, "Ownership transfer from " + currentOwner + " to " 5069 + proposedOwner + " blocked"); 5070 initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME); 5071 } 5072 } else { 5073 // Otherwise no ownership changes are allowed 5074 initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME); 5075 } 5076 } 5077 5078 if (!isCallingPackageSelf()) { 5079 Trace.beginSection("filter"); 5080 5081 // We default to filtering mutable columns, except when we know 5082 // the single item being updated is pending; when it's finally 5083 // published we'll overwrite these values. 5084 final Uri finalUri = uri; 5085 final Supplier<Boolean> isPending = new CachedSupplier<>(() -> { 5086 return isPending(finalUri); 5087 }); 5088 5089 // Column values controlled by media scanner aren't writable by 5090 // apps, since any edits here don't reflect the metadata on 5091 // disk, and they'd be overwritten during a rescan. 5092 for (String column : new ArraySet<>(initialValues.keySet())) { 5093 if (sMutableColumns.contains(column)) { 5094 // Mutation normally allowed 5095 } else if (isPending.get()) { 5096 // Mutation relaxed while pending 5097 } else { 5098 Log.w(TAG, "Ignoring mutation of " + column + " from " 5099 + getCallingPackageOrSelf()); 5100 initialValues.remove(column); 5101 triggerScan = true; 5102 } 5103 5104 // If we're publishing this item, perform a blocking scan to 5105 // make sure metadata is updated 5106 if (MediaColumns.IS_PENDING.equals(column)) { 5107 triggerScan = true; 5108 5109 // Explicitly clear columns used to ignore no-op scans, 5110 // since we need to force a scan on publish 5111 initialValues.putNull(MediaColumns.DATE_MODIFIED); 5112 initialValues.putNull(MediaColumns.SIZE); 5113 } 5114 } 5115 5116 Trace.endSection(); 5117 } 5118 5119 if ("files".equals(qb.getTables())) { 5120 maybeMarkAsDownload(initialValues); 5121 } 5122 5123 // We no longer track location metadata 5124 if (initialValues.containsKey(ImageColumns.LATITUDE)) { 5125 initialValues.putNull(ImageColumns.LATITUDE); 5126 } 5127 if (initialValues.containsKey(ImageColumns.LONGITUDE)) { 5128 initialValues.putNull(ImageColumns.LONGITUDE); 5129 } 5130 } 5131 5132 // If we're not updating anything, then we can skip 5133 if (initialValues.isEmpty()) return 0; 5134 5135 final boolean isThumbnail; 5136 switch (match) { 5137 case IMAGES_THUMBNAILS: 5138 case IMAGES_THUMBNAILS_ID: 5139 case VIDEO_THUMBNAILS: 5140 case VIDEO_THUMBNAILS_ID: 5141 case AUDIO_ALBUMART: 5142 case AUDIO_ALBUMART_ID: 5143 isThumbnail = true; 5144 break; 5145 default: 5146 isThumbnail = false; 5147 break; 5148 } 5149 5150 switch (match) { 5151 case AUDIO_PLAYLISTS: 5152 case AUDIO_PLAYLISTS_ID: 5153 // Playlist names are stored as display names, but leave 5154 // values untouched if the caller is ModernMediaScanner 5155 if (!isCallingPackageSelf()) { 5156 if (initialValues.containsKey(Playlists.NAME)) { 5157 initialValues.put(MediaColumns.DISPLAY_NAME, 5158 initialValues.getAsString(Playlists.NAME)); 5159 } 5160 if (!initialValues.containsKey(MediaColumns.MIME_TYPE)) { 5161 initialValues.put(MediaColumns.MIME_TYPE, "audio/mpegurl"); 5162 } 5163 } 5164 break; 5165 } 5166 5167 // If we're touching columns that would change placement of a file, 5168 // blend in current values and recalculate path 5169 final boolean allowMovement = extras.getBoolean(MediaStore.QUERY_ARG_ALLOW_MOVEMENT, 5170 !isCallingPackageSelf()); 5171 if (containsAny(initialValues.keySet(), sPlacementColumns) 5172 && !initialValues.containsKey(MediaColumns.DATA) 5173 && !isThumbnail 5174 && allowMovement) { 5175 Trace.beginSection("movement"); 5176 5177 // We only support movement under well-defined collections 5178 switch (match) { 5179 case AUDIO_MEDIA_ID: 5180 case AUDIO_PLAYLISTS_ID: 5181 case VIDEO_MEDIA_ID: 5182 case IMAGES_MEDIA_ID: 5183 case DOWNLOADS_ID: 5184 case FILES_ID: 5185 break; 5186 default: 5187 throw new IllegalArgumentException("Movement of " + uri 5188 + " which isn't part of well-defined collection not allowed"); 5189 } 5190 5191 final LocalCallingIdentity token = clearLocalCallingIdentity(); 5192 final Uri genericUri = MediaStore.Files.getContentUri(volumeName, 5193 ContentUris.parseId(uri)); 5194 try (Cursor c = queryForSingleItem(genericUri, 5195 sPlacementColumns.toArray(new String[0]), userWhere, userWhereArgs, null)) { 5196 for (int i = 0; i < c.getColumnCount(); i++) { 5197 final String column = c.getColumnName(i); 5198 if (!initialValues.containsKey(column)) { 5199 initialValues.put(column, c.getString(i)); 5200 } 5201 } 5202 } catch (FileNotFoundException e) { 5203 throw new IllegalStateException(e); 5204 } finally { 5205 restoreLocalCallingIdentity(token); 5206 } 5207 5208 // Regenerate path using blended values; this will throw if caller 5209 // is attempting to place file into invalid location 5210 final String beforePath = initialValues.getAsString(MediaColumns.DATA); 5211 final String beforeVolume = extractVolumeName(beforePath); 5212 final String beforeOwner = extractPathOwnerPackageName(beforePath); 5213 5214 initialValues.remove(MediaColumns.DATA); 5215 ensureNonUniqueFileColumns(match, uri, extras, initialValues, beforePath); 5216 5217 final String probePath = initialValues.getAsString(MediaColumns.DATA); 5218 final String probeVolume = extractVolumeName(probePath); 5219 final String probeOwner = extractPathOwnerPackageName(probePath); 5220 if (Objects.equals(beforePath, probePath)) { 5221 Log.d(TAG, "Identical paths " + beforePath + "; not moving"); 5222 } else if (!Objects.equals(beforeVolume, probeVolume)) { 5223 throw new IllegalArgumentException("Changing volume from " + beforePath + " to " 5224 + probePath + " not allowed"); 5225 } else if (!Objects.equals(beforeOwner, probeOwner)) { 5226 throw new IllegalArgumentException("Changing ownership from " + beforePath + " to " 5227 + probePath + " not allowed"); 5228 } else { 5229 // Now that we've confirmed an actual movement is taking place, 5230 // ensure we have a unique destination 5231 initialValues.remove(MediaColumns.DATA); 5232 ensureUniqueFileColumns(match, uri, extras, initialValues, beforePath); 5233 5234 final String afterPath = initialValues.getAsString(MediaColumns.DATA); 5235 5236 Log.d(TAG, "Moving " + beforePath + " to " + afterPath); 5237 try { 5238 Os.rename(beforePath, afterPath); 5239 invalidateFuseDentry(beforePath); 5240 invalidateFuseDentry(afterPath); 5241 } catch (ErrnoException e) { 5242 if (e.errno == OsConstants.ENOENT) { 5243 Log.d(TAG, "Missing file at " + beforePath + "; continuing anyway"); 5244 } else { 5245 throw new IllegalStateException(e); 5246 } 5247 } 5248 initialValues.put(MediaColumns.DATA, afterPath); 5249 5250 // Some indexed metadata may have been derived from the path on 5251 // disk, so scan this item again to update it 5252 triggerScan = true; 5253 } 5254 5255 Trace.endSection(); 5256 } 5257 5258 // Make sure any updated paths look sane 5259 assertFileColumnsSane(match, uri, initialValues); 5260 5261 if (initialValues.containsKey(FileColumns.DATA)) { 5262 // If we're changing paths, invalidate any thumbnails 5263 triggerInvalidate = true; 5264 5265 // If the new file exists, trigger a scan to adjust any metadata 5266 // that might be derived from the path 5267 final String data = initialValues.getAsString(FileColumns.DATA); 5268 if (!TextUtils.isEmpty(data) && new File(data).exists()) { 5269 triggerScan = true; 5270 } 5271 } 5272 5273 // If we're already doing this update from an internal scan, no need to 5274 // kick off another no-op scan 5275 if (isCallingPackageSelf()) { 5276 triggerScan = false; 5277 } 5278 5279 // Since the update mutation may prevent us from matching items after 5280 // it's applied, we need to snapshot affected IDs here 5281 final LongArray updatedIds = new LongArray(); 5282 if (triggerInvalidate || triggerScan) { 5283 Trace.beginSection("snapshot"); 5284 final LocalCallingIdentity token = clearLocalCallingIdentity(); 5285 try (Cursor c = qb.query(helper, new String[] { FileColumns._ID }, 5286 userWhere, userWhereArgs, null, null, null, null, null)) { 5287 while (c.moveToNext()) { 5288 updatedIds.add(c.getLong(0)); 5289 } 5290 } finally { 5291 restoreLocalCallingIdentity(token); 5292 Trace.endSection(); 5293 } 5294 } 5295 5296 final ContentValues values = new ContentValues(initialValues); 5297 switch (match) { 5298 case AUDIO_MEDIA_ID: 5299 case AUDIO_PLAYLISTS_ID: 5300 case VIDEO_MEDIA_ID: 5301 case IMAGES_MEDIA_ID: 5302 case FILES_ID: 5303 case DOWNLOADS_ID: { 5304 FileUtils.computeValuesFromData(values, isFuseThread()); 5305 break; 5306 } 5307 } 5308 5309 if (initialValues.containsKey(FileColumns.MEDIA_TYPE)) { 5310 final int mediaType = initialValues.getAsInteger(FileColumns.MEDIA_TYPE); 5311 switch (mediaType) { 5312 case FileColumns.MEDIA_TYPE_AUDIO: { 5313 computeAudioLocalizedValues(values); 5314 computeAudioKeyValues(values); 5315 break; 5316 } 5317 } 5318 } 5319 5320 count = updateAllowingReplace(qb, helper, values, userWhere, userWhereArgs); 5321 5322 // If the caller tried (and failed) to update metadata, the file on disk 5323 // might have changed, to scan it to collect the latest metadata. 5324 if (triggerInvalidate || triggerScan) { 5325 Trace.beginSection("invalidate"); 5326 final LocalCallingIdentity token = clearLocalCallingIdentity(); 5327 try { 5328 for (int i = 0; i < updatedIds.size(); i++) { 5329 final long updatedId = updatedIds.get(i); 5330 final Uri updatedUri = Files.getContentUri(volumeName, updatedId); 5331 helper.postBackground(() -> { 5332 invalidateThumbnails(updatedUri); 5333 }); 5334 5335 if (triggerScan) { 5336 try (Cursor c = queryForSingleItem(updatedUri, 5337 new String[] { FileColumns.DATA }, null, null, null)) { 5338 final File file = new File(c.getString(0)); 5339 helper.postBlocking(() -> { 5340 final LocalCallingIdentity tokenInner = clearLocalCallingIdentity(); 5341 try { 5342 mMediaScanner.scanFile(file, REASON_DEMAND); 5343 } finally { 5344 restoreLocalCallingIdentity(tokenInner); 5345 } 5346 }); 5347 } catch (Exception e) { 5348 Log.w(TAG, "Failed to update metadata for " + updatedUri, e); 5349 } 5350 } 5351 } 5352 } finally { 5353 restoreLocalCallingIdentity(token); 5354 Trace.endSection(); 5355 } 5356 } 5357 5358 return count; 5359 } 5360 5361 /** 5362 * Update row(s) that match {@code userWhere} in MediaProvider database with {@code values}. 5363 * Treats update as replace for updates with conflicts. 5364 */ 5365 private int updateAllowingReplace(@NonNull SQLiteQueryBuilder qb, 5366 @NonNull DatabaseHelper helper, @NonNull ContentValues values, String userWhere, 5367 String[] userWhereArgs) throws SQLiteConstraintException { 5368 return helper.runWithTransaction((db) -> { 5369 try { 5370 return qb.update(helper, values, userWhere, userWhereArgs); 5371 } catch (SQLiteConstraintException e) { 5372 // b/155320967 Apps sometimes create a file via file path and then update another 5373 // explicitly inserted db row to this file. We have to resolve this update with a 5374 // replace. 5375 5376 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) { 5377 // We don't support replace for non-legacy apps. Non legacy apps should have 5378 // clearer interactions with MediaProvider. 5379 throw e; 5380 } 5381 5382 final String path = values.getAsString(FileColumns.DATA); 5383 5384 // We will only handle UNIQUE constraint error for FileColumns.DATA. We will not try 5385 // update and replace if no file exists for conflicting db row. 5386 if (path == null || !new File(path).exists()) { 5387 throw e; 5388 } 5389 5390 final Uri uri = FileUtils.getContentUriForPath(path); 5391 final boolean allowHidden = isCallingPackageAllowedHidden(); 5392 // The db row which caused UNIQUE constraint error may not match all column values 5393 // of the given queryBuilder, hence using a generic queryBuilder with Files uri. 5394 Bundle extras = new Bundle(); 5395 extras.putInt(QUERY_ARG_MATCH_PENDING, MATCH_INCLUDE); 5396 extras.putInt(QUERY_ARG_MATCH_TRASHED, MATCH_INCLUDE); 5397 final SQLiteQueryBuilder qbForReplace = getQueryBuilder(TYPE_DELETE, 5398 matchUri(uri, allowHidden), uri, extras, null); 5399 final long rowId = getIdIfPathOwnedByPackages(qbForReplace, helper, path, 5400 getSharedPackages()); 5401 5402 if (rowId != -1 && qbForReplace.delete(helper, "_id=?", 5403 new String[] {Long.toString(rowId)}) == 1) { 5404 Log.i(TAG, "Retrying database update after deleting conflicting entry"); 5405 return qb.update(helper, values, userWhere, userWhereArgs); 5406 } 5407 // Rethrow SQLiteConstraintException if app doesn't own the conflicting db row. 5408 throw e; 5409 } 5410 }); 5411 } 5412 5413 /** 5414 * Update the internal table of {@link MediaStore.Audio.Playlists.Members} 5415 * by parsing the playlist file on disk and resolving it against scanned 5416 * audio items. 5417 * <p> 5418 * When a playlist references a missing audio item, the associated 5419 * {@link Playlists.Members#PLAY_ORDER} is skipped, leaving a gap to ensure 5420 * that the playlist entry is retained to avoid user data loss. 5421 */ 5422 private void resolvePlaylistMembers(@NonNull Uri playlistUri) { 5423 Trace.beginSection("resolvePlaylistMembers"); 5424 try { 5425 final DatabaseHelper helper; 5426 try { 5427 helper = getDatabaseForUri(playlistUri); 5428 } catch (VolumeNotFoundException e) { 5429 throw e.rethrowAsIllegalArgumentException(); 5430 } 5431 5432 helper.runWithTransaction((db) -> { 5433 resolvePlaylistMembersInternal(playlistUri, db); 5434 return null; 5435 }); 5436 } finally { 5437 Trace.endSection(); 5438 } 5439 } 5440 5441 private void resolvePlaylistMembersInternal(@NonNull Uri playlistUri, 5442 @NonNull SQLiteDatabase db) { 5443 try { 5444 // Refresh playlist members based on what we parse from disk 5445 final String volumeName = getVolumeName(playlistUri); 5446 final long playlistId = ContentUris.parseId(playlistUri); 5447 db.delete("audio_playlists_map", "playlist_id=" + playlistId, null); 5448 5449 final Path playlistPath = queryForDataFile(playlistUri, null).toPath(); 5450 final Playlist playlist = new Playlist(); 5451 playlist.read(playlistPath.toFile()); 5452 5453 final List<Path> members = playlist.asList(); 5454 for (int i = 0; i < members.size(); i++) { 5455 try { 5456 final Path audioPath = playlistPath.getParent().resolve(members.get(i)); 5457 final long audioId = queryForPlaylistMember(volumeName, audioPath); 5458 5459 final ContentValues values = new ContentValues(); 5460 values.put(Playlists.Members.PLAY_ORDER, i + 1); 5461 values.put(Playlists.Members.PLAYLIST_ID, playlistId); 5462 values.put(Playlists.Members.AUDIO_ID, audioId); 5463 db.insert("audio_playlists_map", null, values); 5464 } catch (IOException e) { 5465 Log.w(TAG, "Failed to resolve playlist member", e); 5466 } 5467 } 5468 } catch (IOException e) { 5469 Log.w(TAG, "Failed to refresh playlist", e); 5470 } 5471 } 5472 5473 /** 5474 * Make two attempts to query this playlist member: first based on the exact 5475 * path, and if that fails, fall back to picking a single item matching the 5476 * display name. When there are multiple items with the same display name, 5477 * we can't resolve between them, and leave this member unresolved. 5478 */ 5479 private long queryForPlaylistMember(@NonNull String volumeName, @NonNull Path path) 5480 throws IOException { 5481 final Uri audioUri = Audio.Media.getContentUri(volumeName); 5482 try (Cursor c = queryForSingleItem(audioUri, 5483 new String[] { BaseColumns._ID }, MediaColumns.DATA + "=?", 5484 new String[] { path.toFile().getCanonicalPath() }, null)) { 5485 return c.getLong(0); 5486 } catch (FileNotFoundException ignored) { 5487 } 5488 try (Cursor c = queryForSingleItem(audioUri, 5489 new String[] { BaseColumns._ID }, MediaColumns.DISPLAY_NAME + "=?", 5490 new String[] { path.toFile().getName() }, null)) { 5491 return c.getLong(0); 5492 } catch (FileNotFoundException ignored) { 5493 } 5494 throw new FileNotFoundException(); 5495 } 5496 5497 /** 5498 * Add the given audio item to the given playlist. Defaults to adding at the 5499 * end of the playlist when no {@link Playlists.Members#PLAY_ORDER} is 5500 * defined. 5501 */ 5502 private long addPlaylistMembers(@NonNull Uri playlistUri, @NonNull ContentValues values) 5503 throws FallbackException { 5504 final long audioId = values.getAsLong(Audio.Playlists.Members.AUDIO_ID); 5505 final Uri audioUri = Audio.Media.getContentUri(getVolumeName(playlistUri), audioId); 5506 5507 Integer playOrder = values.getAsInteger(Playlists.Members.PLAY_ORDER); 5508 playOrder = (playOrder != null) ? (playOrder - 1) : Integer.MAX_VALUE; 5509 5510 try { 5511 final File playlistFile = queryForDataFile(playlistUri, null); 5512 final File audioFile = queryForDataFile(audioUri, null); 5513 5514 final Playlist playlist = new Playlist(); 5515 playlist.read(playlistFile); 5516 playOrder = playlist.add(playOrder, 5517 playlistFile.toPath().getParent().relativize(audioFile.toPath())); 5518 playlist.write(playlistFile); 5519 5520 resolvePlaylistMembers(playlistUri); 5521 5522 // Callers are interested in the actual ID we generated 5523 final Uri membersUri = Playlists.Members.getContentUri( 5524 getVolumeName(playlistUri), ContentUris.parseId(playlistUri)); 5525 try (Cursor c = query(membersUri, new String[] { BaseColumns._ID }, 5526 Playlists.Members.PLAY_ORDER + "=" + (playOrder + 1), null, null)) { 5527 c.moveToFirst(); 5528 return c.getLong(0); 5529 } 5530 } catch (IOException e) { 5531 throw new FallbackException("Failed to update playlist", e, 5532 android.os.Build.VERSION_CODES.R); 5533 } 5534 } 5535 5536 /** 5537 * Move an audio item within the given playlist. 5538 */ 5539 private int movePlaylistMembers(@NonNull Uri playlistUri, @NonNull ContentValues values, 5540 @NonNull Bundle queryArgs) throws FallbackException { 5541 final int fromIndex = resolvePlaylistIndex(playlistUri, queryArgs); 5542 final int toIndex = values.getAsInteger(Playlists.Members.PLAY_ORDER) - 1; 5543 if (fromIndex == -1) { 5544 throw new FallbackException("Failed to resolve playlist member " + queryArgs, 5545 android.os.Build.VERSION_CODES.R); 5546 } 5547 try { 5548 final File playlistFile = queryForDataFile(playlistUri, null); 5549 5550 final Playlist playlist = new Playlist(); 5551 playlist.read(playlistFile); 5552 final int finalIndex = playlist.move(fromIndex, toIndex); 5553 playlist.write(playlistFile); 5554 5555 resolvePlaylistMembers(playlistUri); 5556 return finalIndex; 5557 } catch (IOException e) { 5558 throw new FallbackException("Failed to update playlist", e, 5559 android.os.Build.VERSION_CODES.R); 5560 } 5561 } 5562 5563 /** 5564 * Remove an audio item from the given playlist. 5565 */ 5566 private int removePlaylistMembers(@NonNull Uri playlistUri, @NonNull Bundle queryArgs) 5567 throws FallbackException { 5568 final int index = resolvePlaylistIndex(playlistUri, queryArgs); 5569 try { 5570 final File playlistFile = queryForDataFile(playlistUri, null); 5571 5572 final Playlist playlist = new Playlist(); 5573 playlist.read(playlistFile); 5574 final int count; 5575 if (index == -1) { 5576 count = playlist.asList().size(); 5577 playlist.clear(); 5578 } else { 5579 count = 1; 5580 playlist.remove(index); 5581 } 5582 playlist.write(playlistFile); 5583 5584 resolvePlaylistMembers(playlistUri); 5585 return count; 5586 } catch (IOException e) { 5587 throw new FallbackException("Failed to update playlist", e, 5588 android.os.Build.VERSION_CODES.R); 5589 } 5590 } 5591 5592 /** 5593 * Resolve query arguments that are designed to select a specific playlist 5594 * item using its {@link Playlists.Members#PLAY_ORDER}. 5595 */ 5596 private int resolvePlaylistIndex(@NonNull Uri playlistUri, @NonNull Bundle queryArgs) { 5597 final Uri membersUri = Playlists.Members.getContentUri( 5598 getVolumeName(playlistUri), ContentUris.parseId(playlistUri)); 5599 5600 final DatabaseHelper helper; 5601 final SQLiteQueryBuilder qb; 5602 try { 5603 helper = getDatabaseForUri(membersUri); 5604 qb = getQueryBuilder(TYPE_DELETE, AUDIO_PLAYLISTS_ID_MEMBERS, 5605 membersUri, queryArgs, null); 5606 } catch (VolumeNotFoundException ignored) { 5607 return -1; 5608 } 5609 5610 try (Cursor c = qb.query(helper, 5611 new String[] { Playlists.Members.PLAY_ORDER }, queryArgs, null)) { 5612 if ((c.getCount() == 1) && c.moveToFirst()) { 5613 return c.getInt(0) - 1; 5614 } else { 5615 return -1; 5616 } 5617 } 5618 } 5619 5620 @Override 5621 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 5622 return openFileCommon(uri, mode, null); 5623 } 5624 5625 @Override 5626 public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) 5627 throws FileNotFoundException { 5628 return openFileCommon(uri, mode, signal); 5629 } 5630 5631 private ParcelFileDescriptor openFileCommon(Uri uri, String mode, CancellationSignal signal) 5632 throws FileNotFoundException { 5633 uri = safeUncanonicalize(uri); 5634 5635 final boolean allowHidden = isCallingPackageAllowedHidden(); 5636 final int match = matchUri(uri, allowHidden); 5637 final String volumeName = getVolumeName(uri); 5638 5639 // Handle some legacy cases where we need to redirect thumbnails 5640 switch (match) { 5641 case AUDIO_ALBUMART_ID: { 5642 final long albumId = Long.parseLong(uri.getPathSegments().get(3)); 5643 final Uri targetUri = ContentUris 5644 .withAppendedId(Audio.Albums.getContentUri(volumeName), albumId); 5645 return ensureThumbnail(targetUri, signal); 5646 } 5647 case AUDIO_ALBUMART_FILE_ID: { 5648 final long audioId = Long.parseLong(uri.getPathSegments().get(3)); 5649 final Uri targetUri = ContentUris 5650 .withAppendedId(Audio.Media.getContentUri(volumeName), audioId); 5651 return ensureThumbnail(targetUri, signal); 5652 } 5653 case VIDEO_MEDIA_ID_THUMBNAIL: { 5654 final long videoId = Long.parseLong(uri.getPathSegments().get(3)); 5655 final Uri targetUri = ContentUris 5656 .withAppendedId(Video.Media.getContentUri(volumeName), videoId); 5657 return ensureThumbnail(targetUri, signal); 5658 } 5659 case IMAGES_MEDIA_ID_THUMBNAIL: { 5660 final long imageId = Long.parseLong(uri.getPathSegments().get(3)); 5661 final Uri targetUri = ContentUris 5662 .withAppendedId(Images.Media.getContentUri(volumeName), imageId); 5663 return ensureThumbnail(targetUri, signal); 5664 } 5665 } 5666 5667 return openFileAndEnforcePathPermissionsHelper(uri, match, mode, signal); 5668 } 5669 5670 @Override 5671 public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) 5672 throws FileNotFoundException { 5673 return openTypedAssetFileCommon(uri, mimeTypeFilter, opts, null); 5674 } 5675 5676 @Override 5677 public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts, 5678 CancellationSignal signal) throws FileNotFoundException { 5679 return openTypedAssetFileCommon(uri, mimeTypeFilter, opts, signal); 5680 } 5681 5682 private AssetFileDescriptor openTypedAssetFileCommon(Uri uri, String mimeTypeFilter, 5683 Bundle opts, CancellationSignal signal) throws FileNotFoundException { 5684 uri = safeUncanonicalize(uri); 5685 5686 // TODO: enforce that caller has access to this uri 5687 5688 // Offer thumbnail of media, when requested 5689 final boolean wantsThumb = (opts != null) && opts.containsKey(ContentResolver.EXTRA_SIZE) 5690 && MimeUtils.startsWithIgnoreCase(mimeTypeFilter, "image/"); 5691 if (wantsThumb) { 5692 final ParcelFileDescriptor pfd = ensureThumbnail(uri, signal); 5693 return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH); 5694 } 5695 5696 // Worst case, return the underlying file 5697 return new AssetFileDescriptor(openFileCommon(uri, "r", signal), 0, 5698 AssetFileDescriptor.UNKNOWN_LENGTH); 5699 } 5700 5701 private ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal) 5702 throws FileNotFoundException { 5703 final boolean allowHidden = isCallingPackageAllowedHidden(); 5704 final int match = matchUri(uri, allowHidden); 5705 5706 Trace.beginSection("ensureThumbnail"); 5707 final LocalCallingIdentity token = clearLocalCallingIdentity(); 5708 try { 5709 switch (match) { 5710 case AUDIO_ALBUMS_ID: { 5711 final String volumeName = MediaStore.getVolumeName(uri); 5712 final Uri baseUri = MediaStore.Audio.Media.getContentUri(volumeName); 5713 final long albumId = ContentUris.parseId(uri); 5714 try (Cursor c = query(baseUri, new String[] { MediaStore.Audio.Media._ID }, 5715 MediaStore.Audio.Media.ALBUM_ID + "=" + albumId, null, null, signal)) { 5716 if (c.moveToFirst()) { 5717 final long audioId = c.getLong(0); 5718 final Uri targetUri = ContentUris.withAppendedId(baseUri, audioId); 5719 return mAudioThumbnailer.ensureThumbnail(targetUri, signal); 5720 } else { 5721 throw new FileNotFoundException("No media for album " + uri); 5722 } 5723 } 5724 } 5725 case AUDIO_MEDIA_ID: 5726 return mAudioThumbnailer.ensureThumbnail(uri, signal); 5727 case VIDEO_MEDIA_ID: 5728 return mVideoThumbnailer.ensureThumbnail(uri, signal); 5729 case IMAGES_MEDIA_ID: 5730 return mImageThumbnailer.ensureThumbnail(uri, signal); 5731 case FILES_ID: 5732 case DOWNLOADS_ID: { 5733 // When item is referenced in a generic way, resolve to actual type 5734 final int mediaType = MimeUtils.resolveMediaType(getType(uri)); 5735 switch (mediaType) { 5736 case FileColumns.MEDIA_TYPE_AUDIO: 5737 return mAudioThumbnailer.ensureThumbnail(uri, signal); 5738 case FileColumns.MEDIA_TYPE_VIDEO: 5739 return mVideoThumbnailer.ensureThumbnail(uri, signal); 5740 case FileColumns.MEDIA_TYPE_IMAGE: 5741 return mImageThumbnailer.ensureThumbnail(uri, signal); 5742 default: 5743 throw new FileNotFoundException(); 5744 } 5745 } 5746 default: 5747 throw new FileNotFoundException(); 5748 } 5749 } catch (IOException e) { 5750 Log.w(TAG, e); 5751 throw new FileNotFoundException(e.getMessage()); 5752 } finally { 5753 restoreLocalCallingIdentity(token); 5754 Trace.endSection(); 5755 } 5756 } 5757 5758 /** 5759 * Update the metadata columns for the image residing at given {@link Uri} 5760 * by reading data from the underlying image. 5761 */ 5762 private void updateImageMetadata(ContentValues values, File file) { 5763 final BitmapFactory.Options bitmapOpts = new BitmapFactory.Options(); 5764 bitmapOpts.inJustDecodeBounds = true; 5765 BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOpts); 5766 5767 values.put(MediaColumns.WIDTH, bitmapOpts.outWidth); 5768 values.put(MediaColumns.HEIGHT, bitmapOpts.outHeight); 5769 } 5770 5771 private void handleInsertedRowForFuse(long rowId) { 5772 if (isFuseThread()) { 5773 // Removes restored row ID saved list. 5774 mCallingIdentity.get().removeDeletedRowId(rowId); 5775 } 5776 } 5777 5778 private void handleUpdatedRowForFuse(@NonNull String oldPath, @NonNull String ownerPackage, 5779 long oldRowId, long newRowId) { 5780 if (oldRowId == newRowId) { 5781 // Update didn't delete or add row ID. We don't need to save row ID or remove saved 5782 // deleted ID. 5783 return; 5784 } 5785 5786 handleDeletedRowForFuse(oldPath, ownerPackage, oldRowId); 5787 handleInsertedRowForFuse(newRowId); 5788 } 5789 5790 private void handleDeletedRowForFuse(@NonNull String path, @NonNull String ownerPackage, 5791 long rowId) { 5792 if (!isFuseThread()) { 5793 return; 5794 } 5795 5796 // Invalidate saved owned ID's of the previous owner of the deleted path, this prevents old 5797 // owner from gaining access to newly created file with restored row ID. 5798 if (!ownerPackage.equals("null") && !ownerPackage.equals(getCallingPackageOrSelf())) { 5799 invalidateLocalCallingIdentityCache(ownerPackage, "owned_database_row_deleted:" 5800 + path); 5801 } 5802 // Saves row ID corresponding to deleted path. Saved row ID will be restored on subsequent 5803 // create or rename. 5804 mCallingIdentity.get().addDeletedRowId(path, rowId); 5805 } 5806 5807 private void handleOwnerPackageNameChange(@NonNull String oldPath, 5808 @NonNull String oldOwnerPackage, @NonNull String newOwnerPackage) { 5809 if (Objects.equals(oldOwnerPackage, newOwnerPackage)) { 5810 return; 5811 } 5812 // Invalidate saved owned ID's of the previous owner of the renamed path, this prevents old 5813 // owner from gaining access to replaced file. 5814 invalidateLocalCallingIdentityCache(oldOwnerPackage, "owner_package_changed:" + oldPath); 5815 } 5816 5817 /** 5818 * Return the {@link MediaColumns#DATA} field for the given {@code Uri}. 5819 */ 5820 File queryForDataFile(Uri uri, CancellationSignal signal) 5821 throws FileNotFoundException { 5822 return queryForDataFile(uri, null, null, signal); 5823 } 5824 5825 /** 5826 * Return the {@link MediaColumns#DATA} field for the given {@code Uri}. 5827 */ 5828 File queryForDataFile(Uri uri, String selection, String[] selectionArgs, 5829 CancellationSignal signal) throws FileNotFoundException { 5830 try (Cursor cursor = queryForSingleItem(uri, new String[] { MediaColumns.DATA }, 5831 selection, selectionArgs, signal)) { 5832 final String data = cursor.getString(0); 5833 if (TextUtils.isEmpty(data)) { 5834 throw new FileNotFoundException("Missing path for " + uri); 5835 } else { 5836 return new File(data); 5837 } 5838 } 5839 } 5840 5841 /** 5842 * Return the {@link Uri} for the given {@code File}. 5843 */ 5844 Uri queryForMediaUri(File file, CancellationSignal signal) throws FileNotFoundException { 5845 final String volumeName = FileUtils.getVolumeName(getContext(), file); 5846 final Uri uri = Files.getContentUri(volumeName); 5847 try (Cursor cursor = queryForSingleItem(uri, new String[] { MediaColumns._ID }, 5848 MediaColumns.DATA + "=?", new String[] { file.getAbsolutePath() }, signal)) { 5849 return ContentUris.withAppendedId(uri, cursor.getLong(0)); 5850 } 5851 } 5852 5853 /** 5854 * Query the given {@link Uri}, expecting only a single item to be found. 5855 * 5856 * @throws FileNotFoundException if no items were found, or multiple items 5857 * were found, or there was trouble reading the data. 5858 */ 5859 Cursor queryForSingleItem(Uri uri, String[] projection, String selection, 5860 String[] selectionArgs, CancellationSignal signal) throws FileNotFoundException { 5861 final Cursor c = query(uri, projection, 5862 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null), signal); 5863 if (c == null) { 5864 throw new FileNotFoundException("Missing cursor for " + uri); 5865 } else if (c.getCount() < 1) { 5866 FileUtils.closeQuietly(c); 5867 throw new FileNotFoundException("No item at " + uri); 5868 } else if (c.getCount() > 1) { 5869 FileUtils.closeQuietly(c); 5870 throw new FileNotFoundException("Multiple items at " + uri); 5871 } 5872 5873 if (c.moveToFirst()) { 5874 return c; 5875 } else { 5876 FileUtils.closeQuietly(c); 5877 throw new FileNotFoundException("Failed to read row from " + uri); 5878 } 5879 } 5880 5881 /** 5882 * Compares {@code itemOwner} with package name of {@link LocalCallingIdentity} and throws 5883 * {@link IllegalStateException} if it doesn't match. 5884 * Make sure to set calling identity properly before calling. 5885 */ 5886 private void requireOwnershipForItem(@Nullable String itemOwner, Uri item) { 5887 final boolean hasOwner = (itemOwner != null); 5888 final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), itemOwner); 5889 if (hasOwner && !callerIsOwner) { 5890 throw new IllegalStateException( 5891 "Only owner is able to interact with pending item " + item); 5892 } 5893 } 5894 5895 private File getFuseFile(File file) { 5896 String filePath = file.getPath().replaceFirst( 5897 "/storage/", "/mnt/user/" + UserHandle.myUserId() + "/"); 5898 return new File(filePath); 5899 } 5900 5901 private @NonNull FuseDaemon getFuseDaemonForFile(@NonNull File file) 5902 throws FileNotFoundException { 5903 final FuseDaemon daemon = ExternalStorageServiceImpl.getFuseDaemon(getVolumeId(file)); 5904 if (daemon == null) { 5905 throw new FileNotFoundException("Missing FUSE daemon for " + file); 5906 } else { 5907 return daemon; 5908 } 5909 } 5910 5911 private void invalidateFuseDentry(@NonNull File file) { 5912 invalidateFuseDentry(file.getAbsolutePath()); 5913 } 5914 5915 private void invalidateFuseDentry(@NonNull String path) { 5916 try { 5917 final FuseDaemon daemon = getFuseDaemonForFile(new File(path)); 5918 if (isFuseThread()) { 5919 // If we are on a FUSE thread, we don't need to invalidate, 5920 // (and *must* not, otherwise we'd crash) because the invalidation 5921 // is already reflected in the lower filesystem 5922 return; 5923 } else { 5924 daemon.invalidateFuseDentryCache(path); 5925 } 5926 } catch (FileNotFoundException e) { 5927 Log.w(TAG, "Failed to invalidate FUSE dentry", e); 5928 } 5929 } 5930 5931 /** 5932 * Replacement for {@link #openFileHelper(Uri, String)} which enforces any 5933 * permissions applicable to the path before returning. 5934 * 5935 * <p>This function should never be called from the fuse thread since it tries to open 5936 * a "/mnt/user" path. 5937 */ 5938 private ParcelFileDescriptor openFileAndEnforcePathPermissionsHelper(Uri uri, int match, 5939 String mode, CancellationSignal signal) throws FileNotFoundException { 5940 int modeBits = ParcelFileDescriptor.parseMode(mode); 5941 boolean forWrite = (modeBits & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0; 5942 if (forWrite) { 5943 // Upgrade 'w' only to 'rw'. This allows us acquire a WR_LOCK when calling 5944 // #shouldOpenWithFuse 5945 modeBits |= ParcelFileDescriptor.MODE_READ_WRITE; 5946 } 5947 5948 final boolean hasOwnerPackageName = hasOwnerPackageName(uri); 5949 final String[] projection = new String[] { 5950 MediaColumns.DATA, 5951 hasOwnerPackageName ? MediaColumns.OWNER_PACKAGE_NAME : "NULL", 5952 hasOwnerPackageName ? MediaColumns.IS_PENDING : "0", 5953 }; 5954 5955 final File file; 5956 final String ownerPackageName; 5957 final boolean isPending; 5958 final LocalCallingIdentity token = clearLocalCallingIdentity(); 5959 try (Cursor c = queryForSingleItem(uri, projection, null, null, signal)) { 5960 final String data = c.getString(0); 5961 if (TextUtils.isEmpty(data)) { 5962 throw new FileNotFoundException("Missing path for " + uri); 5963 } else { 5964 file = new File(data).getCanonicalFile(); 5965 } 5966 ownerPackageName = c.getString(1); 5967 isPending = c.getInt(2) != 0; 5968 } catch (IOException e) { 5969 throw new FileNotFoundException(e.toString()); 5970 } finally { 5971 restoreLocalCallingIdentity(token); 5972 } 5973 5974 checkAccess(uri, Bundle.EMPTY, file, forWrite); 5975 5976 // We don't check ownership for files with IS_PENDING set by FUSE 5977 if (isPending && !isPendingFromFuse(file)) { 5978 requireOwnershipForItem(ownerPackageName, uri); 5979 } 5980 5981 final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), ownerPackageName); 5982 // Figure out if we need to redact contents 5983 final boolean redactionNeeded = callerIsOwner ? false : isRedactionNeeded(uri); 5984 final RedactionInfo redactionInfo; 5985 try { 5986 redactionInfo = redactionNeeded ? getRedactionRanges(file) 5987 : new RedactionInfo(new long[0], new long[0]); 5988 } catch(IOException e) { 5989 throw new IllegalStateException(e); 5990 } 5991 5992 // Yell if caller requires original, since we can't give it to them 5993 // unless they have access granted above 5994 if (redactionNeeded && MediaStore.getRequireOriginal(uri)) { 5995 throw new UnsupportedOperationException( 5996 "Caller must hold ACCESS_MEDIA_LOCATION permission to access original"); 5997 } 5998 5999 // Kick off metadata update when writing is finished 6000 final OnCloseListener listener = (e) -> { 6001 // We always update metadata to reflect the state on disk, even when 6002 // the remote writer tried claiming an exception 6003 invalidateThumbnails(uri); 6004 6005 try { 6006 switch (match) { 6007 case IMAGES_THUMBNAILS_ID: 6008 case VIDEO_THUMBNAILS_ID: 6009 final ContentValues values = new ContentValues(); 6010 updateImageMetadata(values, file); 6011 update(uri, values, null, null); 6012 break; 6013 default: 6014 mMediaScanner.scanFile(file, REASON_DEMAND); 6015 break; 6016 } 6017 } catch (Exception e2) { 6018 Log.w(TAG, "Failed to update metadata for " + uri, e2); 6019 } 6020 }; 6021 6022 try { 6023 // First, handle any redaction that is needed for caller 6024 final ParcelFileDescriptor pfd; 6025 final String filePath = file.getPath(); 6026 if (redactionInfo.redactionRanges.length > 0) { 6027 if (SystemProperties.getBoolean(PROP_FUSE, false)) { 6028 // If fuse is enabled, we can provide an fd that points to the fuse 6029 // file system and handle redaction in the fuse handler when the caller reads. 6030 Log.i(TAG, "Redacting with new FUSE for " + filePath); 6031 long tid = android.os.Process.myTid(); 6032 synchronized (mShouldRedactThreadIds) { 6033 mShouldRedactThreadIds.add(tid); 6034 } 6035 try { 6036 pfd = FileUtils.openSafely(getFuseFile(file), modeBits); 6037 } finally { 6038 synchronized (mShouldRedactThreadIds) { 6039 mShouldRedactThreadIds.remove(mShouldRedactThreadIds.indexOf(tid)); 6040 } 6041 } 6042 } else { 6043 // TODO(b/135341978): Remove this and associated code 6044 // when fuse is on by default. 6045 Log.i(TAG, "Redacting with old FUSE for " + filePath); 6046 pfd = RedactingFileDescriptor.open( 6047 getContext(), 6048 file, 6049 modeBits, 6050 redactionInfo.redactionRanges, 6051 redactionInfo.freeOffsets); 6052 } 6053 } else { 6054 FuseDaemon daemon = null; 6055 try { 6056 daemon = getFuseDaemonForFile(file); 6057 } catch (FileNotFoundException ignored) { 6058 } 6059 ParcelFileDescriptor lowerFsFd = FileUtils.openSafely(file, modeBits); 6060 // Always acquire a readLock. This allows us make multiple opens via lower 6061 // filesystem 6062 boolean shouldOpenWithFuse = daemon != null 6063 && daemon.shouldOpenWithFuse(filePath, true /* forRead */, lowerFsFd.getFd()); 6064 6065 if (SystemProperties.getBoolean(PROP_FUSE, false) && shouldOpenWithFuse) { 6066 // If the file is already opened on the FUSE mount with VFS caching enabled 6067 // we return an upper filesystem fd (via FUSE) to avoid file corruption 6068 // resulting from cache inconsistencies between the upper and lower 6069 // filesystem caches 6070 Log.w(TAG, "Using FUSE for " + filePath); 6071 pfd = FileUtils.openSafely(getFuseFile(file), modeBits); 6072 try { 6073 lowerFsFd.close(); 6074 } catch (IOException e) { 6075 Log.w(TAG, "Failed to close lower filesystem fd " + file.getPath(), e); 6076 } 6077 } else { 6078 Log.i(TAG, "Using lower FS for " + filePath); 6079 if (forWrite) { 6080 // When opening for write on the lower filesystem, invalidate the VFS dentry 6081 // so subsequent open/getattr calls will return correctly. 6082 // 6083 // A 'dirty' dentry with write back cache enabled can cause the kernel to 6084 // ignore file attributes or even see stale page cache data when the lower 6085 // filesystem has been modified outside of the FUSE driver 6086 invalidateFuseDentry(file); 6087 } 6088 6089 pfd = lowerFsFd; 6090 } 6091 } 6092 6093 // Second, wrap in any listener that we've requested 6094 if (!isPending && forWrite && listener != null) { 6095 return ParcelFileDescriptor.wrap(pfd, BackgroundThread.getHandler(), listener); 6096 } else { 6097 return pfd; 6098 } 6099 } catch (IOException e) { 6100 if (e instanceof FileNotFoundException) { 6101 throw (FileNotFoundException) e; 6102 } else { 6103 throw new IllegalStateException(e); 6104 } 6105 } 6106 } 6107 6108 private void deleteAndInvalidate(@NonNull Path path) { 6109 deleteAndInvalidate(path.toFile()); 6110 } 6111 6112 private void deleteAndInvalidate(@NonNull File file) { 6113 file.delete(); 6114 invalidateFuseDentry(file); 6115 } 6116 6117 private void deleteIfAllowed(Uri uri, Bundle extras, String path) { 6118 try { 6119 final File file = new File(path); 6120 checkAccess(uri, extras, file, true); 6121 deleteAndInvalidate(file); 6122 } catch (Exception e) { 6123 Log.e(TAG, "Couldn't delete " + path, e); 6124 } 6125 } 6126 6127 @Deprecated 6128 private boolean isPending(Uri uri) { 6129 final int match = matchUri(uri, true); 6130 switch (match) { 6131 case AUDIO_MEDIA_ID: 6132 case VIDEO_MEDIA_ID: 6133 case IMAGES_MEDIA_ID: 6134 try (Cursor c = queryForSingleItem(uri, 6135 new String[] { MediaColumns.IS_PENDING }, null, null, null)) { 6136 return (c.getInt(0) != 0); 6137 } catch (FileNotFoundException e) { 6138 throw new IllegalStateException(e); 6139 } 6140 default: 6141 return false; 6142 } 6143 } 6144 6145 @Deprecated 6146 private boolean isRedactionNeeded(Uri uri) { 6147 return mCallingIdentity.get().hasPermission(PERMISSION_IS_REDACTION_NEEDED); 6148 } 6149 6150 private boolean isRedactionNeeded() { 6151 return mCallingIdentity.get().hasPermission(PERMISSION_IS_REDACTION_NEEDED); 6152 } 6153 6154 private boolean isCallingPackageRequestingLegacy() { 6155 return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_GRANTED); 6156 } 6157 6158 private static int getFileMediaType(String path) { 6159 final File file = new File(path); 6160 final String mimeType = MimeUtils.resolveMimeType(file); 6161 return MimeUtils.resolveMediaType(mimeType); 6162 } 6163 6164 private boolean canAccessMediaFile(String filePath, boolean allowLegacy) { 6165 if (!allowLegacy && isCallingPackageRequestingLegacy()) { 6166 return false; 6167 } 6168 switch (getFileMediaType(filePath)) { 6169 case FileColumns.MEDIA_TYPE_IMAGE: 6170 return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_IMAGES); 6171 case FileColumns.MEDIA_TYPE_VIDEO: 6172 return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_VIDEO); 6173 default: 6174 return false; 6175 } 6176 } 6177 6178 /** 6179 * Returns true if: 6180 * <ul> 6181 * <li>the calling identity is an app targeting Q or older versions AND is requesting legacy 6182 * storage 6183 * <li>the calling identity holds {@code MANAGE_EXTERNAL_STORAGE} 6184 * <li>the calling identity owns filePath (eg /Android/data/com.foo) 6185 * <li>the calling identity has permission to write images and the given file is an image file 6186 * <li>the calling identity has permission to write video and the given file is an video file 6187 * </ul> 6188 */ 6189 private boolean shouldBypassFuseRestrictions(boolean forWrite, String filePath) { 6190 boolean isRequestingLegacyStorage = forWrite ? isCallingPackageLegacyWrite() 6191 : isCallingPackageLegacyRead(); 6192 if (isRequestingLegacyStorage) { 6193 return true; 6194 } 6195 6196 if (isCallingPackageManager()) { 6197 return true; 6198 } 6199 6200 // Files under the apps own private directory 6201 final String appSpecificDir = extractPathOwnerPackageName(filePath); 6202 if (appSpecificDir != null && isCallingIdentitySharedPackageName(appSpecificDir)) { 6203 return true; 6204 } 6205 6206 // Apps with write access to images and/or videos can bypass our restrictions if all of the 6207 // the files they're accessing are of the compatible media type. 6208 if (canAccessMediaFile(filePath, /*allowLegacy*/ true)) { 6209 return true; 6210 } 6211 6212 return false; 6213 } 6214 6215 /** 6216 * Returns true if the passed in path is an application-private data directory 6217 * (such as Android/data/com.foo or Android/obb/com.foo) that does not belong to the caller. 6218 */ 6219 private boolean isPrivatePackagePathNotOwnedByCaller(String path) { 6220 // Files under the apps own private directory 6221 final String appSpecificDir = extractPathOwnerPackageName(path); 6222 6223 if (appSpecificDir == null) { 6224 return false; 6225 } 6226 6227 final String relativePath = extractRelativePath(path); 6228 // Android/media is not considered private, because it contains media that is explicitly 6229 // scanned and shared by other apps 6230 if (relativePath.startsWith("Android/media")) { 6231 return false; 6232 } 6233 6234 // This is a private-package path; return true if not owned by the caller 6235 return !isCallingIdentitySharedPackageName(appSpecificDir); 6236 } 6237 6238 /** 6239 * Set of Exif tags that should be considered for redaction. 6240 */ 6241 private static final String[] REDACTED_EXIF_TAGS = new String[] { 6242 ExifInterface.TAG_GPS_ALTITUDE, 6243 ExifInterface.TAG_GPS_ALTITUDE_REF, 6244 ExifInterface.TAG_GPS_AREA_INFORMATION, 6245 ExifInterface.TAG_GPS_DOP, 6246 ExifInterface.TAG_GPS_DATESTAMP, 6247 ExifInterface.TAG_GPS_DEST_BEARING, 6248 ExifInterface.TAG_GPS_DEST_BEARING_REF, 6249 ExifInterface.TAG_GPS_DEST_DISTANCE, 6250 ExifInterface.TAG_GPS_DEST_DISTANCE_REF, 6251 ExifInterface.TAG_GPS_DEST_LATITUDE, 6252 ExifInterface.TAG_GPS_DEST_LATITUDE_REF, 6253 ExifInterface.TAG_GPS_DEST_LONGITUDE, 6254 ExifInterface.TAG_GPS_DEST_LONGITUDE_REF, 6255 ExifInterface.TAG_GPS_DIFFERENTIAL, 6256 ExifInterface.TAG_GPS_IMG_DIRECTION, 6257 ExifInterface.TAG_GPS_IMG_DIRECTION_REF, 6258 ExifInterface.TAG_GPS_LATITUDE, 6259 ExifInterface.TAG_GPS_LATITUDE_REF, 6260 ExifInterface.TAG_GPS_LONGITUDE, 6261 ExifInterface.TAG_GPS_LONGITUDE_REF, 6262 ExifInterface.TAG_GPS_MAP_DATUM, 6263 ExifInterface.TAG_GPS_MEASURE_MODE, 6264 ExifInterface.TAG_GPS_PROCESSING_METHOD, 6265 ExifInterface.TAG_GPS_SATELLITES, 6266 ExifInterface.TAG_GPS_SPEED, 6267 ExifInterface.TAG_GPS_SPEED_REF, 6268 ExifInterface.TAG_GPS_STATUS, 6269 ExifInterface.TAG_GPS_TIMESTAMP, 6270 ExifInterface.TAG_GPS_TRACK, 6271 ExifInterface.TAG_GPS_TRACK_REF, 6272 ExifInterface.TAG_GPS_VERSION_ID, 6273 }; 6274 6275 /** 6276 * Set of ISO boxes that should be considered for redaction. 6277 */ 6278 private static final int[] REDACTED_ISO_BOXES = new int[] { 6279 IsoInterface.BOX_LOCI, 6280 IsoInterface.BOX_XYZ, 6281 IsoInterface.BOX_GPS, 6282 IsoInterface.BOX_GPS0, 6283 }; 6284 6285 public static final Set<String> sRedactedExifTags = new ArraySet<>( 6286 Arrays.asList(REDACTED_EXIF_TAGS)); 6287 6288 private static final class RedactionInfo { 6289 public final long[] redactionRanges; 6290 public final long[] freeOffsets; 6291 public RedactionInfo(long[] redactionRanges, long[] freeOffsets) { 6292 this.redactionRanges = redactionRanges; 6293 this.freeOffsets = freeOffsets; 6294 } 6295 } 6296 6297 /** 6298 * Calculates the ranges that need to be redacted for the given file and user that wants to 6299 * access the file. 6300 * 6301 * @param uid UID of the package wanting to access the file 6302 * @param path File path 6303 * @param tid thread id making IO on the FUSE filesystem 6304 * @return Ranges that should be redacted. 6305 * 6306 * @throws IOException if an error occurs while calculating the redaction ranges 6307 * 6308 * Called from JNI in jni/MediaProviderWrapper.cpp 6309 */ 6310 @Keep 6311 @NonNull 6312 public long[] getRedactionRangesForFuse(String path, int uid, int tid) throws IOException { 6313 final File file = new File(path); 6314 6315 // When we're calculating redaction ranges for MediaProvider, it means we're actually 6316 // calculating redaction ranges for another app that called to MediaProvider through Binder. 6317 // If the tid is in mShouldRedactThreadIds, we should redact, otherwise, we don't redact 6318 if (uid == android.os.Process.myUid()) { 6319 boolean shouldRedact = false; 6320 synchronized (mShouldRedactThreadIds) { 6321 shouldRedact = mShouldRedactThreadIds.indexOf(tid) != -1; 6322 } 6323 if (shouldRedact) { 6324 return getRedactionRanges(file).redactionRanges; 6325 } else { 6326 return new long[0]; 6327 } 6328 } 6329 6330 final LocalCallingIdentity token = 6331 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 6332 6333 long[] res = new long[0]; 6334 try { 6335 if (!isRedactionNeeded() 6336 || shouldBypassFuseRestrictions(/*forWrite*/ false, path)) { 6337 return res; 6338 } 6339 6340 final Uri contentUri = FileUtils.getContentUriForPath(path); 6341 final String[] projection = new String[]{ 6342 MediaColumns.OWNER_PACKAGE_NAME, MediaColumns._ID }; 6343 final String selection = MediaColumns.DATA + "=?"; 6344 final String[] selectionArgs = new String[] { path }; 6345 final String ownerPackageName; 6346 final Uri item; 6347 try (final Cursor c = queryForSingleItem(contentUri, projection, selection, 6348 selectionArgs, null)) { 6349 c.moveToFirst(); 6350 ownerPackageName = c.getString(0); 6351 item = ContentUris.withAppendedId(contentUri, /*item id*/ c.getInt(1)); 6352 } catch (FileNotFoundException e) { 6353 // Ideally, this shouldn't happen unless the file was deleted after we checked its 6354 // existence and before we get to the redaction logic here. In this case we throw 6355 // and fail the operation and FuseDaemon should handle this and fail the whole open 6356 // operation gracefully. 6357 throw new FileNotFoundException( 6358 path + " not found while calculating redaction ranges: " + e.getMessage()); 6359 } 6360 6361 final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), 6362 ownerPackageName); 6363 final boolean callerHasUriPermission = getContext().checkUriPermission( 6364 item, mCallingIdentity.get().pid, mCallingIdentity.get().uid, 6365 Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == PERMISSION_GRANTED; 6366 6367 if (!callerIsOwner && !callerHasUriPermission) { 6368 res = getRedactionRanges(file).redactionRanges; 6369 } 6370 } finally { 6371 restoreLocalCallingIdentity(token); 6372 } 6373 return res; 6374 } 6375 6376 /** 6377 * Calculates the ranges containing sensitive metadata that should be redacted if the caller 6378 * doesn't have the required permissions. 6379 * 6380 * @param file file to be redacted 6381 * @return the ranges to be redacted in a RedactionInfo object, could be empty redaction ranges 6382 * if there's sensitive metadata 6383 * @throws IOException if an IOException happens while calculating the redaction ranges 6384 */ 6385 @VisibleForTesting 6386 public static RedactionInfo getRedactionRanges(File file) throws IOException { 6387 Trace.beginSection("getRedactionRanges"); 6388 final LongArray res = new LongArray(); 6389 final LongArray freeOffsets = new LongArray(); 6390 try (FileInputStream is = new FileInputStream(file)) { 6391 final String mimeType = MimeUtils.resolveMimeType(file); 6392 if (ExifInterface.isSupportedMimeType(mimeType)) { 6393 final ExifInterface exif = new ExifInterface(is.getFD()); 6394 for (String tag : REDACTED_EXIF_TAGS) { 6395 final long[] range = exif.getAttributeRange(tag); 6396 if (range != null) { 6397 res.add(range[0]); 6398 res.add(range[0] + range[1]); 6399 } 6400 } 6401 // Redact xmp where present 6402 final XmpInterface exifXmp = XmpInterface.fromContainer(exif); 6403 res.addAll(exifXmp.getRedactionRanges()); 6404 } 6405 6406 if (IsoInterface.isSupportedMimeType(mimeType)) { 6407 final IsoInterface iso = IsoInterface.fromFileDescriptor(is.getFD()); 6408 for (int box : REDACTED_ISO_BOXES) { 6409 final long[] ranges = iso.getBoxRanges(box); 6410 for (int i = 0; i < ranges.length; i += 2) { 6411 long boxTypeOffset = ranges[i] - 4; 6412 freeOffsets.add(boxTypeOffset); 6413 res.add(boxTypeOffset); 6414 res.add(ranges[i + 1]); 6415 } 6416 } 6417 // Redact xmp where present 6418 final XmpInterface isoXmp = XmpInterface.fromContainer(iso); 6419 res.addAll(isoXmp.getRedactionRanges()); 6420 } 6421 } catch (FileNotFoundException ignored) { 6422 // If file not found, then there's nothing to redact 6423 } catch (IOException e) { 6424 throw new IOException("Failed to redact " + file, e); 6425 } 6426 Trace.endSection(); 6427 return new RedactionInfo(res.toArray(), freeOffsets.toArray()); 6428 } 6429 6430 /** 6431 * @return {@code true} if {@code file} is pending from FUSE, {@code false} otherwise. 6432 * Files pending from FUSE will not have pending file pattern. 6433 */ 6434 private static boolean isPendingFromFuse(@NonNull File file) { 6435 final Matcher matcher = 6436 FileUtils.PATTERN_EXPIRES_FILE.matcher(extractDisplayName(file.getName())); 6437 return !matcher.matches(); 6438 } 6439 6440 /** 6441 * Checks if the app identified by the given UID is allowed to open the given file for the given 6442 * access mode. 6443 * 6444 * @param path the path of the file to be opened 6445 * @param uid UID of the app requesting to open the file 6446 * @param forWrite specifies if the file is to be opened for write 6447 * @return 0 upon success. {@link OsConstants#EACCES} if the operation is illegal or not 6448 * permitted for the given {@code uid} or if the calling package is a legacy app that doesn't 6449 * have right storage permission. 6450 * 6451 * Called from JNI in jni/MediaProviderWrapper.cpp 6452 */ 6453 @Keep 6454 public int isOpenAllowedForFuse(String path, int uid, boolean forWrite) { 6455 final LocalCallingIdentity token = 6456 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 6457 6458 try { 6459 if (isPrivatePackagePathNotOwnedByCaller(path)) { 6460 Log.e(TAG, "Can't open a file in another app's external directory!"); 6461 return OsConstants.ENOENT; 6462 } 6463 6464 if (shouldBypassFuseRestrictions(forWrite, path)) { 6465 return 0; 6466 } 6467 // Legacy apps that made is this far don't have the right storage permission and hence 6468 // are not allowed to access anything other than their external app directory 6469 if (isCallingPackageRequestingLegacy()) { 6470 return OsConstants.EACCES; 6471 } 6472 6473 final Uri contentUri = FileUtils.getContentUriForPath(path); 6474 final String[] projection = new String[]{ 6475 MediaColumns._ID, 6476 MediaColumns.OWNER_PACKAGE_NAME, 6477 MediaColumns.IS_PENDING}; 6478 final String selection = MediaColumns.DATA + "=?"; 6479 final String[] selectionArgs = new String[] { path }; 6480 final Uri fileUri; 6481 final boolean isPending; 6482 String ownerPackageName = null; 6483 try (final Cursor c = queryForSingleItem(contentUri, projection, selection, 6484 selectionArgs, null)) { 6485 fileUri = ContentUris.withAppendedId(contentUri, c.getInt(0)); 6486 ownerPackageName = c.getString(1); 6487 isPending = c.getInt(2) != 0; 6488 } 6489 6490 final File file = new File(path); 6491 checkAccess(fileUri, Bundle.EMPTY, file, forWrite); 6492 6493 // We don't check ownership for files with IS_PENDING set by FUSE 6494 if (isPending && !isPendingFromFuse(new File(path))) { 6495 requireOwnershipForItem(ownerPackageName, fileUri); 6496 } 6497 return 0; 6498 } catch (FileNotFoundException e) { 6499 // We are here because 6500 // * App doesn't have read permission to the requested path, hence queryForSingleItem 6501 // couldn't return a valid db row, or, 6502 // * There is no db row corresponding to the requested path, which is more unlikely. 6503 // In both of these cases, it means that app doesn't have access permission to the file. 6504 Log.e(TAG, "Couldn't find file: " + path); 6505 return OsConstants.EACCES; 6506 } catch (IllegalStateException | SecurityException e) { 6507 Log.e(TAG, "Permission to access file: " + path + " is denied"); 6508 return OsConstants.EACCES; 6509 } finally { 6510 restoreLocalCallingIdentity(token); 6511 } 6512 } 6513 6514 /** 6515 * Returns {@code true} if {@link #mCallingIdentity#getSharedPackages(String)} contains the 6516 * given package name, {@code false} otherwise. 6517 * <p> Assumes that {@code mCallingIdentity} has been properly set to reflect the calling 6518 * package. 6519 */ 6520 private boolean isCallingIdentitySharedPackageName(@NonNull String packageName) { 6521 for (String sharedPkgName : mCallingIdentity.get().getSharedPackageNames()) { 6522 if (packageName.toLowerCase(Locale.ROOT) 6523 .equals(sharedPkgName.toLowerCase(Locale.ROOT))) { 6524 return true; 6525 } 6526 } 6527 return false; 6528 } 6529 6530 /** 6531 * @throws IllegalStateException if path is invalid or doesn't match a volume. 6532 */ 6533 @NonNull 6534 private Uri getContentUriForFile(@NonNull String filePath, @NonNull String mimeType) { 6535 final String volName = FileUtils.getVolumeName(getContext(), new File(filePath)); 6536 Uri uri = Files.getContentUri(volName); 6537 final String topLevelDir = extractTopLevelDir(filePath); 6538 if (topLevelDir == null) { 6539 // If the file path doesn't match the external storage directory, we use the files URI 6540 // as default and let #insert enforce the restrictions 6541 return uri; 6542 } 6543 switch (topLevelDir) { 6544 case DIRECTORY_PODCASTS: 6545 case DIRECTORY_RINGTONES: 6546 case DIRECTORY_ALARMS: 6547 case DIRECTORY_NOTIFICATIONS: 6548 case DIRECTORY_AUDIOBOOKS: 6549 uri = Audio.Media.getContentUri(volName); 6550 break; 6551 case DIRECTORY_MUSIC: 6552 if (MimeUtils.isPlaylistMimeType(mimeType)) { 6553 uri = Audio.Playlists.getContentUri(volName); 6554 } else if (!MimeUtils.isSubtitleMimeType(mimeType)) { 6555 // Send Files uri for media type subtitle 6556 uri = Audio.Media.getContentUri(volName); 6557 } 6558 break; 6559 case DIRECTORY_MOVIES: 6560 if (MimeUtils.isPlaylistMimeType(mimeType)) { 6561 uri = Audio.Playlists.getContentUri(volName); 6562 } else if (!MimeUtils.isSubtitleMimeType(mimeType)) { 6563 // Send Files uri for media type subtitle 6564 uri = Video.Media.getContentUri(volName); 6565 } 6566 break; 6567 case DIRECTORY_DCIM: 6568 case DIRECTORY_PICTURES: 6569 if (MimeUtils.isImageMimeType(mimeType)) { 6570 uri = Images.Media.getContentUri(volName); 6571 } else { 6572 uri = Video.Media.getContentUri(volName); 6573 } 6574 break; 6575 case DIRECTORY_DOWNLOADS: 6576 case DIRECTORY_DOCUMENTS: 6577 break; 6578 default: 6579 Log.w(TAG, "Forgot to handle a top level directory in getContentUriForFile?"); 6580 } 6581 return uri; 6582 } 6583 6584 private boolean fileExists(@NonNull String absolutePath) { 6585 // We don't care about specific columns in the match, 6586 // we just want to check IF there's a match 6587 final String[] projection = {}; 6588 final String selection = FileColumns.DATA + " = ?"; 6589 final String[] selectionArgs = {absolutePath}; 6590 final Uri uri = FileUtils.getContentUriForPath(absolutePath); 6591 6592 final LocalCallingIdentity token = clearLocalCallingIdentity(); 6593 try { 6594 try (final Cursor c = query(uri, projection, selection, selectionArgs, null)) { 6595 // Shouldn't return null 6596 return c.getCount() > 0; 6597 } 6598 } finally { 6599 clearLocalCallingIdentity(token); 6600 } 6601 } 6602 6603 private boolean isExternalMediaDirectory(@NonNull String path) { 6604 final String relativePath = extractRelativePath(path); 6605 if (relativePath != null) { 6606 return relativePath.startsWith("Android/media"); 6607 } 6608 return false; 6609 } 6610 6611 private Uri insertFileForFuse(@NonNull String path, @NonNull Uri uri, @NonNull String mimeType, 6612 boolean useData) { 6613 ContentValues values = new ContentValues(); 6614 values.put(FileColumns.OWNER_PACKAGE_NAME, getCallingPackageOrSelf()); 6615 values.put(MediaColumns.MIME_TYPE, mimeType); 6616 values.put(FileColumns.IS_PENDING, 1); 6617 6618 if (useData) { 6619 values.put(FileColumns.DATA, path); 6620 } else { 6621 values.put(FileColumns.VOLUME_NAME, extractVolumeName(path)); 6622 values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path)); 6623 values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path)); 6624 } 6625 return insert(uri, values, Bundle.EMPTY); 6626 } 6627 6628 /** 6629 * Enforces file creation restrictions (see return values) for the given file on behalf of the 6630 * app with the given {@code uid}. If the file is is added to the shared storage, creates a 6631 * database entry for it. 6632 * <p> Does NOT create file. 6633 * 6634 * @param path the path of the file 6635 * @param uid UID of the app requesting to create the file 6636 * @return In case of success, 0. If the operation is illegal or not permitted, returns the 6637 * appropriate {@code errno} value: 6638 * <ul> 6639 * <li>{@link OsConstants#ENOENT} if the app tries to create file in other app's external dir 6640 * <li>{@link OsConstants#EEXIST} if the file already exists 6641 * <li>{@link OsConstants#EPERM} if the file type doesn't match the relative path, or if the 6642 * calling package is a legacy app that doesn't have WRITE_EXTERNAL_STORAGE permission. 6643 * <li>{@link OsConstants#EIO} in case of any other I/O exception 6644 * </ul> 6645 * 6646 * @throws IllegalStateException if given path is invalid. 6647 * 6648 * Called from JNI in jni/MediaProviderWrapper.cpp 6649 */ 6650 @Keep 6651 public int insertFileIfNecessaryForFuse(@NonNull String path, int uid) { 6652 final LocalCallingIdentity token = 6653 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 6654 6655 try { 6656 if (isPrivatePackagePathNotOwnedByCaller(path)) { 6657 Log.e(TAG, "Can't create a file in another app's external directory"); 6658 return OsConstants.ENOENT; 6659 } 6660 6661 if (!path.equals(getAbsoluteSanitizedPath(path))) { 6662 Log.e(TAG, "File name contains invalid characters"); 6663 return OsConstants.EPERM; 6664 } 6665 6666 final String mimeType = MimeUtils.resolveMimeType(new File(path)); 6667 6668 if (shouldBypassFuseRestrictions(/*forWrite*/ true, path)) { 6669 final boolean callerRequestingLegacy = isCallingPackageRequestingLegacy(); 6670 if (!fileExists(path)) { 6671 // If app has already inserted the db row, inserting the row again might set 6672 // IS_PENDING=1. We shouldn't overwrite existing entry as part of FUSE 6673 // operation, hence, insert the db row only when it doesn't exist. 6674 try { 6675 insertFileForFuse(path, FileUtils.getContentUriForPath(path), 6676 mimeType, /*useData*/ callerRequestingLegacy); 6677 } catch (Exception ignored) { 6678 } 6679 } else { 6680 // Upon creating a file via FUSE, if a row matching the path already exists 6681 // but a file doesn't exist on the filesystem, we transfer ownership to the 6682 // app attempting to create the file. If we don't update ownership, then the 6683 // app that inserted the original row may be able to observe the contents of 6684 // written file even though they don't hold the right permissions to do so. 6685 if (callerRequestingLegacy) { 6686 final String owner = getCallingPackageOrSelf(); 6687 if (owner != null && !updateOwnerForPath(path, owner)) { 6688 return OsConstants.EPERM; 6689 } 6690 } 6691 } 6692 6693 return 0; 6694 } 6695 6696 // Legacy apps that made is this far don't have the right storage permission and hence 6697 // are not allowed to access anything other than their external app directory 6698 if (isCallingPackageRequestingLegacy()) { 6699 return OsConstants.EPERM; 6700 } 6701 6702 if (fileExists(path)) { 6703 // If the file already exists in the db, we shouldn't allow the file creation. 6704 return OsConstants.EEXIST; 6705 } 6706 6707 final Uri contentUri = getContentUriForFile(path, mimeType); 6708 final Uri item = insertFileForFuse(path, contentUri, mimeType, /*useData*/ false); 6709 if (item == null) { 6710 return OsConstants.EPERM; 6711 } 6712 return 0; 6713 } catch (IllegalArgumentException e) { 6714 Log.e(TAG, "insertFileIfNecessary failed", e); 6715 return OsConstants.EPERM; 6716 } finally { 6717 restoreLocalCallingIdentity(token); 6718 } 6719 } 6720 6721 private boolean updateOwnerForPath(@NonNull String path, @NonNull String newOwner) { 6722 final DatabaseHelper helper; 6723 try { 6724 helper = getDatabaseForUri(FileUtils.getContentUriForPath(path)); 6725 } catch (VolumeNotFoundException e) { 6726 // Cannot happen, as this is a path that we already resolved. 6727 throw new AssertionError("Path must already be resolved", e); 6728 } 6729 6730 ContentValues values = new ContentValues(1); 6731 values.put(FileColumns.OWNER_PACKAGE_NAME, newOwner); 6732 6733 return helper.runWithoutTransaction((db) -> { 6734 return db.update("files", values, "_data=?", new String[] { path }); 6735 }) == 1; 6736 } 6737 6738 private static int deleteFileUnchecked(@NonNull String path) { 6739 final File toDelete = new File(path); 6740 if (toDelete.delete()) { 6741 return 0; 6742 } else { 6743 return OsConstants.ENOENT; 6744 } 6745 } 6746 6747 /** 6748 * Deletes file with the given {@code path} on behalf of the app with the given {@code uid}. 6749 * <p>Before deleting, checks if app has permissions to delete this file. 6750 * 6751 * @param path the path of the file 6752 * @param uid UID of the app requesting to delete the file 6753 * @return 0 upon success. 6754 * In case of error, return the appropriate negated {@code errno} value: 6755 * <ul> 6756 * <li>{@link OsConstants#ENOENT} if the file does not exist or if the app tries to delete file 6757 * in another app's external dir 6758 * <li>{@link OsConstants#EPERM} a security exception was thrown by {@link #delete}, or if the 6759 * calling package is a legacy app that doesn't have WRITE_EXTERNAL_STORAGE permission. 6760 * </ul> 6761 * 6762 * Called from JNI in jni/MediaProviderWrapper.cpp 6763 */ 6764 @Keep 6765 public int deleteFileForFuse(@NonNull String path, int uid) throws IOException { 6766 final LocalCallingIdentity token = 6767 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 6768 try { 6769 if (isPrivatePackagePathNotOwnedByCaller(path)) { 6770 Log.e(TAG, "Can't delete a file in another app's external directory!"); 6771 return OsConstants.ENOENT; 6772 } 6773 6774 final boolean shouldBypass = shouldBypassFuseRestrictions(/*forWrite*/ true, path); 6775 6776 // Legacy apps that made is this far don't have the right storage permission and hence 6777 // are not allowed to access anything other than their external app directory 6778 if (!shouldBypass && isCallingPackageRequestingLegacy()) { 6779 return OsConstants.EPERM; 6780 } 6781 6782 final Uri contentUri = FileUtils.getContentUriForPath(path); 6783 final String where = FileColumns.DATA + " = ?"; 6784 final String[] whereArgs = {path}; 6785 6786 if (delete(contentUri, where, whereArgs) == 0) { 6787 if (shouldBypass) { 6788 return deleteFileUnchecked(path); 6789 } 6790 return OsConstants.ENOENT; 6791 } else { 6792 // success - 1 file was deleted 6793 return 0; 6794 } 6795 6796 } catch (SecurityException e) { 6797 Log.e(TAG, "File deletion not allowed", e); 6798 return OsConstants.EPERM; 6799 } finally { 6800 restoreLocalCallingIdentity(token); 6801 } 6802 } 6803 6804 /** 6805 * Checks if the app with the given UID is allowed to create or delete the directory with the 6806 * given path. 6807 * 6808 * @param path File path of the directory that the app wants to create/delete 6809 * @param uid UID of the app that wants to create/delete the directory 6810 * @param forCreate denotes whether the operation is directory creation or deletion 6811 * @return 0 if the operation is allowed, or the following {@code errno} values: 6812 * <ul> 6813 * <li>{@link OsConstants#EACCES} if the app tries to create/delete a dir in another app's 6814 * external directory, or if the calling package is a legacy app that doesn't have 6815 * WRITE_EXTERNAL_STORAGE permission. 6816 * <li>{@link OsConstants#EPERM} if the app tries to create/delete a top-level directory. 6817 * </ul> 6818 * 6819 * Called from JNI in jni/MediaProviderWrapper.cpp 6820 */ 6821 @Keep 6822 public int isDirectoryCreationOrDeletionAllowedForFuse( 6823 @NonNull String path, int uid, boolean forCreate) { 6824 final LocalCallingIdentity token = 6825 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 6826 6827 try { 6828 // App dirs are not indexed, so we don't create an entry for the file. 6829 if (isPrivatePackagePathNotOwnedByCaller(path)) { 6830 Log.e(TAG, "Can't modify another app's external directory!"); 6831 return OsConstants.EACCES; 6832 } 6833 6834 if (shouldBypassFuseRestrictions(/*forWrite*/ true, path)) { 6835 return 0; 6836 } 6837 // Legacy apps that made is this far don't have the right storage permission and hence 6838 // are not allowed to access anything other than their external app directory 6839 if (isCallingPackageRequestingLegacy()) { 6840 return OsConstants.EACCES; 6841 } 6842 6843 final String[] relativePath = sanitizePath(extractRelativePath(path)); 6844 final boolean isTopLevelDir = 6845 relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]); 6846 if (isTopLevelDir) { 6847 // We allow creating the default top level directories only, all other operations on 6848 // top level directories are not allowed. 6849 if (forCreate && isDefaultDirectoryName(extractDisplayName(path))) { 6850 return 0; 6851 } 6852 Log.e(TAG, 6853 "Creating a non-default top level directory or deleting an existing" 6854 + " one is not allowed!"); 6855 return OsConstants.EPERM; 6856 } 6857 return 0; 6858 } finally { 6859 restoreLocalCallingIdentity(token); 6860 } 6861 } 6862 6863 /** 6864 * Checks whether the app with the given UID is allowed to open the directory denoted by the 6865 * given path. 6866 * 6867 * @param path directory's path 6868 * @param uid UID of the requesting app 6869 * @return 0 if it's allowed to open the diretory, {@link OsConstants#EACCES} if the calling 6870 * package is a legacy app that doesn't have READ_EXTERNAL_STORAGE permission, 6871 * {@link OsConstants#ENOENT} otherwise. 6872 * 6873 * Called from JNI in jni/MediaProviderWrapper.cpp 6874 */ 6875 @Keep 6876 public int isOpendirAllowedForFuse(@NonNull String path, int uid, boolean forWrite) { 6877 final LocalCallingIdentity token = 6878 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 6879 try { 6880 if ("/storage/emulated".equals(path)) { 6881 return OsConstants.EPERM; 6882 } 6883 if (isPrivatePackagePathNotOwnedByCaller(path)) { 6884 Log.e(TAG, "Can't access another app's external directory!"); 6885 return OsConstants.ENOENT; 6886 } 6887 6888 // Do not allow apps to open Android/data or Android/obb dirs. Installer and 6889 // MOUNT_EXTERNAL_ANDROID_WRITABLE apps won't be blocked by this, as their OBB dirs 6890 // are mounted to lowerfs directly. 6891 if (isDataOrObbPath(path)) { 6892 return OsConstants.EACCES; 6893 } 6894 6895 if (shouldBypassFuseRestrictions(forWrite, path)) { 6896 return 0; 6897 } 6898 // Legacy apps that made is this far don't have the right storage permission and hence 6899 // are not allowed to access anything other than their external app directory 6900 if (isCallingPackageRequestingLegacy()) { 6901 return OsConstants.EACCES; 6902 } 6903 // This is a non-legacy app. Rest of the directories are generally writable 6904 // except for non-default top-level directories. 6905 if (forWrite) { 6906 final String[] relativePath = sanitizePath(extractRelativePath(path)); 6907 if (relativePath.length == 0) { 6908 Log.e(TAG, "Directoy write not allowed on invalid relative path for " + path); 6909 return OsConstants.EPERM; 6910 } 6911 final boolean isTopLevelDir = 6912 relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]); 6913 if (isTopLevelDir) { 6914 if (isDefaultDirectoryName(extractDisplayName(path))) { 6915 return 0; 6916 } else { 6917 Log.e(TAG, 6918 "Writing to a non-default top level directory is not allowed!"); 6919 return OsConstants.EACCES; 6920 } 6921 } 6922 } 6923 6924 return 0; 6925 } finally { 6926 restoreLocalCallingIdentity(token); 6927 } 6928 } 6929 6930 @Keep 6931 public boolean isUidForPackageForFuse(@NonNull String packageName, int uid) { 6932 final LocalCallingIdentity token = 6933 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); 6934 try { 6935 return isCallingIdentitySharedPackageName(packageName); 6936 } finally { 6937 restoreLocalCallingIdentity(token); 6938 } 6939 } 6940 6941 private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) { 6942 // System internals can work with all media 6943 if (isCallingPackageSelf() || isCallingPackageShell()) { 6944 return true; 6945 } 6946 6947 // Apps that have permission to manage external storage can work with all files 6948 if (isCallingPackageManager()) { 6949 return true; 6950 } 6951 6952 // Check if caller is known to be owner of this item, to speed up 6953 // performance of our permission checks 6954 final int table = matchUri(uri, true); 6955 switch (table) { 6956 case AUDIO_MEDIA_ID: 6957 case VIDEO_MEDIA_ID: 6958 case IMAGES_MEDIA_ID: 6959 case FILES_ID: 6960 case DOWNLOADS_ID: 6961 final long id = ContentUris.parseId(uri); 6962 if (mCallingIdentity.get().isOwned(id)) { 6963 return true; 6964 } 6965 } 6966 6967 // Outstanding grant means they get access 6968 if (getContext().checkUriPermission(uri, mCallingIdentity.get().pid, 6969 mCallingIdentity.get().uid, forWrite 6970 ? Intent.FLAG_GRANT_WRITE_URI_PERMISSION 6971 : Intent.FLAG_GRANT_READ_URI_PERMISSION) == PERMISSION_GRANTED) { 6972 return true; 6973 } 6974 6975 return false; 6976 } 6977 6978 @VisibleForTesting 6979 public boolean isFuseThread() { 6980 return FuseDaemon.native_is_fuse_thread(); 6981 } 6982 6983 @Deprecated 6984 private boolean checkCallingPermissionAudio(boolean forWrite, String callingPackage) { 6985 if (forWrite) { 6986 return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_AUDIO); 6987 } else { 6988 // write permission should be enough for reading as well 6989 return mCallingIdentity.get().hasPermission(PERMISSION_READ_AUDIO) 6990 || mCallingIdentity.get().hasPermission(PERMISSION_WRITE_AUDIO); 6991 } 6992 } 6993 6994 @Deprecated 6995 private boolean checkCallingPermissionVideo(boolean forWrite, String callingPackage) { 6996 if (forWrite) { 6997 return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_VIDEO); 6998 } else { 6999 // write permission should be enough for reading as well 7000 return mCallingIdentity.get().hasPermission(PERMISSION_READ_VIDEO) 7001 || mCallingIdentity.get().hasPermission(PERMISSION_WRITE_VIDEO); 7002 } 7003 } 7004 7005 @Deprecated 7006 private boolean checkCallingPermissionImages(boolean forWrite, String callingPackage) { 7007 if (forWrite) { 7008 return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_IMAGES); 7009 } else { 7010 // write permission should be enough for reading as well 7011 return mCallingIdentity.get().hasPermission(PERMISSION_READ_IMAGES) 7012 || mCallingIdentity.get().hasPermission(PERMISSION_WRITE_IMAGES); 7013 } 7014 } 7015 7016 /** 7017 * Enforce that caller has access to the given {@link Uri}. 7018 * 7019 * @throws SecurityException if access isn't allowed. 7020 */ 7021 private void enforceCallingPermission(@NonNull Uri uri, @NonNull Bundle extras, 7022 boolean forWrite) { 7023 Trace.beginSection("enforceCallingPermission"); 7024 try { 7025 enforceCallingPermissionInternal(uri, extras, forWrite); 7026 } finally { 7027 Trace.endSection(); 7028 } 7029 } 7030 7031 private void enforceCallingPermissionInternal(@NonNull Uri uri, @NonNull Bundle extras, 7032 boolean forWrite) { 7033 Objects.requireNonNull(uri); 7034 Objects.requireNonNull(extras); 7035 7036 // Try a simple global check first before falling back to performing a 7037 // simple query to probe for access. 7038 if (checkCallingPermissionGlobal(uri, forWrite)) { 7039 // Access allowed, yay! 7040 return; 7041 } 7042 7043 final DatabaseHelper helper; 7044 try { 7045 helper = getDatabaseForUri(uri); 7046 } catch (VolumeNotFoundException e) { 7047 throw e.rethrowAsIllegalArgumentException(); 7048 } 7049 7050 final boolean allowHidden = isCallingPackageAllowedHidden(); 7051 final int table = matchUri(uri, allowHidden); 7052 7053 // First, check to see if caller has direct write access 7054 if (forWrite) { 7055 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, table, uri, extras, null); 7056 try (Cursor c = qb.query(helper, new String[0], 7057 null, null, null, null, null, null, null)) { 7058 if (c.moveToFirst()) { 7059 // Direct write access granted, yay! 7060 return; 7061 } 7062 } 7063 } 7064 7065 // We only allow the user to grant access to specific media items in 7066 // strongly typed collections; never to broad collections 7067 boolean allowUserGrant = false; 7068 final int matchUri = matchUri(uri, true); 7069 switch (matchUri) { 7070 case IMAGES_MEDIA_ID: 7071 case AUDIO_MEDIA_ID: 7072 case VIDEO_MEDIA_ID: 7073 allowUserGrant = true; 7074 break; 7075 } 7076 7077 // Second, check to see if caller has direct read access 7078 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, extras, null); 7079 try (Cursor c = qb.query(helper, new String[0], 7080 null, null, null, null, null, null, null)) { 7081 if (c.moveToFirst()) { 7082 if (!forWrite) { 7083 // Direct read access granted, yay! 7084 return; 7085 } else if (allowUserGrant) { 7086 // Caller has read access, but they wanted to write, and 7087 // they'll need to get the user to grant that access 7088 final Context context = getContext(); 7089 final Collection<Uri> uris = Arrays.asList(uri); 7090 final PendingIntent intent = MediaStore 7091 .createWriteRequest(ContentResolver.wrap(this), uris); 7092 7093 final Icon icon = getCollectionIcon(uri); 7094 final RemoteAction action = new RemoteAction(icon, 7095 context.getText(R.string.permission_required_action), 7096 context.getText(R.string.permission_required_action), 7097 intent); 7098 7099 throw new RecoverableSecurityException(new SecurityException( 7100 getCallingPackageOrSelf() + " has no access to " + uri), 7101 context.getText(R.string.permission_required), action); 7102 } 7103 } 7104 } 7105 7106 throw new SecurityException(getCallingPackageOrSelf() + " has no access to " + uri); 7107 } 7108 7109 private Icon getCollectionIcon(Uri uri) { 7110 final PackageManager pm = getContext().getPackageManager(); 7111 final String type = uri.getPathSegments().get(1); 7112 final String groupName; 7113 switch (type) { 7114 default: groupName = android.Manifest.permission_group.STORAGE; break; 7115 } 7116 try { 7117 final PermissionGroupInfo perm = pm.getPermissionGroupInfo(groupName, 0); 7118 return Icon.createWithResource(perm.packageName, perm.icon); 7119 } catch (NameNotFoundException e) { 7120 throw new RuntimeException(e); 7121 } 7122 } 7123 7124 private void checkAccess(@NonNull Uri uri, @NonNull Bundle extras, @NonNull File file, 7125 boolean isWrite) throws FileNotFoundException { 7126 // First, does caller have the needed row-level access? 7127 enforceCallingPermission(uri, extras, isWrite); 7128 7129 // Second, does the path look sane? 7130 if (!FileUtils.contains(Environment.getStorageDirectory(), file)) { 7131 checkWorldReadAccess(file.getAbsolutePath()); 7132 } 7133 } 7134 7135 /** 7136 * Check whether the path is a world-readable file 7137 */ 7138 @VisibleForTesting 7139 public static void checkWorldReadAccess(String path) throws FileNotFoundException { 7140 // Path has already been canonicalized, and we relax the check to look 7141 // at groups to support runtime storage permissions. 7142 final int accessBits = path.startsWith("/storage/") ? OsConstants.S_IRGRP 7143 : OsConstants.S_IROTH; 7144 try { 7145 StructStat stat = Os.stat(path); 7146 if (OsConstants.S_ISREG(stat.st_mode) && 7147 ((stat.st_mode & accessBits) == accessBits)) { 7148 checkLeadingPathComponentsWorldExecutable(path); 7149 return; 7150 } 7151 } catch (ErrnoException e) { 7152 // couldn't stat the file, either it doesn't exist or isn't 7153 // accessible to us 7154 } 7155 7156 throw new FileNotFoundException("Can't access " + path); 7157 } 7158 7159 private static void checkLeadingPathComponentsWorldExecutable(String filePath) 7160 throws FileNotFoundException { 7161 File parent = new File(filePath).getParentFile(); 7162 7163 // Path has already been canonicalized, and we relax the check to look 7164 // at groups to support runtime storage permissions. 7165 final int accessBits = filePath.startsWith("/storage/") ? OsConstants.S_IXGRP 7166 : OsConstants.S_IXOTH; 7167 7168 while (parent != null) { 7169 if (! parent.exists()) { 7170 // parent dir doesn't exist, give up 7171 throw new FileNotFoundException("access denied"); 7172 } 7173 try { 7174 StructStat stat = Os.stat(parent.getPath()); 7175 if ((stat.st_mode & accessBits) != accessBits) { 7176 // the parent dir doesn't have the appropriate access 7177 throw new FileNotFoundException("Can't access " + filePath); 7178 } 7179 } catch (ErrnoException e1) { 7180 // couldn't stat() parent 7181 throw new FileNotFoundException("Can't access " + filePath); 7182 } 7183 parent = parent.getParentFile(); 7184 } 7185 } 7186 7187 @VisibleForTesting 7188 static class FallbackException extends Exception { 7189 private final int mThrowSdkVersion; 7190 7191 public FallbackException(String message, int throwSdkVersion) { 7192 super(message); 7193 mThrowSdkVersion = throwSdkVersion; 7194 } 7195 7196 public FallbackException(String message, Throwable cause, int throwSdkVersion) { 7197 super(message, cause); 7198 mThrowSdkVersion = throwSdkVersion; 7199 } 7200 7201 @Override 7202 public String getMessage() { 7203 if (getCause() != null) { 7204 return super.getMessage() + ": " + getCause().getMessage(); 7205 } else { 7206 return super.getMessage(); 7207 } 7208 } 7209 7210 public IllegalArgumentException rethrowAsIllegalArgumentException() { 7211 throw new IllegalArgumentException(getMessage()); 7212 } 7213 7214 public Cursor translateForQuery(int targetSdkVersion) { 7215 if (targetSdkVersion >= mThrowSdkVersion) { 7216 throw new IllegalArgumentException(getMessage()); 7217 } else { 7218 Log.w(TAG, getMessage()); 7219 return null; 7220 } 7221 } 7222 7223 public Uri translateForInsert(int targetSdkVersion) { 7224 if (targetSdkVersion >= mThrowSdkVersion) { 7225 throw new IllegalArgumentException(getMessage()); 7226 } else { 7227 Log.w(TAG, getMessage()); 7228 return null; 7229 } 7230 } 7231 7232 public int translateForUpdateDelete(int targetSdkVersion) { 7233 if (targetSdkVersion >= mThrowSdkVersion) { 7234 throw new IllegalArgumentException(getMessage()); 7235 } else { 7236 Log.w(TAG, getMessage()); 7237 return 0; 7238 } 7239 } 7240 } 7241 7242 @VisibleForTesting 7243 static class VolumeNotFoundException extends FallbackException { 7244 public VolumeNotFoundException(String volumeName) { 7245 super("Volume " + volumeName + " not found", Build.VERSION_CODES.Q); 7246 } 7247 } 7248 7249 @VisibleForTesting 7250 static class VolumeArgumentException extends FallbackException { 7251 public VolumeArgumentException(File actual, Collection<File> allowed) { 7252 super("Requested path " + actual + " doesn't appear under " + allowed, 7253 Build.VERSION_CODES.Q); 7254 } 7255 } 7256 7257 private @NonNull DatabaseHelper getDatabaseForUri(Uri uri) throws VolumeNotFoundException { 7258 final String volumeName = resolveVolumeName(uri); 7259 synchronized (mAttachedVolumeNames) { 7260 if (!mAttachedVolumeNames.contains(volumeName)) { 7261 throw new VolumeNotFoundException(volumeName); 7262 } 7263 } 7264 if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) { 7265 return mInternalDatabase; 7266 } else { 7267 return mExternalDatabase; 7268 } 7269 } 7270 7271 static boolean isMediaDatabaseName(String name) { 7272 if (INTERNAL_DATABASE_NAME.equals(name)) { 7273 return true; 7274 } 7275 if (EXTERNAL_DATABASE_NAME.equals(name)) { 7276 return true; 7277 } 7278 if (name.startsWith("external-") && name.endsWith(".db")) { 7279 return true; 7280 } 7281 return false; 7282 } 7283 7284 static boolean isInternalMediaDatabaseName(String name) { 7285 if (INTERNAL_DATABASE_NAME.equals(name)) { 7286 return true; 7287 } 7288 return false; 7289 } 7290 7291 private @NonNull Uri getBaseContentUri(@NonNull String volumeName) { 7292 return MediaStore.AUTHORITY_URI.buildUpon().appendPath(volumeName).build(); 7293 } 7294 7295 public Uri attachVolume(String volume, boolean validate) { 7296 if (mCallingIdentity.get().pid != android.os.Process.myPid()) { 7297 throw new SecurityException( 7298 "Opening and closing databases not allowed."); 7299 } 7300 7301 // Quick sanity check for shady volume names 7302 MediaStore.checkArgumentVolumeName(volume); 7303 7304 // Quick sanity check that volume actually exists 7305 if (!MediaStore.VOLUME_INTERNAL.equals(volume) && validate) { 7306 try { 7307 getVolumePath(volume); 7308 } catch (IOException e) { 7309 throw new IllegalArgumentException( 7310 "Volume " + volume + " currently unavailable", e); 7311 } 7312 } 7313 7314 synchronized (mAttachedVolumeNames) { 7315 mAttachedVolumeNames.add(volume); 7316 } 7317 7318 final ContentResolver resolver = getContext().getContentResolver(); 7319 final Uri uri = getBaseContentUri(volume); 7320 resolver.notifyChange(getBaseContentUri(volume), null); 7321 7322 if (LOGV) Log.v(TAG, "Attached volume: " + volume); 7323 if (!MediaStore.VOLUME_INTERNAL.equals(volume)) { 7324 // Also notify on synthetic view of all devices 7325 resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null); 7326 7327 ForegroundThread.getExecutor().execute(() -> { 7328 final DatabaseHelper helper = MediaStore.VOLUME_INTERNAL.equals(volume) 7329 ? mInternalDatabase : mExternalDatabase; 7330 helper.runWithTransaction((db) -> { 7331 ensureDefaultFolders(volume, db); 7332 ensureThumbnailsValid(volume, db); 7333 return null; 7334 }); 7335 7336 // We just finished the database operation above, we know that 7337 // it's ready to answer queries, so notify our DocumentProvider 7338 // so it can answer queries without risking ANR 7339 MediaDocumentsProvider.onMediaStoreReady(getContext(), volume); 7340 }); 7341 } 7342 return uri; 7343 } 7344 7345 private void detachVolume(Uri uri) { 7346 detachVolume(MediaStore.getVolumeName(uri)); 7347 } 7348 7349 public void detachVolume(String volume) { 7350 if (mCallingIdentity.get().pid != android.os.Process.myPid()) { 7351 throw new SecurityException( 7352 "Opening and closing databases not allowed."); 7353 } 7354 7355 // Quick sanity check for shady volume names 7356 MediaStore.checkArgumentVolumeName(volume); 7357 7358 if (MediaStore.VOLUME_INTERNAL.equals(volume)) { 7359 throw new UnsupportedOperationException( 7360 "Deleting the internal volume is not allowed"); 7361 } 7362 7363 // Signal any scanning to shut down 7364 mMediaScanner.onDetachVolume(volume); 7365 7366 synchronized (mAttachedVolumeNames) { 7367 mAttachedVolumeNames.remove(volume); 7368 } 7369 7370 final ContentResolver resolver = getContext().getContentResolver(); 7371 final Uri uri = getBaseContentUri(volume); 7372 resolver.notifyChange(getBaseContentUri(volume), null); 7373 7374 if (!MediaStore.VOLUME_INTERNAL.equals(volume)) { 7375 // Also notify on synthetic view of all devices 7376 resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null); 7377 } 7378 7379 if (LOGV) Log.v(TAG, "Detached volume: " + volume); 7380 } 7381 7382 @GuardedBy("mAttachedVolumeNames") 7383 private final ArraySet<String> mAttachedVolumeNames = new ArraySet<>(); 7384 @GuardedBy("mCustomCollators") 7385 private final ArraySet<String> mCustomCollators = new ArraySet<>(); 7386 7387 private MediaScanner mMediaScanner; 7388 7389 private DatabaseHelper mInternalDatabase; 7390 private DatabaseHelper mExternalDatabase; 7391 7392 // name of the volume currently being scanned by the media scanner (or null) 7393 private String mMediaScannerVolume; 7394 7395 // current FAT volume ID 7396 private int mVolumeId = -1; 7397 7398 // WARNING: the values of IMAGES_MEDIA, AUDIO_MEDIA, and VIDEO_MEDIA and AUDIO_PLAYLISTS 7399 // are stored in the "files" table, so do not renumber them unless you also add 7400 // a corresponding database upgrade step for it. 7401 static final int IMAGES_MEDIA = 1; 7402 static final int IMAGES_MEDIA_ID = 2; 7403 static final int IMAGES_MEDIA_ID_THUMBNAIL = 3; 7404 static final int IMAGES_THUMBNAILS = 4; 7405 static final int IMAGES_THUMBNAILS_ID = 5; 7406 7407 static final int AUDIO_MEDIA = 100; 7408 static final int AUDIO_MEDIA_ID = 101; 7409 static final int AUDIO_MEDIA_ID_GENRES = 102; 7410 static final int AUDIO_MEDIA_ID_GENRES_ID = 103; 7411 static final int AUDIO_GENRES = 106; 7412 static final int AUDIO_GENRES_ID = 107; 7413 static final int AUDIO_GENRES_ID_MEMBERS = 108; 7414 static final int AUDIO_GENRES_ALL_MEMBERS = 109; 7415 static final int AUDIO_PLAYLISTS = 110; 7416 static final int AUDIO_PLAYLISTS_ID = 111; 7417 static final int AUDIO_PLAYLISTS_ID_MEMBERS = 112; 7418 static final int AUDIO_PLAYLISTS_ID_MEMBERS_ID = 113; 7419 static final int AUDIO_ARTISTS = 114; 7420 static final int AUDIO_ARTISTS_ID = 115; 7421 static final int AUDIO_ALBUMS = 116; 7422 static final int AUDIO_ALBUMS_ID = 117; 7423 static final int AUDIO_ARTISTS_ID_ALBUMS = 118; 7424 static final int AUDIO_ALBUMART = 119; 7425 static final int AUDIO_ALBUMART_ID = 120; 7426 static final int AUDIO_ALBUMART_FILE_ID = 121; 7427 7428 static final int VIDEO_MEDIA = 200; 7429 static final int VIDEO_MEDIA_ID = 201; 7430 static final int VIDEO_MEDIA_ID_THUMBNAIL = 202; 7431 static final int VIDEO_THUMBNAILS = 203; 7432 static final int VIDEO_THUMBNAILS_ID = 204; 7433 7434 static final int VOLUMES = 300; 7435 static final int VOLUMES_ID = 301; 7436 7437 static final int MEDIA_SCANNER = 500; 7438 7439 static final int FS_ID = 600; 7440 static final int VERSION = 601; 7441 7442 static final int FILES = 700; 7443 static final int FILES_ID = 701; 7444 7445 static final int DOWNLOADS = 800; 7446 static final int DOWNLOADS_ID = 801; 7447 7448 private LocalUriMatcher mUriMatcher; 7449 7450 private static final String[] PATH_PROJECTION = new String[] { 7451 MediaStore.MediaColumns._ID, 7452 MediaStore.MediaColumns.DATA, 7453 }; 7454 7455 private int matchUri(Uri uri, boolean allowHidden) { 7456 return mUriMatcher.matchUri(uri, allowHidden); 7457 } 7458 7459 static class LocalUriMatcher { 7460 private final UriMatcher mPublic = new UriMatcher(UriMatcher.NO_MATCH); 7461 private final UriMatcher mHidden = new UriMatcher(UriMatcher.NO_MATCH); 7462 7463 public int matchUri(Uri uri, boolean allowHidden) { 7464 final int publicMatch = mPublic.match(uri); 7465 if (publicMatch != UriMatcher.NO_MATCH) { 7466 return publicMatch; 7467 } 7468 7469 final int hiddenMatch = mHidden.match(uri); 7470 if (hiddenMatch != UriMatcher.NO_MATCH) { 7471 // Detect callers asking about hidden behavior by looking closer when 7472 // the matchers diverge; we only care about apps that are explicitly 7473 // targeting a specific public API level. 7474 if (!allowHidden) { 7475 throw new IllegalStateException("Unknown URL: " + uri + " is hidden API"); 7476 } 7477 return hiddenMatch; 7478 } 7479 7480 return UriMatcher.NO_MATCH; 7481 } 7482 7483 public LocalUriMatcher(String auth) { 7484 mPublic.addURI(auth, "*/images/media", IMAGES_MEDIA); 7485 mPublic.addURI(auth, "*/images/media/#", IMAGES_MEDIA_ID); 7486 mPublic.addURI(auth, "*/images/media/#/thumbnail", IMAGES_MEDIA_ID_THUMBNAIL); 7487 mPublic.addURI(auth, "*/images/thumbnails", IMAGES_THUMBNAILS); 7488 mPublic.addURI(auth, "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID); 7489 7490 mPublic.addURI(auth, "*/audio/media", AUDIO_MEDIA); 7491 mPublic.addURI(auth, "*/audio/media/#", AUDIO_MEDIA_ID); 7492 mPublic.addURI(auth, "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES); 7493 mPublic.addURI(auth, "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID); 7494 mPublic.addURI(auth, "*/audio/genres", AUDIO_GENRES); 7495 mPublic.addURI(auth, "*/audio/genres/#", AUDIO_GENRES_ID); 7496 mPublic.addURI(auth, "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS); 7497 // TODO: not actually defined in API, but CTS tested 7498 mPublic.addURI(auth, "*/audio/genres/all/members", AUDIO_GENRES_ALL_MEMBERS); 7499 mPublic.addURI(auth, "*/audio/playlists", AUDIO_PLAYLISTS); 7500 mPublic.addURI(auth, "*/audio/playlists/#", AUDIO_PLAYLISTS_ID); 7501 mPublic.addURI(auth, "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS); 7502 mPublic.addURI(auth, "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID); 7503 mPublic.addURI(auth, "*/audio/artists", AUDIO_ARTISTS); 7504 mPublic.addURI(auth, "*/audio/artists/#", AUDIO_ARTISTS_ID); 7505 mPublic.addURI(auth, "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS); 7506 mPublic.addURI(auth, "*/audio/albums", AUDIO_ALBUMS); 7507 mPublic.addURI(auth, "*/audio/albums/#", AUDIO_ALBUMS_ID); 7508 // TODO: not actually defined in API, but CTS tested 7509 mPublic.addURI(auth, "*/audio/albumart", AUDIO_ALBUMART); 7510 // TODO: not actually defined in API, but CTS tested 7511 mPublic.addURI(auth, "*/audio/albumart/#", AUDIO_ALBUMART_ID); 7512 // TODO: not actually defined in API, but CTS tested 7513 mPublic.addURI(auth, "*/audio/media/#/albumart", AUDIO_ALBUMART_FILE_ID); 7514 7515 mPublic.addURI(auth, "*/video/media", VIDEO_MEDIA); 7516 mPublic.addURI(auth, "*/video/media/#", VIDEO_MEDIA_ID); 7517 mPublic.addURI(auth, "*/video/media/#/thumbnail", VIDEO_MEDIA_ID_THUMBNAIL); 7518 mPublic.addURI(auth, "*/video/thumbnails", VIDEO_THUMBNAILS); 7519 mPublic.addURI(auth, "*/video/thumbnails/#", VIDEO_THUMBNAILS_ID); 7520 7521 mPublic.addURI(auth, "*/media_scanner", MEDIA_SCANNER); 7522 7523 // NOTE: technically hidden, since Uri is never exposed 7524 mPublic.addURI(auth, "*/fs_id", FS_ID); 7525 // NOTE: technically hidden, since Uri is never exposed 7526 mPublic.addURI(auth, "*/version", VERSION); 7527 7528 mHidden.addURI(auth, "*", VOLUMES_ID); 7529 mHidden.addURI(auth, null, VOLUMES); 7530 7531 mPublic.addURI(auth, "*/file", FILES); 7532 mPublic.addURI(auth, "*/file/#", FILES_ID); 7533 7534 mPublic.addURI(auth, "*/downloads", DOWNLOADS); 7535 mPublic.addURI(auth, "*/downloads/#", DOWNLOADS_ID); 7536 } 7537 } 7538 7539 /** 7540 * Set of columns that can be safely mutated by external callers; all other 7541 * columns are treated as read-only, since they reflect what the media 7542 * scanner found on disk, and any mutations would be overwritten the next 7543 * time the media was scanned. 7544 */ 7545 private static final ArraySet<String> sMutableColumns = new ArraySet<>(); 7546 7547 { 7548 sMutableColumns.add(MediaStore.MediaColumns.DATA); 7549 sMutableColumns.add(MediaStore.MediaColumns.RELATIVE_PATH); 7550 sMutableColumns.add(MediaStore.MediaColumns.DISPLAY_NAME); 7551 sMutableColumns.add(MediaStore.MediaColumns.IS_PENDING); 7552 sMutableColumns.add(MediaStore.MediaColumns.IS_TRASHED); 7553 sMutableColumns.add(MediaStore.MediaColumns.IS_FAVORITE); 7554 sMutableColumns.add(MediaStore.MediaColumns.OWNER_PACKAGE_NAME); 7555 7556 sMutableColumns.add(MediaStore.Audio.AudioColumns.BOOKMARK); 7557 7558 sMutableColumns.add(MediaStore.Video.VideoColumns.TAGS); 7559 sMutableColumns.add(MediaStore.Video.VideoColumns.CATEGORY); 7560 sMutableColumns.add(MediaStore.Video.VideoColumns.BOOKMARK); 7561 7562 sMutableColumns.add(MediaStore.Audio.Playlists.NAME); 7563 sMutableColumns.add(MediaStore.Audio.Playlists.Members.AUDIO_ID); 7564 sMutableColumns.add(MediaStore.Audio.Playlists.Members.PLAY_ORDER); 7565 7566 sMutableColumns.add(MediaStore.DownloadColumns.DOWNLOAD_URI); 7567 sMutableColumns.add(MediaStore.DownloadColumns.REFERER_URI); 7568 7569 sMutableColumns.add(MediaStore.Files.FileColumns.MIME_TYPE); 7570 sMutableColumns.add(MediaStore.Files.FileColumns.MEDIA_TYPE); 7571 } 7572 7573 /** 7574 * Set of columns that affect placement of files on disk. 7575 */ 7576 private static final ArraySet<String> sPlacementColumns = new ArraySet<>(); 7577 7578 { 7579 sPlacementColumns.add(MediaStore.MediaColumns.DATA); 7580 sPlacementColumns.add(MediaStore.MediaColumns.RELATIVE_PATH); 7581 sPlacementColumns.add(MediaStore.MediaColumns.DISPLAY_NAME); 7582 sPlacementColumns.add(MediaStore.MediaColumns.MIME_TYPE); 7583 sPlacementColumns.add(MediaStore.MediaColumns.IS_PENDING); 7584 sPlacementColumns.add(MediaStore.MediaColumns.IS_TRASHED); 7585 sPlacementColumns.add(MediaStore.MediaColumns.DATE_EXPIRES); 7586 } 7587 7588 /** 7589 * List of abusive custom columns that we're willing to allow via 7590 * {@link SQLiteQueryBuilder#setProjectionGreylist(List)}. 7591 */ 7592 static final ArrayList<Pattern> sGreylist = new ArrayList<>(); 7593 7594 private static void addGreylistPattern(String pattern) { 7595 sGreylist.add(Pattern.compile(" *" + pattern + " *")); 7596 } 7597 7598 static { 7599 final String maybeAs = "( (as )?[_a-z0-9]+)?"; 7600 addGreylistPattern("(?i)[_a-z0-9]+" + maybeAs); 7601 addGreylistPattern("audio\\._id AS _id"); 7602 addGreylistPattern("(?i)(min|max|sum|avg|total|count|cast)\\(([_a-z0-9]+" + maybeAs + "|\\*)\\)" + maybeAs); 7603 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"); 7604 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\\)"); 7605 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\\)"); 7606 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\\)"); 7607 addGreylistPattern("\"content://media/[a-z]+/audio/media\""); 7608 addGreylistPattern("substr\\(_data, length\\(_data\\)-length\\(_display_name\\), 1\\) as filename_prevchar"); 7609 addGreylistPattern("\\*" + maybeAs); 7610 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"); 7611 } 7612 7613 public ArrayMap<String, String> getProjectionMap(Class<?>... clazzes) { 7614 return mExternalDatabase.getProjectionMap(clazzes); 7615 } 7616 7617 static <T> boolean containsAny(Set<T> a, Set<T> b) { 7618 for (T i : b) { 7619 if (a.contains(i)) { 7620 return true; 7621 } 7622 } 7623 return false; 7624 } 7625 7626 @VisibleForTesting 7627 static @Nullable Uri computeCommonPrefix(@NonNull List<Uri> uris) { 7628 if (uris.isEmpty()) return null; 7629 7630 final Uri base = uris.get(0); 7631 final List<String> basePath = new ArrayList<>(base.getPathSegments()); 7632 for (int i = 1; i < uris.size(); i++) { 7633 final List<String> probePath = uris.get(i).getPathSegments(); 7634 for (int j = 0; j < basePath.size() && j < probePath.size(); j++) { 7635 if (!Objects.equals(basePath.get(j), probePath.get(j))) { 7636 // Trim away all remaining common elements 7637 while (basePath.size() > j) { 7638 basePath.remove(j); 7639 } 7640 } 7641 } 7642 7643 final int probeSize = probePath.size(); 7644 while (basePath.size() > probeSize) { 7645 basePath.remove(probeSize); 7646 } 7647 } 7648 7649 final Uri.Builder builder = base.buildUpon().path(null); 7650 for (int i = 0; i < basePath.size(); i++) { 7651 builder.appendPath(basePath.get(i)); 7652 } 7653 return builder.build(); 7654 } 7655 7656 @Deprecated 7657 private String getCallingPackageOrSelf() { 7658 return mCallingIdentity.get().getPackageName(); 7659 } 7660 7661 @Deprecated 7662 @VisibleForTesting 7663 public int getCallingPackageTargetSdkVersion() { 7664 return mCallingIdentity.get().getTargetSdkVersion(); 7665 } 7666 7667 @Deprecated 7668 private boolean isCallingPackageAllowedHidden() { 7669 return isCallingPackageSelf(); 7670 } 7671 7672 @Deprecated 7673 private boolean isCallingPackageSelf() { 7674 return mCallingIdentity.get().hasPermission(PERMISSION_IS_SELF); 7675 } 7676 7677 @Deprecated 7678 private boolean isCallingPackageShell() { 7679 return mCallingIdentity.get().hasPermission(PERMISSION_IS_SHELL); 7680 } 7681 7682 @Deprecated 7683 private boolean isCallingPackageManager() { 7684 return mCallingIdentity.get().hasPermission(PERMISSION_IS_MANAGER); 7685 } 7686 7687 @Deprecated 7688 private boolean isCallingPackageDelegator() { 7689 return mCallingIdentity.get().hasPermission(PERMISSION_IS_DELEGATOR); 7690 } 7691 7692 @Deprecated 7693 private boolean isCallingPackageLegacyRead() { 7694 return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_READ); 7695 } 7696 7697 @Deprecated 7698 private boolean isCallingPackageLegacyWrite() { 7699 return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_WRITE); 7700 } 7701 7702 @Override 7703 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 7704 writer.println("mThumbSize=" + mThumbSize); 7705 synchronized (mAttachedVolumeNames) { 7706 writer.println("mAttachedVolumeNames=" + mAttachedVolumeNames); 7707 } 7708 writer.println(); 7709 7710 Logging.dumpPersistent(writer); 7711 } 7712 } 7713