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.app.PendingIntent.FLAG_CANCEL_CURRENT; 20 import static android.app.PendingIntent.FLAG_IMMUTABLE; 21 import static android.app.PendingIntent.FLAG_ONE_SHOT; 22 import static android.content.pm.PackageManager.PERMISSION_GRANTED; 23 import static android.os.Environment.buildPath; 24 import static android.os.Trace.TRACE_TAG_DATABASE; 25 import static android.provider.MediaStore.AUTHORITY; 26 import static android.provider.MediaStore.Downloads.PATTERN_DOWNLOADS_FILE; 27 import static android.provider.MediaStore.Downloads.isDownload; 28 import static android.provider.MediaStore.getVolumeName; 29 30 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY; 31 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED; 32 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SYSTEM; 33 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_AUDIO; 34 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_IMAGES; 35 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_VIDEO; 36 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_AUDIO; 37 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_IMAGES; 38 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_VIDEO; 39 40 import android.annotation.BytesLong; 41 import android.annotation.NonNull; 42 import android.annotation.Nullable; 43 import android.app.AppGlobals; 44 import android.app.AppOpsManager; 45 import android.app.AppOpsManager.OnOpActiveChangedListener; 46 import android.app.PendingIntent; 47 import android.app.RecoverableSecurityException; 48 import android.app.RemoteAction; 49 import android.content.BroadcastReceiver; 50 import android.content.ContentProvider; 51 import android.content.ContentProviderClient; 52 import android.content.ContentProviderOperation; 53 import android.content.ContentProviderResult; 54 import android.content.ContentResolver; 55 import android.content.ContentUris; 56 import android.content.ContentValues; 57 import android.content.Context; 58 import android.content.Intent; 59 import android.content.IntentFilter; 60 import android.content.OperationApplicationException; 61 import android.content.SharedPreferences; 62 import android.content.UriMatcher; 63 import android.content.pm.PackageManager; 64 import android.content.pm.PackageManager.NameNotFoundException; 65 import android.content.pm.PermissionGroupInfo; 66 import android.content.res.AssetFileDescriptor; 67 import android.content.res.Configuration; 68 import android.content.res.Resources; 69 import android.database.AbstractCursor; 70 import android.database.Cursor; 71 import android.database.DatabaseUtils; 72 import android.database.MatrixCursor; 73 import android.database.sqlite.SQLiteDatabase; 74 import android.database.sqlite.SQLiteOpenHelper; 75 import android.database.sqlite.SQLiteQueryBuilder; 76 import android.graphics.Bitmap; 77 import android.graphics.BitmapFactory; 78 import android.graphics.drawable.Icon; 79 import android.media.ExifInterface; 80 import android.media.MediaFile; 81 import android.media.ThumbnailUtils; 82 import android.mtp.MtpConstants; 83 import android.net.Uri; 84 import android.os.Binder; 85 import android.os.Build; 86 import android.os.Bundle; 87 import android.os.CancellationSignal; 88 import android.os.Environment; 89 import android.os.FileUtils; 90 import android.os.IBinder; 91 import android.os.ParcelFileDescriptor; 92 import android.os.ParcelFileDescriptor.OnCloseListener; 93 import android.os.RedactingFileDescriptor; 94 import android.os.RemoteException; 95 import android.os.SystemClock; 96 import android.os.SystemProperties; 97 import android.os.Trace; 98 import android.os.UserHandle; 99 import android.os.UserManager; 100 import android.os.storage.StorageEventListener; 101 import android.os.storage.StorageManager; 102 import android.os.storage.StorageVolume; 103 import android.os.storage.VolumeInfo; 104 import android.os.storage.VolumeRecord; 105 import android.preference.PreferenceManager; 106 import android.provider.BaseColumns; 107 import android.provider.Column; 108 import android.provider.DocumentsContract; 109 import android.provider.MediaStore; 110 import android.provider.MediaStore.Audio; 111 import android.provider.MediaStore.Audio.AudioColumns; 112 import android.provider.MediaStore.Audio.Playlists; 113 import android.provider.MediaStore.Downloads; 114 import android.provider.MediaStore.Files; 115 import android.provider.MediaStore.Files.FileColumns; 116 import android.provider.MediaStore.Images; 117 import android.provider.MediaStore.Images.ImageColumns; 118 import android.provider.MediaStore.MediaColumns; 119 import android.provider.MediaStore.Video; 120 import android.system.ErrnoException; 121 import android.system.Os; 122 import android.system.OsConstants; 123 import android.system.StructStat; 124 import android.text.TextUtils; 125 import android.text.format.DateUtils; 126 import android.util.ArrayMap; 127 import android.util.ArraySet; 128 import android.util.DisplayMetrics; 129 import android.util.Log; 130 import android.util.LongArray; 131 import android.util.LongSparseArray; 132 import android.util.Pair; 133 import android.util.Size; 134 import android.util.SparseArray; 135 136 import com.android.internal.annotations.GuardedBy; 137 import com.android.internal.annotations.VisibleForTesting; 138 import com.android.internal.os.BackgroundThread; 139 import com.android.internal.util.ArrayUtils; 140 import com.android.internal.util.IndentingPrintWriter; 141 import com.android.providers.media.scan.MediaScanner; 142 import com.android.providers.media.scan.ModernMediaScanner; 143 import com.android.providers.media.util.CachedSupplier; 144 import com.android.providers.media.util.IsoInterface; 145 import com.android.providers.media.util.XmpInterface; 146 147 import libcore.io.IoUtils; 148 import libcore.util.EmptyArray; 149 150 import java.io.File; 151 import java.io.FileDescriptor; 152 import java.io.FileInputStream; 153 import java.io.FileNotFoundException; 154 import java.io.FileOutputStream; 155 import java.io.FilenameFilter; 156 import java.io.IOException; 157 import java.io.OutputStream; 158 import java.io.PrintWriter; 159 import java.lang.reflect.Field; 160 import java.util.ArrayList; 161 import java.util.Arrays; 162 import java.util.Collection; 163 import java.util.List; 164 import java.util.Locale; 165 import java.util.Map; 166 import java.util.Objects; 167 import java.util.Set; 168 import java.util.UUID; 169 import java.util.concurrent.CountDownLatch; 170 import java.util.concurrent.TimeUnit; 171 import java.util.function.Consumer; 172 import java.util.function.Supplier; 173 import java.util.regex.Matcher; 174 import java.util.regex.Pattern; 175 176 /** 177 * Media content provider. See {@link android.provider.MediaStore} for details. 178 * Separate databases are kept for each external storage card we see (using the 179 * card's ID as an index). The content visible at content://media/external/... 180 * changes with the card. 181 */ 182 public class MediaProvider extends ContentProvider { 183 public static final boolean ENABLE_MODERN_SCANNER = SystemProperties 184 .getBoolean("persist.sys.modern_scanner", true); 185 186 /** 187 * Regex that matches paths in all well-known package-specific directories, 188 * and which captures the package name as the first group. 189 */ 190 private static final Pattern PATTERN_OWNED_PATH = Pattern.compile( 191 "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|media|obb|sandbox)/([^/]+)/.*"); 192 193 /** 194 * Regex that matches paths under well-known storage paths. 195 */ 196 private static final Pattern PATTERN_STORAGE_PATH = Pattern.compile( 197 "(?i)^/storage/[^/]+/(?:[0-9]+/)?"); 198 199 /** 200 * Regex that matches paths for {@link MediaColumns#RELATIVE_PATH}; it 201 * captures both top-level paths and sandboxed paths. 202 */ 203 private static final Pattern PATTERN_RELATIVE_PATH = Pattern.compile( 204 "(?i)^/storage/[^/]+/(?:[0-9]+/)?(Android/sandbox/([^/]+)/)?"); 205 206 /** 207 * Regex that matches paths under well-known storage paths. 208 */ 209 private static final Pattern PATTERN_VOLUME_NAME = Pattern.compile( 210 "(?i)^/storage/([^/]+)"); 211 212 /** 213 * Regex of a selection string that matches a specific ID. 214 */ 215 private static final Pattern PATTERN_SELECTION_ID = Pattern.compile( 216 "(?:image_id|video_id)\\s*=\\s*(\\d+)"); 217 218 /** 219 * Set of {@link Cursor} columns that refer to raw filesystem paths. 220 */ 221 private static final ArrayMap<String, Object> sDataColumns = new ArrayMap<>(); 222 223 { sDataColumns.put(MediaStore.MediaColumns.DATA, null)224 sDataColumns.put(MediaStore.MediaColumns.DATA, null); sDataColumns.put(MediaStore.Images.Thumbnails.DATA, null)225 sDataColumns.put(MediaStore.Images.Thumbnails.DATA, null); sDataColumns.put(MediaStore.Video.Thumbnails.DATA, null)226 sDataColumns.put(MediaStore.Video.Thumbnails.DATA, null); sDataColumns.put(MediaStore.Audio.PlaylistsColumns.DATA, null)227 sDataColumns.put(MediaStore.Audio.PlaylistsColumns.DATA, null); sDataColumns.put(MediaStore.Audio.AlbumColumns.ALBUM_ART, null)228 sDataColumns.put(MediaStore.Audio.AlbumColumns.ALBUM_ART, null); 229 } 230 231 private static final Object sCacheLock = new Object(); 232 233 @GuardedBy("sCacheLock") 234 private static final List<VolumeInfo> sCachedVolumes = new ArrayList<>(); 235 @GuardedBy("sCacheLock") 236 private static final Set<String> sCachedExternalVolumeNames = new ArraySet<>(); 237 @GuardedBy("sCacheLock") 238 private static final Map<String, Collection<File>> sCachedVolumeScanPaths = new ArrayMap<>(); 239 updateVolumes()240 private void updateVolumes() { 241 synchronized (sCacheLock) { 242 sCachedVolumes.clear(); 243 sCachedVolumes.addAll(mStorageManager.getVolumes()); 244 245 sCachedExternalVolumeNames.clear(); 246 sCachedExternalVolumeNames.addAll(MediaStore.getExternalVolumeNames(getContext())); 247 248 sCachedVolumeScanPaths.clear(); 249 try { 250 sCachedVolumeScanPaths.put(MediaStore.VOLUME_INTERNAL, 251 MediaStore.getVolumeScanPaths(MediaStore.VOLUME_INTERNAL)); 252 for (String volumeName : sCachedExternalVolumeNames) { 253 sCachedVolumeScanPaths.put(volumeName, 254 MediaStore.getVolumeScanPaths(volumeName)); 255 } 256 } catch (FileNotFoundException e) { 257 throw new IllegalStateException(e.getMessage()); 258 } 259 } 260 } 261 getVolumePath(String volumeName)262 public static File getVolumePath(String volumeName) throws FileNotFoundException { 263 synchronized (sCacheLock) { 264 return MediaStore.getVolumePath(sCachedVolumes, volumeName); 265 } 266 } 267 getExternalVolumeNames()268 public static Set<String> getExternalVolumeNames() { 269 synchronized (sCacheLock) { 270 return new ArraySet<>(sCachedExternalVolumeNames); 271 } 272 } 273 getVolumeScanPaths(String volumeName)274 public static Collection<File> getVolumeScanPaths(String volumeName) { 275 synchronized (sCacheLock) { 276 return new ArrayList<>(sCachedVolumeScanPaths.get(volumeName)); 277 } 278 } 279 280 private StorageManager mStorageManager; 281 private AppOpsManager mAppOpsManager; 282 private PackageManager mPackageManager; 283 284 private Size mThumbSize; 285 286 /** 287 * Map from UID to cached {@link LocalCallingIdentity}. Values are only 288 * maintained in this map while the UID is actively working with a 289 * performance-critical component, such as camera. 290 */ 291 @GuardedBy("mCachedCallingIdentity") 292 private final SparseArray<LocalCallingIdentity> mCachedCallingIdentity = new SparseArray<>(); 293 294 private static volatile long sBackgroundDelay = 0; 295 296 private final OnOpActiveChangedListener mActiveListener = (code, uid, packageName, active) -> { 297 synchronized (mCachedCallingIdentity) { 298 if (active) { 299 mCachedCallingIdentity.put(uid, 300 LocalCallingIdentity.fromExternal(uid, packageName)); 301 } else { 302 mCachedCallingIdentity.remove(uid); 303 } 304 305 if (mCachedCallingIdentity.size() > 0) { 306 sBackgroundDelay = 10 * DateUtils.SECOND_IN_MILLIS; 307 } else { 308 sBackgroundDelay = 0; 309 } 310 } 311 }; 312 313 /** 314 * Calling identity state about on the current thread. Populated on demand, 315 * and invalidated by {@link #onCallingPackageChanged()} when each remote 316 * call is finished. 317 */ 318 private final ThreadLocal<LocalCallingIdentity> mCallingIdentity = ThreadLocal 319 .withInitial(() -> { 320 synchronized (mCachedCallingIdentity) { 321 final LocalCallingIdentity cached = mCachedCallingIdentity 322 .get(Binder.getCallingUid()); 323 return (cached != null) ? cached : LocalCallingIdentity.fromBinder(this); 324 } 325 }); 326 327 // In memory cache of path<->id mappings, to speed up inserts during media scan 328 @GuardedBy("mDirectoryCache") 329 private final ArrayMap<String, Long> mDirectoryCache = new ArrayMap<>(); 330 331 private static final String[] sMediaTableColumns = new String[] { 332 FileColumns._ID, 333 FileColumns.MEDIA_TYPE, 334 }; 335 336 private static final String[] sIdOnlyColumn = new String[] { 337 FileColumns._ID 338 }; 339 340 private static final String[] sDataOnlyColumn = new String[] { 341 FileColumns.DATA 342 }; 343 344 private static final String[] sPlaylistIdPlayOrder = new String[] { 345 Playlists.Members.PLAYLIST_ID, 346 Playlists.Members.PLAY_ORDER 347 }; 348 349 private static final String ID_NOT_PARENT_CLAUSE = 350 "_id NOT IN (SELECT parent FROM files)"; 351 352 private static final String CANONICAL = "canonical"; 353 354 private BroadcastReceiver mMediaReceiver = new BroadcastReceiver() { 355 @Override 356 public void onReceive(Context context, Intent intent) { 357 final StorageVolume sv = intent.getParcelableExtra(StorageVolume.EXTRA_STORAGE_VOLUME); 358 if (sv != null) { 359 final String volumeName; 360 if (sv.isPrimary()) { 361 volumeName = MediaStore.VOLUME_EXTERNAL_PRIMARY; 362 } else { 363 try { 364 volumeName = MediaStore.checkArgumentVolumeName(sv.getNormalizedUuid()); 365 } catch (IllegalArgumentException ignored) { 366 return; 367 } 368 } 369 370 switch (intent.getAction()) { 371 case Intent.ACTION_MEDIA_MOUNTED: 372 attachVolume(volumeName); 373 break; 374 case Intent.ACTION_MEDIA_UNMOUNTED: 375 case Intent.ACTION_MEDIA_EJECT: 376 case Intent.ACTION_MEDIA_REMOVED: 377 case Intent.ACTION_MEDIA_BAD_REMOVAL: 378 detachVolume(volumeName); 379 break; 380 } 381 } 382 } 383 }; 384 385 private final SQLiteDatabase.CustomFunction mObjectRemovedCallback = 386 new SQLiteDatabase.CustomFunction() { 387 @Override 388 public void callback(String[] args) { 389 // We could remove only the deleted entry from the cache, but that 390 // requires the path, which we don't have here, so instead we just 391 // clear the entire cache. 392 // TODO: include the path in the callback and only remove the affected 393 // entry from the cache 394 synchronized (mDirectoryCache) { 395 mDirectoryCache.clear(); 396 } 397 } 398 }; 399 400 /** 401 * Wrapper class for a specific database (associated with one particular 402 * external card, or with internal storage). Can open the actual database 403 * on demand, create and upgrade the schema, etc. 404 */ 405 static class DatabaseHelper extends SQLiteOpenHelper implements AutoCloseable { 406 final Context mContext; 407 final String mName; 408 final int mVersion; 409 final boolean mInternal; // True if this is the internal database 410 final boolean mEarlyUpgrade; 411 final SQLiteDatabase.CustomFunction mObjectRemovedCallback; 412 long mScanStartTime; 413 long mScanStopTime; 414 415 // In memory caches of artist and album data. 416 ArrayMap<String, Long> mArtistCache = new ArrayMap<String, Long>(); 417 ArrayMap<String, Long> mAlbumCache = new ArrayMap<String, Long>(); 418 DatabaseHelper(Context context, String name, boolean internal, boolean earlyUpgrade, SQLiteDatabase.CustomFunction objectRemovedCallback)419 public DatabaseHelper(Context context, String name, boolean internal, 420 boolean earlyUpgrade, SQLiteDatabase.CustomFunction objectRemovedCallback) { 421 this(context, name, getDatabaseVersion(context), internal, earlyUpgrade, 422 objectRemovedCallback); 423 } 424 DatabaseHelper(Context context, String name, int version, boolean internal, boolean earlyUpgrade, SQLiteDatabase.CustomFunction objectRemovedCallback)425 public DatabaseHelper(Context context, String name, int version, boolean internal, 426 boolean earlyUpgrade, SQLiteDatabase.CustomFunction objectRemovedCallback) { 427 super(context, name, null, version); 428 mContext = context; 429 mName = name; 430 mVersion = version; 431 mInternal = internal; 432 mEarlyUpgrade = earlyUpgrade; 433 mObjectRemovedCallback = objectRemovedCallback; 434 setWriteAheadLoggingEnabled(true); 435 setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS); 436 } 437 438 @Override onCreate(final SQLiteDatabase db)439 public void onCreate(final SQLiteDatabase db) { 440 Log.v(TAG, "onCreate() for " + mName); 441 updateDatabase(mContext, db, mInternal, 0, mVersion); 442 } 443 444 @Override onUpgrade(final SQLiteDatabase db, final int oldV, final int newV)445 public void onUpgrade(final SQLiteDatabase db, final int oldV, final int newV) { 446 Log.v(TAG, "onUpgrade() for " + mName + " from " + oldV + " to " + newV); 447 updateDatabase(mContext, db, mInternal, oldV, newV); 448 } 449 450 @Override onDowngrade(final SQLiteDatabase db, final int oldV, final int newV)451 public void onDowngrade(final SQLiteDatabase db, final int oldV, final int newV) { 452 Log.v(TAG, "onDowngrade() for " + mName + " from " + oldV + " to " + newV); 453 downgradeDatabase(mContext, db, mInternal, oldV, newV); 454 } 455 456 /** 457 * For devices that have removable storage, we support keeping multiple databases 458 * to allow users to switch between a number of cards. 459 * On such devices, touch this particular database and garbage collect old databases. 460 * An LRU cache system is used to clean up databases for old external 461 * storage volumes. 462 */ 463 @Override onOpen(SQLiteDatabase db)464 public void onOpen(SQLiteDatabase db) { 465 466 if (mEarlyUpgrade) return; // Doing early upgrade. 467 468 if (mObjectRemovedCallback != null) { 469 db.addCustomFunction("_OBJECT_REMOVED", 1, mObjectRemovedCallback); 470 } 471 472 if (mInternal) return; // The internal database is kept separately. 473 474 // the code below is only needed on devices with removable storage 475 if (!Environment.isExternalStorageRemovable()) return; 476 477 // touch the database file to show it is most recently used 478 File file = new File(db.getPath()); 479 long now = System.currentTimeMillis(); 480 file.setLastModified(now); 481 482 // delete least recently used databases if we are over the limit 483 String[] databases = mContext.databaseList(); 484 // Don't delete wal auxiliary files(db-shm and db-wal) directly because db file may 485 // not be deleted, and it will cause Disk I/O error when accessing this database. 486 List<String> dbList = new ArrayList<String>(); 487 for (String database : databases) { 488 if (database != null && database.endsWith(".db")) { 489 dbList.add(database); 490 } 491 } 492 databases = dbList.toArray(new String[0]); 493 int count = databases.length; 494 int limit = MAX_EXTERNAL_DATABASES; 495 496 // delete external databases that have not been used in the past two months 497 long twoMonthsAgo = now - OBSOLETE_DATABASE_DB; 498 for (int i = 0; i < databases.length; i++) { 499 File other = mContext.getDatabasePath(databases[i]); 500 if (INTERNAL_DATABASE_NAME.equals(databases[i]) || file.equals(other)) { 501 databases[i] = null; 502 count--; 503 if (file.equals(other)) { 504 // reduce limit to account for the existence of the database we 505 // are about to open, which we removed from the list. 506 limit--; 507 } 508 } else { 509 long time = other.lastModified(); 510 if (time < twoMonthsAgo) { 511 if (LOCAL_LOGV) Log.v(TAG, "Deleting old database " + databases[i]); 512 mContext.deleteDatabase(databases[i]); 513 databases[i] = null; 514 count--; 515 } 516 } 517 } 518 519 // delete least recently used databases until 520 // we are no longer over the limit 521 while (count > limit) { 522 int lruIndex = -1; 523 long lruTime = 0; 524 525 for (int i = 0; i < databases.length; i++) { 526 if (databases[i] != null) { 527 long time = mContext.getDatabasePath(databases[i]).lastModified(); 528 if (lruTime == 0 || time < lruTime) { 529 lruIndex = i; 530 lruTime = time; 531 } 532 } 533 } 534 535 // delete least recently used database 536 if (lruIndex != -1) { 537 if (LOCAL_LOGV) Log.v(TAG, "Deleting old database " + databases[lruIndex]); 538 mContext.deleteDatabase(databases[lruIndex]); 539 databases[lruIndex] = null; 540 count--; 541 } 542 } 543 } 544 545 /** 546 * List of {@link Uri} that would have been sent directly via 547 * {@link ContentResolver#notifyChange}, but are instead being collected 548 * due to an ongoing transaction. 549 */ 550 private final ThreadLocal<List<Uri>> mNotifyChanges = new ThreadLocal<>(); 551 beginTransaction()552 public void beginTransaction() { 553 getWritableDatabase().beginTransaction(); 554 mNotifyChanges.set(new ArrayList<>()); 555 } 556 setTransactionSuccessful()557 public void setTransactionSuccessful() { 558 getWritableDatabase().setTransactionSuccessful(); 559 final List<Uri> uris = mNotifyChanges.get(); 560 if (uris != null) { 561 BackgroundThread.getHandler().postDelayed(() -> { 562 for (Uri uri : uris) { 563 notifyChangeInternal(uri); 564 } 565 }, sBackgroundDelay); 566 } 567 mNotifyChanges.remove(); 568 } 569 endTransaction()570 public void endTransaction() { 571 getWritableDatabase().endTransaction(); 572 } 573 574 /** 575 * Notify that the given {@link Uri} has changed. This enqueues the 576 * notification if currently inside a transaction, and they'll be 577 * clustered and sent when the transaction completes. 578 */ notifyChange(Uri uri)579 public void notifyChange(Uri uri) { 580 if (LOCAL_LOGV) Log.v(TAG, "Notifying " + uri); 581 final List<Uri> uris = mNotifyChanges.get(); 582 if (uris != null) { 583 uris.add(uri); 584 } else { 585 BackgroundThread.getHandler().postDelayed(() -> { 586 notifyChangeInternal(uri); 587 }, sBackgroundDelay); 588 } 589 } 590 notifyChangeInternal(Uri uri)591 private void notifyChangeInternal(Uri uri) { 592 Trace.traceBegin(TRACE_TAG_DATABASE, "notifyChange"); 593 try { 594 mContext.getContentResolver().notifyChange(uri, null); 595 } finally { 596 Trace.traceEnd(TRACE_TAG_DATABASE); 597 } 598 } 599 } 600 601 /** 602 * Apply {@link Consumer#accept} to the given {@link Uri}. 603 * <p> 604 * Since media items can be exposed through multiple collections or views, 605 * this method expands the single item being accepted to also accept all 606 * relevant views. 607 */ acceptWithExpansion(Consumer<Uri> consumer, Uri uri)608 public static void acceptWithExpansion(Consumer<Uri> consumer, Uri uri) { 609 final int match = matchUri(uri, true); 610 acceptWithExpansionInternal(consumer, uri, match); 611 612 try { 613 // When targeting a specific volume, we need to expand to also 614 // notify the top-level view 615 final String volumeName = getVolumeName(uri); 616 switch (volumeName) { 617 case MediaStore.VOLUME_INTERNAL: 618 case MediaStore.VOLUME_EXTERNAL: 619 // Already a top-level view, no need to expand 620 break; 621 default: 622 final List<String> segments = new ArrayList<>(uri.getPathSegments()); 623 segments.set(0, MediaStore.VOLUME_EXTERNAL); 624 final Uri.Builder builder = uri.buildUpon().path(null); 625 for (String segment : segments) { 626 builder.appendPath(segment); 627 } 628 acceptWithExpansionInternal(consumer, builder.build(), match); 629 break; 630 } 631 } catch (IllegalArgumentException ignored) { 632 } 633 } 634 acceptWithExpansionInternal(Consumer<Uri> consumer, Uri uri, int match)635 private static void acceptWithExpansionInternal(Consumer<Uri> consumer, Uri uri, int match) { 636 // Start by always notifying the base item 637 consumer.accept(uri); 638 639 // Some items can be exposed through multiple collections, 640 // so we need to notify all possible views of those items 641 switch (match) { 642 case AUDIO_MEDIA_ID: 643 case VIDEO_MEDIA_ID: 644 case IMAGES_MEDIA_ID: { 645 final String volumeName = getVolumeName(uri); 646 final long id = ContentUris.parseId(uri); 647 consumer.accept(Files.getContentUri(volumeName, id)); 648 consumer.accept(Downloads.getContentUri(volumeName, id)); 649 break; 650 } 651 case AUDIO_MEDIA: 652 case VIDEO_MEDIA: 653 case IMAGES_MEDIA: { 654 final String volumeName = getVolumeName(uri); 655 consumer.accept(Files.getContentUri(volumeName)); 656 consumer.accept(Downloads.getContentUri(volumeName)); 657 break; 658 } 659 case FILES_ID: 660 case DOWNLOADS_ID: { 661 final String volumeName = getVolumeName(uri); 662 final long id = ContentUris.parseId(uri); 663 consumer.accept(Audio.Media.getContentUri(volumeName, id)); 664 consumer.accept(Video.Media.getContentUri(volumeName, id)); 665 consumer.accept(Images.Media.getContentUri(volumeName, id)); 666 break; 667 } 668 case FILES: 669 case DOWNLOADS: { 670 final String volumeName = getVolumeName(uri); 671 consumer.accept(Audio.Media.getContentUri(volumeName)); 672 consumer.accept(Video.Media.getContentUri(volumeName)); 673 consumer.accept(Images.Media.getContentUri(volumeName)); 674 break; 675 } 676 } 677 678 // Any changing audio items mean we probably need to invalidate all 679 // indexed views built from that media 680 switch (match) { 681 case AUDIO_MEDIA: 682 case AUDIO_MEDIA_ID: { 683 final String volumeName = getVolumeName(uri); 684 consumer.accept(Audio.Genres.getContentUri(volumeName)); 685 consumer.accept(Audio.Playlists.getContentUri(volumeName)); 686 consumer.accept(Audio.Artists.getContentUri(volumeName)); 687 consumer.accept(Audio.Albums.getContentUri(volumeName)); 688 break; 689 } 690 } 691 } 692 693 private static final String[] sDefaultFolderNames = { 694 Environment.DIRECTORY_MUSIC, 695 Environment.DIRECTORY_PODCASTS, 696 Environment.DIRECTORY_RINGTONES, 697 Environment.DIRECTORY_ALARMS, 698 Environment.DIRECTORY_NOTIFICATIONS, 699 Environment.DIRECTORY_PICTURES, 700 Environment.DIRECTORY_MOVIES, 701 Environment.DIRECTORY_DOWNLOADS, 702 Environment.DIRECTORY_DCIM, 703 }; 704 705 /** 706 * This method cleans up any files created by android.media.MiniThumbFile, removed after P. 707 * It's triggered during database update only, in order to run only once. 708 */ deleteLegacyThumbnailData()709 private static void deleteLegacyThumbnailData() { 710 File directory = new File(Environment.getExternalStorageDirectory(), "/DCIM/.thumbnails"); 711 712 FilenameFilter filter = (dir, filename) -> filename.startsWith(".thumbdata"); 713 for (File f : ArrayUtils.defeatNullable(directory.listFiles(filter))) { 714 if (!f.delete()) { 715 Log.e(TAG, "Failed to delete legacy thumbnail data " + f.getAbsolutePath()); 716 } 717 } 718 } 719 720 /** 721 * Ensure that default folders are created on mounted primary storage 722 * devices. We only do this once per volume so we don't annoy the user if 723 * deleted manually. 724 */ ensureDefaultFolders(String volumeName, DatabaseHelper helper, SQLiteDatabase db)725 private void ensureDefaultFolders(String volumeName, DatabaseHelper helper, SQLiteDatabase db) { 726 try { 727 final File path = getVolumePath(volumeName); 728 final StorageVolume vol = mStorageManager.getStorageVolume(path); 729 final String key; 730 if (VolumeInfo.ID_EMULATED_INTERNAL.equals(vol.getId())) { 731 key = "created_default_folders"; 732 } else { 733 key = "created_default_folders_" + vol.getNormalizedUuid(); 734 } 735 736 final SharedPreferences prefs = PreferenceManager 737 .getDefaultSharedPreferences(getContext()); 738 if (prefs.getInt(key, 0) == 0) { 739 for (String folderName : sDefaultFolderNames) { 740 final File folder = new File(vol.getPathFile(), folderName); 741 if (!folder.exists()) { 742 folder.mkdirs(); 743 insertDirectory(helper, db, folder.getAbsolutePath()); 744 } 745 } 746 747 SharedPreferences.Editor editor = prefs.edit(); 748 editor.putInt(key, 1); 749 editor.commit(); 750 } 751 } catch (IOException e) { 752 Log.w(TAG, "Failed to ensure default folders for " + volumeName, e); 753 } 754 } 755 getDatabaseVersion(Context context)756 public static int getDatabaseVersion(Context context) { 757 try { 758 return context.getPackageManager().getPackageInfo( 759 context.getPackageName(), 0).versionCode; 760 } catch (NameNotFoundException e) { 761 throw new RuntimeException("couldn't get version code for " + context); 762 } 763 } 764 765 @Override onCreate()766 public boolean onCreate() { 767 final Context context = getContext(); 768 769 // Enable verbose transport logging when requested 770 setTransportLoggingEnabled(LOCAL_LOGV); 771 772 // Shift call statistics back to the original caller 773 Binder.setProxyTransactListener( 774 new Binder.PropagateWorkSourceTransactListener()); 775 776 mStorageManager = context.getSystemService(StorageManager.class); 777 mAppOpsManager = context.getSystemService(AppOpsManager.class); 778 mPackageManager = context.getPackageManager(); 779 780 // Reasonable thumbnail size is half of the smallest screen edge width 781 final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); 782 final int thumbSize = Math.min(metrics.widthPixels, metrics.heightPixels) / 2; 783 mThumbSize = new Size(thumbSize, thumbSize); 784 785 mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, true, 786 false, mObjectRemovedCallback); 787 mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME, false, 788 false, mObjectRemovedCallback); 789 790 final IntentFilter filter = new IntentFilter(); 791 filter.setPriority(10); 792 filter.addDataScheme("file"); 793 filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); 794 filter.addAction(Intent.ACTION_MEDIA_MOUNTED); 795 filter.addAction(Intent.ACTION_MEDIA_EJECT); 796 filter.addAction(Intent.ACTION_MEDIA_REMOVED); 797 filter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL); 798 context.registerReceiver(mMediaReceiver, filter); 799 800 // Watch for invalidation of cached volumes 801 mStorageManager.registerListener(new StorageEventListener() { 802 @Override 803 public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) { 804 updateVolumes(); 805 } 806 }); 807 updateVolumes(); 808 809 attachVolume(MediaStore.VOLUME_INTERNAL); 810 811 // Attach all currently mounted external volumes 812 for (String volumeName : getExternalVolumeNames()) { 813 attachVolume(volumeName); 814 } 815 816 // Watch for performance-sensitive activity 817 mAppOpsManager.startWatchingActive(new int[] { 818 AppOpsManager.OP_CAMERA 819 }, mActiveListener); 820 821 return true; 822 } 823 824 @Override onCallingPackageChanged()825 public void onCallingPackageChanged() { 826 // Identity of the current thread has changed, so invalidate caches 827 mCallingIdentity.remove(); 828 } 829 clearLocalCallingIdentity()830 public LocalCallingIdentity clearLocalCallingIdentity() { 831 final LocalCallingIdentity token = mCallingIdentity.get(); 832 mCallingIdentity.set(LocalCallingIdentity.fromSelf()); 833 return token; 834 } 835 restoreLocalCallingIdentity(LocalCallingIdentity token)836 public void restoreLocalCallingIdentity(LocalCallingIdentity token) { 837 mCallingIdentity.set(token); 838 } 839 onIdleMaintenance(@onNull CancellationSignal signal)840 public void onIdleMaintenance(@NonNull CancellationSignal signal) { 841 final DatabaseHelper helper = mExternalDatabase; 842 final SQLiteDatabase db = helper.getReadableDatabase(); 843 844 // Scan all volumes to resolve any staleness 845 for (String volumeName : getExternalVolumeNames()) { 846 // Possibly bail before digging into each volume 847 signal.throwIfCanceled(); 848 849 try { 850 final File file = getVolumePath(volumeName); 851 MediaService.onScanVolume(getContext(), Uri.fromFile(file)); 852 } catch (IOException e) { 853 Log.w(TAG, e); 854 } 855 } 856 857 // Delete any stale thumbnails 858 pruneThumbnails(signal); 859 860 // Finished orphaning any content whose package no longer exists 861 final ArraySet<String> unknownPackages = new ArraySet<>(); 862 try (Cursor c = db.query(true, "files", new String[] { "owner_package_name" }, 863 null, null, null, null, null, null, signal)) { 864 while (c.moveToNext()) { 865 final String packageName = c.getString(0); 866 if (TextUtils.isEmpty(packageName)) continue; 867 try { 868 getContext().getPackageManager().getPackageInfo(packageName, 869 PackageManager.MATCH_UNINSTALLED_PACKAGES); 870 } catch (NameNotFoundException e) { 871 unknownPackages.add(packageName); 872 } 873 } 874 } 875 876 Log.d(TAG, "Found " + unknownPackages.size() + " unknown packages"); 877 for (String packageName : unknownPackages) { 878 onPackageOrphaned(packageName); 879 } 880 881 // Delete any expired content; we're paranoid about wildly changing 882 // clocks, so only delete items within the last week 883 final long from = ((System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS) / 1000); 884 final long to = (System.currentTimeMillis() / 1000); 885 try (Cursor c = db.query(true, "files", new String[] { "volume_name", "_id" }, 886 FileColumns.DATE_EXPIRES + " BETWEEN " + from + " AND " + to, null, 887 null, null, null, null, signal)) { 888 while (c.moveToNext()) { 889 final String volumeName = c.getString(0); 890 final long id = c.getLong(1); 891 delete(Files.getContentUri(volumeName, id), null, null); 892 } 893 Log.d(TAG, "Deleted " + c.getCount() + " expired items on " + helper.mName); 894 } 895 896 // Forget any stale volumes 897 final long lastWeek = System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS; 898 for (VolumeRecord rec : mStorageManager.getVolumeRecords()) { 899 // Skip volumes without valid UUIDs 900 if (TextUtils.isEmpty(rec.fsUuid)) continue; 901 902 // Skip volumes that are currently mounted 903 final VolumeInfo vol = mStorageManager.findVolumeByUuid(rec.fsUuid); 904 if (vol != null && vol.isMountedReadable()) continue; 905 906 if (rec.lastSeenMillis > 0 && rec.lastSeenMillis < lastWeek) { 907 final int num = db.delete("files", FileColumns.VOLUME_NAME + "=?", 908 new String[] { rec.getNormalizedFsUuid() }); 909 Log.d(TAG, "Forgot " + num + " stale items from " + rec.fsUuid); 910 } 911 } 912 } 913 onPackageOrphaned(String packageName)914 public void onPackageOrphaned(String packageName) { 915 final DatabaseHelper helper = mExternalDatabase; 916 final SQLiteDatabase db = helper.getWritableDatabase(); 917 918 final ContentValues values = new ContentValues(); 919 values.putNull(FileColumns.OWNER_PACKAGE_NAME); 920 921 final int count = db.update("files", values, 922 "owner_package_name=?", new String[] { packageName }); 923 if (count > 0) { 924 Log.d(TAG, "Orphaned " + count + " items belonging to " 925 + packageName + " on " + helper.mName); 926 } 927 } 928 enforceShellRestrictions()929 private void enforceShellRestrictions() { 930 if (UserHandle.getCallingAppId() == android.os.Process.SHELL_UID 931 && getContext().getSystemService(UserManager.class) 932 .hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER)) { 933 throw new SecurityException( 934 "Shell user cannot access files for user " + UserHandle.myUserId()); 935 } 936 } 937 938 @Override enforceReadPermissionInner(Uri uri, String callingPkg, IBinder callerToken)939 protected int enforceReadPermissionInner(Uri uri, String callingPkg, IBinder callerToken) 940 throws SecurityException { 941 enforceShellRestrictions(); 942 return super.enforceReadPermissionInner(uri, callingPkg, callerToken); 943 } 944 945 @Override enforceWritePermissionInner(Uri uri, String callingPkg, IBinder callerToken)946 protected int enforceWritePermissionInner(Uri uri, String callingPkg, IBinder callerToken) 947 throws SecurityException { 948 enforceShellRestrictions(); 949 return super.enforceWritePermissionInner(uri, callingPkg, callerToken); 950 } 951 952 @VisibleForTesting makePristineSchema(SQLiteDatabase db)953 static void makePristineSchema(SQLiteDatabase db) { 954 // drop all triggers 955 Cursor c = db.query("sqlite_master", new String[] {"name"}, "type is 'trigger'", 956 null, null, null, null); 957 while (c.moveToNext()) { 958 if (c.getString(0).startsWith("sqlite_")) continue; 959 db.execSQL("DROP TRIGGER IF EXISTS " + c.getString(0)); 960 } 961 c.close(); 962 963 // drop all views 964 c = db.query("sqlite_master", new String[] {"name"}, "type is 'view'", 965 null, null, null, null); 966 while (c.moveToNext()) { 967 if (c.getString(0).startsWith("sqlite_")) continue; 968 db.execSQL("DROP VIEW IF EXISTS " + c.getString(0)); 969 } 970 c.close(); 971 972 // drop all indexes 973 c = db.query("sqlite_master", new String[] {"name"}, "type is 'index'", 974 null, null, null, null); 975 while (c.moveToNext()) { 976 if (c.getString(0).startsWith("sqlite_")) continue; 977 db.execSQL("DROP INDEX IF EXISTS " + c.getString(0)); 978 } 979 c.close(); 980 981 // drop all tables 982 c = db.query("sqlite_master", new String[] {"name"}, "type is 'table'", 983 null, null, null, null); 984 while (c.moveToNext()) { 985 if (c.getString(0).startsWith("sqlite_")) continue; 986 db.execSQL("DROP TABLE IF EXISTS " + c.getString(0)); 987 } 988 c.close(); 989 } 990 createLatestSchema(SQLiteDatabase db, boolean internal)991 private static void createLatestSchema(SQLiteDatabase db, boolean internal) { 992 // We're about to start all ID numbering from scratch, so revoke any 993 // outstanding permission grants to ensure we don't leak data 994 AppGlobals.getInitialApplication().revokeUriPermission(MediaStore.AUTHORITY_URI, 995 Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 996 MediaDocumentsProvider.revokeAllUriGrants(AppGlobals.getInitialApplication()); 997 BackgroundThread.getHandler().post(() -> { 998 try (ContentProviderClient client = AppGlobals.getInitialApplication() 999 .getContentResolver().acquireContentProviderClient( 1000 android.provider.Downloads.Impl.AUTHORITY)) { 1001 client.call(android.provider.Downloads.CALL_REVOKE_MEDIASTORE_URI_PERMS, 1002 null, null); 1003 } catch (RemoteException e) { 1004 // Should not happen 1005 } 1006 }); 1007 1008 makePristineSchema(db); 1009 1010 db.execSQL("CREATE TABLE android_metadata (locale TEXT)"); 1011 db.execSQL("CREATE TABLE thumbnails (_id INTEGER PRIMARY KEY,_data TEXT,image_id INTEGER," 1012 + "kind INTEGER,width INTEGER,height INTEGER)"); 1013 db.execSQL("CREATE TABLE artists (artist_id INTEGER PRIMARY KEY," 1014 + "artist_key TEXT NOT NULL UNIQUE,artist TEXT NOT NULL)"); 1015 db.execSQL("CREATE TABLE albums (album_id INTEGER PRIMARY KEY," 1016 + "album_key TEXT NOT NULL UNIQUE,album TEXT NOT NULL)"); 1017 db.execSQL("CREATE TABLE album_art (album_id INTEGER PRIMARY KEY,_data TEXT)"); 1018 db.execSQL("CREATE TABLE videothumbnails (_id INTEGER PRIMARY KEY,_data TEXT," 1019 + "video_id INTEGER,kind INTEGER,width INTEGER,height INTEGER)"); 1020 db.execSQL("CREATE TABLE files (_id INTEGER PRIMARY KEY AUTOINCREMENT," 1021 + "_data TEXT UNIQUE COLLATE NOCASE,_size INTEGER,format INTEGER,parent INTEGER," 1022 + "date_added INTEGER,date_modified INTEGER,mime_type TEXT,title TEXT," 1023 + "description TEXT,_display_name TEXT,picasa_id TEXT,orientation INTEGER," 1024 + "latitude DOUBLE,longitude DOUBLE,datetaken INTEGER,mini_thumb_magic INTEGER," 1025 + "bucket_id TEXT,bucket_display_name TEXT,isprivate INTEGER,title_key TEXT," 1026 + "artist_id INTEGER,album_id INTEGER,composer TEXT,track INTEGER," 1027 + "year INTEGER CHECK(year!=0),is_ringtone INTEGER,is_music INTEGER," 1028 + "is_alarm INTEGER,is_notification INTEGER,is_podcast INTEGER,album_artist TEXT," 1029 + "duration INTEGER,bookmark INTEGER,artist TEXT,album TEXT,resolution TEXT," 1030 + "tags TEXT,category TEXT,language TEXT,mini_thumb_data TEXT,name TEXT," 1031 + "media_type INTEGER,old_id INTEGER,is_drm INTEGER," 1032 + "width INTEGER, height INTEGER, title_resource_uri TEXT," 1033 + "owner_package_name TEXT DEFAULT NULL," 1034 + "color_standard INTEGER, color_transfer INTEGER, color_range INTEGER," 1035 + "_hash BLOB DEFAULT NULL, is_pending INTEGER DEFAULT 0," 1036 + "is_download INTEGER DEFAULT 0, download_uri TEXT DEFAULT NULL," 1037 + "referer_uri TEXT DEFAULT NULL, is_audiobook INTEGER DEFAULT 0," 1038 + "date_expires INTEGER DEFAULT NULL,is_trashed INTEGER DEFAULT 0," 1039 + "group_id INTEGER DEFAULT NULL,primary_directory TEXT DEFAULT NULL," 1040 + "secondary_directory TEXT DEFAULT NULL,document_id TEXT DEFAULT NULL," 1041 + "instance_id TEXT DEFAULT NULL,original_document_id TEXT DEFAULT NULL," 1042 + "relative_path TEXT DEFAULT NULL,volume_name TEXT DEFAULT NULL)"); 1043 1044 db.execSQL("CREATE TABLE log (time DATETIME, message TEXT)"); 1045 if (!internal) { 1046 db.execSQL("CREATE TABLE audio_genres (_id INTEGER PRIMARY KEY,name TEXT NOT NULL)"); 1047 db.execSQL("CREATE TABLE audio_genres_map (_id INTEGER PRIMARY KEY," 1048 + "audio_id INTEGER NOT NULL,genre_id INTEGER NOT NULL," 1049 + "UNIQUE (audio_id,genre_id) ON CONFLICT IGNORE)"); 1050 db.execSQL("CREATE TABLE audio_playlists_map (_id INTEGER PRIMARY KEY," 1051 + "audio_id INTEGER NOT NULL,playlist_id INTEGER NOT NULL," 1052 + "play_order INTEGER NOT NULL)"); 1053 db.execSQL("CREATE TRIGGER audio_genres_cleanup DELETE ON audio_genres BEGIN DELETE" 1054 + " FROM audio_genres_map WHERE genre_id = old._id;END"); 1055 db.execSQL("CREATE TRIGGER audio_playlists_cleanup DELETE ON files" 1056 + " WHEN old.media_type=4" 1057 + " BEGIN DELETE FROM audio_playlists_map WHERE playlist_id = old._id;" 1058 + "SELECT _DELETE_FILE(old._data);END"); 1059 db.execSQL("CREATE TRIGGER files_cleanup DELETE ON files" 1060 + " BEGIN SELECT _OBJECT_REMOVED(old._id);END"); 1061 } 1062 1063 db.execSQL("CREATE INDEX image_id_index on thumbnails(image_id)"); 1064 db.execSQL("CREATE INDEX album_idx on albums(album)"); 1065 db.execSQL("CREATE INDEX albumkey_index on albums(album_key)"); 1066 db.execSQL("CREATE INDEX artist_idx on artists(artist)"); 1067 db.execSQL("CREATE INDEX artistkey_index on artists(artist_key)"); 1068 db.execSQL("CREATE INDEX video_id_index on videothumbnails(video_id)"); 1069 db.execSQL("CREATE INDEX album_id_idx ON files(album_id)"); 1070 db.execSQL("CREATE INDEX artist_id_idx ON files(artist_id)"); 1071 db.execSQL("CREATE INDEX bucket_index on files(bucket_id,media_type,datetaken, _id)"); 1072 db.execSQL("CREATE INDEX bucket_name on files(bucket_id,media_type,bucket_display_name)"); 1073 db.execSQL("CREATE INDEX format_index ON files(format)"); 1074 db.execSQL("CREATE INDEX media_type_index ON files(media_type)"); 1075 db.execSQL("CREATE INDEX parent_index ON files(parent)"); 1076 db.execSQL("CREATE INDEX path_index ON files(_data)"); 1077 db.execSQL("CREATE INDEX sort_index ON files(datetaken ASC, _id ASC)"); 1078 db.execSQL("CREATE INDEX title_idx ON files(title)"); 1079 db.execSQL("CREATE INDEX titlekey_index ON files(title_key)"); 1080 1081 db.execSQL("CREATE TRIGGER albumart_cleanup1 DELETE ON albums BEGIN DELETE FROM album_art" 1082 + " WHERE album_id = old.album_id;END"); 1083 db.execSQL("CREATE TRIGGER albumart_cleanup2 DELETE ON album_art" 1084 + " BEGIN SELECT _DELETE_FILE(old._data);END"); 1085 1086 createLatestViews(db, internal); 1087 } 1088 makePristineViews(SQLiteDatabase db)1089 private static void makePristineViews(SQLiteDatabase db) { 1090 // drop all views 1091 Cursor c = db.query("sqlite_master", new String[] {"name"}, "type is 'view'", 1092 null, null, null, null); 1093 while (c.moveToNext()) { 1094 db.execSQL("DROP VIEW IF EXISTS " + c.getString(0)); 1095 } 1096 c.close(); 1097 } 1098 createLatestViews(SQLiteDatabase db, boolean internal)1099 private static void createLatestViews(SQLiteDatabase db, boolean internal) { 1100 makePristineViews(db); 1101 1102 if (!internal) { 1103 db.execSQL("CREATE VIEW audio_playlists AS SELECT _id,_data,name,date_added," 1104 + "date_modified,owner_package_name,_hash,is_pending,date_expires,is_trashed," 1105 + "volume_name FROM files WHERE media_type=4"); 1106 } 1107 1108 db.execSQL("CREATE VIEW audio_meta AS SELECT _id,_data,_display_name,_size,mime_type," 1109 + "date_added,is_drm,date_modified,title,title_key,duration,artist_id,composer," 1110 + "album_id,track,year,is_ringtone,is_music,is_alarm,is_notification,is_podcast," 1111 + "bookmark,album_artist,owner_package_name,_hash,is_pending,is_audiobook," 1112 + "date_expires,is_trashed,group_id,primary_directory,secondary_directory," 1113 + "document_id,instance_id,original_document_id,title_resource_uri,relative_path," 1114 + "volume_name,datetaken,bucket_id,bucket_display_name,group_id,orientation" 1115 + " FROM files WHERE media_type=2"); 1116 1117 db.execSQL("CREATE VIEW artists_albums_map AS SELECT DISTINCT artist_id, album_id" 1118 + " FROM audio_meta"); 1119 db.execSQL("CREATE VIEW audio as SELECT *, NULL AS width, NULL as height" 1120 + " FROM audio_meta LEFT OUTER JOIN artists" 1121 + " ON audio_meta.artist_id=artists.artist_id LEFT OUTER JOIN albums" 1122 + " ON audio_meta.album_id=albums.album_id"); 1123 db.execSQL("CREATE VIEW album_info AS SELECT audio.album_id AS _id, album, album_key," 1124 + " MIN(year) AS minyear, MAX(year) AS maxyear, artist, artist_id, artist_key," 1125 + " count(*) AS numsongs,album_art._data AS album_art FROM audio" 1126 + " LEFT OUTER JOIN album_art ON audio.album_id=album_art.album_id WHERE is_music=1" 1127 + " GROUP BY audio.album_id"); 1128 db.execSQL("CREATE VIEW searchhelpertitle AS SELECT * FROM audio ORDER BY title_key"); 1129 db.execSQL("CREATE VIEW artist_info AS SELECT artist_id AS _id, artist, artist_key," 1130 + " COUNT(DISTINCT album_key) AS number_of_albums, COUNT(*) AS number_of_tracks" 1131 + " FROM audio" 1132 + " WHERE is_music=1 GROUP BY artist_key"); 1133 db.execSQL("CREATE VIEW search AS SELECT _id,'artist' AS mime_type,artist,NULL AS album," 1134 + "NULL AS title,artist AS text1,NULL AS text2,number_of_albums AS data1," 1135 + "number_of_tracks AS data2,artist_key AS match," 1136 + "'content://media/external/audio/artists/'||_id AS suggest_intent_data," 1137 + "1 AS grouporder FROM artist_info WHERE (artist!='<unknown>')" 1138 + " UNION ALL SELECT _id,'album' AS mime_type,artist,album," 1139 + "NULL AS title,album AS text1,artist AS text2,NULL AS data1," 1140 + "NULL AS data2,artist_key||' '||album_key AS match," 1141 + "'content://media/external/audio/albums/'||_id AS suggest_intent_data," 1142 + "2 AS grouporder FROM album_info" 1143 + " WHERE (album!='<unknown>')" 1144 + " UNION ALL SELECT searchhelpertitle._id AS _id,mime_type,artist,album,title," 1145 + "title AS text1,artist AS text2,NULL AS data1," 1146 + "NULL AS data2,artist_key||' '||album_key||' '||title_key AS match," 1147 + "'content://media/external/audio/media/'||searchhelpertitle._id" 1148 + " AS suggest_intent_data," 1149 + "3 AS grouporder FROM searchhelpertitle WHERE (title != '')"); 1150 db.execSQL("CREATE VIEW audio_genres_map_noid AS SELECT audio_id,genre_id" 1151 + " FROM audio_genres_map"); 1152 1153 db.execSQL("CREATE VIEW video AS SELECT " 1154 + String.join(",", getProjectionMap(Video.Media.class).keySet()) 1155 + " FROM files WHERE media_type=3"); 1156 db.execSQL("CREATE VIEW images AS SELECT " 1157 + String.join(",", getProjectionMap(Images.Media.class).keySet()) 1158 + " FROM files WHERE media_type=1"); 1159 db.execSQL("CREATE VIEW downloads AS SELECT " 1160 + String.join(",", getProjectionMap(Downloads.class).keySet()) 1161 + " FROM files WHERE is_download=1"); 1162 } 1163 updateCollationKeys(SQLiteDatabase db)1164 private static void updateCollationKeys(SQLiteDatabase db) { 1165 // Delete albums and artists, then clear the modification time on songs, which 1166 // will cause the media scanner to rescan everything, rebuilding the artist and 1167 // album tables along the way, while preserving playlists. 1168 // We need this rescan because ICU also changed, and now generates different 1169 // collation keys 1170 db.execSQL("DELETE from albums"); 1171 db.execSQL("DELETE from artists"); 1172 db.execSQL("UPDATE files SET date_modified=0;"); 1173 } 1174 updateAddTitleResource(SQLiteDatabase db)1175 private static void updateAddTitleResource(SQLiteDatabase db) { 1176 // Add the column used for title localization, and force a rescan of any 1177 // ringtones, alarms and notifications that may be using it. 1178 db.execSQL("ALTER TABLE files ADD COLUMN title_resource_uri TEXT"); 1179 db.execSQL("UPDATE files SET date_modified=0" 1180 + " WHERE (is_alarm IS 1) OR (is_ringtone IS 1) OR (is_notification IS 1)"); 1181 } 1182 updateAddOwnerPackageName(SQLiteDatabase db, boolean internal)1183 private static void updateAddOwnerPackageName(SQLiteDatabase db, boolean internal) { 1184 db.execSQL("ALTER TABLE files ADD COLUMN owner_package_name TEXT DEFAULT NULL"); 1185 1186 // Derive new column value based on well-known paths 1187 try (Cursor c = db.query("files", new String[] { FileColumns._ID, FileColumns.DATA }, 1188 FileColumns.DATA + " REGEXP '" + PATTERN_OWNED_PATH.pattern() + "'", 1189 null, null, null, null, null)) { 1190 Log.d(TAG, "Updating " + c.getCount() + " entries with well-known owners"); 1191 1192 final Matcher m = PATTERN_OWNED_PATH.matcher(""); 1193 final ContentValues values = new ContentValues(); 1194 1195 while (c.moveToNext()) { 1196 final long id = c.getLong(0); 1197 final String data = c.getString(1); 1198 m.reset(data); 1199 if (m.matches()) { 1200 final String packageName = m.group(1); 1201 values.clear(); 1202 values.put(FileColumns.OWNER_PACKAGE_NAME, packageName); 1203 db.update("files", values, "_id=" + id, null); 1204 } 1205 } 1206 } 1207 } 1208 updateAddColorSpaces(SQLiteDatabase db)1209 private static void updateAddColorSpaces(SQLiteDatabase db) { 1210 // Add the color aspects related column used for HDR detection etc. 1211 db.execSQL("ALTER TABLE files ADD COLUMN color_standard INTEGER;"); 1212 db.execSQL("ALTER TABLE files ADD COLUMN color_transfer INTEGER;"); 1213 db.execSQL("ALTER TABLE files ADD COLUMN color_range INTEGER;"); 1214 } 1215 updateAddHashAndPending(SQLiteDatabase db, boolean internal)1216 private static void updateAddHashAndPending(SQLiteDatabase db, boolean internal) { 1217 db.execSQL("ALTER TABLE files ADD COLUMN _hash BLOB DEFAULT NULL;"); 1218 db.execSQL("ALTER TABLE files ADD COLUMN is_pending INTEGER DEFAULT 0;"); 1219 } 1220 updateAddDownloadInfo(SQLiteDatabase db, boolean internal)1221 private static void updateAddDownloadInfo(SQLiteDatabase db, boolean internal) { 1222 db.execSQL("ALTER TABLE files ADD COLUMN is_download INTEGER DEFAULT 0;"); 1223 db.execSQL("ALTER TABLE files ADD COLUMN download_uri TEXT DEFAULT NULL;"); 1224 db.execSQL("ALTER TABLE files ADD COLUMN referer_uri TEXT DEFAULT NULL;"); 1225 } 1226 updateAddAudiobook(SQLiteDatabase db, boolean internal)1227 private static void updateAddAudiobook(SQLiteDatabase db, boolean internal) { 1228 db.execSQL("ALTER TABLE files ADD COLUMN is_audiobook INTEGER DEFAULT 0;"); 1229 } 1230 updateClearLocation(SQLiteDatabase db, boolean internal)1231 private static void updateClearLocation(SQLiteDatabase db, boolean internal) { 1232 db.execSQL("UPDATE files SET latitude=NULL, longitude=NULL;"); 1233 } 1234 updateSetIsDownload(SQLiteDatabase db, boolean internal)1235 private static void updateSetIsDownload(SQLiteDatabase db, boolean internal) { 1236 db.execSQL("UPDATE files SET is_download=1 WHERE _data REGEXP '" 1237 + PATTERN_DOWNLOADS_FILE + "'"); 1238 } 1239 updateAddExpiresAndTrashed(SQLiteDatabase db, boolean internal)1240 private static void updateAddExpiresAndTrashed(SQLiteDatabase db, boolean internal) { 1241 db.execSQL("ALTER TABLE files ADD COLUMN date_expires INTEGER DEFAULT NULL;"); 1242 db.execSQL("ALTER TABLE files ADD COLUMN is_trashed INTEGER DEFAULT 0;"); 1243 } 1244 updateAddGroupId(SQLiteDatabase db, boolean internal)1245 private static void updateAddGroupId(SQLiteDatabase db, boolean internal) { 1246 db.execSQL("ALTER TABLE files ADD COLUMN group_id INTEGER DEFAULT NULL;"); 1247 } 1248 updateAddDirectories(SQLiteDatabase db, boolean internal)1249 private static void updateAddDirectories(SQLiteDatabase db, boolean internal) { 1250 db.execSQL("ALTER TABLE files ADD COLUMN primary_directory TEXT DEFAULT NULL;"); 1251 db.execSQL("ALTER TABLE files ADD COLUMN secondary_directory TEXT DEFAULT NULL;"); 1252 } 1253 updateAddXmp(SQLiteDatabase db, boolean internal)1254 private static void updateAddXmp(SQLiteDatabase db, boolean internal) { 1255 db.execSQL("ALTER TABLE files ADD COLUMN document_id TEXT DEFAULT NULL;"); 1256 db.execSQL("ALTER TABLE files ADD COLUMN instance_id TEXT DEFAULT NULL;"); 1257 db.execSQL("ALTER TABLE files ADD COLUMN original_document_id TEXT DEFAULT NULL;"); 1258 } 1259 updateAddPath(SQLiteDatabase db, boolean internal)1260 private static void updateAddPath(SQLiteDatabase db, boolean internal) { 1261 db.execSQL("ALTER TABLE files ADD COLUMN relative_path TEXT DEFAULT NULL;"); 1262 } 1263 updateAddVolumeName(SQLiteDatabase db, boolean internal)1264 private static void updateAddVolumeName(SQLiteDatabase db, boolean internal) { 1265 db.execSQL("ALTER TABLE files ADD COLUMN volume_name TEXT DEFAULT NULL;"); 1266 } 1267 updateDirsMimeType(SQLiteDatabase db, boolean internal)1268 private static void updateDirsMimeType(SQLiteDatabase db, boolean internal) { 1269 db.execSQL("UPDATE files SET mime_type=NULL WHERE format=" 1270 + MtpConstants.FORMAT_ASSOCIATION); 1271 } 1272 updateRelativePath(SQLiteDatabase db, boolean internal)1273 private static void updateRelativePath(SQLiteDatabase db, boolean internal) { 1274 db.execSQL("UPDATE files" 1275 + " SET " + MediaColumns.RELATIVE_PATH + "=" + MediaColumns.RELATIVE_PATH + "||'/'" 1276 + " WHERE " + MediaColumns.RELATIVE_PATH + " IS NOT NULL" 1277 + " AND " + MediaColumns.RELATIVE_PATH + " NOT LIKE '%/';"); 1278 } 1279 recomputeDataValues(SQLiteDatabase db, boolean internal)1280 private static void recomputeDataValues(SQLiteDatabase db, boolean internal) { 1281 try (Cursor c = db.query("files", new String[] { FileColumns._ID, FileColumns.DATA }, 1282 null, null, null, null, null, null)) { 1283 Log.d(TAG, "Recomputing " + c.getCount() + " data values"); 1284 1285 final ContentValues values = new ContentValues(); 1286 while (c.moveToNext()) { 1287 values.clear(); 1288 final long id = c.getLong(0); 1289 final String data = c.getString(1); 1290 values.put(FileColumns.DATA, data); 1291 computeDataValues(values); 1292 values.remove(FileColumns.DATA); 1293 if (!values.isEmpty()) { 1294 db.update("files", values, "_id=" + id, null); 1295 } 1296 } 1297 } 1298 } 1299 1300 static final int VERSION_J = 509; 1301 static final int VERSION_K = 700; 1302 static final int VERSION_L = 700; 1303 static final int VERSION_M = 800; 1304 static final int VERSION_N = 800; 1305 static final int VERSION_O = 800; 1306 static final int VERSION_P = 900; 1307 static final int VERSION_Q = 1023; 1308 1309 /** 1310 * This method takes care of updating all the tables in the database to the 1311 * current version, creating them if necessary. 1312 * This method can only update databases at schema 700 or higher, which was 1313 * used by the KitKat release. Older database will be cleared and recreated. 1314 * @param db Database 1315 * @param internal True if this is the internal media database 1316 */ updateDatabase(Context context, SQLiteDatabase db, boolean internal, int fromVersion, int toVersion)1317 private static void updateDatabase(Context context, SQLiteDatabase db, boolean internal, 1318 int fromVersion, int toVersion) { 1319 final long startTime = SystemClock.elapsedRealtime(); 1320 1321 if (fromVersion < 700) { 1322 // Anything older than KK is recreated from scratch 1323 createLatestSchema(db, internal); 1324 } else { 1325 boolean recomputeDataValues = false; 1326 if (fromVersion < 800) { 1327 updateCollationKeys(db); 1328 } 1329 if (fromVersion < 900) { 1330 updateAddTitleResource(db); 1331 } 1332 if (fromVersion < 1000) { 1333 updateAddOwnerPackageName(db, internal); 1334 } 1335 if (fromVersion < 1003) { 1336 updateAddColorSpaces(db); 1337 } 1338 if (fromVersion < 1004) { 1339 updateAddHashAndPending(db, internal); 1340 } 1341 if (fromVersion < 1005) { 1342 updateAddDownloadInfo(db, internal); 1343 } 1344 if (fromVersion < 1006) { 1345 updateAddAudiobook(db, internal); 1346 } 1347 if (fromVersion < 1007) { 1348 updateClearLocation(db, internal); 1349 } 1350 if (fromVersion < 1008) { 1351 updateSetIsDownload(db, internal); 1352 } 1353 if (fromVersion < 1009) { 1354 // This database version added "secondary_bucket_id", but that 1355 // column name was refactored in version 1013 below, so this 1356 // update step is no longer needed. 1357 } 1358 if (fromVersion < 1010) { 1359 updateAddExpiresAndTrashed(db, internal); 1360 } 1361 if (fromVersion < 1012) { 1362 recomputeDataValues = true; 1363 } 1364 if (fromVersion < 1013) { 1365 updateAddGroupId(db, internal); 1366 updateAddDirectories(db, internal); 1367 recomputeDataValues = true; 1368 } 1369 if (fromVersion < 1014) { 1370 updateAddXmp(db, internal); 1371 } 1372 if (fromVersion < 1015) { 1373 // Empty version bump to ensure views are recreated 1374 } 1375 if (fromVersion < 1016) { 1376 // Empty version bump to ensure views are recreated 1377 } 1378 if (fromVersion < 1017) { 1379 updateSetIsDownload(db, internal); 1380 recomputeDataValues = true; 1381 } 1382 if (fromVersion < 1018) { 1383 updateAddPath(db, internal); 1384 recomputeDataValues = true; 1385 } 1386 if (fromVersion < 1019) { 1387 // Only trigger during "external", so that it runs only once. 1388 if (!internal) { 1389 deleteLegacyThumbnailData(); 1390 } 1391 } 1392 if (fromVersion < 1020) { 1393 updateAddVolumeName(db, internal); 1394 recomputeDataValues = true; 1395 } 1396 if (fromVersion < 1021) { 1397 // Empty version bump to ensure views are recreated 1398 } 1399 if (fromVersion < 1022) { 1400 updateDirsMimeType(db, internal); 1401 } 1402 if (fromVersion < 1023) { 1403 updateRelativePath(db, internal); 1404 } 1405 1406 if (recomputeDataValues) { 1407 recomputeDataValues(db, internal); 1408 } 1409 } 1410 1411 // Always recreate latest views during upgrade; they're cheap and it's 1412 // an easy way to ensure they're defined consistently 1413 createLatestViews(db, internal); 1414 1415 sanityCheck(db, fromVersion); 1416 1417 getOrCreateUuid(db); 1418 1419 final long elapsedSeconds = (SystemClock.elapsedRealtime() - startTime) 1420 / DateUtils.SECOND_IN_MILLIS; 1421 logToDb(db, "Database upgraded from version " + fromVersion + " to " + toVersion 1422 + " in " + elapsedSeconds + " seconds"); 1423 } 1424 downgradeDatabase(Context context, SQLiteDatabase db, boolean internal, int fromVersion, int toVersion)1425 private static void downgradeDatabase(Context context, SQLiteDatabase db, boolean internal, 1426 int fromVersion, int toVersion) { 1427 final long startTime = SystemClock.elapsedRealtime(); 1428 1429 // The best we can do is wipe and start over 1430 createLatestSchema(db, internal); 1431 1432 final long elapsedSeconds = (SystemClock.elapsedRealtime() - startTime) 1433 / DateUtils.SECOND_IN_MILLIS; 1434 logToDb(db, "Database downgraded from version " + fromVersion + " to " + toVersion 1435 + " in " + elapsedSeconds + " seconds"); 1436 } 1437 1438 /** 1439 * Write a persistent diagnostic message to the log table. 1440 */ logToDb(SQLiteDatabase db, String message)1441 static void logToDb(SQLiteDatabase db, String message) { 1442 db.execSQL("INSERT OR REPLACE" + 1443 " INTO log (time,message) VALUES (strftime('%Y-%m-%d %H:%M:%f','now'),?);", 1444 new String[] { message }); 1445 // delete all but the last 500 rows 1446 db.execSQL("DELETE FROM log WHERE rowid IN" + 1447 " (SELECT rowid FROM log ORDER BY rowid DESC LIMIT 500,-1);"); 1448 } 1449 1450 /** 1451 * Perform a simple sanity check on the database. Currently this tests 1452 * whether all the _data entries in audio_meta are unique 1453 */ sanityCheck(SQLiteDatabase db, int fromVersion)1454 private static void sanityCheck(SQLiteDatabase db, int fromVersion) { 1455 Cursor c1 = null; 1456 Cursor c2 = null; 1457 try { 1458 c1 = db.query("audio_meta", new String[] {"count(*)"}, 1459 null, null, null, null, null); 1460 c2 = db.query("audio_meta", new String[] {"count(distinct _data)"}, 1461 null, null, null, null, null); 1462 c1.moveToFirst(); 1463 c2.moveToFirst(); 1464 int num1 = c1.getInt(0); 1465 int num2 = c2.getInt(0); 1466 if (num1 != num2) { 1467 Log.e(TAG, "audio_meta._data column is not unique while upgrading" + 1468 " from schema " +fromVersion + " : " + num1 +"/" + num2); 1469 // Delete all audio_meta rows so they will be rebuilt by the media scanner 1470 db.execSQL("DELETE FROM audio_meta;"); 1471 } 1472 } finally { 1473 IoUtils.closeQuietly(c1); 1474 IoUtils.closeQuietly(c2); 1475 } 1476 } 1477 1478 private static final String XATTR_UUID = "user.uuid"; 1479 1480 /** 1481 * Return a UUID for the given database. If the database is deleted or 1482 * otherwise corrupted, then a new UUID will automatically be generated. 1483 */ getOrCreateUuid(@onNull SQLiteDatabase db)1484 private static @NonNull String getOrCreateUuid(@NonNull SQLiteDatabase db) { 1485 try { 1486 return new String(Os.getxattr(db.getPath(), XATTR_UUID)); 1487 } catch (ErrnoException e) { 1488 if (e.errno == OsConstants.ENODATA) { 1489 // Doesn't exist yet, so generate and persist a UUID 1490 final String uuid = UUID.randomUUID().toString(); 1491 try { 1492 Os.setxattr(db.getPath(), XATTR_UUID, uuid.getBytes(), 0); 1493 } catch (ErrnoException e2) { 1494 throw new RuntimeException(e); 1495 } 1496 return uuid; 1497 } else { 1498 throw new RuntimeException(e); 1499 } 1500 } 1501 } 1502 1503 @VisibleForTesting computeDataValues(ContentValues values)1504 static void computeDataValues(ContentValues values) { 1505 // Worst case we have to assume no bucket details 1506 values.remove(ImageColumns.BUCKET_ID); 1507 values.remove(ImageColumns.BUCKET_DISPLAY_NAME); 1508 values.remove(ImageColumns.GROUP_ID); 1509 values.remove(ImageColumns.VOLUME_NAME); 1510 values.remove(ImageColumns.RELATIVE_PATH); 1511 values.remove(ImageColumns.PRIMARY_DIRECTORY); 1512 values.remove(ImageColumns.SECONDARY_DIRECTORY); 1513 1514 final String data = values.getAsString(MediaColumns.DATA); 1515 if (TextUtils.isEmpty(data)) return; 1516 1517 final File file = new File(data); 1518 final File fileLower = new File(data.toLowerCase()); 1519 1520 values.put(ImageColumns.VOLUME_NAME, extractVolumeName(data)); 1521 values.put(ImageColumns.RELATIVE_PATH, extractRelativePath(data)); 1522 values.put(ImageColumns.DISPLAY_NAME, extractDisplayName(data)); 1523 1524 // Buckets are the parent directory 1525 final String parent = fileLower.getParent(); 1526 if (parent != null) { 1527 values.put(ImageColumns.BUCKET_ID, parent.hashCode()); 1528 // The relative path for files in the top directory is "/" 1529 if (!"/".equals(values.getAsString(ImageColumns.RELATIVE_PATH))) { 1530 values.put(ImageColumns.BUCKET_DISPLAY_NAME, file.getParentFile().getName()); 1531 } 1532 } 1533 1534 // Groups are the first part of name 1535 final String name = fileLower.getName(); 1536 final int firstDot = name.indexOf('.'); 1537 if (firstDot > 0) { 1538 values.put(ImageColumns.GROUP_ID, 1539 name.substring(0, firstDot).hashCode()); 1540 } 1541 1542 // Directories are first two levels of storage paths 1543 final String relativePath = values.getAsString(ImageColumns.RELATIVE_PATH); 1544 if (TextUtils.isEmpty(relativePath)) return; 1545 1546 final String[] segments = relativePath.split("/"); 1547 if (segments.length > 0) { 1548 values.put(ImageColumns.PRIMARY_DIRECTORY, segments[0]); 1549 } 1550 if (segments.length > 1) { 1551 values.put(ImageColumns.SECONDARY_DIRECTORY, segments[1]); 1552 } 1553 } 1554 1555 @Override canonicalize(Uri uri)1556 public Uri canonicalize(Uri uri) { 1557 final boolean allowHidden = isCallingPackageAllowedHidden(); 1558 final int match = matchUri(uri, allowHidden); 1559 1560 // Skip when we have nothing to canonicalize 1561 if ("1".equals(uri.getQueryParameter(CANONICAL))) { 1562 return uri; 1563 } 1564 1565 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) { 1566 switch (match) { 1567 case AUDIO_MEDIA_ID: { 1568 final String title = getDefaultTitleFromCursor(c); 1569 if (!TextUtils.isEmpty(title)) { 1570 final Uri.Builder builder = uri.buildUpon(); 1571 builder.appendQueryParameter(AudioColumns.TITLE, title); 1572 builder.appendQueryParameter(CANONICAL, "1"); 1573 return builder.build(); 1574 } 1575 } 1576 case VIDEO_MEDIA_ID: 1577 case IMAGES_MEDIA_ID: { 1578 final String documentId = c 1579 .getString(c.getColumnIndexOrThrow(MediaColumns.DOCUMENT_ID)); 1580 if (!TextUtils.isEmpty(documentId)) { 1581 final Uri.Builder builder = uri.buildUpon(); 1582 builder.appendQueryParameter(MediaColumns.DOCUMENT_ID, documentId); 1583 builder.appendQueryParameter(CANONICAL, "1"); 1584 return builder.build(); 1585 } 1586 } 1587 } 1588 } catch (FileNotFoundException e) { 1589 Log.w(TAG, e.getMessage()); 1590 } 1591 return null; 1592 } 1593 1594 @Override uncanonicalize(Uri uri)1595 public Uri uncanonicalize(Uri uri) { 1596 final boolean allowHidden = isCallingPackageAllowedHidden(); 1597 final int match = matchUri(uri, allowHidden); 1598 1599 // Skip when we have nothing to uncanonicalize 1600 if (!"1".equals(uri.getQueryParameter(CANONICAL))) { 1601 return uri; 1602 } 1603 1604 // Extract values and then clear to avoid recursive lookups 1605 final String title = uri.getQueryParameter(AudioColumns.TITLE); 1606 final String documentId = uri.getQueryParameter(MediaColumns.DOCUMENT_ID); 1607 uri = uri.buildUpon().clearQuery().build(); 1608 1609 switch (match) { 1610 case AUDIO_MEDIA_ID: { 1611 // First check for an exact match 1612 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) { 1613 if (Objects.equals(title, getDefaultTitleFromCursor(c))) { 1614 return uri; 1615 } 1616 } catch (FileNotFoundException e) { 1617 Log.w(TAG, "Trouble resolving " + uri + "; falling back to search: " + e); 1618 } 1619 1620 // Otherwise fallback to searching 1621 final Uri baseUri = ContentUris.removeId(uri); 1622 try (Cursor c = queryForSingleItem(baseUri, 1623 new String[] { BaseColumns._ID }, 1624 AudioColumns.TITLE + "=?", new String[] { title }, null)) { 1625 return ContentUris.withAppendedId(baseUri, c.getLong(0)); 1626 } catch (FileNotFoundException e) { 1627 Log.w(TAG, "Failed to resolve " + uri + ": " + e); 1628 return null; 1629 } 1630 } 1631 case VIDEO_MEDIA_ID: 1632 case IMAGES_MEDIA_ID: { 1633 // First check for an exact match 1634 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) { 1635 if (Objects.equals(title, getDefaultTitleFromCursor(c))) { 1636 return uri; 1637 } 1638 } catch (FileNotFoundException e) { 1639 Log.w(TAG, "Trouble resolving " + uri + "; falling back to search: " + e); 1640 } 1641 1642 // Otherwise fallback to searching 1643 final Uri baseUri = ContentUris.removeId(uri); 1644 try (Cursor c = queryForSingleItem(baseUri, 1645 new String[] { BaseColumns._ID }, 1646 MediaColumns.DOCUMENT_ID + "=?", new String[] { documentId }, null)) { 1647 return ContentUris.withAppendedId(baseUri, c.getLong(0)); 1648 } catch (FileNotFoundException e) { 1649 Log.w(TAG, "Failed to resolve " + uri + ": " + e); 1650 return null; 1651 } 1652 } 1653 } 1654 1655 return uri; 1656 } 1657 safeUncanonicalize(Uri uri)1658 private Uri safeUncanonicalize(Uri uri) { 1659 Uri newUri = uncanonicalize(uri); 1660 if (newUri != null) { 1661 return newUri; 1662 } 1663 return uri; 1664 } 1665 1666 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)1667 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 1668 String sortOrder) { 1669 return query(uri, projection, 1670 ContentResolver.createSqlQueryBundle(selection, selectionArgs, sortOrder), null); 1671 } 1672 1673 @Override query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal)1674 public Cursor query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal) { 1675 Trace.traceBegin(TRACE_TAG_DATABASE, "query"); 1676 try { 1677 return queryInternal(uri, projection, queryArgs, signal); 1678 } finally { 1679 Trace.traceEnd(TRACE_TAG_DATABASE); 1680 } 1681 } 1682 queryInternal(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal)1683 private Cursor queryInternal(Uri uri, String[] projection, Bundle queryArgs, 1684 CancellationSignal signal) { 1685 String selection = null; 1686 String[] selectionArgs = null; 1687 String sortOrder = null; 1688 1689 if (queryArgs != null) { 1690 selection = queryArgs.getString(ContentResolver.QUERY_ARG_SQL_SELECTION); 1691 selectionArgs = queryArgs.getStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS); 1692 sortOrder = queryArgs.getString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER); 1693 if (sortOrder == null 1694 && queryArgs.containsKey(ContentResolver.QUERY_ARG_SORT_COLUMNS)) { 1695 sortOrder = ContentResolver.createSqlSortClause(queryArgs); 1696 } 1697 } 1698 1699 uri = safeUncanonicalize(uri); 1700 1701 final String volumeName = getVolumeName(uri); 1702 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 1703 final boolean allowHidden = isCallingPackageAllowedHidden(); 1704 final int table = matchUri(uri, allowHidden); 1705 1706 //Log.v(TAG, "query: uri="+uri+", selection="+selection); 1707 // handle MEDIA_SCANNER before calling getDatabaseForUri() 1708 if (table == MEDIA_SCANNER) { 1709 // create a cursor to return volume currently being scanned by the media scanner 1710 MatrixCursor c = new MatrixCursor(new String[] {MediaStore.MEDIA_SCANNER_VOLUME}); 1711 c.addRow(new String[] {mMediaScannerVolume}); 1712 return c; 1713 } 1714 1715 // Used temporarily (until we have unique media IDs) to get an identifier 1716 // for the current sd card, so that the music app doesn't have to use the 1717 // non-public getFatVolumeId method 1718 if (table == FS_ID) { 1719 MatrixCursor c = new MatrixCursor(new String[] {"fsid"}); 1720 c.addRow(new Integer[] {mVolumeId}); 1721 return c; 1722 } 1723 1724 if (table == VERSION) { 1725 MatrixCursor c = new MatrixCursor(new String[] {"version"}); 1726 c.addRow(new Integer[] {getDatabaseVersion(getContext())}); 1727 return c; 1728 } 1729 1730 final DatabaseHelper helper; 1731 final SQLiteDatabase db; 1732 try { 1733 helper = getDatabaseForUri(uri); 1734 db = helper.getReadableDatabase(); 1735 } catch (VolumeNotFoundException e) { 1736 return e.translateForQuery(targetSdkVersion); 1737 } 1738 1739 if (table == MTP_OBJECT_REFERENCES) { 1740 final int handle = Integer.parseInt(uri.getPathSegments().get(2)); 1741 return getObjectReferences(helper, db, handle); 1742 } 1743 1744 SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, uri, table, queryArgs); 1745 String limit = uri.getQueryParameter(MediaStore.PARAM_LIMIT); 1746 String filter = uri.getQueryParameter("filter"); 1747 String [] keywords = null; 1748 if (filter != null) { 1749 filter = Uri.decode(filter).trim(); 1750 if (!TextUtils.isEmpty(filter)) { 1751 String [] searchWords = filter.split(" "); 1752 keywords = new String[searchWords.length]; 1753 for (int i = 0; i < searchWords.length; i++) { 1754 String key = MediaStore.Audio.keyFor(searchWords[i]); 1755 key = key.replace("\\", "\\\\"); 1756 key = key.replace("%", "\\%"); 1757 key = key.replace("_", "\\_"); 1758 keywords[i] = key; 1759 } 1760 } 1761 } 1762 1763 String keywordColumn = null; 1764 switch (table) { 1765 case AUDIO_MEDIA: 1766 case AUDIO_GENRES_ALL_MEMBERS: 1767 case AUDIO_GENRES_ID_MEMBERS: 1768 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 1769 case AUDIO_PLAYLISTS_ID_MEMBERS: 1770 keywordColumn = MediaStore.Audio.Media.ARTIST_KEY + 1771 "||" + MediaStore.Audio.Media.ALBUM_KEY + 1772 "||" + MediaStore.Audio.Media.TITLE_KEY; 1773 break; 1774 case AUDIO_ARTISTS_ID_ALBUMS: 1775 case AUDIO_ALBUMS: 1776 keywordColumn = MediaStore.Audio.Media.ARTIST_KEY + "||" 1777 + MediaStore.Audio.Media.ALBUM_KEY; 1778 break; 1779 case AUDIO_ARTISTS: 1780 keywordColumn = MediaStore.Audio.Media.ARTIST_KEY; 1781 break; 1782 } 1783 1784 if (keywordColumn != null) { 1785 for (int i = 0; keywords != null && i < keywords.length; i++) { 1786 appendWhereStandalone(qb, keywordColumn + " LIKE ? ESCAPE '\\'", 1787 "%" + keywords[i] + "%"); 1788 } 1789 } 1790 1791 String groupBy = null; 1792 if (table == AUDIO_ARTISTS_ID_ALBUMS) { 1793 groupBy = "audio.album_id"; 1794 } 1795 1796 if (getCallingPackageTargetSdkVersion() < Build.VERSION_CODES.Q) { 1797 // Some apps are abusing the "WHERE" clause by injecting "GROUP BY" 1798 // clauses; gracefully lift them out. 1799 final Pair<String, String> selectionAndGroupBy = recoverAbusiveGroupBy( 1800 Pair.create(selection, groupBy)); 1801 selection = selectionAndGroupBy.first; 1802 groupBy = selectionAndGroupBy.second; 1803 1804 // Some apps are abusing the first column to inject "DISTINCT"; 1805 // gracefully lift them out. 1806 if (!ArrayUtils.isEmpty(projection) && projection[0].startsWith("DISTINCT ")) { 1807 projection[0] = projection[0].substring("DISTINCT ".length()); 1808 qb.setDistinct(true); 1809 } 1810 1811 // Some apps are generating thumbnails with getThumbnail(), but then 1812 // ignoring the returned Bitmap and querying the raw table; give 1813 // them a row with enough information to find the original image. 1814 if ((table == IMAGES_THUMBNAILS || table == VIDEO_THUMBNAILS) 1815 && !TextUtils.isEmpty(selection)) { 1816 final Matcher matcher = PATTERN_SELECTION_ID.matcher(selection); 1817 if (matcher.matches()) { 1818 final long id = Long.parseLong(matcher.group(1)); 1819 1820 final Uri fullUri; 1821 if (table == IMAGES_THUMBNAILS) { 1822 fullUri = ContentUris.withAppendedId( 1823 Images.Media.getContentUri(volumeName), id); 1824 } else if (table == VIDEO_THUMBNAILS) { 1825 fullUri = ContentUris.withAppendedId( 1826 Video.Media.getContentUri(volumeName), id); 1827 } else { 1828 throw new IllegalArgumentException(); 1829 } 1830 1831 final MatrixCursor cursor = new MatrixCursor(projection); 1832 try { 1833 String data = null; 1834 if (ContentResolver.DEPRECATE_DATA_COLUMNS) { 1835 // Go through provider to escape sandbox 1836 data = ContentResolver.translateDeprecatedDataPath( 1837 fullUri.buildUpon().appendPath("thumbnail").build()); 1838 } else { 1839 // Go directly to thumbnail file on disk 1840 data = ensureThumbnail(fullUri, signal).getAbsolutePath(); 1841 } 1842 cursor.newRow().add(MediaColumns._ID, null) 1843 .add(Images.Thumbnails.IMAGE_ID, id) 1844 .add(Video.Thumbnails.VIDEO_ID, id) 1845 .add(MediaColumns.DATA, data); 1846 } catch (FileNotFoundException ignored) { 1847 // Return empty cursor if we had thumbnail trouble 1848 } 1849 return cursor; 1850 } 1851 } 1852 } 1853 1854 final String having = null; 1855 final Cursor c = qb.query(db, projection, 1856 selection, selectionArgs, groupBy, having, sortOrder, limit, signal); 1857 1858 if (c != null) { 1859 ((AbstractCursor) c).setNotificationUris(getContext().getContentResolver(), 1860 Arrays.asList(uri), UserHandle.myUserId(), false); 1861 } 1862 1863 return c; 1864 } 1865 1866 @Override getType(Uri url)1867 public String getType(Uri url) { 1868 final int match = matchUri(url, true); 1869 switch (match) { 1870 case IMAGES_MEDIA_ID: 1871 case AUDIO_MEDIA_ID: 1872 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 1873 case VIDEO_MEDIA_ID: 1874 case DOWNLOADS_ID: 1875 case FILES_ID: 1876 final LocalCallingIdentity token = clearLocalCallingIdentity(); 1877 try (Cursor cursor = queryForSingleItem(url, 1878 new String[] { MediaColumns.MIME_TYPE }, null, null, null)) { 1879 return cursor.getString(0); 1880 } catch (FileNotFoundException e) { 1881 throw new IllegalArgumentException(e.getMessage()); 1882 } finally { 1883 restoreLocalCallingIdentity(token); 1884 } 1885 1886 case IMAGES_MEDIA: 1887 case IMAGES_THUMBNAILS: 1888 return Images.Media.CONTENT_TYPE; 1889 1890 case AUDIO_ALBUMART_ID: 1891 case AUDIO_ALBUMART_FILE_ID: 1892 case IMAGES_THUMBNAILS_ID: 1893 case VIDEO_THUMBNAILS_ID: 1894 return "image/jpeg"; 1895 1896 case AUDIO_MEDIA: 1897 case AUDIO_GENRES_ID_MEMBERS: 1898 case AUDIO_PLAYLISTS_ID_MEMBERS: 1899 return Audio.Media.CONTENT_TYPE; 1900 1901 case AUDIO_GENRES: 1902 case AUDIO_MEDIA_ID_GENRES: 1903 return Audio.Genres.CONTENT_TYPE; 1904 case AUDIO_GENRES_ID: 1905 case AUDIO_MEDIA_ID_GENRES_ID: 1906 return Audio.Genres.ENTRY_CONTENT_TYPE; 1907 case AUDIO_PLAYLISTS: 1908 case AUDIO_MEDIA_ID_PLAYLISTS: 1909 return Audio.Playlists.CONTENT_TYPE; 1910 case AUDIO_PLAYLISTS_ID: 1911 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 1912 return Audio.Playlists.ENTRY_CONTENT_TYPE; 1913 1914 case VIDEO_MEDIA: 1915 return Video.Media.CONTENT_TYPE; 1916 case DOWNLOADS: 1917 return Downloads.CONTENT_TYPE; 1918 } 1919 throw new IllegalStateException("Unknown URL : " + url); 1920 } 1921 1922 @VisibleForTesting ensureFileColumns(Uri uri, ContentValues values)1923 static void ensureFileColumns(Uri uri, ContentValues values) throws VolumeArgumentException { 1924 ensureNonUniqueFileColumns(matchUri(uri, true), uri, values, null /* currentPath */); 1925 } 1926 ensureUniqueFileColumns(int match, Uri uri, ContentValues values)1927 private static void ensureUniqueFileColumns(int match, Uri uri, ContentValues values) 1928 throws VolumeArgumentException { 1929 ensureFileColumns(match, uri, values, true, null /* currentPath */); 1930 } 1931 ensureNonUniqueFileColumns(int match, Uri uri, ContentValues values, @Nullable String currentPath)1932 private static void ensureNonUniqueFileColumns(int match, Uri uri, ContentValues values, 1933 @Nullable String currentPath) throws VolumeArgumentException { 1934 ensureFileColumns(match, uri, values, false, currentPath); 1935 } 1936 1937 /** 1938 * Get the various file-related {@link MediaColumns} in the given 1939 * {@link ContentValues} into sane condition. Also validates that defined 1940 * columns are valid for the given {@link Uri}, such as ensuring that only 1941 * {@code image/*} can be inserted into 1942 * {@link android.provider.MediaStore.Images}. 1943 */ ensureFileColumns(int match, Uri uri, ContentValues values, boolean makeUnique, @Nullable String currentPath)1944 private static void ensureFileColumns(int match, Uri uri, ContentValues values, 1945 boolean makeUnique, @Nullable String currentPath) throws VolumeArgumentException { 1946 Trace.traceBegin(TRACE_TAG_DATABASE, "ensureFileColumns"); 1947 1948 // Figure out defaults based on Uri being modified 1949 String defaultMimeType = ContentResolver.MIME_TYPE_DEFAULT; 1950 String defaultPrimary = Environment.DIRECTORY_DOWNLOADS; 1951 String defaultSecondary = null; 1952 List<String> allowedPrimary = Arrays.asList( 1953 Environment.DIRECTORY_DOWNLOADS, 1954 Environment.DIRECTORY_DOCUMENTS); 1955 switch (match) { 1956 case AUDIO_MEDIA: 1957 case AUDIO_MEDIA_ID: 1958 defaultMimeType = "audio/mpeg"; 1959 defaultPrimary = Environment.DIRECTORY_MUSIC; 1960 allowedPrimary = Arrays.asList( 1961 Environment.DIRECTORY_ALARMS, 1962 Environment.DIRECTORY_MUSIC, 1963 Environment.DIRECTORY_NOTIFICATIONS, 1964 Environment.DIRECTORY_PODCASTS, 1965 Environment.DIRECTORY_RINGTONES); 1966 break; 1967 case VIDEO_MEDIA: 1968 case VIDEO_MEDIA_ID: 1969 defaultMimeType = "video/mp4"; 1970 defaultPrimary = Environment.DIRECTORY_MOVIES; 1971 allowedPrimary = Arrays.asList( 1972 Environment.DIRECTORY_DCIM, 1973 Environment.DIRECTORY_MOVIES); 1974 break; 1975 case IMAGES_MEDIA: 1976 case IMAGES_MEDIA_ID: 1977 defaultMimeType = "image/jpeg"; 1978 defaultPrimary = Environment.DIRECTORY_PICTURES; 1979 allowedPrimary = Arrays.asList( 1980 Environment.DIRECTORY_DCIM, 1981 Environment.DIRECTORY_PICTURES); 1982 break; 1983 case AUDIO_ALBUMART: 1984 case AUDIO_ALBUMART_ID: 1985 defaultMimeType = "image/jpeg"; 1986 defaultPrimary = Environment.DIRECTORY_MUSIC; 1987 allowedPrimary = Arrays.asList(defaultPrimary); 1988 defaultSecondary = ".thumbnails"; 1989 break; 1990 case VIDEO_THUMBNAILS: 1991 case VIDEO_THUMBNAILS_ID: 1992 defaultMimeType = "image/jpeg"; 1993 defaultPrimary = Environment.DIRECTORY_MOVIES; 1994 allowedPrimary = Arrays.asList(defaultPrimary); 1995 defaultSecondary = ".thumbnails"; 1996 break; 1997 case IMAGES_THUMBNAILS: 1998 case IMAGES_THUMBNAILS_ID: 1999 defaultMimeType = "image/jpeg"; 2000 defaultPrimary = Environment.DIRECTORY_PICTURES; 2001 allowedPrimary = Arrays.asList(defaultPrimary); 2002 defaultSecondary = ".thumbnails"; 2003 break; 2004 case AUDIO_PLAYLISTS: 2005 case AUDIO_PLAYLISTS_ID: 2006 defaultPrimary = Environment.DIRECTORY_MUSIC; 2007 allowedPrimary = Arrays.asList(defaultPrimary); 2008 break; 2009 case DOWNLOADS: 2010 case DOWNLOADS_ID: 2011 defaultPrimary = Environment.DIRECTORY_DOWNLOADS; 2012 allowedPrimary = Arrays.asList(defaultPrimary); 2013 break; 2014 case FILES: 2015 case FILES_ID: 2016 // Use defaults above 2017 break; 2018 default: 2019 Log.w(TAG, "Unhandled location " + uri + "; assuming generic files"); 2020 break; 2021 } 2022 2023 final String resolvedVolumeName = resolveVolumeName(uri); 2024 2025 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA)) 2026 && MediaStore.VOLUME_INTERNAL.equals(resolvedVolumeName)) { 2027 // TODO: promote this to top-level check 2028 throw new UnsupportedOperationException( 2029 "Writing to internal storage is not supported."); 2030 } 2031 2032 // Force values when raw path provided 2033 if (!TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) { 2034 final String data = values.getAsString(MediaColumns.DATA); 2035 2036 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DISPLAY_NAME))) { 2037 values.put(MediaColumns.DISPLAY_NAME, extractDisplayName(data)); 2038 } 2039 if (TextUtils.isEmpty(values.getAsString(MediaColumns.MIME_TYPE))) { 2040 values.put(MediaColumns.MIME_TYPE, MediaFile.getMimeTypeForFile(data)); 2041 } 2042 } 2043 2044 // Give ourselves sane defaults when missing 2045 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DISPLAY_NAME))) { 2046 values.put(MediaColumns.DISPLAY_NAME, 2047 String.valueOf(System.currentTimeMillis())); 2048 } 2049 final Integer formatObject = values.getAsInteger(FileColumns.FORMAT); 2050 final int format = formatObject == null ? 0 : formatObject.intValue(); 2051 if (format == MtpConstants.FORMAT_ASSOCIATION) { 2052 values.putNull(MediaColumns.MIME_TYPE); 2053 } else if (TextUtils.isEmpty(values.getAsString(MediaColumns.MIME_TYPE))) { 2054 values.put(MediaColumns.MIME_TYPE, defaultMimeType); 2055 } 2056 2057 // Sanity check MIME type against table 2058 final String mimeType = values.getAsString(MediaColumns.MIME_TYPE); 2059 if (mimeType != null && !defaultMimeType.equals(ContentResolver.MIME_TYPE_DEFAULT)) { 2060 final String[] split = defaultMimeType.split("/"); 2061 if (!mimeType.startsWith(split[0])) { 2062 throw new IllegalArgumentException( 2063 "MIME type " + mimeType + " cannot be inserted into " + uri 2064 + "; expected MIME type under " + split[0] + "/*"); 2065 } 2066 } 2067 2068 // Generate path when undefined 2069 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) { 2070 // Combine together deprecated columns when path undefined 2071 if (TextUtils.isEmpty(values.getAsString(MediaColumns.RELATIVE_PATH))) { 2072 String primary = values.getAsString(MediaColumns.PRIMARY_DIRECTORY); 2073 String secondary = values.getAsString(MediaColumns.SECONDARY_DIRECTORY); 2074 2075 // Fall back to defaults when caller left undefined 2076 if (TextUtils.isEmpty(primary)) primary = defaultPrimary; 2077 if (TextUtils.isEmpty(secondary)) secondary = defaultSecondary; 2078 2079 if (primary != null) { 2080 if (secondary != null) { 2081 values.put(MediaColumns.RELATIVE_PATH, primary + '/' + secondary + '/'); 2082 } else { 2083 values.put(MediaColumns.RELATIVE_PATH, primary + '/'); 2084 } 2085 } 2086 } 2087 2088 final String[] relativePath = sanitizePath( 2089 values.getAsString(MediaColumns.RELATIVE_PATH)); 2090 final String displayName = sanitizeDisplayName( 2091 values.getAsString(MediaColumns.DISPLAY_NAME)); 2092 2093 // Create result file 2094 File res; 2095 try { 2096 res = getVolumePath(resolvedVolumeName); 2097 } catch (FileNotFoundException e) { 2098 throw new IllegalArgumentException(e); 2099 } 2100 res = Environment.buildPath(res, relativePath); 2101 try { 2102 if (makeUnique) { 2103 res = FileUtils.buildUniqueFile(res, mimeType, displayName); 2104 } else { 2105 res = FileUtils.buildNonUniqueFile(res, mimeType, displayName); 2106 } 2107 } catch (FileNotFoundException e) { 2108 throw new IllegalStateException( 2109 "Failed to build unique file: " + res + " " + displayName + " " + mimeType); 2110 } 2111 2112 // Check for shady looking paths 2113 2114 // Require content live under specific directories, but allow in-place updates of 2115 // existing content that lives in the invalid directory. 2116 final String primary = relativePath[0]; 2117 if (!res.getAbsolutePath().equals(currentPath) && !allowedPrimary.contains(primary)) { 2118 throw new IllegalArgumentException( 2119 "Primary directory " + primary + " not allowed for " + uri 2120 + "; allowed directories are " + allowedPrimary); 2121 } 2122 2123 // Ensure all parent folders of result file exist 2124 res.getParentFile().mkdirs(); 2125 if (!res.getParentFile().exists()) { 2126 throw new IllegalStateException("Failed to create directory: " + res); 2127 } 2128 values.put(MediaColumns.DATA, res.getAbsolutePath()); 2129 } else { 2130 assertFileColumnsSane(match, uri, values); 2131 } 2132 2133 // Drop columns that aren't relevant for special tables 2134 switch (match) { 2135 case AUDIO_ALBUMART: 2136 case VIDEO_THUMBNAILS: 2137 case IMAGES_THUMBNAILS: 2138 case AUDIO_PLAYLISTS: 2139 values.remove(MediaColumns.DISPLAY_NAME); 2140 values.remove(MediaColumns.MIME_TYPE); 2141 break; 2142 } 2143 2144 Trace.traceEnd(TRACE_TAG_DATABASE); 2145 } 2146 sanitizePath(@ullable String path)2147 private static @NonNull String[] sanitizePath(@Nullable String path) { 2148 if (path == null) { 2149 return EmptyArray.STRING; 2150 } else { 2151 final String[] segments = path.split("/"); 2152 for (int i = 0; i < segments.length; i++) { 2153 segments[i] = sanitizeDisplayName(segments[i]); 2154 } 2155 return segments; 2156 } 2157 } 2158 sanitizeDisplayName(@ullable String name)2159 private static @Nullable String sanitizeDisplayName(@Nullable String name) { 2160 if (name == null) { 2161 return null; 2162 } else if (name.startsWith(".")) { 2163 // The resulting file must not be hidden. 2164 return FileUtils.buildValidFatFilename("_" + name); 2165 } else { 2166 return FileUtils.buildValidFatFilename(name); 2167 } 2168 } 2169 2170 /** 2171 * Sanity check that any requested {@link MediaColumns#DATA} paths actually 2172 * live on the storage volume being targeted. 2173 */ assertFileColumnsSane(int match, Uri uri, ContentValues values)2174 private static void assertFileColumnsSane(int match, Uri uri, ContentValues values) 2175 throws VolumeArgumentException { 2176 if (!values.containsKey(MediaColumns.DATA)) return; 2177 try { 2178 // Sanity check that the requested path actually lives on volume 2179 final String volumeName = resolveVolumeName(uri); 2180 final Collection<File> allowed = getVolumeScanPaths(volumeName); 2181 final File actual = new File(values.getAsString(MediaColumns.DATA)) 2182 .getCanonicalFile(); 2183 if (!FileUtils.contains(allowed, actual)) { 2184 throw new VolumeArgumentException(actual, allowed); 2185 } 2186 } catch (IOException e) { 2187 throw new IllegalArgumentException(e); 2188 } 2189 } 2190 2191 @Override bulkInsert(Uri uri, ContentValues values[])2192 public int bulkInsert(Uri uri, ContentValues values[]) { 2193 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 2194 final boolean allowHidden = isCallingPackageAllowedHidden(); 2195 final int match = matchUri(uri, allowHidden); 2196 2197 if (match == VOLUMES) { 2198 return super.bulkInsert(uri, values); 2199 } 2200 2201 final DatabaseHelper helper; 2202 final SQLiteDatabase db; 2203 try { 2204 helper = getDatabaseForUri(uri); 2205 db = helper.getWritableDatabase(); 2206 } catch (VolumeNotFoundException e) { 2207 return e.translateForUpdateDelete(targetSdkVersion); 2208 } 2209 2210 if (match == MTP_OBJECT_REFERENCES) { 2211 int handle = Integer.parseInt(uri.getPathSegments().get(2)); 2212 return setObjectReferences(helper, db, handle, values); 2213 } 2214 2215 helper.beginTransaction(); 2216 try { 2217 final int result = super.bulkInsert(uri, values); 2218 helper.setTransactionSuccessful(); 2219 return result; 2220 } finally { 2221 helper.endTransaction(); 2222 } 2223 } 2224 playlistBulkInsert(SQLiteDatabase db, Uri uri, ContentValues values[])2225 private int playlistBulkInsert(SQLiteDatabase db, Uri uri, ContentValues values[]) { 2226 DatabaseUtils.InsertHelper helper = 2227 new DatabaseUtils.InsertHelper(db, "audio_playlists_map"); 2228 int audioidcolidx = helper.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID); 2229 int playlistididx = helper.getColumnIndex(Audio.Playlists.Members.PLAYLIST_ID); 2230 int playorderidx = helper.getColumnIndex(MediaStore.Audio.Playlists.Members.PLAY_ORDER); 2231 long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 2232 2233 db.beginTransaction(); 2234 int numInserted = 0; 2235 try { 2236 int len = values.length; 2237 for (int i = 0; i < len; i++) { 2238 helper.prepareForInsert(); 2239 // getting the raw Object and converting it long ourselves saves 2240 // an allocation (the alternative is ContentValues.getAsLong, which 2241 // returns a Long object) 2242 long audioid = ((Number) values[i].get( 2243 MediaStore.Audio.Playlists.Members.AUDIO_ID)).longValue(); 2244 helper.bind(audioidcolidx, audioid); 2245 helper.bind(playlistididx, playlistId); 2246 // convert to int ourselves to save an allocation. 2247 int playorder = ((Number) values[i].get( 2248 MediaStore.Audio.Playlists.Members.PLAY_ORDER)).intValue(); 2249 helper.bind(playorderidx, playorder); 2250 helper.execute(); 2251 } 2252 numInserted = len; 2253 db.setTransactionSuccessful(); 2254 } finally { 2255 db.endTransaction(); 2256 helper.close(); 2257 } 2258 getContext().getContentResolver().notifyChange(uri, null); 2259 return numInserted; 2260 } 2261 insertDirectory(DatabaseHelper helper, SQLiteDatabase db, String path)2262 private long insertDirectory(DatabaseHelper helper, SQLiteDatabase db, String path) { 2263 if (LOCAL_LOGV) Log.v(TAG, "inserting directory " + path); 2264 ContentValues values = new ContentValues(); 2265 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION); 2266 values.put(FileColumns.DATA, path); 2267 values.put(FileColumns.PARENT, getParent(helper, db, path)); 2268 values.put(FileColumns.OWNER_PACKAGE_NAME, extractPathOwnerPackageName(path)); 2269 values.put(FileColumns.VOLUME_NAME, extractVolumeName(path)); 2270 values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path)); 2271 values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path)); 2272 values.put(FileColumns.IS_DOWNLOAD, isDownload(path)); 2273 File file = new File(path); 2274 if (file.exists()) { 2275 values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000); 2276 } 2277 long rowId = db.insert("files", FileColumns.DATE_MODIFIED, values); 2278 return rowId; 2279 } 2280 extractVolumeName(@ullable String data)2281 private static @Nullable String extractVolumeName(@Nullable String data) { 2282 if (data == null) return null; 2283 final Matcher matcher = PATTERN_VOLUME_NAME.matcher(data); 2284 if (matcher.find()) { 2285 final String volumeName = matcher.group(1); 2286 if (volumeName.equals("emulated")) { 2287 return MediaStore.VOLUME_EXTERNAL_PRIMARY; 2288 } else { 2289 return StorageVolume.normalizeUuid(volumeName); 2290 } 2291 } else { 2292 return MediaStore.VOLUME_INTERNAL; 2293 } 2294 } 2295 extractRelativePath(@ullable String data)2296 private static @Nullable String extractRelativePath(@Nullable String data) { 2297 if (data == null) return null; 2298 final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(data); 2299 if (matcher.find()) { 2300 final int lastSlash = data.lastIndexOf('/'); 2301 if (lastSlash == -1 || lastSlash < matcher.end()) { 2302 // This is a file in the top-level directory, so relative path is "/" 2303 // which is different than null, which means unknown path 2304 return "/"; 2305 } else { 2306 return data.substring(matcher.end(), lastSlash + 1); 2307 } 2308 } else { 2309 return null; 2310 } 2311 } 2312 extractDisplayName(@ullable String data)2313 private static @Nullable String extractDisplayName(@Nullable String data) { 2314 if (data == null) return null; 2315 if (data.endsWith("/")) { 2316 data = data.substring(0, data.length() - 1); 2317 } 2318 return data.substring(data.lastIndexOf('/') + 1); 2319 } 2320 getParent(DatabaseHelper helper, SQLiteDatabase db, String path)2321 private long getParent(DatabaseHelper helper, SQLiteDatabase db, String path) { 2322 final String parentPath = new File(path).getParent(); 2323 if (Objects.equals("/", parentPath)) { 2324 return -1; 2325 } else { 2326 synchronized (mDirectoryCache) { 2327 Long id = mDirectoryCache.get(parentPath); 2328 if (id != null) { 2329 return id; 2330 } 2331 } 2332 2333 final long id; 2334 try (Cursor c = db.query("files", new String[] { FileColumns._ID }, 2335 FileColumns.DATA + "=?", new String[] { parentPath }, null, null, null)) { 2336 if (c.moveToFirst()) { 2337 id = c.getLong(0); 2338 } else { 2339 id = insertDirectory(helper, db, parentPath); 2340 } 2341 } 2342 2343 synchronized (mDirectoryCache) { 2344 mDirectoryCache.put(parentPath, id); 2345 } 2346 return id; 2347 } 2348 } 2349 2350 /** 2351 * @param c the Cursor whose title to retrieve 2352 * @return the result of {@link #getDefaultTitle(String)} if the result is valid; otherwise 2353 * the value of the {@code MediaStore.Audio.Media.TITLE} column 2354 */ getDefaultTitleFromCursor(Cursor c)2355 private String getDefaultTitleFromCursor(Cursor c) { 2356 String title = null; 2357 final int columnIndex = c.getColumnIndex("title_resource_uri"); 2358 // Necessary to check for existence because we may be reading from an old DB version 2359 if (columnIndex > -1) { 2360 final String titleResourceUri = c.getString(columnIndex); 2361 if (titleResourceUri != null) { 2362 try { 2363 title = getDefaultTitle(titleResourceUri); 2364 } catch (Exception e) { 2365 // Best attempt only 2366 } 2367 } 2368 } 2369 if (title == null) { 2370 title = c.getString(c.getColumnIndex(MediaStore.Audio.Media.TITLE)); 2371 } 2372 return title; 2373 } 2374 2375 /** 2376 * @param title_resource_uri The title resource for which to retrieve the default localization 2377 * @return The title localized to {@code Locale.US}, or {@code null} if unlocalizable 2378 * @throws Exception Thrown if the title appears to be localizable, but the localization failed 2379 * for any reason. For example, the application from which the localized title is fetched is not 2380 * installed, or it does not have the resource which needs to be localized 2381 */ getDefaultTitle(String title_resource_uri)2382 private String getDefaultTitle(String title_resource_uri) throws Exception{ 2383 try { 2384 return getTitleFromResourceUri(title_resource_uri, false); 2385 } catch (Exception e) { 2386 Log.e(TAG, "Error getting default title for " + title_resource_uri, e); 2387 throw e; 2388 } 2389 } 2390 2391 /** 2392 * @param title_resource_uri The title resource to localize 2393 * @return The localized title, or {@code null} if unlocalizable 2394 * @throws Exception Thrown if the title appears to be localizable, but the localization failed 2395 * for any reason. For example, the application from which the localized title is fetched is not 2396 * installed, or it does not have the resource which needs to be localized 2397 */ getLocalizedTitle(String title_resource_uri)2398 private String getLocalizedTitle(String title_resource_uri) throws Exception { 2399 try { 2400 return getTitleFromResourceUri(title_resource_uri, true); 2401 } catch (Exception e) { 2402 Log.e(TAG, "Error getting localized title for " + title_resource_uri, e); 2403 throw e; 2404 } 2405 } 2406 2407 /** 2408 * Localizable titles conform to this URI pattern: 2409 * Scheme: {@link ContentResolver.SCHEME_ANDROID_RESOURCE} 2410 * Authority: Package Name of ringtone title provider 2411 * First Path Segment: Type of resource (must be "string") 2412 * Second Path Segment: Resource name of title 2413 * 2414 * @param title_resource_uri The title resource to retrieve 2415 * @param localize Whether or not to localize the title 2416 * @return The title, or {@code null} if unlocalizable 2417 * @throws Exception Thrown if the title appears to be localizable, but the localization failed 2418 * for any reason. For example, the application from which the localized title is fetched is not 2419 * installed, or it does not have the resource which needs to be localized 2420 */ getTitleFromResourceUri(String title_resource_uri, boolean localize)2421 private String getTitleFromResourceUri(String title_resource_uri, boolean localize) 2422 throws Exception { 2423 if (TextUtils.isEmpty(title_resource_uri)) { 2424 return null; 2425 } 2426 final Uri titleUri = Uri.parse(title_resource_uri); 2427 final String scheme = titleUri.getScheme(); 2428 if (!ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) { 2429 return null; 2430 } 2431 final List<String> pathSegments = titleUri.getPathSegments(); 2432 if (pathSegments.size() != 2) { 2433 Log.e(TAG, "Error getting localized title for " + title_resource_uri 2434 + ", must have 2 path segments"); 2435 return null; 2436 } 2437 final String type = pathSegments.get(0); 2438 if (!"string".equals(type)) { 2439 Log.e(TAG, "Error getting localized title for " + title_resource_uri 2440 + ", first path segment must be \"string\""); 2441 return null; 2442 } 2443 final String packageName = titleUri.getAuthority(); 2444 final Resources resources; 2445 if (localize) { 2446 resources = mPackageManager.getResourcesForApplication(packageName); 2447 } else { 2448 final Context packageContext = getContext().createPackageContext(packageName, 0); 2449 final Configuration configuration = packageContext.getResources().getConfiguration(); 2450 configuration.setLocale(Locale.US); 2451 resources = packageContext.createConfigurationContext(configuration).getResources(); 2452 } 2453 final String resourceIdentifier = pathSegments.get(1); 2454 final int id = resources.getIdentifier(resourceIdentifier, type, packageName); 2455 return resources.getString(id); 2456 } 2457 onLocaleChanged()2458 public void onLocaleChanged() { 2459 localizeTitles(); 2460 } 2461 localizeTitles()2462 private void localizeTitles() { 2463 final DatabaseHelper helper = mInternalDatabase; 2464 final SQLiteDatabase db = helper.getWritableDatabase(); 2465 2466 try (Cursor c = db.query("files", new String[]{"_id", "title_resource_uri"}, 2467 "title_resource_uri IS NOT NULL", null, null, null, null)) { 2468 while (c.moveToNext()) { 2469 final String id = c.getString(0); 2470 final String titleResourceUri = c.getString(1); 2471 final ContentValues values = new ContentValues(); 2472 try { 2473 final String localizedTitle = getLocalizedTitle(titleResourceUri); 2474 values.put("title_key", MediaStore.Audio.keyFor(localizedTitle)); 2475 // do a final trim of the title, in case it started with the special 2476 // "sort first" character (ascii \001) 2477 values.put("title", localizedTitle.trim()); 2478 db.update("files", values, "_id=?", new String[]{id}); 2479 } catch (Exception e) { 2480 Log.e(TAG, "Error updating localized title for " + titleResourceUri 2481 + ", keeping old localization"); 2482 } 2483 } 2484 } 2485 } 2486 insertFile(DatabaseHelper helper, int match, Uri uri, ContentValues values, int mediaType, boolean notify)2487 private long insertFile(DatabaseHelper helper, int match, Uri uri, ContentValues values, 2488 int mediaType, boolean notify) { 2489 final SQLiteDatabase db = helper.getWritableDatabase(); 2490 2491 boolean wasPathEmpty = !values.containsKey(MediaStore.MediaColumns.DATA) 2492 || TextUtils.isEmpty(values.getAsString(MediaStore.MediaColumns.DATA)); 2493 2494 // Make sure all file-related columns are defined 2495 try { 2496 ensureUniqueFileColumns(match, uri, values); 2497 } catch (VolumeArgumentException e) { 2498 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.Q) { 2499 throw new IllegalArgumentException(e.getMessage()); 2500 } else { 2501 Log.w(TAG, e.getMessage()); 2502 return 0; 2503 } 2504 } 2505 2506 switch (mediaType) { 2507 case FileColumns.MEDIA_TYPE_IMAGE: { 2508 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 2509 break; 2510 } 2511 2512 case FileColumns.MEDIA_TYPE_AUDIO: { 2513 // SQLite Views are read-only, so we need to deconstruct this 2514 // insert and do inserts into the underlying tables. 2515 // If doing this here turns out to be a performance bottleneck, 2516 // consider moving this to native code and using triggers on 2517 // the view. 2518 String albumartist = values.getAsString(MediaStore.Audio.Media.ALBUM_ARTIST); 2519 String compilation = values.getAsString(MediaStore.Audio.Media.COMPILATION); 2520 values.remove(MediaStore.Audio.Media.COMPILATION); 2521 2522 // Insert the artist into the artist table and remove it from 2523 // the input values 2524 Object so = values.get("artist"); 2525 String s = (so == null ? "" : so.toString()); 2526 values.remove("artist"); 2527 long artistRowId; 2528 ArrayMap<String, Long> artistCache = helper.mArtistCache; 2529 String path = values.getAsString(MediaStore.MediaColumns.DATA); 2530 synchronized(artistCache) { 2531 Long temp = artistCache.get(s); 2532 if (temp == null) { 2533 artistRowId = getKeyIdForName(helper, db, 2534 "artists", "artist_key", "artist", 2535 s, s, path, 0, null, artistCache, uri); 2536 } else { 2537 artistRowId = temp.longValue(); 2538 } 2539 } 2540 String artist = s; 2541 2542 // Do the same for the album field 2543 so = values.get("album"); 2544 s = (so == null ? "" : so.toString()); 2545 values.remove("album"); 2546 long albumRowId; 2547 ArrayMap<String, Long> albumCache = helper.mAlbumCache; 2548 synchronized(albumCache) { 2549 int albumhash = 0; 2550 if (albumartist != null) { 2551 albumhash = albumartist.hashCode(); 2552 } else if (compilation != null && compilation.equals("1")) { 2553 // nothing to do, hash already set 2554 } else { 2555 albumhash = path.substring(0, path.lastIndexOf('/')).hashCode(); 2556 } 2557 String cacheName = s + albumhash; 2558 Long temp = albumCache.get(cacheName); 2559 if (temp == null) { 2560 albumRowId = getKeyIdForName(helper, db, 2561 "albums", "album_key", "album", 2562 s, cacheName, path, albumhash, artist, albumCache, uri); 2563 } else { 2564 albumRowId = temp; 2565 } 2566 } 2567 2568 values.put("artist_id", Integer.toString((int)artistRowId)); 2569 values.put("album_id", Integer.toString((int)albumRowId)); 2570 so = values.getAsString("title"); 2571 s = (so == null ? "" : so.toString()); 2572 2573 try { 2574 final String localizedTitle = getLocalizedTitle(s); 2575 if (localizedTitle != null) { 2576 values.put("title_resource_uri", s); 2577 s = localizedTitle; 2578 } else { 2579 values.putNull("title_resource_uri"); 2580 } 2581 } catch (Exception e) { 2582 values.put("title_resource_uri", s); 2583 } 2584 values.put("title_key", MediaStore.Audio.keyFor(s)); 2585 // do a final trim of the title, in case it started with the special 2586 // "sort first" character (ascii \001) 2587 values.put("title", s.trim()); 2588 break; 2589 } 2590 2591 case FileColumns.MEDIA_TYPE_VIDEO: { 2592 break; 2593 } 2594 } 2595 2596 // compute bucket_id and bucket_display_name for all files 2597 String path = values.getAsString(MediaStore.MediaColumns.DATA); 2598 computeDataValues(values); 2599 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 2600 2601 long rowId = 0; 2602 Integer i = values.getAsInteger( 2603 MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID); 2604 if (i != null) { 2605 rowId = i.intValue(); 2606 values = new ContentValues(values); 2607 values.remove(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID); 2608 } 2609 2610 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 2611 if (title == null && path != null) { 2612 title = MediaFile.getFileTitle(path); 2613 } 2614 values.put(FileColumns.TITLE, title); 2615 2616 String mimeType = null; 2617 int format = MtpConstants.FORMAT_ASSOCIATION; 2618 if (path != null && new File(path).isDirectory()) { 2619 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION); 2620 values.putNull(MediaStore.MediaColumns.MIME_TYPE); 2621 } else { 2622 mimeType = values.getAsString(MediaStore.MediaColumns.MIME_TYPE); 2623 final Integer formatObject = values.getAsInteger(FileColumns.FORMAT); 2624 format = (formatObject == null ? 0 : formatObject.intValue()); 2625 } 2626 2627 if (format == 0) { 2628 if (TextUtils.isEmpty(path) || wasPathEmpty) { 2629 // special case device created playlists 2630 if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 2631 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST); 2632 // create a file path for the benefit of MTP 2633 path = Environment.getExternalStorageDirectory() 2634 + "/Playlists/" + values.getAsString(Audio.Playlists.NAME); 2635 values.put(MediaStore.MediaColumns.DATA, path); 2636 values.put(FileColumns.PARENT, 0); 2637 } 2638 } else { 2639 format = MediaFile.getFormatCode(path, mimeType); 2640 } 2641 } 2642 if (path != null && path.endsWith("/")) { 2643 Log.e(TAG, "directory has trailing slash: " + path); 2644 return 0; 2645 } 2646 if (format != 0) { 2647 values.put(FileColumns.FORMAT, format); 2648 if (mimeType == null && format != MtpConstants.FORMAT_ASSOCIATION) { 2649 mimeType = MediaFile.getMimeTypeForFormatCode(format); 2650 } 2651 } 2652 2653 if (mimeType == null && path != null && format != MtpConstants.FORMAT_ASSOCIATION) { 2654 mimeType = MediaFile.getMimeTypeForFile(path); 2655 } 2656 2657 if (mimeType != null) { 2658 values.put(FileColumns.MIME_TYPE, mimeType); 2659 2660 // If 'values' contained the media type, then the caller wants us 2661 // to use that exact type, so don't override it based on mimetype 2662 if (!values.containsKey(FileColumns.MEDIA_TYPE) && 2663 mediaType == FileColumns.MEDIA_TYPE_NONE && 2664 !android.media.MediaScanner.isNoMediaPath(path)) { 2665 if (MediaFile.isAudioMimeType(mimeType)) { 2666 mediaType = FileColumns.MEDIA_TYPE_AUDIO; 2667 } else if (MediaFile.isVideoMimeType(mimeType)) { 2668 mediaType = FileColumns.MEDIA_TYPE_VIDEO; 2669 } else if (MediaFile.isImageMimeType(mimeType)) { 2670 mediaType = FileColumns.MEDIA_TYPE_IMAGE; 2671 } else if (MediaFile.isPlayListMimeType(mimeType)) { 2672 mediaType = FileColumns.MEDIA_TYPE_PLAYLIST; 2673 } 2674 } 2675 } 2676 values.put(FileColumns.MEDIA_TYPE, mediaType); 2677 2678 if (rowId == 0) { 2679 if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 2680 String name = values.getAsString(Audio.Playlists.NAME); 2681 if (name == null && path == null) { 2682 // MediaScanner will compute the name from the path if we have one 2683 throw new IllegalArgumentException( 2684 "no name was provided when inserting abstract playlist"); 2685 } 2686 } else { 2687 if (path == null) { 2688 // path might be null for playlists created on the device 2689 // or transfered via MTP 2690 throw new IllegalArgumentException( 2691 "no path was provided when inserting new file"); 2692 } 2693 } 2694 2695 // make sure modification date and size are set 2696 if (path != null) { 2697 File file = new File(path); 2698 if (file.exists()) { 2699 values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000); 2700 if (!values.containsKey(FileColumns.SIZE)) { 2701 values.put(FileColumns.SIZE, file.length()); 2702 } 2703 } 2704 } 2705 2706 Long parent = values.getAsLong(FileColumns.PARENT); 2707 if (parent == null) { 2708 if (path != null) { 2709 long parentId = getParent(helper, db, path); 2710 values.put(FileColumns.PARENT, parentId); 2711 } 2712 } 2713 2714 rowId = db.insert("files", FileColumns.DATE_MODIFIED, values); 2715 } else { 2716 db.update("files", values, FileColumns._ID + "=?", 2717 new String[] { Long.toString(rowId) }); 2718 } 2719 if (format == MtpConstants.FORMAT_ASSOCIATION) { 2720 synchronized (mDirectoryCache) { 2721 mDirectoryCache.put(path, rowId); 2722 } 2723 } 2724 2725 return rowId; 2726 } 2727 getObjectReferences(DatabaseHelper helper, SQLiteDatabase db, int handle)2728 private Cursor getObjectReferences(DatabaseHelper helper, SQLiteDatabase db, int handle) { 2729 Cursor c = db.query("files", sMediaTableColumns, "_id=?", 2730 new String[] { Integer.toString(handle) }, 2731 null, null, null); 2732 try { 2733 if (c != null && c.moveToNext()) { 2734 long playlistId = c.getLong(0); 2735 int mediaType = c.getInt(1); 2736 if (mediaType != FileColumns.MEDIA_TYPE_PLAYLIST) { 2737 // we only support object references for playlist objects 2738 return null; 2739 } 2740 return db.rawQuery(OBJECT_REFERENCES_QUERY, 2741 new String[] { Long.toString(playlistId) } ); 2742 } 2743 } finally { 2744 IoUtils.closeQuietly(c); 2745 } 2746 return null; 2747 } 2748 setObjectReferences(DatabaseHelper helper, SQLiteDatabase db, int handle, ContentValues values[])2749 private int setObjectReferences(DatabaseHelper helper, SQLiteDatabase db, 2750 int handle, ContentValues values[]) { 2751 // first look up the media table and media ID for the object 2752 long playlistId = 0; 2753 Cursor c = db.query("files", sMediaTableColumns, "_id=?", 2754 new String[] { Integer.toString(handle) }, 2755 null, null, null); 2756 try { 2757 if (c != null && c.moveToNext()) { 2758 int mediaType = c.getInt(1); 2759 if (mediaType != FileColumns.MEDIA_TYPE_PLAYLIST) { 2760 // we only support object references for playlist objects 2761 return 0; 2762 } 2763 playlistId = c.getLong(0); 2764 } 2765 } finally { 2766 IoUtils.closeQuietly(c); 2767 } 2768 if (playlistId == 0) { 2769 return 0; 2770 } 2771 2772 // next delete any existing entries 2773 db.delete("audio_playlists_map", "playlist_id=?", 2774 new String[] { Long.toString(playlistId) }); 2775 2776 // finally add the new entries 2777 int count = values.length; 2778 int added = 0; 2779 ContentValues[] valuesList = new ContentValues[count]; 2780 for (int i = 0; i < count; i++) { 2781 // convert object ID to audio ID 2782 long audioId = 0; 2783 long objectId = values[i].getAsLong(MediaStore.MediaColumns._ID); 2784 c = db.query("files", sMediaTableColumns, "_id=?", 2785 new String[] { Long.toString(objectId) }, 2786 null, null, null); 2787 try { 2788 if (c != null && c.moveToNext()) { 2789 int mediaType = c.getInt(1); 2790 if (mediaType != FileColumns.MEDIA_TYPE_AUDIO) { 2791 // we only allow audio files in playlists, so skip 2792 continue; 2793 } 2794 audioId = c.getLong(0); 2795 } 2796 } finally { 2797 IoUtils.closeQuietly(c); 2798 } 2799 if (audioId != 0) { 2800 ContentValues v = new ContentValues(); 2801 v.put(MediaStore.Audio.Playlists.Members.PLAYLIST_ID, playlistId); 2802 v.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audioId); 2803 v.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, added); 2804 valuesList[added++] = v; 2805 } 2806 } 2807 if (added < count) { 2808 // we weren't able to find everything on the list, so lets resize the array 2809 // and pass what we have. 2810 ContentValues[] newValues = new ContentValues[added]; 2811 System.arraycopy(valuesList, 0, newValues, 0, added); 2812 valuesList = newValues; 2813 } 2814 2815 int rowsChanged = playlistBulkInsert(db, 2816 Audio.Playlists.Members.getContentUri(MediaStore.VOLUME_EXTERNAL, playlistId), 2817 valuesList); 2818 2819 if (rowsChanged > 0) { 2820 updatePlaylistDateModifiedToNow(db, playlistId); 2821 } 2822 2823 return rowsChanged; 2824 } 2825 2826 private static final String[] GENRE_LOOKUP_PROJECTION = new String[] { 2827 Audio.Genres._ID, // 0 2828 Audio.Genres.NAME, // 1 2829 }; 2830 updateGenre(long rowId, String genre, String volumeName)2831 private void updateGenre(long rowId, String genre, String volumeName) { 2832 Uri uri = null; 2833 Cursor cursor = null; 2834 Uri genresUri = MediaStore.Audio.Genres.getContentUri(volumeName); 2835 try { 2836 // see if the genre already exists 2837 cursor = query(genresUri, GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?", 2838 new String[] { genre }, null); 2839 if (cursor == null || cursor.getCount() == 0) { 2840 // genre does not exist, so create the genre in the genre table 2841 ContentValues values = new ContentValues(); 2842 values.put(MediaStore.Audio.Genres.NAME, genre); 2843 uri = insert(genresUri, values); 2844 } else { 2845 // genre already exists, so compute its Uri 2846 cursor.moveToNext(); 2847 uri = ContentUris.withAppendedId(genresUri, cursor.getLong(0)); 2848 } 2849 if (uri != null) { 2850 uri = Uri.withAppendedPath(uri, MediaStore.Audio.Genres.Members.CONTENT_DIRECTORY); 2851 } 2852 } finally { 2853 IoUtils.closeQuietly(cursor); 2854 } 2855 2856 if (uri != null) { 2857 // add entry to audio_genre_map 2858 ContentValues values = new ContentValues(); 2859 values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId)); 2860 insert(uri, values); 2861 } 2862 } 2863 2864 @VisibleForTesting extractPathOwnerPackageName(@ullable String path)2865 static @Nullable String extractPathOwnerPackageName(@Nullable String path) { 2866 if (path == null) return null; 2867 final Matcher m = PATTERN_OWNED_PATH.matcher(path); 2868 if (m.matches()) { 2869 return m.group(1); 2870 } else { 2871 return null; 2872 } 2873 } 2874 maybePut(@onNull ContentValues values, @NonNull String key, @Nullable String value)2875 private void maybePut(@NonNull ContentValues values, @NonNull String key, 2876 @Nullable String value) { 2877 if (value != null) { 2878 values.put(key, value); 2879 } 2880 } 2881 maybeMarkAsDownload(@onNull ContentValues values)2882 private boolean maybeMarkAsDownload(@NonNull ContentValues values) { 2883 final String path = values.getAsString(MediaColumns.DATA); 2884 if (path != null && isDownload(path)) { 2885 values.put(FileColumns.IS_DOWNLOAD, true); 2886 return true; 2887 } 2888 return false; 2889 } 2890 resolveVolumeName(@onNull Uri uri)2891 private static @NonNull String resolveVolumeName(@NonNull Uri uri) { 2892 final String volumeName = getVolumeName(uri); 2893 if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) { 2894 return MediaStore.VOLUME_EXTERNAL_PRIMARY; 2895 } else { 2896 return volumeName; 2897 } 2898 } 2899 2900 @Override insert(Uri uri, ContentValues initialValues)2901 public Uri insert(Uri uri, ContentValues initialValues) { 2902 Trace.traceBegin(TRACE_TAG_DATABASE, "insert"); 2903 try { 2904 return insertInternal(uri, initialValues); 2905 } finally { 2906 Trace.traceEnd(TRACE_TAG_DATABASE); 2907 } 2908 } 2909 insertInternal(Uri uri, ContentValues initialValues)2910 private Uri insertInternal(Uri uri, ContentValues initialValues) { 2911 final boolean allowHidden = isCallingPackageAllowedHidden(); 2912 final int match = matchUri(uri, allowHidden); 2913 2914 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 2915 final String originalVolumeName = getVolumeName(uri); 2916 final String resolvedVolumeName = resolveVolumeName(uri); 2917 2918 // handle MEDIA_SCANNER before calling getDatabaseForUri() 2919 if (match == MEDIA_SCANNER) { 2920 mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME); 2921 2922 final DatabaseHelper helper; 2923 try { 2924 helper = getDatabaseForUri(MediaStore.Files.getContentUri(mMediaScannerVolume)); 2925 } catch (VolumeNotFoundException e) { 2926 return e.translateForInsert(targetSdkVersion); 2927 } 2928 2929 helper.mScanStartTime = SystemClock.currentTimeMicro(); 2930 return MediaStore.getMediaScannerUri(); 2931 } 2932 2933 if (match == VOLUMES) { 2934 String name = initialValues.getAsString("name"); 2935 Uri attachedVolume = attachVolume(name); 2936 if (mMediaScannerVolume != null && mMediaScannerVolume.equals(name)) { 2937 final DatabaseHelper helper; 2938 try { 2939 helper = getDatabaseForUri( 2940 MediaStore.Files.getContentUri(mMediaScannerVolume)); 2941 } catch (VolumeNotFoundException e) { 2942 return e.translateForInsert(targetSdkVersion); 2943 } 2944 helper.mScanStartTime = SystemClock.currentTimeMicro(); 2945 } 2946 return attachedVolume; 2947 } 2948 2949 String genre = null; 2950 String path = null; 2951 String ownerPackageName = null; 2952 if (initialValues != null) { 2953 // Ignore or augment incoming raw filesystem paths 2954 for (String column : sDataColumns.keySet()) { 2955 if (!initialValues.containsKey(column)) continue; 2956 2957 if (isCallingPackageSystem() || isCallingPackageLegacy()) { 2958 // Mutation allowed 2959 } else { 2960 Log.w(TAG, "Ignoring mutation of " + column + " from " 2961 + getCallingPackageOrSelf()); 2962 initialValues.remove(column); 2963 } 2964 } 2965 2966 genre = initialValues.getAsString(Audio.AudioColumns.GENRE); 2967 initialValues.remove(Audio.AudioColumns.GENRE); 2968 path = initialValues.getAsString(MediaStore.MediaColumns.DATA); 2969 2970 if (!isCallingPackageSystem()) { 2971 initialValues.remove(FileColumns.IS_DOWNLOAD); 2972 } 2973 2974 // We no longer track location metadata 2975 if (initialValues.containsKey(ImageColumns.LATITUDE)) { 2976 initialValues.putNull(ImageColumns.LATITUDE); 2977 } 2978 if (initialValues.containsKey(ImageColumns.LONGITUDE)) { 2979 initialValues.putNull(ImageColumns.LONGITUDE); 2980 } 2981 2982 if (isCallingPackageSystem()) { 2983 // When media inserted by ourselves, the best we can do is guess 2984 // ownership based on path. 2985 ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME); 2986 if (TextUtils.isEmpty(ownerPackageName)) { 2987 ownerPackageName = extractPathOwnerPackageName(path); 2988 } 2989 } else { 2990 // Remote callers have no direct control over owner column; we force 2991 // it be whoever is creating the content. 2992 initialValues.remove(FileColumns.OWNER_PACKAGE_NAME); 2993 ownerPackageName = getCallingPackageOrSelf(); 2994 } 2995 } 2996 2997 long rowId = -1; 2998 Uri newUri = null; 2999 3000 final DatabaseHelper helper; 3001 final SQLiteDatabase db; 3002 try { 3003 helper = getDatabaseForUri(uri); 3004 db = helper.getWritableDatabase(); 3005 } catch (VolumeNotFoundException e) { 3006 return e.translateForInsert(targetSdkVersion); 3007 } 3008 3009 switch (match) { 3010 case IMAGES_MEDIA: { 3011 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 3012 final boolean isDownload = maybeMarkAsDownload(initialValues); 3013 rowId = insertFile(helper, match, uri, initialValues, 3014 FileColumns.MEDIA_TYPE_IMAGE, true); 3015 if (rowId > 0) { 3016 MediaDocumentsProvider.onMediaStoreInsert( 3017 getContext(), resolvedVolumeName, FileColumns.MEDIA_TYPE_IMAGE, rowId); 3018 newUri = ContentUris.withAppendedId( 3019 Images.Media.getContentUri(originalVolumeName), rowId); 3020 } 3021 break; 3022 } 3023 3024 case IMAGES_THUMBNAILS: { 3025 if (helper.mInternal) { 3026 throw new UnsupportedOperationException( 3027 "Writing to internal storage is not supported."); 3028 } 3029 3030 // Require that caller has write access to underlying media 3031 final long imageId = initialValues.getAsLong(MediaStore.Images.Thumbnails.IMAGE_ID); 3032 enforceCallingPermission(ContentUris.withAppendedId( 3033 MediaStore.Images.Media.getContentUri(resolvedVolumeName), imageId), true); 3034 3035 try { 3036 ensureUniqueFileColumns(match, uri, initialValues); 3037 } catch (VolumeArgumentException e) { 3038 return e.translateForInsert(targetSdkVersion); 3039 } 3040 3041 rowId = db.insert("thumbnails", "name", initialValues); 3042 if (rowId > 0) { 3043 newUri = ContentUris.withAppendedId(Images.Thumbnails. 3044 getContentUri(originalVolumeName), rowId); 3045 } 3046 break; 3047 } 3048 3049 case VIDEO_THUMBNAILS: { 3050 if (helper.mInternal) { 3051 throw new UnsupportedOperationException( 3052 "Writing to internal storage is not supported."); 3053 } 3054 3055 // Require that caller has write access to underlying media 3056 final long videoId = initialValues.getAsLong(MediaStore.Video.Thumbnails.VIDEO_ID); 3057 enforceCallingPermission(ContentUris.withAppendedId( 3058 MediaStore.Video.Media.getContentUri(resolvedVolumeName), videoId), true); 3059 3060 try { 3061 ensureUniqueFileColumns(match, uri, initialValues); 3062 } catch (VolumeArgumentException e) { 3063 return e.translateForInsert(targetSdkVersion); 3064 } 3065 3066 rowId = db.insert("videothumbnails", "name", initialValues); 3067 if (rowId > 0) { 3068 newUri = ContentUris.withAppendedId(Video.Thumbnails. 3069 getContentUri(originalVolumeName), rowId); 3070 } 3071 break; 3072 } 3073 3074 case AUDIO_MEDIA: { 3075 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 3076 final boolean isDownload = maybeMarkAsDownload(initialValues); 3077 rowId = insertFile(helper, match, uri, initialValues, 3078 FileColumns.MEDIA_TYPE_AUDIO, true); 3079 if (rowId > 0) { 3080 MediaDocumentsProvider.onMediaStoreInsert( 3081 getContext(), resolvedVolumeName, FileColumns.MEDIA_TYPE_AUDIO, rowId); 3082 newUri = ContentUris.withAppendedId( 3083 Audio.Media.getContentUri(originalVolumeName), rowId); 3084 if (genre != null) { 3085 updateGenre(rowId, genre, resolvedVolumeName); 3086 } 3087 } 3088 break; 3089 } 3090 3091 case AUDIO_MEDIA_ID_GENRES: { 3092 // Require that caller has write access to underlying media 3093 final long audioId = Long.parseLong(uri.getPathSegments().get(2)); 3094 enforceCallingPermission(ContentUris.withAppendedId( 3095 MediaStore.Audio.Media.getContentUri(resolvedVolumeName), audioId), true); 3096 3097 ContentValues values = new ContentValues(initialValues); 3098 values.put(Audio.Genres.Members.AUDIO_ID, audioId); 3099 rowId = db.insert("audio_genres_map", "genre_id", values); 3100 if (rowId > 0) { 3101 newUri = ContentUris.withAppendedId(uri, rowId); 3102 } 3103 break; 3104 } 3105 3106 case AUDIO_MEDIA_ID_PLAYLISTS: { 3107 // Require that caller has write access to underlying media 3108 final long audioId = Long.parseLong(uri.getPathSegments().get(2)); 3109 enforceCallingPermission(ContentUris.withAppendedId( 3110 MediaStore.Audio.Media.getContentUri(resolvedVolumeName), audioId), true); 3111 final long playlistId = initialValues 3112 .getAsLong(MediaStore.Audio.Playlists.Members.PLAYLIST_ID); 3113 enforceCallingPermission(ContentUris.withAppendedId( 3114 MediaStore.Audio.Playlists.getContentUri(resolvedVolumeName), playlistId), true); 3115 3116 ContentValues values = new ContentValues(initialValues); 3117 values.put(Audio.Playlists.Members.AUDIO_ID, audioId); 3118 rowId = db.insert("audio_playlists_map", "playlist_id", 3119 values); 3120 if (rowId > 0) { 3121 newUri = ContentUris.withAppendedId(uri, rowId); 3122 updatePlaylistDateModifiedToNow(db, playlistId); 3123 } 3124 break; 3125 } 3126 3127 case AUDIO_GENRES: { 3128 // NOTE: No permission enforcement on genres 3129 rowId = db.insert("audio_genres", "audio_id", initialValues); 3130 if (rowId > 0) { 3131 newUri = ContentUris.withAppendedId( 3132 Audio.Genres.getContentUri(originalVolumeName), rowId); 3133 } 3134 break; 3135 } 3136 3137 case AUDIO_GENRES_ID_MEMBERS: { 3138 // Require that caller has write access to underlying media 3139 final long audioId = initialValues 3140 .getAsLong(MediaStore.Audio.Genres.Members.AUDIO_ID); 3141 enforceCallingPermission(ContentUris.withAppendedId( 3142 MediaStore.Audio.Media.getContentUri(resolvedVolumeName), audioId), true); 3143 3144 Long genreId = Long.parseLong(uri.getPathSegments().get(3)); 3145 ContentValues values = new ContentValues(initialValues); 3146 values.put(Audio.Genres.Members.GENRE_ID, genreId); 3147 rowId = db.insert("audio_genres_map", "genre_id", values); 3148 if (rowId > 0) { 3149 newUri = ContentUris.withAppendedId(uri, rowId); 3150 } 3151 break; 3152 } 3153 3154 case AUDIO_PLAYLISTS: { 3155 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 3156 final boolean isDownload = maybeMarkAsDownload(initialValues); 3157 ContentValues values = new ContentValues(initialValues); 3158 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000); 3159 rowId = insertFile(helper, match, uri, values, 3160 FileColumns.MEDIA_TYPE_PLAYLIST, true); 3161 if (rowId > 0) { 3162 newUri = ContentUris.withAppendedId( 3163 Audio.Playlists.getContentUri(originalVolumeName), rowId); 3164 } 3165 break; 3166 } 3167 3168 case AUDIO_PLAYLISTS_ID: 3169 case AUDIO_PLAYLISTS_ID_MEMBERS: { 3170 // Require that caller has write access to underlying media 3171 final long audioId = initialValues 3172 .getAsLong(MediaStore.Audio.Playlists.Members.AUDIO_ID); 3173 enforceCallingPermission(ContentUris.withAppendedId( 3174 MediaStore.Audio.Media.getContentUri(resolvedVolumeName), audioId), true); 3175 final long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 3176 enforceCallingPermission(ContentUris.withAppendedId( 3177 MediaStore.Audio.Playlists.getContentUri(resolvedVolumeName), playlistId), true); 3178 3179 ContentValues values = new ContentValues(initialValues); 3180 values.put(Audio.Playlists.Members.PLAYLIST_ID, playlistId); 3181 rowId = db.insert("audio_playlists_map", "playlist_id", values); 3182 if (rowId > 0) { 3183 newUri = ContentUris.withAppendedId(uri, rowId); 3184 updatePlaylistDateModifiedToNow(db, playlistId); 3185 } 3186 break; 3187 } 3188 3189 case VIDEO_MEDIA: { 3190 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 3191 final boolean isDownload = maybeMarkAsDownload(initialValues); 3192 rowId = insertFile(helper, match, uri, initialValues, 3193 FileColumns.MEDIA_TYPE_VIDEO, true); 3194 if (rowId > 0) { 3195 MediaDocumentsProvider.onMediaStoreInsert( 3196 getContext(), resolvedVolumeName, FileColumns.MEDIA_TYPE_VIDEO, rowId); 3197 newUri = ContentUris.withAppendedId( 3198 Video.Media.getContentUri(originalVolumeName), rowId); 3199 } 3200 break; 3201 } 3202 3203 case AUDIO_ALBUMART: { 3204 if (helper.mInternal) { 3205 throw new UnsupportedOperationException("no internal album art allowed"); 3206 } 3207 3208 try { 3209 ensureUniqueFileColumns(match, uri, initialValues); 3210 } catch (VolumeArgumentException e) { 3211 return e.translateForInsert(targetSdkVersion); 3212 } 3213 3214 rowId = db.insert("album_art", MediaStore.MediaColumns.DATA, initialValues); 3215 if (rowId > 0) { 3216 newUri = ContentUris.withAppendedId(uri, rowId); 3217 } 3218 break; 3219 } 3220 3221 case FILES: { 3222 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 3223 final boolean isDownload = maybeMarkAsDownload(initialValues); 3224 rowId = insertFile(helper, match, uri, initialValues, 3225 FileColumns.MEDIA_TYPE_NONE, true); 3226 if (rowId > 0) { 3227 MediaDocumentsProvider.onMediaStoreInsert( 3228 getContext(), resolvedVolumeName, FileColumns.MEDIA_TYPE_NONE, rowId); 3229 newUri = Files.getContentUri(originalVolumeName, rowId); 3230 } 3231 break; 3232 } 3233 3234 case MTP_OBJECTS: 3235 // We don't send a notification if the insert originated from MTP 3236 final boolean isDownload = maybeMarkAsDownload(initialValues); 3237 rowId = insertFile(helper, match, uri, initialValues, 3238 FileColumns.MEDIA_TYPE_NONE, false); 3239 if (rowId > 0) { 3240 newUri = Files.getMtpObjectsUri(originalVolumeName, rowId); 3241 } 3242 break; 3243 3244 case FILES_DIRECTORY: 3245 rowId = insertDirectory(helper, helper.getWritableDatabase(), 3246 initialValues.getAsString(FileColumns.DATA)); 3247 if (rowId > 0) { 3248 newUri = Files.getContentUri(originalVolumeName, rowId); 3249 } 3250 break; 3251 3252 case DOWNLOADS: 3253 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 3254 initialValues.put(FileColumns.IS_DOWNLOAD, true); 3255 rowId = insertFile(helper, match, uri, initialValues, 3256 FileColumns.MEDIA_TYPE_NONE, false); 3257 if (rowId > 0) { 3258 final int mediaType = initialValues.getAsInteger(FileColumns.MEDIA_TYPE); 3259 MediaDocumentsProvider.onMediaStoreInsert( 3260 getContext(), resolvedVolumeName, mediaType, rowId); 3261 newUri = ContentUris.withAppendedId( 3262 MediaStore.Downloads.getContentUri(originalVolumeName), rowId); 3263 } 3264 break; 3265 3266 default: 3267 throw new UnsupportedOperationException("Invalid URI " + uri); 3268 } 3269 3270 // Remember that caller is owner of this item, to speed up future 3271 // permission checks for this caller 3272 mCallingIdentity.get().setOwned(rowId, true); 3273 3274 if (path != null && path.toLowerCase(Locale.US).endsWith("/.nomedia")) { 3275 MediaScanner.instance(getContext()).scanFile(new File(path).getParentFile()); 3276 } 3277 3278 if (newUri != null) { 3279 acceptWithExpansion(helper::notifyChange, newUri); 3280 } 3281 return newUri; 3282 } 3283 3284 @Override applyBatch(ArrayList<ContentProviderOperation> operations)3285 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 3286 throws OperationApplicationException { 3287 // Open transactions on databases for requested volumes 3288 final ArrayMap<String, DatabaseHelper> transactions = new ArrayMap<>(); 3289 try { 3290 for (ContentProviderOperation op : operations) { 3291 final String volumeName = MediaStore.getVolumeName(op.getUri()); 3292 if (!transactions.containsKey(volumeName)) { 3293 try { 3294 final DatabaseHelper helper = getDatabaseForUri(op.getUri()); 3295 helper.beginTransaction(); 3296 transactions.put(volumeName, helper); 3297 } catch (VolumeNotFoundException e) { 3298 Log.w(TAG, e.getMessage()); 3299 } 3300 } 3301 } 3302 3303 final ContentProviderResult[] result = super.applyBatch(operations); 3304 for (DatabaseHelper helper : transactions.values()) { 3305 helper.setTransactionSuccessful(); 3306 } 3307 return result; 3308 } finally { 3309 for (DatabaseHelper helper : transactions.values()) { 3310 helper.endTransaction(); 3311 } 3312 } 3313 } 3314 appendWhereStandalone(@onNull SQLiteQueryBuilder qb, @Nullable String selection, @Nullable Object... selectionArgs)3315 private static void appendWhereStandalone(@NonNull SQLiteQueryBuilder qb, 3316 @Nullable String selection, @Nullable Object... selectionArgs) { 3317 qb.appendWhereStandalone(DatabaseUtils.bindSelection(selection, selectionArgs)); 3318 } 3319 bindList(@onNull Object... args)3320 static @NonNull String bindList(@NonNull Object... args) { 3321 final StringBuilder sb = new StringBuilder(); 3322 sb.append('('); 3323 for (int i = 0; i < args.length; i++) { 3324 sb.append('?'); 3325 if (i < args.length - 1) { 3326 sb.append(','); 3327 } 3328 } 3329 sb.append(')'); 3330 return DatabaseUtils.bindSelection(sb.toString(), args); 3331 } 3332 parseBoolean(String value)3333 private static boolean parseBoolean(String value) { 3334 if (value == null) return false; 3335 if ("1".equals(value)) return true; 3336 if ("true".equalsIgnoreCase(value)) return true; 3337 return false; 3338 } 3339 3340 @Deprecated getSharedPackages(String callingPackage)3341 private String getSharedPackages(String callingPackage) { 3342 final String[] sharedPackageNames = mCallingIdentity.get().getSharedPackageNames(); 3343 return bindList((Object[]) sharedPackageNames); 3344 } 3345 3346 private static final int TYPE_QUERY = 0; 3347 private static final int TYPE_UPDATE = 1; 3348 private static final int TYPE_DELETE = 2; 3349 3350 /** 3351 * Generate a {@link SQLiteQueryBuilder} that is filtered based on the 3352 * runtime permissions and/or {@link Uri} grants held by the caller. 3353 * <ul> 3354 * <li>If caller holds a {@link Uri} grant, access is allowed according to 3355 * that grant. 3356 * <li>If caller holds the write permission for a collection, they can 3357 * read/write all contents of that collection. 3358 * <li>If caller holds the read permission for a collection, they can read 3359 * all contents of that collection, but writes are limited to content they 3360 * own. 3361 * <li>If caller holds no permissions for a collection, all reads/write are 3362 * limited to content they own. 3363 * </ul> 3364 */ getQueryBuilder(int type, Uri uri, int match, Bundle queryArgs)3365 private SQLiteQueryBuilder getQueryBuilder(int type, Uri uri, int match, Bundle queryArgs) { 3366 Trace.traceBegin(TRACE_TAG_DATABASE, "getQueryBuilder"); 3367 try { 3368 return getQueryBuilderInternal(type, uri, match, queryArgs); 3369 } finally { 3370 Trace.traceEnd(TRACE_TAG_DATABASE); 3371 } 3372 } 3373 getQueryBuilderInternal(int type, Uri uri, int match, Bundle queryArgs)3374 private SQLiteQueryBuilder getQueryBuilderInternal(int type, Uri uri, int match, 3375 Bundle queryArgs) { 3376 final boolean forWrite; 3377 switch (type) { 3378 case TYPE_QUERY: forWrite = false; break; 3379 case TYPE_UPDATE: forWrite = true; break; 3380 case TYPE_DELETE: forWrite = true; break; 3381 default: throw new IllegalStateException(); 3382 } 3383 3384 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 3385 if (parseBoolean(uri.getQueryParameter("distinct"))) { 3386 qb.setDistinct(true); 3387 } 3388 qb.setProjectionAggregationAllowed(true); 3389 qb.setStrict(true); 3390 3391 final String callingPackage = getCallingPackageOrSelf(); 3392 3393 // TODO: throw when requesting a currently unmounted volume 3394 final String volumeName = MediaStore.getVolumeName(uri); 3395 final String includeVolumes; 3396 if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) { 3397 includeVolumes = bindList(getExternalVolumeNames().toArray()); 3398 } else { 3399 includeVolumes = bindList(volumeName); 3400 } 3401 final String sharedPackages = getSharedPackages(callingPackage); 3402 final boolean allowGlobal = checkCallingPermissionGlobal(uri, forWrite); 3403 final boolean allowLegacy = checkCallingPermissionLegacy(uri, forWrite, callingPackage); 3404 final boolean allowLegacyRead = allowLegacy && !forWrite; 3405 3406 boolean includePending = parseBoolean( 3407 uri.getQueryParameter(MediaStore.PARAM_INCLUDE_PENDING)); 3408 boolean includeTrashed = parseBoolean( 3409 uri.getQueryParameter(MediaStore.PARAM_INCLUDE_TRASHED)); 3410 boolean includeAllVolumes = false; 3411 3412 switch (match) { 3413 case IMAGES_MEDIA_ID: 3414 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3415 includePending = true; 3416 includeTrashed = true; 3417 // fall-through 3418 case IMAGES_MEDIA: 3419 if (type == TYPE_QUERY) { 3420 qb.setTables("images"); 3421 qb.setProjectionMap(getProjectionMap(Images.Media.class)); 3422 } else { 3423 qb.setTables("files"); 3424 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 3425 FileColumns.MEDIA_TYPE_IMAGE); 3426 } 3427 if (!allowGlobal && !checkCallingPermissionImages(forWrite, callingPackage)) { 3428 appendWhereStandalone(qb, FileColumns.OWNER_PACKAGE_NAME + " IN " 3429 + sharedPackages); 3430 } 3431 if (!includePending) { 3432 appendWhereStandalone(qb, FileColumns.IS_PENDING + "=?", 0); 3433 } 3434 if (!includeTrashed) { 3435 appendWhereStandalone(qb, FileColumns.IS_TRASHED + "=?", 0); 3436 } 3437 if (!includeAllVolumes) { 3438 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 3439 } 3440 break; 3441 3442 case IMAGES_THUMBNAILS_ID: 3443 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3444 // fall-through 3445 case IMAGES_THUMBNAILS: { 3446 qb.setTables("thumbnails"); 3447 3448 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 3449 getProjectionMap(Images.Thumbnails.class)); 3450 projectionMap.put(Images.Thumbnails.THUMB_DATA, 3451 "NULL AS " + Images.Thumbnails.THUMB_DATA); 3452 qb.setProjectionMap(projectionMap); 3453 3454 if (!allowGlobal && !checkCallingPermissionImages(forWrite, callingPackage)) { 3455 appendWhereStandalone(qb, 3456 "image_id IN (SELECT _id FROM images WHERE owner_package_name IN " 3457 + sharedPackages + ")"); 3458 } 3459 break; 3460 } 3461 3462 case AUDIO_MEDIA_ID: 3463 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3464 includePending = true; 3465 includeTrashed = true; 3466 // fall-through 3467 case AUDIO_MEDIA: 3468 if (type == TYPE_QUERY) { 3469 qb.setTables("audio"); 3470 qb.setProjectionMap(getProjectionMap(Audio.Media.class)); 3471 } else { 3472 qb.setTables("files"); 3473 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 3474 FileColumns.MEDIA_TYPE_AUDIO); 3475 } 3476 if (!allowGlobal && !checkCallingPermissionAudio(forWrite, callingPackage)) { 3477 // Apps without Audio permission can only see their own 3478 // media, but we also let them see ringtone-style media to 3479 // support legacy use-cases. 3480 appendWhereStandalone(qb, 3481 DatabaseUtils.bindSelection(FileColumns.OWNER_PACKAGE_NAME 3482 + " IN " + sharedPackages 3483 + " OR is_ringtone=1 OR is_alarm=1 OR is_notification=1")); 3484 } 3485 if (!includePending) { 3486 appendWhereStandalone(qb, FileColumns.IS_PENDING + "=?", 0); 3487 } 3488 if (!includeTrashed) { 3489 appendWhereStandalone(qb, FileColumns.IS_TRASHED + "=?", 0); 3490 } 3491 if (!includeAllVolumes) { 3492 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 3493 } 3494 break; 3495 3496 case AUDIO_MEDIA_ID_GENRES_ID: 3497 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(5)); 3498 // fall-through 3499 case AUDIO_MEDIA_ID_GENRES: 3500 qb.setTables("audio_genres"); 3501 qb.setProjectionMap(getProjectionMap(Audio.Genres.class)); 3502 appendWhereStandalone(qb, "_id IN (SELECT genre_id FROM " + 3503 "audio_genres_map WHERE audio_id=?)", uri.getPathSegments().get(3)); 3504 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3505 // We don't have a great way to filter parsed metadata by 3506 // owner, so callers need to hold READ_MEDIA_AUDIO 3507 appendWhereStandalone(qb, "0"); 3508 } 3509 break; 3510 3511 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 3512 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(5)); 3513 // fall-through 3514 case AUDIO_MEDIA_ID_PLAYLISTS: 3515 qb.setTables("audio_playlists"); 3516 qb.setProjectionMap(getProjectionMap(Audio.Playlists.class)); 3517 appendWhereStandalone(qb, "_id IN (SELECT playlist_id FROM " + 3518 "audio_playlists_map WHERE audio_id=?)", uri.getPathSegments().get(3)); 3519 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3520 // We don't have a great way to filter parsed metadata by 3521 // owner, so callers need to hold READ_MEDIA_AUDIO 3522 appendWhereStandalone(qb, "0"); 3523 } 3524 break; 3525 3526 case AUDIO_GENRES_ID: 3527 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3528 // fall-through 3529 case AUDIO_GENRES: 3530 qb.setTables("audio_genres"); 3531 qb.setProjectionMap(getProjectionMap(Audio.Genres.class)); 3532 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3533 // We don't have a great way to filter parsed metadata by 3534 // owner, so callers need to hold READ_MEDIA_AUDIO 3535 appendWhereStandalone(qb, "0"); 3536 } 3537 break; 3538 3539 case AUDIO_GENRES_ID_MEMBERS: 3540 appendWhereStandalone(qb, "genre_id=?", uri.getPathSegments().get(3)); 3541 // fall-through 3542 case AUDIO_GENRES_ALL_MEMBERS: 3543 if (type == TYPE_QUERY) { 3544 qb.setTables("audio_genres_map_noid, audio"); 3545 qb.setProjectionMap(getProjectionMap(Audio.Genres.Members.class)); 3546 appendWhereStandalone(qb, "audio._id = audio_id"); 3547 } else { 3548 qb.setTables("audio_genres_map"); 3549 } 3550 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3551 // We don't have a great way to filter parsed metadata by 3552 // owner, so callers need to hold READ_MEDIA_AUDIO 3553 appendWhereStandalone(qb, "0"); 3554 } 3555 break; 3556 3557 case AUDIO_PLAYLISTS_ID: 3558 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3559 includePending = true; 3560 includeTrashed = true; 3561 // fall-through 3562 case AUDIO_PLAYLISTS: 3563 if (type == TYPE_QUERY) { 3564 qb.setTables("audio_playlists"); 3565 qb.setProjectionMap(getProjectionMap(Audio.Playlists.class)); 3566 } else { 3567 qb.setTables("files"); 3568 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 3569 FileColumns.MEDIA_TYPE_PLAYLIST); 3570 } 3571 if (!allowGlobal && !checkCallingPermissionAudio(forWrite, callingPackage)) { 3572 appendWhereStandalone(qb, FileColumns.OWNER_PACKAGE_NAME + " IN " 3573 + sharedPackages); 3574 } 3575 if (!includePending) { 3576 appendWhereStandalone(qb, FileColumns.IS_PENDING + "=?", 0); 3577 } 3578 if (!includeTrashed) { 3579 appendWhereStandalone(qb, FileColumns.IS_TRASHED + "=?", 0); 3580 } 3581 if (!includeAllVolumes) { 3582 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 3583 } 3584 break; 3585 3586 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 3587 appendWhereStandalone(qb, "audio_playlists_map._id=?", 3588 uri.getPathSegments().get(5)); 3589 // fall-through 3590 case AUDIO_PLAYLISTS_ID_MEMBERS: { 3591 appendWhereStandalone(qb, "playlist_id=?", uri.getPathSegments().get(3)); 3592 if (type == TYPE_QUERY) { 3593 qb.setTables("audio_playlists_map, audio"); 3594 3595 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 3596 getProjectionMap(Audio.Playlists.Members.class)); 3597 projectionMap.put(Audio.Playlists.Members._ID, 3598 "audio_playlists_map._id AS " + Audio.Playlists.Members._ID); 3599 qb.setProjectionMap(projectionMap); 3600 3601 appendWhereStandalone(qb, "audio._id = audio_id"); 3602 } else { 3603 qb.setTables("audio_playlists_map"); 3604 } 3605 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3606 // We don't have a great way to filter parsed metadata by 3607 // owner, so callers need to hold READ_MEDIA_AUDIO 3608 appendWhereStandalone(qb, "0"); 3609 } 3610 break; 3611 } 3612 3613 case AUDIO_ALBUMART_ID: 3614 appendWhereStandalone(qb, "album_id=?", uri.getPathSegments().get(3)); 3615 // fall-through 3616 case AUDIO_ALBUMART: { 3617 qb.setTables("album_art"); 3618 3619 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 3620 getProjectionMap(Audio.Thumbnails.class)); 3621 projectionMap.put(Audio.Thumbnails._ID, 3622 "album_id AS " + Audio.Thumbnails._ID); 3623 qb.setProjectionMap(projectionMap); 3624 3625 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3626 // We don't have a great way to filter parsed metadata by 3627 // owner, so callers need to hold READ_MEDIA_AUDIO 3628 appendWhereStandalone(qb, "0"); 3629 } 3630 break; 3631 } 3632 case AUDIO_ARTISTS_ID_ALBUMS: { 3633 if (type == TYPE_QUERY) { 3634 final String artistId = uri.getPathSegments().get(3); 3635 qb.setTables("audio LEFT OUTER JOIN album_art ON" + 3636 " audio.album_id=album_art.album_id"); 3637 appendWhereStandalone(qb, 3638 "is_music=1 AND audio.album_id IN (SELECT album_id FROM " + 3639 "artists_albums_map WHERE artist_id=?)", artistId); 3640 3641 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 3642 getProjectionMap(Audio.Artists.Albums.class)); 3643 projectionMap.put(Audio.Artists.Albums.ALBUM_ART, 3644 "album_art._data AS " + Audio.Artists.Albums.ALBUM_ART); 3645 projectionMap.put(Audio.Artists.Albums.NUMBER_OF_SONGS, 3646 "count(*) AS " + Audio.Artists.Albums.NUMBER_OF_SONGS); 3647 projectionMap.put(Audio.Artists.Albums.NUMBER_OF_SONGS_FOR_ARTIST, 3648 "count(CASE WHEN artist_id==" + artistId 3649 + " THEN 'foo' ELSE NULL END) AS " 3650 + Audio.Artists.Albums.NUMBER_OF_SONGS_FOR_ARTIST); 3651 projectionMap.put(Audio.Artists.Albums.FIRST_YEAR, 3652 "MIN(year) AS " + Audio.Artists.Albums.FIRST_YEAR); 3653 projectionMap.put(Audio.Artists.Albums.LAST_YEAR, 3654 "MAX(year) AS " + Audio.Artists.Albums.LAST_YEAR); 3655 projectionMap.put(Audio.Artists.Albums.ALBUM_ID, 3656 "audio.album_id AS " + Audio.Artists.Albums.ALBUM_ID); 3657 qb.setProjectionMap(projectionMap); 3658 } else { 3659 throw new UnsupportedOperationException("Albums cannot be directly modified"); 3660 } 3661 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3662 // We don't have a great way to filter parsed metadata by 3663 // owner, so callers need to hold READ_MEDIA_AUDIO 3664 appendWhereStandalone(qb, "0"); 3665 } 3666 break; 3667 } 3668 3669 case AUDIO_ARTISTS_ID: 3670 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3671 // fall-through 3672 case AUDIO_ARTISTS: 3673 if (type == TYPE_QUERY) { 3674 qb.setTables("artist_info"); 3675 qb.setProjectionMap(getProjectionMap(Audio.Artists.class)); 3676 } else { 3677 throw new UnsupportedOperationException("Artists cannot be directly modified"); 3678 } 3679 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3680 // We don't have a great way to filter parsed metadata by 3681 // owner, so callers need to hold READ_MEDIA_AUDIO 3682 appendWhereStandalone(qb, "0"); 3683 } 3684 break; 3685 3686 case AUDIO_ALBUMS_ID: 3687 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3688 // fall-through 3689 case AUDIO_ALBUMS: { 3690 if (type == TYPE_QUERY) { 3691 qb.setTables("album_info"); 3692 3693 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 3694 getProjectionMap(Audio.Albums.class)); 3695 projectionMap.put(Audio.Artists.Albums.NUMBER_OF_SONGS_FOR_ARTIST, 3696 "NULL AS " + Audio.Artists.Albums.NUMBER_OF_SONGS_FOR_ARTIST); 3697 projectionMap.put(Audio.Artists.Albums.ALBUM_ID, 3698 BaseColumns._ID + " AS " + Audio.Artists.Albums.ALBUM_ID); 3699 qb.setProjectionMap(projectionMap); 3700 } else { 3701 throw new UnsupportedOperationException("Albums cannot be directly modified"); 3702 } 3703 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3704 // We don't have a great way to filter parsed metadata by 3705 // owner, so callers need to hold READ_MEDIA_AUDIO 3706 appendWhereStandalone(qb, "0"); 3707 } 3708 break; 3709 } 3710 3711 case VIDEO_MEDIA_ID: 3712 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3713 includePending = true; 3714 includeTrashed = true; 3715 // fall-through 3716 case VIDEO_MEDIA: 3717 if (type == TYPE_QUERY) { 3718 qb.setTables("video"); 3719 qb.setProjectionMap(getProjectionMap(Video.Media.class)); 3720 } else { 3721 qb.setTables("files"); 3722 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 3723 FileColumns.MEDIA_TYPE_VIDEO); 3724 } 3725 if (!allowGlobal && !checkCallingPermissionVideo(forWrite, callingPackage)) { 3726 appendWhereStandalone(qb, FileColumns.OWNER_PACKAGE_NAME + " IN " 3727 + sharedPackages); 3728 } 3729 if (!includePending) { 3730 appendWhereStandalone(qb, FileColumns.IS_PENDING + "=?", 0); 3731 } 3732 if (!includeTrashed) { 3733 appendWhereStandalone(qb, FileColumns.IS_TRASHED + "=?", 0); 3734 } 3735 if (!includeAllVolumes) { 3736 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 3737 } 3738 break; 3739 3740 case VIDEO_THUMBNAILS_ID: 3741 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3742 // fall-through 3743 case VIDEO_THUMBNAILS: 3744 qb.setTables("videothumbnails"); 3745 qb.setProjectionMap(getProjectionMap(Video.Thumbnails.class)); 3746 if (!allowGlobal && !checkCallingPermissionVideo(forWrite, callingPackage)) { 3747 appendWhereStandalone(qb, 3748 "video_id IN (SELECT _id FROM video WHERE owner_package_name IN " 3749 + sharedPackages + ")"); 3750 } 3751 break; 3752 3753 case FILES_ID: 3754 case MTP_OBJECTS_ID: 3755 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2)); 3756 includePending = true; 3757 includeTrashed = true; 3758 // fall-through 3759 case FILES: 3760 case FILES_DIRECTORY: 3761 case MTP_OBJECTS: { 3762 qb.setTables("files"); 3763 qb.setProjectionMap(getProjectionMap(Files.FileColumns.class)); 3764 3765 final ArrayList<String> options = new ArrayList<>(); 3766 if (!allowGlobal && !allowLegacyRead) { 3767 options.add(DatabaseUtils.bindSelection("owner_package_name IN " 3768 + sharedPackages)); 3769 if (allowLegacy) { 3770 options.add(DatabaseUtils.bindSelection("volume_name=?", 3771 MediaStore.VOLUME_EXTERNAL_PRIMARY)); 3772 } 3773 if (checkCallingPermissionAudio(forWrite, callingPackage)) { 3774 options.add(DatabaseUtils.bindSelection("media_type=?", 3775 FileColumns.MEDIA_TYPE_AUDIO)); 3776 options.add(DatabaseUtils.bindSelection("media_type=?", 3777 FileColumns.MEDIA_TYPE_PLAYLIST)); 3778 options.add("media_type=0 AND mime_type LIKE 'audio/%'"); 3779 } 3780 if (checkCallingPermissionVideo(forWrite, callingPackage)) { 3781 options.add(DatabaseUtils.bindSelection("media_type=?", 3782 FileColumns.MEDIA_TYPE_VIDEO)); 3783 options.add("media_type=0 AND mime_type LIKE 'video/%'"); 3784 } 3785 if (checkCallingPermissionImages(forWrite, callingPackage)) { 3786 options.add(DatabaseUtils.bindSelection("media_type=?", 3787 FileColumns.MEDIA_TYPE_IMAGE)); 3788 options.add("media_type=0 AND mime_type LIKE 'image/%'"); 3789 } 3790 } 3791 if (options.size() > 0) { 3792 appendWhereStandalone(qb, TextUtils.join(" OR ", options)); 3793 } 3794 3795 if (!includePending) { 3796 appendWhereStandalone(qb, FileColumns.IS_PENDING + "=?", 0); 3797 } 3798 if (!includeTrashed) { 3799 appendWhereStandalone(qb, FileColumns.IS_TRASHED + "=?", 0); 3800 } 3801 if (!includeAllVolumes) { 3802 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 3803 } 3804 break; 3805 } 3806 case DOWNLOADS_ID: 3807 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2)); 3808 includePending = true; 3809 includeTrashed = true; 3810 // fall-through 3811 case DOWNLOADS: { 3812 if (type == TYPE_QUERY) { 3813 qb.setTables("downloads"); 3814 qb.setProjectionMap(getProjectionMap(Downloads.class)); 3815 } else { 3816 qb.setTables("files"); 3817 appendWhereStandalone(qb, FileColumns.IS_DOWNLOAD + "=1"); 3818 } 3819 3820 final ArrayList<String> options = new ArrayList<>(); 3821 if (!allowGlobal && !allowLegacyRead) { 3822 options.add(DatabaseUtils.bindSelection("owner_package_name IN " 3823 + sharedPackages)); 3824 if (allowLegacy) { 3825 options.add(DatabaseUtils.bindSelection("volume_name=?", 3826 MediaStore.VOLUME_EXTERNAL_PRIMARY)); 3827 } 3828 } 3829 if (options.size() > 0) { 3830 appendWhereStandalone(qb, TextUtils.join(" OR ", options)); 3831 } 3832 3833 if (!includePending) { 3834 appendWhereStandalone(qb, FileColumns.IS_PENDING + "=?", 0); 3835 } 3836 if (!includeTrashed) { 3837 appendWhereStandalone(qb, FileColumns.IS_TRASHED + "=?", 0); 3838 } 3839 if (!includeAllVolumes) { 3840 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 3841 } 3842 break; 3843 } 3844 default: 3845 throw new UnsupportedOperationException( 3846 "Unknown or unsupported URL: " + uri.toString()); 3847 } 3848 3849 if (type == TYPE_QUERY) { 3850 // To ensure we're enforcing our security model, all queries must 3851 // have a projection map configured 3852 if (qb.getProjectionMap() == null) { 3853 throw new IllegalStateException("All queries must have a projection map"); 3854 } 3855 3856 // If caller is an older app, we're willing to let through a 3857 // greylist of technically invalid columns 3858 if (getCallingPackageTargetSdkVersion() < Build.VERSION_CODES.Q) { 3859 qb.setProjectionGreylist(sGreylist); 3860 } 3861 } 3862 3863 return qb; 3864 } 3865 3866 /** 3867 * Determine if given {@link Uri} has a 3868 * {@link MediaColumns#OWNER_PACKAGE_NAME} column. 3869 */ hasOwnerPackageName(Uri uri)3870 private static boolean hasOwnerPackageName(Uri uri) { 3871 // It's easier to maintain this as an inverted list 3872 final int table = matchUri(uri, true); 3873 switch (table) { 3874 case IMAGES_THUMBNAILS_ID: 3875 case IMAGES_THUMBNAILS: 3876 case VIDEO_THUMBNAILS_ID: 3877 case VIDEO_THUMBNAILS: 3878 case AUDIO_ALBUMART: 3879 case AUDIO_ALBUMART_ID: 3880 case AUDIO_ALBUMART_FILE_ID: 3881 return false; 3882 default: 3883 return true; 3884 } 3885 } 3886 3887 @Override delete(Uri uri, String userWhere, String[] userWhereArgs)3888 public int delete(Uri uri, String userWhere, String[] userWhereArgs) { 3889 Trace.traceBegin(TRACE_TAG_DATABASE, "insert"); 3890 try { 3891 return deleteInternal(uri, userWhere, userWhereArgs); 3892 } finally { 3893 Trace.traceEnd(TRACE_TAG_DATABASE); 3894 } 3895 } 3896 deleteInternal(Uri uri, String userWhere, String[] userWhereArgs)3897 private int deleteInternal(Uri uri, String userWhere, String[] userWhereArgs) { 3898 uri = safeUncanonicalize(uri); 3899 3900 int count; 3901 3902 final String volumeName = getVolumeName(uri); 3903 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 3904 final boolean allowHidden = isCallingPackageAllowedHidden(); 3905 final int match = matchUri(uri, allowHidden); 3906 3907 // handle MEDIA_SCANNER before calling getDatabaseForUri() 3908 if (match == MEDIA_SCANNER) { 3909 if (mMediaScannerVolume == null) { 3910 return 0; 3911 } 3912 3913 final DatabaseHelper helper; 3914 try { 3915 helper = getDatabaseForUri(MediaStore.Files.getContentUri(mMediaScannerVolume)); 3916 } catch (VolumeNotFoundException e) { 3917 return e.translateForUpdateDelete(targetSdkVersion); 3918 } 3919 3920 helper.mScanStopTime = SystemClock.currentTimeMicro(); 3921 String msg = dump(helper, false); 3922 logToDb(helper.getWritableDatabase(), msg); 3923 3924 if (MediaStore.VOLUME_INTERNAL.equals(mMediaScannerVolume)) { 3925 // persist current build fingerprint as fingerprint for system (internal) sound scan 3926 final SharedPreferences scanSettings = getContext().getSharedPreferences( 3927 android.media.MediaScanner.SCANNED_BUILD_PREFS_NAME, 3928 Context.MODE_PRIVATE); 3929 final SharedPreferences.Editor editor = scanSettings.edit(); 3930 editor.putString(android.media.MediaScanner.LAST_INTERNAL_SCAN_FINGERPRINT, 3931 Build.FINGERPRINT); 3932 editor.apply(); 3933 } 3934 mMediaScannerVolume = null; 3935 return 1; 3936 } 3937 3938 if (match == VOLUMES_ID) { 3939 detachVolume(uri); 3940 count = 1; 3941 } 3942 3943 final DatabaseHelper helper; 3944 final SQLiteDatabase db; 3945 try { 3946 helper = getDatabaseForUri(uri); 3947 db = helper.getWritableDatabase(); 3948 } catch (VolumeNotFoundException e) { 3949 return e.translateForUpdateDelete(targetSdkVersion); 3950 } 3951 3952 { 3953 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, uri, match, null); 3954 3955 // Give callers interacting with a specific media item a chance to 3956 // escalate access if they don't already have it 3957 switch (match) { 3958 case AUDIO_MEDIA_ID: 3959 case VIDEO_MEDIA_ID: 3960 case IMAGES_MEDIA_ID: 3961 enforceCallingPermission(uri, true); 3962 } 3963 3964 final String[] projection = new String[] { 3965 FileColumns.MEDIA_TYPE, 3966 FileColumns.DATA, 3967 FileColumns._ID, 3968 FileColumns.IS_DOWNLOAD, 3969 FileColumns.MIME_TYPE, 3970 }; 3971 final LongSparseArray<String> deletedDownloadIds = new LongSparseArray<>(); 3972 if (qb.getTables().equals("files")) { 3973 String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA); 3974 if (deleteparam == null || ! deleteparam.equals("false")) { 3975 Cursor c = qb.query(db, projection, userWhere, userWhereArgs, 3976 null, null, null, null); 3977 String [] idvalue = new String[] { "" }; 3978 String [] playlistvalues = new String[] { "", "" }; 3979 try { 3980 while (c.moveToNext()) { 3981 final int mediaType = c.getInt(0); 3982 final String data = c.getString(1); 3983 final long id = c.getLong(2); 3984 final int isDownload = c.getInt(3); 3985 final String mimeType = c.getString(4); 3986 3987 // Forget that caller is owner of this item 3988 mCallingIdentity.get().setOwned(id, false); 3989 3990 // Invalidate thumbnails and revoke all outstanding grants 3991 final Uri deletedUri = Files.getContentUri(volumeName, id); 3992 invalidateThumbnails(deletedUri); 3993 acceptWithExpansion((expandedUri) -> { 3994 getContext().revokeUriPermission(expandedUri, 3995 Intent.FLAG_GRANT_READ_URI_PERMISSION 3996 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 3997 }, deletedUri); 3998 3999 // Only need to inform DownloadProvider about the downloads deleted on 4000 // external volume. 4001 if (isDownload == 1) { 4002 deletedDownloadIds.put(id, mimeType); 4003 } 4004 if (mediaType == FileColumns.MEDIA_TYPE_IMAGE) { 4005 deleteIfAllowed(uri, data); 4006 MediaDocumentsProvider.onMediaStoreDelete(getContext(), 4007 volumeName, FileColumns.MEDIA_TYPE_IMAGE, id); 4008 } else if (mediaType == FileColumns.MEDIA_TYPE_VIDEO) { 4009 deleteIfAllowed(uri, data); 4010 MediaDocumentsProvider.onMediaStoreDelete(getContext(), 4011 volumeName, FileColumns.MEDIA_TYPE_VIDEO, id); 4012 } else if (mediaType == FileColumns.MEDIA_TYPE_AUDIO) { 4013 if (!helper.mInternal) { 4014 deleteIfAllowed(uri, data); 4015 MediaDocumentsProvider.onMediaStoreDelete(getContext(), 4016 volumeName, FileColumns.MEDIA_TYPE_AUDIO, id); 4017 4018 idvalue[0] = String.valueOf(id); 4019 db.delete("audio_genres_map", "audio_id=?", idvalue); 4020 // for each playlist that the item appears in, move 4021 // all the items behind it forward by one 4022 Cursor cc = db.query("audio_playlists_map", 4023 sPlaylistIdPlayOrder, 4024 "audio_id=?", idvalue, null, null, null); 4025 try { 4026 while (cc.moveToNext()) { 4027 long playlistId = cc.getLong(0); 4028 playlistvalues[0] = String.valueOf(playlistId); 4029 playlistvalues[1] = String.valueOf(cc.getInt(1)); 4030 int rowsChanged = db.executeSql("UPDATE audio_playlists_map" + 4031 " SET play_order=play_order-1" + 4032 " WHERE playlist_id=? AND play_order>?", 4033 playlistvalues); 4034 4035 if (rowsChanged > 0) { 4036 updatePlaylistDateModifiedToNow(db, playlistId); 4037 } 4038 } 4039 db.delete("audio_playlists_map", "audio_id=?", idvalue); 4040 } finally { 4041 IoUtils.closeQuietly(cc); 4042 } 4043 } 4044 } else if (isDownload == 1) { 4045 deleteIfAllowed(uri, data); 4046 MediaDocumentsProvider.onMediaStoreDelete(getContext(), 4047 volumeName, mediaType, id); 4048 } else if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 4049 // TODO, maybe: remove the audio_playlists_cleanup trigger and 4050 // implement functionality here (clean up the playlist map) 4051 } 4052 } 4053 } finally { 4054 IoUtils.closeQuietly(c); 4055 } 4056 // Do not allow deletion if the file/object is referenced as parent 4057 // by some other entries. It could cause database corruption. 4058 appendWhereStandalone(qb, ID_NOT_PARENT_CLAUSE); 4059 } 4060 } 4061 4062 switch (match) { 4063 case MTP_OBJECTS: 4064 case MTP_OBJECTS_ID: 4065 count = deleteRecursive(qb, db, userWhere, userWhereArgs); 4066 break; 4067 case AUDIO_GENRES_ID_MEMBERS: 4068 count = deleteRecursive(qb, db, userWhere, userWhereArgs); 4069 break; 4070 4071 case IMAGES_THUMBNAILS_ID: 4072 case IMAGES_THUMBNAILS: 4073 case VIDEO_THUMBNAILS_ID: 4074 case VIDEO_THUMBNAILS: 4075 // Delete the referenced files first. 4076 Cursor c = qb.query(db, sDataOnlyColumn, userWhere, userWhereArgs, null, null, 4077 null, null); 4078 if (c != null) { 4079 try { 4080 while (c.moveToNext()) { 4081 deleteIfAllowed(uri, c.getString(0)); 4082 } 4083 } finally { 4084 IoUtils.closeQuietly(c); 4085 } 4086 } 4087 count = deleteRecursive(qb, db, userWhere, userWhereArgs); 4088 break; 4089 4090 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 4091 long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 4092 count = deleteRecursive(qb, db, userWhere, userWhereArgs); 4093 if (count > 0) { 4094 updatePlaylistDateModifiedToNow(db, playlistId); 4095 } 4096 break; 4097 default: 4098 count = deleteRecursive(qb, db, userWhere, userWhereArgs); 4099 break; 4100 } 4101 4102 if (deletedDownloadIds.size() > 0) { 4103 final long token = Binder.clearCallingIdentity(); 4104 try (ContentProviderClient client = getContext().getContentResolver() 4105 .acquireUnstableContentProviderClient( 4106 android.provider.Downloads.Impl.AUTHORITY)) { 4107 final Bundle extras = new Bundle(); 4108 final long[] ids = new long[deletedDownloadIds.size()]; 4109 final String[] mimeTypes = new String[deletedDownloadIds.size()]; 4110 for (int i = deletedDownloadIds.size() - 1; i >= 0; --i) { 4111 ids[i] = deletedDownloadIds.keyAt(i); 4112 mimeTypes[i] = deletedDownloadIds.valueAt(i); 4113 } 4114 extras.putLongArray(android.provider.Downloads.EXTRA_IDS, ids); 4115 extras.putStringArray(android.provider.Downloads.EXTRA_MIME_TYPES, mimeTypes); 4116 client.call(android.provider.Downloads.CALL_MEDIASTORE_DOWNLOADS_DELETED, 4117 null, extras); 4118 } catch (RemoteException e) { 4119 // Should not happen 4120 } finally { 4121 Binder.restoreCallingIdentity(token); 4122 } 4123 } 4124 } 4125 4126 if (count > 0) { 4127 acceptWithExpansion(helper::notifyChange, uri); 4128 } 4129 return count; 4130 } 4131 4132 /** 4133 * Executes identical delete repeatedly within a single transaction until 4134 * stability is reached. Combined with {@link #ID_NOT_PARENT_CLAUSE}, this 4135 * can be used to recursively delete all matching entries, since it only 4136 * deletes parents when no references remaining. 4137 */ deleteRecursive(SQLiteQueryBuilder qb, SQLiteDatabase db, String userWhere, String[] userWhereArgs)4138 private int deleteRecursive(SQLiteQueryBuilder qb, SQLiteDatabase db, String userWhere, 4139 String[] userWhereArgs) { 4140 db.beginTransaction(); 4141 try { 4142 int n = 0; 4143 int total = 0; 4144 do { 4145 n = qb.delete(db, userWhere, userWhereArgs); 4146 total += n; 4147 } while (n > 0); 4148 db.setTransactionSuccessful(); 4149 return total; 4150 } finally { 4151 db.endTransaction(); 4152 } 4153 } 4154 4155 @Override call(String method, String arg, Bundle extras)4156 public Bundle call(String method, String arg, Bundle extras) { 4157 switch (method) { 4158 case MediaStore.WAIT_FOR_IDLE_CALL: { 4159 final CountDownLatch latch = new CountDownLatch(1); 4160 BackgroundThread.getExecutor().execute(() -> { 4161 latch.countDown(); 4162 }); 4163 try { 4164 latch.await(30, TimeUnit.SECONDS); 4165 } catch (InterruptedException e) { 4166 throw new IllegalStateException(e); 4167 } 4168 return null; 4169 } 4170 case MediaStore.SCAN_FILE_CALL: 4171 case MediaStore.SCAN_VOLUME_CALL: { 4172 final LocalCallingIdentity token = clearLocalCallingIdentity(); 4173 final CallingIdentity providerToken = clearCallingIdentity(); 4174 try { 4175 final Uri uri = extras.getParcelable(Intent.EXTRA_STREAM); 4176 final File file = new File(uri.getPath()); 4177 final Bundle res = new Bundle(); 4178 switch (method) { 4179 case MediaStore.SCAN_FILE_CALL: 4180 res.putParcelable(Intent.EXTRA_STREAM, 4181 MediaScanner.instance(getContext()).scanFile(file)); 4182 break; 4183 case MediaStore.SCAN_VOLUME_CALL: 4184 MediaService.onScanVolume(getContext(), Uri.fromFile(file)); 4185 break; 4186 } 4187 return res; 4188 } catch (IOException e) { 4189 throw new RuntimeException(e); 4190 } finally { 4191 restoreCallingIdentity(providerToken); 4192 restoreLocalCallingIdentity(token); 4193 } 4194 } 4195 case MediaStore.UNHIDE_CALL: { 4196 throw new UnsupportedOperationException(); 4197 } 4198 case MediaStore.RETRANSLATE_CALL: { 4199 localizeTitles(); 4200 return null; 4201 } 4202 case MediaStore.GET_VERSION_CALL: { 4203 final String volumeName = extras.getString(Intent.EXTRA_TEXT); 4204 4205 final SQLiteDatabase db; 4206 try { 4207 db = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName)) 4208 .getReadableDatabase(); 4209 } catch (VolumeNotFoundException e) { 4210 throw e.rethrowAsIllegalArgumentException(); 4211 } 4212 4213 final String version = db.getVersion() + ":" + getOrCreateUuid(db); 4214 4215 final Bundle res = new Bundle(); 4216 res.putString(Intent.EXTRA_TEXT, version); 4217 return res; 4218 } 4219 case MediaStore.GET_DOCUMENT_URI_CALL: { 4220 final Uri mediaUri = extras.getParcelable(DocumentsContract.EXTRA_URI); 4221 enforceCallingPermission(mediaUri, false); 4222 4223 final Uri fileUri; 4224 final LocalCallingIdentity token = clearLocalCallingIdentity(); 4225 try { 4226 fileUri = Uri.fromFile(queryForDataFile(mediaUri, null)); 4227 } catch (FileNotFoundException e) { 4228 throw new IllegalArgumentException(e); 4229 } finally { 4230 restoreLocalCallingIdentity(token); 4231 } 4232 4233 try (ContentProviderClient client = getContext().getContentResolver() 4234 .acquireUnstableContentProviderClient( 4235 DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) { 4236 extras.putParcelable(DocumentsContract.EXTRA_URI, fileUri); 4237 return client.call(method, null, extras); 4238 } catch (RemoteException e) { 4239 throw new IllegalStateException(e); 4240 } 4241 } 4242 case MediaStore.GET_MEDIA_URI_CALL: { 4243 final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI); 4244 getContext().enforceCallingUriPermission(documentUri, 4245 Intent.FLAG_GRANT_READ_URI_PERMISSION, TAG); 4246 4247 final Uri fileUri; 4248 try (ContentProviderClient client = getContext().getContentResolver() 4249 .acquireUnstableContentProviderClient( 4250 DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) { 4251 final Bundle res = client.call(method, null, extras); 4252 fileUri = res.getParcelable(DocumentsContract.EXTRA_URI); 4253 } catch (RemoteException e) { 4254 throw new IllegalStateException(e); 4255 } 4256 4257 final LocalCallingIdentity token = clearLocalCallingIdentity(); 4258 try { 4259 final Bundle res = new Bundle(); 4260 res.putParcelable(DocumentsContract.EXTRA_URI, 4261 queryForMediaUri(new File(fileUri.getPath()), null)); 4262 return res; 4263 } catch (FileNotFoundException e) { 4264 throw new IllegalArgumentException(e); 4265 } finally { 4266 restoreLocalCallingIdentity(token); 4267 } 4268 } 4269 case MediaStore.GET_CONTRIBUTED_MEDIA_CALL: { 4270 getContext().enforceCallingOrSelfPermission( 4271 android.Manifest.permission.CLEAR_APP_USER_DATA, TAG); 4272 4273 final String packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME); 4274 final long totalSize = forEachContributedMedia(packageName, null); 4275 final Bundle res = new Bundle(); 4276 res.putLong(Intent.EXTRA_INDEX, totalSize); 4277 return res; 4278 } 4279 case MediaStore.DELETE_CONTRIBUTED_MEDIA_CALL: { 4280 getContext().enforceCallingOrSelfPermission( 4281 android.Manifest.permission.CLEAR_APP_USER_DATA, TAG); 4282 4283 final String packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME); 4284 forEachContributedMedia(packageName, (uri) -> { 4285 delete(uri, null, null); 4286 }); 4287 return null; 4288 } 4289 default: 4290 throw new UnsupportedOperationException("Unsupported call: " + method); 4291 } 4292 } 4293 4294 /** 4295 * Execute the given operation for each media item contributed by given 4296 * package. The meaning of "contributed" means it won't automatically be 4297 * deleted when the app is uninstalled. 4298 */ forEachContributedMedia(String packageName, Consumer<Uri> consumer)4299 private @BytesLong long forEachContributedMedia(String packageName, Consumer<Uri> consumer) { 4300 final DatabaseHelper helper = mExternalDatabase; 4301 final SQLiteDatabase db = helper.getReadableDatabase(); 4302 4303 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 4304 qb.setTables("files"); 4305 qb.appendWhere( 4306 DatabaseUtils.bindSelection(FileColumns.OWNER_PACKAGE_NAME + "=?", packageName) 4307 + " AND NOT " + FileColumns.DATA + " REGEXP '" 4308 + PATTERN_OWNED_PATH.pattern() + "'"); 4309 4310 long totalSize = 0; 4311 final LocalCallingIdentity token = clearLocalCallingIdentity(); 4312 try { 4313 try (Cursor c = qb.query(db, new String[] { 4314 FileColumns.VOLUME_NAME, FileColumns._ID, FileColumns.SIZE, FileColumns.DATA 4315 }, null, null, null, null, null, null)) { 4316 while (c.moveToNext()) { 4317 final String volumeName = c.getString(0); 4318 final long id = c.getLong(1); 4319 final long size = c.getLong(2); 4320 final String data = c.getString(3); 4321 4322 Log.d(TAG, "Found " + data + " from " + packageName + " in " 4323 + helper.mName + " with size " + size); 4324 if (consumer != null) { 4325 consumer.accept(Files.getContentUri(volumeName, id)); 4326 } 4327 totalSize += size; 4328 } 4329 } 4330 } finally { 4331 restoreLocalCallingIdentity(token); 4332 } 4333 return totalSize; 4334 } 4335 pruneThumbnails(@onNull CancellationSignal signal)4336 private void pruneThumbnails(@NonNull CancellationSignal signal) { 4337 final DatabaseHelper helper = mExternalDatabase; 4338 final SQLiteDatabase db = helper.getReadableDatabase(); 4339 4340 // Determine all known media items 4341 final LongArray knownIds = new LongArray(); 4342 try (Cursor c = db.query(true, "files", new String[] { BaseColumns._ID }, 4343 null, null, null, null, null, null, signal)) { 4344 while (c.moveToNext()) { 4345 knownIds.add(c.getLong(0)); 4346 } 4347 } 4348 4349 final long[] knownIdsRaw = knownIds.toArray(); 4350 Arrays.sort(knownIdsRaw); 4351 4352 for (String volumeName : getExternalVolumeNames()) { 4353 final File volumePath; 4354 try { 4355 volumePath = getVolumePath(volumeName); 4356 } catch (FileNotFoundException e) { 4357 Log.w(TAG, "Failed to resolve volume " + volumeName, e); 4358 continue; 4359 } 4360 4361 // Reconcile all thumbnails, deleting stale items 4362 for (File thumbDir : new File[] { 4363 buildPath(volumePath, Environment.DIRECTORY_MUSIC, ".thumbnails"), 4364 buildPath(volumePath, Environment.DIRECTORY_MOVIES, ".thumbnails"), 4365 buildPath(volumePath, Environment.DIRECTORY_PICTURES, ".thumbnails"), 4366 }) { 4367 // Possibly bail before digging into each directory 4368 signal.throwIfCanceled(); 4369 4370 for (File thumbFile : FileUtils.listFilesOrEmpty(thumbDir)) { 4371 final String name = ModernMediaScanner.extractName(thumbFile); 4372 try { 4373 final long id = Long.parseLong(name); 4374 if (Arrays.binarySearch(knownIdsRaw, id) >= 0) { 4375 // Thumbnail belongs to known media, keep it 4376 continue; 4377 } 4378 } catch (NumberFormatException e) { 4379 } 4380 4381 Log.v(TAG, "Deleting stale thumbnail " + thumbFile); 4382 thumbFile.delete(); 4383 } 4384 } 4385 } 4386 4387 // Also delete stale items from legacy tables 4388 db.execSQL("delete from thumbnails " 4389 + "where image_id not in (select _id from images)"); 4390 db.execSQL("delete from videothumbnails " 4391 + "where video_id not in (select _id from video)"); 4392 } 4393 4394 static abstract class Thumbnailer { 4395 final String directoryName; 4396 Thumbnailer(String directoryName)4397 public Thumbnailer(String directoryName) { 4398 this.directoryName = directoryName; 4399 } 4400 getThumbnailFile(Uri uri)4401 private File getThumbnailFile(Uri uri) throws IOException { 4402 final String volumeName = resolveVolumeName(uri); 4403 final File volumePath = getVolumePath(volumeName); 4404 return Environment.buildPath(volumePath, directoryName, 4405 ".thumbnails", ContentUris.parseId(uri) + ".jpg"); 4406 } 4407 getThumbnailBitmap(Uri uri, CancellationSignal signal)4408 public abstract Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) 4409 throws IOException; 4410 ensureThumbnail(Uri uri, CancellationSignal signal)4411 public File ensureThumbnail(Uri uri, CancellationSignal signal) throws IOException { 4412 final File thumbFile = getThumbnailFile(uri); 4413 thumbFile.getParentFile().mkdirs(); 4414 if (!thumbFile.exists()) { 4415 final Bitmap thumbnail = getThumbnailBitmap(uri, signal); 4416 try (OutputStream out = new FileOutputStream(thumbFile)) { 4417 thumbnail.compress(Bitmap.CompressFormat.JPEG, 75, out); 4418 } 4419 } 4420 return thumbFile; 4421 } 4422 invalidateThumbnail(Uri uri)4423 public void invalidateThumbnail(Uri uri) throws IOException { 4424 getThumbnailFile(uri).delete(); 4425 } 4426 } 4427 4428 private Thumbnailer mAudioThumbnailer = new Thumbnailer(Environment.DIRECTORY_MUSIC) { 4429 @Override 4430 public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException { 4431 return ThumbnailUtils.createAudioThumbnail(queryForDataFile(uri, signal), 4432 mThumbSize, signal); 4433 } 4434 }; 4435 4436 private Thumbnailer mVideoThumbnailer = new Thumbnailer(Environment.DIRECTORY_MOVIES) { 4437 @Override 4438 public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException { 4439 return ThumbnailUtils.createVideoThumbnail(queryForDataFile(uri, signal), 4440 mThumbSize, signal); 4441 } 4442 }; 4443 4444 private Thumbnailer mImageThumbnailer = new Thumbnailer(Environment.DIRECTORY_PICTURES) { 4445 @Override 4446 public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException { 4447 return ThumbnailUtils.createImageThumbnail(queryForDataFile(uri, signal), 4448 mThumbSize, signal); 4449 } 4450 }; 4451 invalidateThumbnails(Uri uri)4452 private void invalidateThumbnails(Uri uri) { 4453 Trace.traceBegin(TRACE_TAG_DATABASE, "invalidateThumbnails"); 4454 try { 4455 invalidateThumbnailsInternal(uri); 4456 } finally { 4457 Trace.traceEnd(TRACE_TAG_DATABASE); 4458 } 4459 } 4460 invalidateThumbnailsInternal(Uri uri)4461 private void invalidateThumbnailsInternal(Uri uri) { 4462 final long id = ContentUris.parseId(uri); 4463 try { 4464 mAudioThumbnailer.invalidateThumbnail(uri); 4465 mVideoThumbnailer.invalidateThumbnail(uri); 4466 mImageThumbnailer.invalidateThumbnail(uri); 4467 } catch (IOException ignored) { 4468 } 4469 4470 final DatabaseHelper helper; 4471 final SQLiteDatabase db; 4472 try { 4473 helper = getDatabaseForUri(uri); 4474 db = helper.getWritableDatabase(); 4475 } catch (VolumeNotFoundException e) { 4476 Log.w(TAG, e); 4477 return; 4478 } 4479 4480 final String idString = Long.toString(id); 4481 try (Cursor c = db.rawQuery("select _data from thumbnails where image_id=?" 4482 + " union all select _data from videothumbnails where video_id=?", 4483 new String[] { idString, idString })) { 4484 while (c.moveToNext()) { 4485 String path = c.getString(0); 4486 deleteIfAllowed(uri, path); 4487 } 4488 } 4489 4490 db.execSQL("delete from thumbnails where image_id=?", new String[] { idString }); 4491 db.execSQL("delete from videothumbnails where video_id=?", new String[] { idString }); 4492 } 4493 4494 @Override update(Uri uri, ContentValues initialValues, String userWhere, String[] userWhereArgs)4495 public int update(Uri uri, ContentValues initialValues, String userWhere, 4496 String[] userWhereArgs) { 4497 Trace.traceBegin(TRACE_TAG_DATABASE, "update"); 4498 try { 4499 return updateInternal(uri, initialValues, userWhere, userWhereArgs); 4500 } finally { 4501 Trace.traceEnd(TRACE_TAG_DATABASE); 4502 } 4503 } 4504 updateInternal(Uri uri, ContentValues initialValues, String userWhere, String[] userWhereArgs)4505 private int updateInternal(Uri uri, ContentValues initialValues, String userWhere, 4506 String[] userWhereArgs) { 4507 // Limit the hacky workaround to camera targeting Q and below, to allow newer versions 4508 // of camera that does the right thing to work correctly. 4509 if ("com.google.android.GoogleCamera".equals(getCallingPackageOrSelf()) 4510 && getCallingPackageTargetSdkVersion() <= Build.VERSION_CODES.Q) { 4511 if (matchUri(uri, false) == IMAGES_MEDIA_ID) { 4512 Log.w(TAG, "Working around app bug in b/111966296"); 4513 uri = MediaStore.Files.getContentUri("external", ContentUris.parseId(uri)); 4514 } else if (matchUri(uri, false) == VIDEO_MEDIA_ID) { 4515 Log.w(TAG, "Working around app bug in b/112246630"); 4516 uri = MediaStore.Files.getContentUri("external", ContentUris.parseId(uri)); 4517 } 4518 } 4519 4520 uri = safeUncanonicalize(uri); 4521 4522 int count; 4523 4524 final String volumeName = getVolumeName(uri); 4525 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 4526 final boolean allowHidden = isCallingPackageAllowedHidden(); 4527 final int match = matchUri(uri, allowHidden); 4528 4529 final DatabaseHelper helper; 4530 final SQLiteDatabase db; 4531 try { 4532 helper = getDatabaseForUri(uri); 4533 db = helper.getWritableDatabase(); 4534 } catch (VolumeNotFoundException e) { 4535 return e.translateForUpdateDelete(targetSdkVersion); 4536 } 4537 4538 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, uri, match, null); 4539 4540 // Give callers interacting with a specific media item a chance to 4541 // escalate access if they don't already have it 4542 switch (match) { 4543 case AUDIO_MEDIA_ID: 4544 case VIDEO_MEDIA_ID: 4545 case IMAGES_MEDIA_ID: 4546 enforceCallingPermission(uri, true); 4547 } 4548 4549 boolean triggerInvalidate = false; 4550 boolean triggerScan = false; 4551 String genre = null; 4552 if (initialValues != null) { 4553 // IDs are forever; nobody should be editing them 4554 initialValues.remove(MediaColumns._ID); 4555 4556 // Ignore or augment incoming raw filesystem paths 4557 for (String column : sDataColumns.keySet()) { 4558 if (!initialValues.containsKey(column)) continue; 4559 4560 if (isCallingPackageSystem() || isCallingPackageLegacy()) { 4561 // Mutation allowed 4562 } else { 4563 Log.w(TAG, "Ignoring mutation of " + column + " from " 4564 + getCallingPackageOrSelf()); 4565 initialValues.remove(column); 4566 } 4567 } 4568 4569 if (!isCallingPackageSystem()) { 4570 Trace.traceBegin(TRACE_TAG_DATABASE, "filter"); 4571 4572 // Remote callers have no direct control over owner column; we 4573 // force it be whoever is creating the content. 4574 initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME); 4575 4576 // We default to filtering mutable columns, except when we know 4577 // the single item being updated is pending; when it's finally 4578 // published we'll overwrite these values. 4579 final Uri finalUri = uri; 4580 final Supplier<Boolean> isPending = new CachedSupplier<>(() -> { 4581 return isPending(finalUri); 4582 }); 4583 4584 // Column values controlled by media scanner aren't writable by 4585 // apps, since any edits here don't reflect the metadata on 4586 // disk, and they'd be overwritten during a rescan. 4587 for (String column : new ArraySet<>(initialValues.keySet())) { 4588 if (sMutableColumns.contains(column)) { 4589 // Mutation normally allowed 4590 } else if (isPending.get()) { 4591 // Mutation relaxed while pending 4592 } else { 4593 Log.w(TAG, "Ignoring mutation of " + column + " from " 4594 + getCallingPackageOrSelf()); 4595 initialValues.remove(column); 4596 4597 switch (match) { 4598 default: 4599 triggerScan = true; 4600 break; 4601 // If entry is a playlist, do not re-scan to match previous behavior 4602 // and allow persistence of database-only edits until real re-scan 4603 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 4604 case AUDIO_PLAYLISTS_ID: 4605 break; 4606 } 4607 } 4608 4609 // If we're publishing this item, perform a blocking scan to 4610 // make sure metadata is updated 4611 if (MediaColumns.IS_PENDING.equals(column)) { 4612 triggerScan = true; 4613 } 4614 } 4615 4616 Trace.traceEnd(TRACE_TAG_DATABASE); 4617 } 4618 4619 genre = initialValues.getAsString(Audio.AudioColumns.GENRE); 4620 initialValues.remove(Audio.AudioColumns.GENRE); 4621 4622 if ("files".equals(qb.getTables())) { 4623 maybeMarkAsDownload(initialValues); 4624 } 4625 4626 // We no longer track location metadata 4627 if (initialValues.containsKey(ImageColumns.LATITUDE)) { 4628 initialValues.putNull(ImageColumns.LATITUDE); 4629 } 4630 if (initialValues.containsKey(ImageColumns.LONGITUDE)) { 4631 initialValues.putNull(ImageColumns.LONGITUDE); 4632 } 4633 } 4634 4635 // If we're not updating anything, then we can skip 4636 if (initialValues.isEmpty()) return 0; 4637 4638 final boolean isThumbnail; 4639 switch (match) { 4640 case IMAGES_THUMBNAILS: 4641 case IMAGES_THUMBNAILS_ID: 4642 case VIDEO_THUMBNAILS: 4643 case VIDEO_THUMBNAILS_ID: 4644 case AUDIO_ALBUMART: 4645 case AUDIO_ALBUMART_ID: 4646 isThumbnail = true; 4647 break; 4648 default: 4649 isThumbnail = false; 4650 break; 4651 } 4652 4653 // If we're touching columns that would change placement of a file, 4654 // blend in current values and recalculate path 4655 if (containsAny(initialValues.keySet(), sPlacementColumns) 4656 && !initialValues.containsKey(MediaColumns.DATA) 4657 && !isCallingPackageSystem() 4658 && !isThumbnail) { 4659 Trace.traceBegin(TRACE_TAG_DATABASE, "movement"); 4660 4661 // We only support movement under well-defined collections 4662 switch (match) { 4663 case AUDIO_MEDIA_ID: 4664 case VIDEO_MEDIA_ID: 4665 case IMAGES_MEDIA_ID: 4666 case DOWNLOADS_ID: 4667 break; 4668 default: 4669 throw new IllegalArgumentException("Movement of " + uri 4670 + " which isn't part of well-defined collection not allowed"); 4671 } 4672 4673 final LocalCallingIdentity token = clearLocalCallingIdentity(); 4674 try (Cursor c = queryForSingleItem(uri, 4675 sPlacementColumns.toArray(EmptyArray.STRING), userWhere, userWhereArgs, null)) { 4676 for (int i = 0; i < c.getColumnCount(); i++) { 4677 final String column = c.getColumnName(i); 4678 if (!initialValues.containsKey(column)) { 4679 initialValues.put(column, c.getString(i)); 4680 } 4681 } 4682 } catch (FileNotFoundException e) { 4683 throw new IllegalStateException(e); 4684 } finally { 4685 restoreLocalCallingIdentity(token); 4686 } 4687 4688 // Regenerate path using blended values; this will throw if caller 4689 // is attempting to place file into invalid location 4690 final String beforePath = initialValues.getAsString(MediaColumns.DATA); 4691 final String beforeVolume = extractVolumeName(beforePath); 4692 final String beforeOwner = extractPathOwnerPackageName(beforePath); 4693 initialValues.remove(MediaColumns.DATA); 4694 try { 4695 ensureNonUniqueFileColumns(match, uri, initialValues, beforePath); 4696 } catch (VolumeArgumentException e) { 4697 return e.translateForUpdateDelete(targetSdkVersion); 4698 } 4699 4700 final String probePath = initialValues.getAsString(MediaColumns.DATA); 4701 final String probeVolume = extractVolumeName(probePath); 4702 final String probeOwner = extractPathOwnerPackageName(probePath); 4703 if (Objects.equals(beforePath, probePath)) { 4704 Log.d(TAG, "Identical paths " + beforePath + "; not moving"); 4705 } else if (!Objects.equals(beforeVolume, probeVolume)) { 4706 throw new IllegalArgumentException("Changing volume from " + beforePath + " to " 4707 + probePath + " not allowed"); 4708 } else if (!Objects.equals(beforeOwner, probeOwner)) { 4709 throw new IllegalArgumentException("Changing ownership from " + beforePath + " to " 4710 + probePath + " not allowed"); 4711 } else { 4712 // Now that we've confirmed an actual movement is taking place, 4713 // ensure we have a unique destination 4714 initialValues.remove(MediaColumns.DATA); 4715 try { 4716 ensureUniqueFileColumns(match, uri, initialValues); 4717 } catch (VolumeArgumentException e) { 4718 return e.translateForUpdateDelete(targetSdkVersion); 4719 } 4720 final String afterPath = initialValues.getAsString(MediaColumns.DATA); 4721 4722 Log.d(TAG, "Moving " + beforePath + " to " + afterPath); 4723 try { 4724 Os.rename(beforePath, afterPath); 4725 } catch (ErrnoException e) { 4726 throw new IllegalStateException(e); 4727 } 4728 initialValues.put(MediaColumns.DATA, afterPath); 4729 } 4730 4731 Trace.traceEnd(TRACE_TAG_DATABASE); 4732 } 4733 4734 // Make sure any updated paths look sane 4735 try { 4736 assertFileColumnsSane(match, uri, initialValues); 4737 } catch (VolumeArgumentException e) { 4738 return e.translateForUpdateDelete(targetSdkVersion); 4739 } 4740 4741 // if the media type is being changed, check if it's being changed from image or video 4742 // to something else 4743 if (initialValues.containsKey(FileColumns.MEDIA_TYPE)) { 4744 final int newMediaType = initialValues.getAsInteger(FileColumns.MEDIA_TYPE); 4745 4746 // If we're changing media types, invalidate any cached "empty" 4747 // answers for the new collection type. 4748 MediaDocumentsProvider.onMediaStoreInsert( 4749 getContext(), volumeName, newMediaType, -1); 4750 4751 // If we're changing media types, invalidate any thumbnails 4752 triggerInvalidate = true; 4753 } 4754 4755 if (initialValues.containsKey(FileColumns.DATA)) { 4756 // If we're changing paths, invalidate any thumbnails 4757 triggerInvalidate = true; 4758 } 4759 4760 // Since the update mutation may prevent us from matching items after 4761 // it's applied, we need to snapshot affected IDs here 4762 final LongArray updatedIds = new LongArray(); 4763 if (triggerInvalidate || triggerScan) { 4764 Trace.traceBegin(TRACE_TAG_DATABASE, "snapshot"); 4765 final LocalCallingIdentity token = clearLocalCallingIdentity(); 4766 try (Cursor c = qb.query(db, new String[] { FileColumns._ID }, 4767 userWhere, userWhereArgs, null, null, null)) { 4768 while (c.moveToNext()) { 4769 updatedIds.add(c.getLong(0)); 4770 } 4771 } finally { 4772 restoreLocalCallingIdentity(token); 4773 Trace.traceEnd(TRACE_TAG_DATABASE); 4774 } 4775 } 4776 4777 // special case renaming directories via MTP. 4778 // in this case we must update all paths in the database with 4779 // the directory name as a prefix 4780 if ((match == MTP_OBJECTS || match == MTP_OBJECTS_ID || match == FILES_DIRECTORY) 4781 && initialValues != null 4782 // Is a rename operation 4783 && ((initialValues.size() == 1 && initialValues.containsKey(FileColumns.DATA)) 4784 // Is a move operation 4785 || (initialValues.size() == 2 && initialValues.containsKey(FileColumns.DATA) 4786 && initialValues.containsKey(FileColumns.PARENT)))) { 4787 String oldPath = null; 4788 String newPath = initialValues.getAsString(MediaStore.MediaColumns.DATA); 4789 synchronized (mDirectoryCache) { 4790 mDirectoryCache.remove(newPath); 4791 } 4792 // MtpDatabase will rename the directory first, so we test the new file name 4793 File f = new File(newPath); 4794 if (newPath != null && f.isDirectory()) { 4795 Cursor cursor = qb.query(db, PATH_PROJECTION, userWhere, userWhereArgs, null, null, 4796 null, null); 4797 try { 4798 if (cursor != null && cursor.moveToNext()) { 4799 oldPath = cursor.getString(1); 4800 } 4801 } finally { 4802 IoUtils.closeQuietly(cursor); 4803 } 4804 final boolean isDownload = isDownload(newPath); 4805 if (oldPath != null) { 4806 synchronized (mDirectoryCache) { 4807 mDirectoryCache.remove(oldPath); 4808 } 4809 final boolean wasDownload = isDownload(oldPath); 4810 // first rename the row for the directory 4811 count = qb.update(db, initialValues, userWhere, userWhereArgs); 4812 if (count > 0) { 4813 // update the paths of any files and folders contained in the directory 4814 Object[] bindArgs = new Object[] { 4815 newPath, 4816 oldPath.length() + 1, 4817 oldPath + "/", 4818 oldPath + "0", 4819 // update bucket_display_name and bucket_id based on new path 4820 f.getName(), 4821 f.toString().toLowerCase().hashCode(), 4822 isDownload 4823 }; 4824 db.execSQL("UPDATE files SET _data=?1||SUBSTR(_data, ?2)" + 4825 // also update bucket_display_name 4826 ",bucket_display_name=?5" + 4827 ",bucket_id=?6" + 4828 ",is_download=?7" + 4829 " WHERE _data >= ?3 AND _data < ?4;", 4830 bindArgs); 4831 } 4832 4833 if (count > 0) { 4834 acceptWithExpansion(helper::notifyChange, uri); 4835 } 4836 if (f.getName().startsWith(".")) { 4837 MediaScanner.instance(getContext()).scanFile(new File(newPath)); 4838 } 4839 return count; 4840 } 4841 } else if (newPath.toLowerCase(Locale.US).endsWith("/.nomedia")) { 4842 MediaScanner.instance(getContext()).scanFile(new File(newPath).getParentFile()); 4843 } 4844 } 4845 4846 switch (match) { 4847 case AUDIO_MEDIA: 4848 case AUDIO_MEDIA_ID: 4849 { 4850 ContentValues values = new ContentValues(initialValues); 4851 String albumartist = values.getAsString(MediaStore.Audio.Media.ALBUM_ARTIST); 4852 String compilation = values.getAsString(MediaStore.Audio.Media.COMPILATION); 4853 values.remove(MediaStore.Audio.Media.COMPILATION); 4854 4855 // Insert the artist into the artist table and remove it from 4856 // the input values 4857 String artist = values.getAsString("artist"); 4858 values.remove("artist"); 4859 if (artist != null) { 4860 long artistRowId; 4861 ArrayMap<String, Long> artistCache = helper.mArtistCache; 4862 synchronized(artistCache) { 4863 Long temp = artistCache.get(artist); 4864 if (temp == null) { 4865 artistRowId = getKeyIdForName(helper, db, 4866 "artists", "artist_key", "artist", 4867 artist, artist, null, 0, null, artistCache, uri); 4868 } else { 4869 artistRowId = temp.longValue(); 4870 } 4871 } 4872 values.put("artist_id", Integer.toString((int)artistRowId)); 4873 } 4874 4875 // Do the same for the album field. 4876 String so = values.getAsString("album"); 4877 values.remove("album"); 4878 if (so != null) { 4879 String path = values.getAsString(MediaStore.MediaColumns.DATA); 4880 int albumHash = 0; 4881 if (albumartist != null) { 4882 albumHash = albumartist.hashCode(); 4883 } else if (compilation != null && compilation.equals("1")) { 4884 // nothing to do, hash already set 4885 } else { 4886 if (path == null) { 4887 if (match == AUDIO_MEDIA) { 4888 Log.w(TAG, "Possible multi row album name update without" 4889 + " path could give wrong album key"); 4890 } else { 4891 //Log.w(TAG, "Specify path to avoid extra query"); 4892 Cursor c = query(uri, 4893 new String[] { MediaStore.Audio.Media.DATA}, 4894 null, null, null); 4895 if (c != null) { 4896 try { 4897 int numrows = c.getCount(); 4898 if (numrows == 1) { 4899 c.moveToFirst(); 4900 path = c.getString(0); 4901 } else { 4902 Log.e(TAG, "" + numrows + " rows for " + uri); 4903 } 4904 } finally { 4905 IoUtils.closeQuietly(c); 4906 } 4907 } 4908 } 4909 } 4910 if (path != null) { 4911 albumHash = path.substring(0, path.lastIndexOf('/')).hashCode(); 4912 } 4913 } 4914 4915 String s = so.toString(); 4916 long albumRowId; 4917 ArrayMap<String, Long> albumCache = helper.mAlbumCache; 4918 synchronized(albumCache) { 4919 String cacheName = s + albumHash; 4920 Long temp = albumCache.get(cacheName); 4921 if (temp == null) { 4922 albumRowId = getKeyIdForName(helper, db, 4923 "albums", "album_key", "album", 4924 s, cacheName, path, albumHash, artist, albumCache, uri); 4925 } else { 4926 albumRowId = temp.longValue(); 4927 } 4928 } 4929 values.put("album_id", Integer.toString((int)albumRowId)); 4930 } 4931 4932 // don't allow the title_key field to be updated directly 4933 values.remove("title_key"); 4934 // If the title field is modified, update the title_key 4935 so = values.getAsString("title"); 4936 if (so != null) { 4937 try { 4938 final String localizedTitle = getLocalizedTitle(so); 4939 if (localizedTitle != null) { 4940 values.put("title_resource_uri", so); 4941 so = localizedTitle; 4942 } else { 4943 values.putNull("title_resource_uri"); 4944 } 4945 } catch (Exception e) { 4946 values.put("title_resource_uri", so); 4947 } 4948 values.put("title_key", MediaStore.Audio.keyFor(so)); 4949 // do a final trim of the title, in case it started with the special 4950 // "sort first" character (ascii \001) 4951 values.put("title", so.trim()); 4952 } 4953 4954 count = qb.update(db, values, userWhere, userWhereArgs); 4955 if (genre != null) { 4956 if (count == 1 && match == AUDIO_MEDIA_ID) { 4957 long rowId = Long.parseLong(uri.getPathSegments().get(3)); 4958 updateGenre(rowId, genre, volumeName); 4959 } else { 4960 // can't handle genres for bulk update or for non-audio files 4961 Log.w(TAG, "ignoring genre in update: count = " 4962 + count + " match = " + match); 4963 } 4964 } 4965 } 4966 break; 4967 case IMAGES_MEDIA: 4968 case IMAGES_MEDIA_ID: 4969 case VIDEO_MEDIA: 4970 case VIDEO_MEDIA_ID: 4971 { 4972 ContentValues values = new ContentValues(initialValues); 4973 // Don't allow bucket id or display name to be updated directly. 4974 // The same names are used for both images and table columns, so 4975 // we use the ImageColumns constants here. 4976 values.remove(ImageColumns.BUCKET_ID); 4977 values.remove(ImageColumns.BUCKET_DISPLAY_NAME); 4978 // If the data is being modified update the bucket values 4979 computeDataValues(values); 4980 count = qb.update(db, values, userWhere, userWhereArgs); 4981 } 4982 break; 4983 4984 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 4985 case AUDIO_PLAYLISTS_ID: 4986 long playlistId = ContentUris.parseId(uri); 4987 count = qb.update(db, initialValues, userWhere, userWhereArgs); 4988 if (count > 0) { 4989 updatePlaylistDateModifiedToNow(db, playlistId); 4990 } 4991 break; 4992 case AUDIO_PLAYLISTS_ID_MEMBERS: 4993 long playlistIdMembers = Long.parseLong(uri.getPathSegments().get(3)); 4994 count = qb.update(db, initialValues, userWhere, userWhereArgs); 4995 if (count > 0) { 4996 updatePlaylistDateModifiedToNow(db, playlistIdMembers); 4997 } 4998 break; 4999 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 5000 String moveit = uri.getQueryParameter("move"); 5001 if (moveit != null) { 5002 String key = MediaStore.Audio.Playlists.Members.PLAY_ORDER; 5003 if (initialValues.containsKey(key)) { 5004 int newpos = initialValues.getAsInteger(key); 5005 List <String> segments = uri.getPathSegments(); 5006 long playlist = Long.parseLong(segments.get(3)); 5007 int oldpos = Integer.parseInt(segments.get(5)); 5008 int rowsChanged = movePlaylistEntry(volumeName, helper, db, playlist, oldpos, newpos); 5009 if (rowsChanged > 0) { 5010 updatePlaylistDateModifiedToNow(db, playlist); 5011 } 5012 5013 return rowsChanged; 5014 } 5015 throw new IllegalArgumentException("Need to specify " + key + 5016 " when using 'move' parameter"); 5017 } 5018 // fall through 5019 default: 5020 count = qb.update(db, initialValues, userWhere, userWhereArgs); 5021 break; 5022 } 5023 5024 // If the caller tried (and failed) to update metadata, the file on disk 5025 // might have changed, to scan it to collect the latest metadata. 5026 if (triggerInvalidate || triggerScan) { 5027 Trace.traceBegin(TRACE_TAG_DATABASE, "invalidate"); 5028 final LocalCallingIdentity token = clearLocalCallingIdentity(); 5029 try { 5030 for (int i = 0; i < updatedIds.size(); i++) { 5031 final long updatedId = updatedIds.get(i); 5032 final Uri updatedUri = Files.getContentUri(volumeName, updatedId); 5033 BackgroundThread.getExecutor().execute(() -> { 5034 invalidateThumbnails(updatedUri); 5035 }); 5036 5037 if (triggerScan) { 5038 try (Cursor c = queryForSingleItem(updatedUri, 5039 new String[] { FileColumns.DATA }, null, null, null)) { 5040 MediaScanner.instance(getContext()).scanFile(new File(c.getString(0))); 5041 } catch (Exception e) { 5042 Log.w(TAG, "Failed to update metadata for " + updatedUri, e); 5043 } 5044 } 5045 } 5046 } finally { 5047 restoreLocalCallingIdentity(token); 5048 Trace.traceEnd(TRACE_TAG_DATABASE); 5049 } 5050 } 5051 5052 if (count > 0) { 5053 acceptWithExpansion(helper::notifyChange, uri); 5054 } 5055 return count; 5056 } 5057 movePlaylistEntry(String volumeName, DatabaseHelper helper, SQLiteDatabase db, long playlist, int from, int to)5058 private int movePlaylistEntry(String volumeName, DatabaseHelper helper, SQLiteDatabase db, 5059 long playlist, int from, int to) { 5060 if (from == to) { 5061 return 0; 5062 } 5063 db.beginTransaction(); 5064 int numlines = 0; 5065 Cursor c = null; 5066 try { 5067 c = db.query("audio_playlists_map", 5068 new String [] {"play_order" }, 5069 "playlist_id=?", new String[] {"" + playlist}, null, null, "play_order", 5070 from + ",1"); 5071 c.moveToFirst(); 5072 int from_play_order = c.getInt(0); 5073 IoUtils.closeQuietly(c); 5074 c = db.query("audio_playlists_map", 5075 new String [] {"play_order" }, 5076 "playlist_id=?", new String[] {"" + playlist}, null, null, "play_order", 5077 to + ",1"); 5078 c.moveToFirst(); 5079 int to_play_order = c.getInt(0); 5080 db.execSQL("UPDATE audio_playlists_map SET play_order=-1" + 5081 " WHERE play_order=" + from_play_order + 5082 " AND playlist_id=" + playlist); 5083 // We could just run both of the next two statements, but only one of 5084 // of them will actually do anything, so might as well skip the compile 5085 // and execute steps. 5086 if (from < to) { 5087 db.execSQL("UPDATE audio_playlists_map SET play_order=play_order-1" + 5088 " WHERE play_order<=" + to_play_order + 5089 " AND play_order>" + from_play_order + 5090 " AND playlist_id=" + playlist); 5091 numlines = to - from + 1; 5092 } else { 5093 db.execSQL("UPDATE audio_playlists_map SET play_order=play_order+1" + 5094 " WHERE play_order>=" + to_play_order + 5095 " AND play_order<" + from_play_order + 5096 " AND playlist_id=" + playlist); 5097 numlines = from - to + 1; 5098 } 5099 db.execSQL("UPDATE audio_playlists_map SET play_order=" + to_play_order + 5100 " WHERE play_order=-1 AND playlist_id=" + playlist); 5101 db.setTransactionSuccessful(); 5102 } finally { 5103 db.endTransaction(); 5104 IoUtils.closeQuietly(c); 5105 } 5106 5107 Uri uri = ContentUris.withAppendedId( 5108 MediaStore.Audio.Playlists.getContentUri(volumeName), playlist); 5109 // notifyChange() must be called after the database transaction is ended 5110 // or the listeners will read the old data in the callback 5111 getContext().getContentResolver().notifyChange(uri, null); 5112 5113 return numlines; 5114 } 5115 updatePlaylistDateModifiedToNow(SQLiteDatabase database, long playlistId)5116 private void updatePlaylistDateModifiedToNow(SQLiteDatabase database, long playlistId) { 5117 ContentValues values = new ContentValues(); 5118 values.put( 5119 FileColumns.DATE_MODIFIED, 5120 TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) 5121 ); 5122 5123 database.update( 5124 MediaStore.Files.TABLE, 5125 values, 5126 MediaStore.Files.FileColumns._ID + "=?", 5127 new String[]{String.valueOf(playlistId)} 5128 ); 5129 } 5130 5131 @Override openFile(Uri uri, String mode)5132 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 5133 return openFileCommon(uri, mode, null); 5134 } 5135 5136 @Override openFile(Uri uri, String mode, CancellationSignal signal)5137 public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) 5138 throws FileNotFoundException { 5139 return openFileCommon(uri, mode, signal); 5140 } 5141 openFileCommon(Uri uri, String mode, CancellationSignal signal)5142 private ParcelFileDescriptor openFileCommon(Uri uri, String mode, CancellationSignal signal) 5143 throws FileNotFoundException { 5144 uri = safeUncanonicalize(uri); 5145 5146 final boolean allowHidden = isCallingPackageAllowedHidden(); 5147 final int match = matchUri(uri, allowHidden); 5148 final String volumeName = getVolumeName(uri); 5149 5150 // Handle some legacy cases where we need to redirect thumbnails 5151 switch (match) { 5152 case AUDIO_ALBUMART_ID: { 5153 final long albumId = Long.parseLong(uri.getPathSegments().get(3)); 5154 final Uri targetUri = ContentUris 5155 .withAppendedId(Audio.Albums.getContentUri(volumeName), albumId); 5156 return ParcelFileDescriptor.open(ensureThumbnail(targetUri, signal), 5157 ParcelFileDescriptor.MODE_READ_ONLY); 5158 5159 } 5160 case AUDIO_ALBUMART_FILE_ID: { 5161 final long audioId = Long.parseLong(uri.getPathSegments().get(3)); 5162 final Uri targetUri = ContentUris 5163 .withAppendedId(Audio.Media.getContentUri(volumeName), audioId); 5164 return ParcelFileDescriptor.open(ensureThumbnail(targetUri, signal), 5165 ParcelFileDescriptor.MODE_READ_ONLY); 5166 } 5167 case VIDEO_MEDIA_ID_THUMBNAIL: { 5168 final long videoId = Long.parseLong(uri.getPathSegments().get(3)); 5169 final Uri targetUri = ContentUris 5170 .withAppendedId(Video.Media.getContentUri(volumeName), videoId); 5171 return ParcelFileDescriptor.open(ensureThumbnail(targetUri, signal), 5172 ParcelFileDescriptor.MODE_READ_ONLY); 5173 } 5174 case IMAGES_MEDIA_ID_THUMBNAIL: { 5175 final long imageId = Long.parseLong(uri.getPathSegments().get(3)); 5176 final Uri targetUri = ContentUris 5177 .withAppendedId(Images.Media.getContentUri(volumeName), imageId); 5178 return ParcelFileDescriptor.open(ensureThumbnail(targetUri, signal), 5179 ParcelFileDescriptor.MODE_READ_ONLY); 5180 } 5181 } 5182 5183 return openFileAndEnforcePathPermissionsHelper(uri, match, mode, signal); 5184 } 5185 5186 @Override openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)5187 public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) 5188 throws FileNotFoundException { 5189 return openTypedAssetFileCommon(uri, mimeTypeFilter, opts, null); 5190 } 5191 5192 @Override openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)5193 public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts, 5194 CancellationSignal signal) throws FileNotFoundException { 5195 return openTypedAssetFileCommon(uri, mimeTypeFilter, opts, signal); 5196 } 5197 openTypedAssetFileCommon(Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)5198 private AssetFileDescriptor openTypedAssetFileCommon(Uri uri, String mimeTypeFilter, 5199 Bundle opts, CancellationSignal signal) throws FileNotFoundException { 5200 uri = safeUncanonicalize(uri); 5201 5202 // TODO: enforce that caller has access to this uri 5203 5204 // Offer thumbnail of media, when requested 5205 final boolean wantsThumb = (opts != null) && opts.containsKey(ContentResolver.EXTRA_SIZE) 5206 && (mimeTypeFilter != null) && mimeTypeFilter.startsWith("image/"); 5207 if (wantsThumb) { 5208 final File thumbFile = ensureThumbnail(uri, signal); 5209 return new AssetFileDescriptor( 5210 ParcelFileDescriptor.open(thumbFile, ParcelFileDescriptor.MODE_READ_ONLY), 5211 0, AssetFileDescriptor.UNKNOWN_LENGTH); 5212 } 5213 5214 // Worst case, return the underlying file 5215 return new AssetFileDescriptor(openFileCommon(uri, "r", signal), 0, 5216 AssetFileDescriptor.UNKNOWN_LENGTH); 5217 } 5218 ensureThumbnail(Uri uri, CancellationSignal signal)5219 private File ensureThumbnail(Uri uri, CancellationSignal signal) throws FileNotFoundException { 5220 final boolean allowHidden = isCallingPackageAllowedHidden(); 5221 final int match = matchUri(uri, allowHidden); 5222 5223 Trace.traceBegin(TRACE_TAG_DATABASE, "ensureThumbnail"); 5224 final LocalCallingIdentity token = clearLocalCallingIdentity(); 5225 try { 5226 final File thumbFile; 5227 switch (match) { 5228 case AUDIO_ALBUMS_ID: { 5229 final String volumeName = MediaStore.getVolumeName(uri); 5230 final Uri baseUri = MediaStore.Audio.Media.getContentUri(volumeName); 5231 final long albumId = ContentUris.parseId(uri); 5232 try (Cursor c = query(baseUri, new String[] { MediaStore.Audio.Media._ID }, 5233 MediaStore.Audio.Media.ALBUM_ID + "=" + albumId, null, null, signal)) { 5234 if (c.moveToFirst()) { 5235 final long audioId = c.getLong(0); 5236 final Uri targetUri = ContentUris.withAppendedId(baseUri, audioId); 5237 return mAudioThumbnailer.ensureThumbnail(targetUri, signal); 5238 } else { 5239 throw new FileNotFoundException("No media for album " + uri); 5240 } 5241 } 5242 } 5243 case AUDIO_MEDIA_ID: 5244 return mAudioThumbnailer.ensureThumbnail(uri, signal); 5245 case VIDEO_MEDIA_ID: 5246 return mVideoThumbnailer.ensureThumbnail(uri, signal); 5247 case IMAGES_MEDIA_ID: 5248 return mImageThumbnailer.ensureThumbnail(uri, signal); 5249 default: 5250 throw new FileNotFoundException(); 5251 } 5252 } catch (IOException e) { 5253 Log.w(TAG, e); 5254 throw new FileNotFoundException(e.getMessage()); 5255 } finally { 5256 restoreLocalCallingIdentity(token); 5257 Trace.traceEnd(TRACE_TAG_DATABASE); 5258 } 5259 } 5260 5261 /** 5262 * Update the metadata columns for the image residing at given {@link Uri} 5263 * by reading data from the underlying image. 5264 */ updateImageMetadata(ContentValues values, File file)5265 private void updateImageMetadata(ContentValues values, File file) { 5266 final BitmapFactory.Options bitmapOpts = new BitmapFactory.Options(); 5267 bitmapOpts.inJustDecodeBounds = true; 5268 BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOpts); 5269 5270 values.put(MediaColumns.WIDTH, bitmapOpts.outWidth); 5271 values.put(MediaColumns.HEIGHT, bitmapOpts.outHeight); 5272 } 5273 5274 /** 5275 * Return the {@link MediaColumns#DATA} field for the given {@code Uri}. 5276 */ queryForDataFile(Uri uri, CancellationSignal signal)5277 File queryForDataFile(Uri uri, CancellationSignal signal) 5278 throws FileNotFoundException { 5279 return queryForDataFile(uri, null, null, signal); 5280 } 5281 5282 /** 5283 * Return the {@link MediaColumns#DATA} field for the given {@code Uri}. 5284 */ queryForDataFile(Uri uri, String selection, String[] selectionArgs, CancellationSignal signal)5285 File queryForDataFile(Uri uri, String selection, String[] selectionArgs, 5286 CancellationSignal signal) throws FileNotFoundException { 5287 try (Cursor cursor = queryForSingleItem(uri, new String[] { MediaColumns.DATA }, 5288 selection, selectionArgs, signal)) { 5289 final String data = cursor.getString(0); 5290 if (TextUtils.isEmpty(data)) { 5291 throw new FileNotFoundException("Missing path for " + uri); 5292 } else { 5293 return new File(data); 5294 } 5295 } 5296 } 5297 5298 /** 5299 * Return the {@link Uri} for the given {@code File}. 5300 */ queryForMediaUri(File file, CancellationSignal signal)5301 Uri queryForMediaUri(File file, CancellationSignal signal) throws FileNotFoundException { 5302 final String volumeName = MediaStore.getVolumeName(file); 5303 final Uri uri = Files.getContentUri(volumeName); 5304 try (Cursor cursor = queryForSingleItem(uri, new String[] { MediaColumns._ID }, 5305 MediaColumns.DATA + "=?", new String[] { file.getAbsolutePath() }, signal)) { 5306 return ContentUris.withAppendedId(uri, cursor.getLong(0)); 5307 } 5308 } 5309 5310 /** 5311 * Query the given {@link Uri}, expecting only a single item to be found. 5312 * 5313 * @throws FileNotFoundException if no items were found, or multiple items 5314 * were found, or there was trouble reading the data. 5315 */ queryForSingleItem(Uri uri, String[] projection, String selection, String[] selectionArgs, CancellationSignal signal)5316 Cursor queryForSingleItem(Uri uri, String[] projection, String selection, 5317 String[] selectionArgs, CancellationSignal signal) throws FileNotFoundException { 5318 final Cursor c = query(uri, projection, 5319 ContentResolver.createSqlQueryBundle(selection, selectionArgs, null), signal); 5320 if (c == null) { 5321 throw new FileNotFoundException("Missing cursor for " + uri); 5322 } else if (c.getCount() < 1) { 5323 IoUtils.closeQuietly(c); 5324 throw new FileNotFoundException("No item at " + uri); 5325 } else if (c.getCount() > 1) { 5326 IoUtils.closeQuietly(c); 5327 throw new FileNotFoundException("Multiple items at " + uri); 5328 } 5329 5330 if (c.moveToFirst()) { 5331 return c; 5332 } else { 5333 IoUtils.closeQuietly(c); 5334 throw new FileNotFoundException("Failed to read row from " + uri); 5335 } 5336 } 5337 5338 /** 5339 * Replacement for {@link #openFileHelper(Uri, String)} which enforces any 5340 * permissions applicable to the path before returning. 5341 */ openFileAndEnforcePathPermissionsHelper(Uri uri, int match, String mode, CancellationSignal signal)5342 private ParcelFileDescriptor openFileAndEnforcePathPermissionsHelper(Uri uri, int match, 5343 String mode, CancellationSignal signal) throws FileNotFoundException { 5344 final int modeBits = ParcelFileDescriptor.parseMode(mode); 5345 final boolean forWrite = (modeBits != ParcelFileDescriptor.MODE_READ_ONLY); 5346 5347 final boolean hasOwnerPackageName = hasOwnerPackageName(uri); 5348 final String[] projection = new String[] { 5349 MediaColumns.DATA, 5350 hasOwnerPackageName ? MediaColumns.OWNER_PACKAGE_NAME : "NULL", 5351 hasOwnerPackageName ? MediaColumns.IS_PENDING : "0", 5352 }; 5353 5354 final File file; 5355 final String ownerPackageName; 5356 final boolean isPending; 5357 final LocalCallingIdentity token = clearLocalCallingIdentity(); 5358 try (Cursor c = queryForSingleItem(uri, projection, null, null, signal)) { 5359 final String data = c.getString(0); 5360 if (TextUtils.isEmpty(data)) { 5361 throw new FileNotFoundException("Missing path for " + uri); 5362 } else { 5363 file = new File(data).getCanonicalFile(); 5364 } 5365 ownerPackageName = c.getString(1); 5366 isPending = c.getInt(2) != 0; 5367 } catch (IOException e) { 5368 throw new FileNotFoundException(e.toString()); 5369 } finally { 5370 restoreLocalCallingIdentity(token); 5371 } 5372 5373 checkAccess(uri, file, forWrite); 5374 5375 // Require ownership if item is still pending 5376 final boolean hasOwner = (ownerPackageName != null); 5377 final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), ownerPackageName); 5378 if (isPending && hasOwner && !callerIsOwner) { 5379 throw new IllegalStateException( 5380 "Only owner is able to interact with pending media " + uri); 5381 } 5382 5383 // Figure out if we need to redact contents 5384 final boolean redactionNeeded = callerIsOwner ? false : isRedactionNeeded(uri); 5385 final RedactionInfo redactionInfo = redactionNeeded ? getRedactionRanges(file) 5386 : new RedactionInfo(EmptyArray.LONG, EmptyArray.LONG); 5387 5388 // Yell if caller requires original, since we can't give it to them 5389 // unless they have access granted above 5390 if (redactionNeeded 5391 && parseBoolean(uri.getQueryParameter(MediaStore.PARAM_REQUIRE_ORIGINAL))) { 5392 throw new UnsupportedOperationException( 5393 "Caller must hold ACCESS_MEDIA_LOCATION permission to access original"); 5394 } 5395 5396 // Kick off metadata update when writing is finished 5397 final OnCloseListener listener = (e) -> { 5398 // We always update metadata to reflect the state on disk, even when 5399 // the remote writer tried claiming an exception 5400 invalidateThumbnails(uri); 5401 5402 try { 5403 switch (match) { 5404 case IMAGES_THUMBNAILS_ID: 5405 case VIDEO_THUMBNAILS_ID: 5406 final ContentValues values = new ContentValues(); 5407 updateImageMetadata(values, file); 5408 update(uri, values, null, null); 5409 break; 5410 default: 5411 MediaScanner.instance(getContext()).scanFile(file); 5412 break; 5413 } 5414 } catch (Exception e2) { 5415 Log.w(TAG, "Failed to update metadata for " + uri, e2); 5416 } 5417 }; 5418 5419 try { 5420 // First, handle any redaction that is needed for caller 5421 final ParcelFileDescriptor pfd; 5422 if (redactionInfo.redactionRanges.length > 0) { 5423 pfd = RedactingFileDescriptor.open( 5424 getContext(), 5425 file, 5426 modeBits, 5427 redactionInfo.redactionRanges, 5428 redactionInfo.freeOffsets); 5429 } else { 5430 pfd = ParcelFileDescriptor.open(file, modeBits); 5431 } 5432 5433 // Second, wrap in any listener that we've requested 5434 if (!isPending && forWrite && listener != null) { 5435 return ParcelFileDescriptor.fromPfd(pfd, BackgroundThread.getHandler(), listener); 5436 } else { 5437 return pfd; 5438 } 5439 } catch (IOException e) { 5440 if (e instanceof FileNotFoundException) { 5441 throw (FileNotFoundException) e; 5442 } else { 5443 throw new IllegalStateException(e); 5444 } 5445 } 5446 } 5447 deleteIfAllowed(Uri uri, String path)5448 private void deleteIfAllowed(Uri uri, String path) { 5449 try { 5450 final File file = new File(path); 5451 checkAccess(uri, file, true); 5452 file.delete(); 5453 } catch (Exception e) { 5454 Log.e(TAG, "Couldn't delete " + path, e); 5455 } 5456 } 5457 5458 @Deprecated isPending(Uri uri)5459 private boolean isPending(Uri uri) { 5460 final int match = matchUri(uri, true); 5461 switch (match) { 5462 case AUDIO_MEDIA_ID: 5463 case VIDEO_MEDIA_ID: 5464 case IMAGES_MEDIA_ID: 5465 try (Cursor c = queryForSingleItem(uri, 5466 new String[] { MediaColumns.IS_PENDING }, null, null, null)) { 5467 return (c.getInt(0) != 0); 5468 } catch (FileNotFoundException e) { 5469 throw new IllegalStateException(e); 5470 } 5471 default: 5472 return false; 5473 } 5474 } 5475 5476 @Deprecated isRedactionNeeded(Uri uri)5477 private boolean isRedactionNeeded(Uri uri) { 5478 return mCallingIdentity.get().hasPermission(PERMISSION_IS_REDACTION_NEEDED); 5479 } 5480 5481 /** 5482 * Set of Exif tags that should be considered for redaction. 5483 */ 5484 private static final String[] REDACTED_EXIF_TAGS = new String[] { 5485 ExifInterface.TAG_GPS_ALTITUDE, 5486 ExifInterface.TAG_GPS_ALTITUDE_REF, 5487 ExifInterface.TAG_GPS_AREA_INFORMATION, 5488 ExifInterface.TAG_GPS_DOP, 5489 ExifInterface.TAG_GPS_DATESTAMP, 5490 ExifInterface.TAG_GPS_DEST_BEARING, 5491 ExifInterface.TAG_GPS_DEST_BEARING_REF, 5492 ExifInterface.TAG_GPS_DEST_DISTANCE, 5493 ExifInterface.TAG_GPS_DEST_DISTANCE_REF, 5494 ExifInterface.TAG_GPS_DEST_LATITUDE, 5495 ExifInterface.TAG_GPS_DEST_LATITUDE_REF, 5496 ExifInterface.TAG_GPS_DEST_LONGITUDE, 5497 ExifInterface.TAG_GPS_DEST_LONGITUDE_REF, 5498 ExifInterface.TAG_GPS_DIFFERENTIAL, 5499 ExifInterface.TAG_GPS_IMG_DIRECTION, 5500 ExifInterface.TAG_GPS_IMG_DIRECTION_REF, 5501 ExifInterface.TAG_GPS_LATITUDE, 5502 ExifInterface.TAG_GPS_LATITUDE_REF, 5503 ExifInterface.TAG_GPS_LONGITUDE, 5504 ExifInterface.TAG_GPS_LONGITUDE_REF, 5505 ExifInterface.TAG_GPS_MAP_DATUM, 5506 ExifInterface.TAG_GPS_MEASURE_MODE, 5507 ExifInterface.TAG_GPS_PROCESSING_METHOD, 5508 ExifInterface.TAG_GPS_SATELLITES, 5509 ExifInterface.TAG_GPS_SPEED, 5510 ExifInterface.TAG_GPS_SPEED_REF, 5511 ExifInterface.TAG_GPS_STATUS, 5512 ExifInterface.TAG_GPS_TIMESTAMP, 5513 ExifInterface.TAG_GPS_TRACK, 5514 ExifInterface.TAG_GPS_TRACK_REF, 5515 ExifInterface.TAG_GPS_VERSION_ID, 5516 }; 5517 5518 /** 5519 * Set of ISO boxes that should be considered for redaction. 5520 */ 5521 private static final int[] REDACTED_ISO_BOXES = new int[] { 5522 IsoInterface.BOX_LOCI, 5523 IsoInterface.BOX_XYZ, 5524 IsoInterface.BOX_GPS, 5525 IsoInterface.BOX_GPS0, 5526 }; 5527 5528 private static final class RedactionInfo { 5529 public final long[] redactionRanges; 5530 public final long[] freeOffsets; RedactionInfo(long[] redactionRanges, long[] freeOffsets)5531 public RedactionInfo(long[] redactionRanges, long[] freeOffsets) { 5532 this.redactionRanges = redactionRanges; 5533 this.freeOffsets = freeOffsets; 5534 } 5535 } 5536 getRedactionRanges(File file)5537 private RedactionInfo getRedactionRanges(File file) { 5538 Trace.traceBegin(TRACE_TAG_DATABASE, "getRedactionRanges"); 5539 final LongArray res = new LongArray(); 5540 final LongArray freeOffsets = new LongArray(); 5541 try (FileInputStream is = new FileInputStream(file)) { 5542 final ExifInterface exif = new ExifInterface(is.getFD()); 5543 for (String tag : REDACTED_EXIF_TAGS) { 5544 final long[] range = exif.getAttributeRange(tag); 5545 if (range != null) { 5546 res.add(range[0]); 5547 res.add(range[0] + range[1]); 5548 } 5549 } 5550 5551 final IsoInterface iso = IsoInterface.fromFileDescriptor(is.getFD()); 5552 for (int box : REDACTED_ISO_BOXES) { 5553 final long[] ranges = iso.getBoxRanges(box); 5554 for (int i = 0; i < ranges.length; i += 2) { 5555 long boxTypeOffset = ranges[i] - 4; 5556 freeOffsets.add(boxTypeOffset); 5557 res.add(boxTypeOffset); 5558 res.add(ranges[i + 1]); 5559 } 5560 } 5561 5562 // Redact xmp where present 5563 final Set<String> redactedXmpTags = new ArraySet<>(Arrays.asList(REDACTED_EXIF_TAGS)); 5564 final XmpInterface exifXmp = XmpInterface.fromContainer(exif, redactedXmpTags); 5565 res.addAll(exifXmp.getRedactionRanges()); 5566 final XmpInterface isoXmp = XmpInterface.fromContainer(iso, redactedXmpTags); 5567 res.addAll(isoXmp.getRedactionRanges()); 5568 } catch (IOException e) { 5569 Log.w(TAG, "Failed to redact " + file + ": " + e); 5570 } 5571 Trace.traceEnd(TRACE_TAG_DATABASE); 5572 return new RedactionInfo(res.toArray(), freeOffsets.toArray()); 5573 } 5574 checkCallingPermissionGlobal(Uri uri, boolean forWrite)5575 private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) { 5576 // System internals can work with all media 5577 if (isCallingPackageSystem()) { 5578 return true; 5579 } 5580 5581 // Check if caller is known to be owner of this item, to speed up 5582 // performance of our permission checks 5583 final int table = matchUri(uri, true); 5584 switch (table) { 5585 case AUDIO_MEDIA_ID: 5586 case VIDEO_MEDIA_ID: 5587 case IMAGES_MEDIA_ID: 5588 case FILES_ID: 5589 case DOWNLOADS_ID: 5590 final long id = ContentUris.parseId(uri); 5591 if (mCallingIdentity.get().isOwned(id)) { 5592 return true; 5593 } 5594 } 5595 5596 // Outstanding grant means they get access 5597 if (getContext().checkUriPermission(uri, mCallingIdentity.get().pid, 5598 mCallingIdentity.get().uid, forWrite 5599 ? Intent.FLAG_GRANT_WRITE_URI_PERMISSION 5600 : Intent.FLAG_GRANT_READ_URI_PERMISSION) == PERMISSION_GRANTED) { 5601 return true; 5602 } 5603 5604 return false; 5605 } 5606 checkCallingPermissionLegacy(Uri uri, boolean forWrite, String callingPackage)5607 private boolean checkCallingPermissionLegacy(Uri uri, boolean forWrite, String callingPackage) { 5608 return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY); 5609 } 5610 5611 @Deprecated checkCallingPermissionAudio(boolean forWrite, String callingPackage)5612 private boolean checkCallingPermissionAudio(boolean forWrite, String callingPackage) { 5613 if (forWrite) { 5614 return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_AUDIO); 5615 } else { 5616 return mCallingIdentity.get().hasPermission(PERMISSION_READ_AUDIO); 5617 } 5618 } 5619 5620 @Deprecated checkCallingPermissionVideo(boolean forWrite, String callingPackage)5621 private boolean checkCallingPermissionVideo(boolean forWrite, String callingPackage) { 5622 if (forWrite) { 5623 return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_VIDEO); 5624 } else { 5625 return mCallingIdentity.get().hasPermission(PERMISSION_READ_VIDEO); 5626 } 5627 } 5628 5629 @Deprecated checkCallingPermissionImages(boolean forWrite, String callingPackage)5630 private boolean checkCallingPermissionImages(boolean forWrite, String callingPackage) { 5631 if (forWrite) { 5632 return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_IMAGES); 5633 } else { 5634 return mCallingIdentity.get().hasPermission(PERMISSION_READ_IMAGES); 5635 } 5636 } 5637 5638 /** 5639 * Enforce that caller has access to the given {@link Uri}. 5640 * 5641 * @throws SecurityException if access isn't allowed. 5642 */ enforceCallingPermission(Uri uri, boolean forWrite)5643 private void enforceCallingPermission(Uri uri, boolean forWrite) { 5644 Trace.traceBegin(TRACE_TAG_DATABASE, "enforceCallingPermission"); 5645 try { 5646 enforceCallingPermissionInternal(uri, forWrite); 5647 } finally { 5648 Trace.traceEnd(TRACE_TAG_DATABASE); 5649 } 5650 } 5651 enforceCallingPermissionInternal(Uri uri, boolean forWrite)5652 private void enforceCallingPermissionInternal(Uri uri, boolean forWrite) { 5653 // Try a simple global check first before falling back to performing a 5654 // simple query to probe for access. 5655 if (checkCallingPermissionGlobal(uri, forWrite)) { 5656 // Access allowed, yay! 5657 return; 5658 } 5659 5660 final DatabaseHelper helper; 5661 final SQLiteDatabase db; 5662 try { 5663 helper = getDatabaseForUri(uri); 5664 db = helper.getReadableDatabase(); 5665 } catch (VolumeNotFoundException e) { 5666 throw e.rethrowAsIllegalArgumentException(); 5667 } 5668 5669 final boolean allowHidden = isCallingPackageAllowedHidden(); 5670 final int table = matchUri(uri, allowHidden); 5671 5672 // First, check to see if caller has direct write access 5673 if (forWrite) { 5674 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, uri, table, null); 5675 try (Cursor c = qb.query(db, new String[0], null, null, null, null, null)) { 5676 if (c.moveToFirst()) { 5677 // Direct write access granted, yay! 5678 return; 5679 } 5680 } 5681 } 5682 5683 // We only allow the user to grant access to specific media items in 5684 // strongly typed collections; never to broad collections 5685 boolean allowUserGrant = false; 5686 final int matchUri = matchUri(uri, true); 5687 switch (matchUri) { 5688 case IMAGES_MEDIA_ID: 5689 case AUDIO_MEDIA_ID: 5690 case VIDEO_MEDIA_ID: 5691 allowUserGrant = true; 5692 break; 5693 } 5694 5695 // Second, check to see if caller has direct read access 5696 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, uri, table, null); 5697 try (Cursor c = qb.query(db, new String[0], null, null, null, null, null)) { 5698 if (c.moveToFirst()) { 5699 if (!forWrite) { 5700 // Direct read access granted, yay! 5701 return; 5702 } else if (allowUserGrant) { 5703 // Caller has read access, but they wanted to write, and 5704 // they'll need to get the user to grant that access 5705 final Context context = getContext(); 5706 final PendingIntent intent = PendingIntent.getActivity(context, 42, 5707 new Intent(null, uri, context, PermissionActivity.class), 5708 FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE); 5709 5710 final Icon icon = getCollectionIcon(uri); 5711 final RemoteAction action = new RemoteAction(icon, 5712 context.getText(R.string.permission_required_action), 5713 context.getText(R.string.permission_required_action), 5714 intent); 5715 5716 throw new RecoverableSecurityException(new SecurityException( 5717 getCallingPackageOrSelf() + " has no access to " + uri), 5718 context.getText(R.string.permission_required), action); 5719 } 5720 } 5721 } 5722 5723 throw new SecurityException(getCallingPackageOrSelf() + " has no access to " + uri); 5724 } 5725 getCollectionIcon(Uri uri)5726 private Icon getCollectionIcon(Uri uri) { 5727 final PackageManager pm = getContext().getPackageManager(); 5728 final String type = uri.getPathSegments().get(1); 5729 final String groupName; 5730 switch (type) { 5731 default: groupName = android.Manifest.permission_group.STORAGE; break; 5732 } 5733 try { 5734 final PermissionGroupInfo perm = pm.getPermissionGroupInfo(groupName, 0); 5735 return Icon.createWithResource(perm.packageName, perm.icon); 5736 } catch (NameNotFoundException e) { 5737 throw new RuntimeException(e); 5738 } 5739 } 5740 checkAccess(Uri uri, File file, boolean isWrite)5741 private void checkAccess(Uri uri, File file, boolean isWrite) throws FileNotFoundException { 5742 // First, does caller have the needed row-level access? 5743 enforceCallingPermission(uri, isWrite); 5744 5745 // Second, does the path look sane? 5746 if (!FileUtils.contains(Environment.getStorageDirectory(), file)) { 5747 checkWorldReadAccess(file.getAbsolutePath()); 5748 } 5749 } 5750 5751 /** 5752 * Check whether the path is a world-readable file 5753 */ checkWorldReadAccess(String path)5754 private static void checkWorldReadAccess(String path) throws FileNotFoundException { 5755 // Path has already been canonicalized, and we relax the check to look 5756 // at groups to support runtime storage permissions. 5757 final int accessBits = path.startsWith("/storage/") ? OsConstants.S_IRGRP 5758 : OsConstants.S_IROTH; 5759 try { 5760 StructStat stat = Os.stat(path); 5761 if (OsConstants.S_ISREG(stat.st_mode) && 5762 ((stat.st_mode & accessBits) == accessBits)) { 5763 checkLeadingPathComponentsWorldExecutable(path); 5764 return; 5765 } 5766 } catch (ErrnoException e) { 5767 // couldn't stat the file, either it doesn't exist or isn't 5768 // accessible to us 5769 } 5770 5771 throw new FileNotFoundException("Can't access " + path); 5772 } 5773 checkLeadingPathComponentsWorldExecutable(String filePath)5774 private static void checkLeadingPathComponentsWorldExecutable(String filePath) 5775 throws FileNotFoundException { 5776 File parent = new File(filePath).getParentFile(); 5777 5778 // Path has already been canonicalized, and we relax the check to look 5779 // at groups to support runtime storage permissions. 5780 final int accessBits = filePath.startsWith("/storage/") ? OsConstants.S_IXGRP 5781 : OsConstants.S_IXOTH; 5782 5783 while (parent != null) { 5784 if (! parent.exists()) { 5785 // parent dir doesn't exist, give up 5786 throw new FileNotFoundException("access denied"); 5787 } 5788 try { 5789 StructStat stat = Os.stat(parent.getPath()); 5790 if ((stat.st_mode & accessBits) != accessBits) { 5791 // the parent dir doesn't have the appropriate access 5792 throw new FileNotFoundException("Can't access " + filePath); 5793 } 5794 } catch (ErrnoException e1) { 5795 // couldn't stat() parent 5796 throw new FileNotFoundException("Can't access " + filePath); 5797 } 5798 parent = parent.getParentFile(); 5799 } 5800 } 5801 5802 /** 5803 * Look up the artist or album entry for the given name, creating that entry 5804 * if it does not already exists. 5805 * @param db The database 5806 * @param table The table to store the key/name pair in. 5807 * @param keyField The name of the key-column 5808 * @param nameField The name of the name-column 5809 * @param rawName The name that the calling app was trying to insert into the database 5810 * @param cacheName The string that will be inserted in to the cache 5811 * @param path The full path to the file being inserted in to the audio table 5812 * @param albumHash A hash to distinguish between different albums of the same name 5813 * @param artist The name of the artist, if known 5814 * @param cache The cache to add this entry to 5815 * @param srcuri The Uri that prompted the call to this method, used for determining whether this is 5816 * the internal or external database 5817 * @return The row ID for this artist/album, or -1 if the provided name was invalid 5818 */ getKeyIdForName(DatabaseHelper helper, SQLiteDatabase db, String table, String keyField, String nameField, String rawName, String cacheName, String path, int albumHash, String artist, ArrayMap<String, Long> cache, Uri srcuri)5819 private long getKeyIdForName(DatabaseHelper helper, SQLiteDatabase db, 5820 String table, String keyField, String nameField, 5821 String rawName, String cacheName, String path, int albumHash, 5822 String artist, ArrayMap<String, Long> cache, Uri srcuri) { 5823 long rowId; 5824 5825 if (rawName == null || rawName.length() == 0) { 5826 rawName = MediaStore.UNKNOWN_STRING; 5827 } 5828 String k = MediaStore.Audio.keyFor(rawName); 5829 5830 if (k == null) { 5831 // shouldn't happen, since we only get null keys for null inputs 5832 Log.e(TAG, "null key", new Exception()); 5833 return -1; 5834 } 5835 5836 boolean isAlbum = table.equals("albums"); 5837 boolean isUnknown = MediaStore.UNKNOWN_STRING.equals(rawName); 5838 5839 // To distinguish same-named albums, we append a hash. The hash is based 5840 // on the "album artist" tag if present, otherwise on the "compilation" tag 5841 // if present, otherwise on the path. 5842 // Ideally we would also take things like CDDB ID in to account, so 5843 // we can group files from the same album that aren't in the same 5844 // folder, but this is a quick and easy start that works immediately 5845 // without requiring support from the mp3, mp4 and Ogg meta data 5846 // readers, as long as the albums are in different folders. 5847 if (isAlbum) { 5848 k = k + albumHash; 5849 if (isUnknown) { 5850 k = k + artist; 5851 } 5852 } 5853 5854 String [] selargs = { k }; 5855 Cursor c = db.query(table, null, keyField + "=?", selargs, null, null, null); 5856 5857 try { 5858 switch (c.getCount()) { 5859 case 0: { 5860 // insert new entry into table 5861 ContentValues otherValues = new ContentValues(); 5862 otherValues.put(keyField, k); 5863 otherValues.put(nameField, rawName); 5864 rowId = db.insert(table, "duration", otherValues); 5865 if (rowId > 0) { 5866 String volume = srcuri.toString().substring(16, 24); // extract internal/external 5867 Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId); 5868 getContext().getContentResolver().notifyChange(uri, null); 5869 } 5870 } 5871 break; 5872 case 1: { 5873 // Use the existing entry 5874 c.moveToFirst(); 5875 rowId = c.getLong(0); 5876 5877 // Determine whether the current rawName is better than what's 5878 // currently stored in the table, and update the table if it is. 5879 String currentFancyName = c.getString(2); 5880 String bestName = makeBestName(rawName, currentFancyName); 5881 if (!bestName.equals(currentFancyName)) { 5882 // update the table with the new name 5883 ContentValues newValues = new ContentValues(); 5884 newValues.put(nameField, bestName); 5885 db.update(table, newValues, "rowid="+Integer.toString((int)rowId), null); 5886 String volume = srcuri.toString().substring(16, 24); // extract internal/external 5887 Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId); 5888 getContext().getContentResolver().notifyChange(uri, null); 5889 // We have to remove the previous key from the cache otherwise we will 5890 // not be able to change between upper and lower case letters. 5891 if (isAlbum) { 5892 cache.remove(currentFancyName + albumHash); 5893 } else { 5894 cache.remove(currentFancyName); 5895 } 5896 } 5897 } 5898 break; 5899 default: 5900 // corrupt database 5901 Log.e(TAG, "Multiple entries in table " + table + " for key " + k); 5902 rowId = -1; 5903 break; 5904 } 5905 } finally { 5906 IoUtils.closeQuietly(c); 5907 } 5908 5909 if (cache != null && ! isUnknown) { 5910 cache.put(cacheName, rowId); 5911 } 5912 return rowId; 5913 } 5914 5915 /** 5916 * Returns the best string to use for display, given two names. 5917 * Note that this function does not necessarily return either one 5918 * of the provided names; it may decide to return a better alternative 5919 * (for example, specifying the inputs "Police" and "Police, The" will 5920 * return "The Police") 5921 * 5922 * The basic assumptions are: 5923 * - longer is better ("The police" is better than "Police") 5924 * - prefix is better ("The Police" is better than "Police, The") 5925 * - accents are better ("Motörhead" is better than "Motorhead") 5926 * 5927 * @param one The first of the two names to consider 5928 * @param two The last of the two names to consider 5929 * @return The actual name to use 5930 */ makeBestName(String one, String two)5931 String makeBestName(String one, String two) { 5932 String name; 5933 5934 // Longer names are usually better. 5935 if (one.length() > two.length()) { 5936 name = one; 5937 } else { 5938 // Names with accents are usually better, and conveniently sort later 5939 if (one.toLowerCase().compareTo(two.toLowerCase()) >= 0) { 5940 name = one; 5941 } else { 5942 name = two; 5943 } 5944 } 5945 5946 // Prefixes are better than postfixes. 5947 if (name.endsWith(", the") || name.endsWith(",the") || 5948 name.endsWith(", an") || name.endsWith(",an") || 5949 name.endsWith(", a") || name.endsWith(",a")) { 5950 String fix = name.substring(1 + name.lastIndexOf(',')); 5951 name = fix.trim() + " " + name.substring(0, name.lastIndexOf(',')); 5952 } 5953 5954 // TODO: word-capitalize the resulting name 5955 return name; 5956 } 5957 5958 private static class FallbackException extends Exception { FallbackException(String message)5959 public FallbackException(String message) { 5960 super(message); 5961 } 5962 rethrowAsIllegalArgumentException()5963 public IllegalArgumentException rethrowAsIllegalArgumentException() { 5964 throw new IllegalArgumentException(getMessage()); 5965 } 5966 translateForQuery(int targetSdkVersion)5967 public Cursor translateForQuery(int targetSdkVersion) { 5968 if (targetSdkVersion >= Build.VERSION_CODES.Q) { 5969 throw new IllegalArgumentException(getMessage()); 5970 } else { 5971 Log.w(TAG, getMessage()); 5972 return null; 5973 } 5974 } 5975 translateForInsert(int targetSdkVersion)5976 public Uri translateForInsert(int targetSdkVersion) { 5977 if (targetSdkVersion >= Build.VERSION_CODES.Q) { 5978 throw new IllegalArgumentException(getMessage()); 5979 } else { 5980 Log.w(TAG, getMessage()); 5981 return null; 5982 } 5983 } 5984 translateForUpdateDelete(int targetSdkVersion)5985 public int translateForUpdateDelete(int targetSdkVersion) { 5986 if (targetSdkVersion >= Build.VERSION_CODES.Q) { 5987 throw new IllegalArgumentException(getMessage()); 5988 } else { 5989 Log.w(TAG, getMessage()); 5990 return 0; 5991 } 5992 } 5993 } 5994 5995 static class VolumeNotFoundException extends FallbackException { VolumeNotFoundException(String volumeName)5996 public VolumeNotFoundException(String volumeName) { 5997 super("Volume " + volumeName + " not found"); 5998 } 5999 } 6000 6001 static class VolumeArgumentException extends FallbackException { VolumeArgumentException(File actual, Collection<File> allowed)6002 public VolumeArgumentException(File actual, Collection<File> allowed) { 6003 super("Requested path " + actual + " doesn't appear under " + allowed); 6004 } 6005 } 6006 getDatabaseForUri(Uri uri)6007 private @NonNull DatabaseHelper getDatabaseForUri(Uri uri) throws VolumeNotFoundException { 6008 final String volumeName = resolveVolumeName(uri); 6009 synchronized (mAttachedVolumeNames) { 6010 if (!mAttachedVolumeNames.contains(volumeName)) { 6011 throw new VolumeNotFoundException(volumeName); 6012 } 6013 } 6014 if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) { 6015 return mInternalDatabase; 6016 } else { 6017 return mExternalDatabase; 6018 } 6019 } 6020 isMediaDatabaseName(String name)6021 static boolean isMediaDatabaseName(String name) { 6022 if (INTERNAL_DATABASE_NAME.equals(name)) { 6023 return true; 6024 } 6025 if (EXTERNAL_DATABASE_NAME.equals(name)) { 6026 return true; 6027 } 6028 if (name.startsWith("external-") && name.endsWith(".db")) { 6029 return true; 6030 } 6031 return false; 6032 } 6033 isInternalMediaDatabaseName(String name)6034 static boolean isInternalMediaDatabaseName(String name) { 6035 if (INTERNAL_DATABASE_NAME.equals(name)) { 6036 return true; 6037 } 6038 return false; 6039 } 6040 attachVolume(Uri uri)6041 private void attachVolume(Uri uri) { 6042 attachVolume(MediaStore.getVolumeName(uri)); 6043 } 6044 attachVolume(String volume)6045 public Uri attachVolume(String volume) { 6046 if (mCallingIdentity.get().pid != android.os.Process.myPid()) { 6047 throw new SecurityException( 6048 "Opening and closing databases not allowed."); 6049 } 6050 6051 // Quick sanity check for shady volume names 6052 MediaStore.checkArgumentVolumeName(volume); 6053 6054 // Quick sanity check that volume actually exists 6055 if (!MediaStore.VOLUME_INTERNAL.equals(volume)) { 6056 try { 6057 getVolumePath(volume); 6058 } catch (IOException e) { 6059 throw new IllegalArgumentException( 6060 "Volume " + volume + " currently unavailable", e); 6061 } 6062 } 6063 6064 synchronized (mAttachedVolumeNames) { 6065 mAttachedVolumeNames.add(volume); 6066 } 6067 6068 final Uri uri = MediaStore.AUTHORITY_URI.buildUpon().appendPath(volume).build(); 6069 getContext().getContentResolver().notifyChange(uri, null); 6070 if (LOCAL_LOGV) Log.v(TAG, "Attached volume: " + volume); 6071 if (!MediaStore.VOLUME_INTERNAL.equals(volume)) { 6072 final DatabaseHelper helper = mInternalDatabase; 6073 ensureDefaultFolders(volume, helper, helper.getWritableDatabase()); 6074 } 6075 return uri; 6076 } 6077 detachVolume(Uri uri)6078 private void detachVolume(Uri uri) { 6079 detachVolume(MediaStore.getVolumeName(uri)); 6080 } 6081 detachVolume(String volume)6082 public void detachVolume(String volume) { 6083 if (mCallingIdentity.get().pid != android.os.Process.myPid()) { 6084 throw new SecurityException( 6085 "Opening and closing databases not allowed."); 6086 } 6087 6088 // Quick sanity check for shady volume names 6089 MediaStore.checkArgumentVolumeName(volume); 6090 6091 if (MediaStore.VOLUME_INTERNAL.equals(volume)) { 6092 throw new UnsupportedOperationException( 6093 "Deleting the internal volume is not allowed"); 6094 } 6095 6096 // Signal any scanning to shut down 6097 MediaScanner.instance(getContext()).onDetachVolume(volume); 6098 6099 synchronized (mAttachedVolumeNames) { 6100 mAttachedVolumeNames.remove(volume); 6101 } 6102 6103 final Uri uri = MediaStore.AUTHORITY_URI.buildUpon().appendPath(volume).build(); 6104 getContext().getContentResolver().notifyChange(uri, null); 6105 if (LOCAL_LOGV) Log.v(TAG, "Detached volume: " + volume); 6106 } 6107 6108 /* 6109 * Useful commands to enable debugging: 6110 * $ adb shell setprop log.tag.MediaProvider VERBOSE 6111 * $ adb shell setprop db.log.slow_query_threshold.`adb shell cat \ 6112 * /data/system/packages.list |grep "com.android.providers.media " |cut -b 29-33` 0 6113 * $ adb shell setprop db.log.bindargs 1 6114 */ 6115 6116 static final String TAG = "MediaProvider"; 6117 static final boolean LOCAL_LOGV = Log.isLoggable(TAG, Log.VERBOSE); 6118 6119 private static final String INTERNAL_DATABASE_NAME = "internal.db"; 6120 private static final String EXTERNAL_DATABASE_NAME = "external.db"; 6121 6122 // maximum number of cached external databases to keep 6123 private static final int MAX_EXTERNAL_DATABASES = 3; 6124 6125 // Delete databases that have not been used in two months 6126 // 60 days in milliseconds (1000 * 60 * 60 * 24 * 60) 6127 private static final long OBSOLETE_DATABASE_DB = 5184000000L; 6128 6129 // Memory optimization - close idle connections after 30s of inactivity 6130 private static final int IDLE_CONNECTION_TIMEOUT_MS = 30000; 6131 6132 @GuardedBy("mAttachedVolumeNames") 6133 private final ArraySet<String> mAttachedVolumeNames = new ArraySet<>(); 6134 6135 private DatabaseHelper mInternalDatabase; 6136 private DatabaseHelper mExternalDatabase; 6137 6138 // name of the volume currently being scanned by the media scanner (or null) 6139 private String mMediaScannerVolume; 6140 6141 // current FAT volume ID 6142 private int mVolumeId = -1; 6143 6144 // WARNING: the values of IMAGES_MEDIA, AUDIO_MEDIA, and VIDEO_MEDIA and AUDIO_PLAYLISTS 6145 // are stored in the "files" table, so do not renumber them unless you also add 6146 // a corresponding database upgrade step for it. 6147 private static final int IMAGES_MEDIA = 1; 6148 private static final int IMAGES_MEDIA_ID = 2; 6149 private static final int IMAGES_MEDIA_ID_THUMBNAIL = 3; 6150 private static final int IMAGES_THUMBNAILS = 4; 6151 private static final int IMAGES_THUMBNAILS_ID = 5; 6152 6153 private static final int AUDIO_MEDIA = 100; 6154 private static final int AUDIO_MEDIA_ID = 101; 6155 private static final int AUDIO_MEDIA_ID_GENRES = 102; 6156 private static final int AUDIO_MEDIA_ID_GENRES_ID = 103; 6157 private static final int AUDIO_MEDIA_ID_PLAYLISTS = 104; 6158 private static final int AUDIO_MEDIA_ID_PLAYLISTS_ID = 105; 6159 private static final int AUDIO_GENRES = 106; 6160 private static final int AUDIO_GENRES_ID = 107; 6161 private static final int AUDIO_GENRES_ID_MEMBERS = 108; 6162 private static final int AUDIO_GENRES_ALL_MEMBERS = 109; 6163 private static final int AUDIO_PLAYLISTS = 110; 6164 private static final int AUDIO_PLAYLISTS_ID = 111; 6165 private static final int AUDIO_PLAYLISTS_ID_MEMBERS = 112; 6166 private static final int AUDIO_PLAYLISTS_ID_MEMBERS_ID = 113; 6167 private static final int AUDIO_ARTISTS = 114; 6168 private static final int AUDIO_ARTISTS_ID = 115; 6169 private static final int AUDIO_ALBUMS = 116; 6170 private static final int AUDIO_ALBUMS_ID = 117; 6171 private static final int AUDIO_ARTISTS_ID_ALBUMS = 118; 6172 private static final int AUDIO_ALBUMART = 119; 6173 private static final int AUDIO_ALBUMART_ID = 120; 6174 private static final int AUDIO_ALBUMART_FILE_ID = 121; 6175 6176 private static final int VIDEO_MEDIA = 200; 6177 private static final int VIDEO_MEDIA_ID = 201; 6178 private static final int VIDEO_MEDIA_ID_THUMBNAIL = 202; 6179 private static final int VIDEO_THUMBNAILS = 203; 6180 private static final int VIDEO_THUMBNAILS_ID = 204; 6181 6182 private static final int VOLUMES = 300; 6183 private static final int VOLUMES_ID = 301; 6184 6185 private static final int MEDIA_SCANNER = 500; 6186 6187 private static final int FS_ID = 600; 6188 private static final int VERSION = 601; 6189 6190 private static final int FILES = 700; 6191 private static final int FILES_ID = 701; 6192 6193 // Used only by the MTP implementation 6194 private static final int MTP_OBJECTS = 702; 6195 private static final int MTP_OBJECTS_ID = 703; 6196 private static final int MTP_OBJECT_REFERENCES = 704; 6197 6198 // Used only to invoke special logic for directories 6199 private static final int FILES_DIRECTORY = 706; 6200 6201 private static final int DOWNLOADS = 800; 6202 private static final int DOWNLOADS_ID = 801; 6203 6204 private static final UriMatcher HIDDEN_URI_MATCHER = 6205 new UriMatcher(UriMatcher.NO_MATCH); 6206 6207 private static final UriMatcher PUBLIC_URI_MATCHER = 6208 new UriMatcher(UriMatcher.NO_MATCH); 6209 6210 private static final String[] PATH_PROJECTION = new String[] { 6211 MediaStore.MediaColumns._ID, 6212 MediaStore.MediaColumns.DATA, 6213 }; 6214 6215 private static final String OBJECT_REFERENCES_QUERY = 6216 "SELECT " + Audio.Playlists.Members.AUDIO_ID + " FROM audio_playlists_map" 6217 + " WHERE " + Audio.Playlists.Members.PLAYLIST_ID + "=?" 6218 + " ORDER BY " + Audio.Playlists.Members.PLAY_ORDER; 6219 matchUri(Uri uri, boolean allowHidden)6220 private static int matchUri(Uri uri, boolean allowHidden) { 6221 final int publicMatch = PUBLIC_URI_MATCHER.match(uri); 6222 if (publicMatch != UriMatcher.NO_MATCH) { 6223 return publicMatch; 6224 } 6225 6226 final int hiddenMatch = HIDDEN_URI_MATCHER.match(uri); 6227 if (hiddenMatch != UriMatcher.NO_MATCH) { 6228 // Detect callers asking about hidden behavior by looking closer when 6229 // the matchers diverge; we only care about apps that are explicitly 6230 // targeting a specific public API level. 6231 if (!allowHidden) { 6232 throw new IllegalStateException("Unknown URL: " + uri + " is hidden API"); 6233 } 6234 return hiddenMatch; 6235 } 6236 6237 return UriMatcher.NO_MATCH; 6238 } 6239 6240 static { 6241 final UriMatcher publicMatcher = PUBLIC_URI_MATCHER; 6242 final UriMatcher hiddenMatcher = HIDDEN_URI_MATCHER; 6243 publicMatcher.addURI(AUTHORITY, "*/images/media", IMAGES_MEDIA)6244 publicMatcher.addURI(AUTHORITY, "*/images/media", IMAGES_MEDIA); publicMatcher.addURI(AUTHORITY, "*/images/media/#", IMAGES_MEDIA_ID)6245 publicMatcher.addURI(AUTHORITY, "*/images/media/#", IMAGES_MEDIA_ID); publicMatcher.addURI(AUTHORITY, "*/images/media/#/thumbnail", IMAGES_MEDIA_ID_THUMBNAIL)6246 publicMatcher.addURI(AUTHORITY, "*/images/media/#/thumbnail", IMAGES_MEDIA_ID_THUMBNAIL); publicMatcher.addURI(AUTHORITY, "*/images/thumbnails", IMAGES_THUMBNAILS)6247 publicMatcher.addURI(AUTHORITY, "*/images/thumbnails", IMAGES_THUMBNAILS); publicMatcher.addURI(AUTHORITY, "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID)6248 publicMatcher.addURI(AUTHORITY, "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID); 6249 publicMatcher.addURI(AUTHORITY, "*/audio/media", AUDIO_MEDIA)6250 publicMatcher.addURI(AUTHORITY, "*/audio/media", AUDIO_MEDIA); publicMatcher.addURI(AUTHORITY, "*/audio/media/#", AUDIO_MEDIA_ID)6251 publicMatcher.addURI(AUTHORITY, "*/audio/media/#", AUDIO_MEDIA_ID); publicMatcher.addURI(AUTHORITY, "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES)6252 publicMatcher.addURI(AUTHORITY, "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES); publicMatcher.addURI(AUTHORITY, "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID)6253 publicMatcher.addURI(AUTHORITY, "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID); hiddenMatcher.addURI(AUTHORITY, "*/audio/media/#/playlists", AUDIO_MEDIA_ID_PLAYLISTS)6254 hiddenMatcher.addURI(AUTHORITY, "*/audio/media/#/playlists", AUDIO_MEDIA_ID_PLAYLISTS); hiddenMatcher.addURI(AUTHORITY, "*/audio/media/#/playlists/#", AUDIO_MEDIA_ID_PLAYLISTS_ID)6255 hiddenMatcher.addURI(AUTHORITY, "*/audio/media/#/playlists/#", AUDIO_MEDIA_ID_PLAYLISTS_ID); publicMatcher.addURI(AUTHORITY, "*/audio/genres", AUDIO_GENRES)6256 publicMatcher.addURI(AUTHORITY, "*/audio/genres", AUDIO_GENRES); publicMatcher.addURI(AUTHORITY, "*/audio/genres/#", AUDIO_GENRES_ID)6257 publicMatcher.addURI(AUTHORITY, "*/audio/genres/#", AUDIO_GENRES_ID); publicMatcher.addURI(AUTHORITY, "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS)6258 publicMatcher.addURI(AUTHORITY, "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS); 6259 // TODO: not actually defined in API, but CTS tested publicMatcher.addURI(AUTHORITY, "*/audio/genres/all/members", AUDIO_GENRES_ALL_MEMBERS)6260 publicMatcher.addURI(AUTHORITY, "*/audio/genres/all/members", AUDIO_GENRES_ALL_MEMBERS); publicMatcher.addURI(AUTHORITY, "*/audio/playlists", AUDIO_PLAYLISTS)6261 publicMatcher.addURI(AUTHORITY, "*/audio/playlists", AUDIO_PLAYLISTS); publicMatcher.addURI(AUTHORITY, "*/audio/playlists/#", AUDIO_PLAYLISTS_ID)6262 publicMatcher.addURI(AUTHORITY, "*/audio/playlists/#", AUDIO_PLAYLISTS_ID); publicMatcher.addURI(AUTHORITY, "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS)6263 publicMatcher.addURI(AUTHORITY, "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS); publicMatcher.addURI(AUTHORITY, "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID)6264 publicMatcher.addURI(AUTHORITY, "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID); publicMatcher.addURI(AUTHORITY, "*/audio/artists", AUDIO_ARTISTS)6265 publicMatcher.addURI(AUTHORITY, "*/audio/artists", AUDIO_ARTISTS); publicMatcher.addURI(AUTHORITY, "*/audio/artists/#", AUDIO_ARTISTS_ID)6266 publicMatcher.addURI(AUTHORITY, "*/audio/artists/#", AUDIO_ARTISTS_ID); publicMatcher.addURI(AUTHORITY, "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS)6267 publicMatcher.addURI(AUTHORITY, "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS); publicMatcher.addURI(AUTHORITY, "*/audio/albums", AUDIO_ALBUMS)6268 publicMatcher.addURI(AUTHORITY, "*/audio/albums", AUDIO_ALBUMS); publicMatcher.addURI(AUTHORITY, "*/audio/albums/#", AUDIO_ALBUMS_ID)6269 publicMatcher.addURI(AUTHORITY, "*/audio/albums/#", AUDIO_ALBUMS_ID); 6270 // TODO: not actually defined in API, but CTS tested publicMatcher.addURI(AUTHORITY, "*/audio/albumart", AUDIO_ALBUMART)6271 publicMatcher.addURI(AUTHORITY, "*/audio/albumart", AUDIO_ALBUMART); 6272 // TODO: not actually defined in API, but CTS tested publicMatcher.addURI(AUTHORITY, "*/audio/albumart/#", AUDIO_ALBUMART_ID)6273 publicMatcher.addURI(AUTHORITY, "*/audio/albumart/#", AUDIO_ALBUMART_ID); 6274 // TODO: not actually defined in API, but CTS tested publicMatcher.addURI(AUTHORITY, "*/audio/media/#/albumart", AUDIO_ALBUMART_FILE_ID)6275 publicMatcher.addURI(AUTHORITY, "*/audio/media/#/albumart", AUDIO_ALBUMART_FILE_ID); 6276 publicMatcher.addURI(AUTHORITY, "*/video/media", VIDEO_MEDIA)6277 publicMatcher.addURI(AUTHORITY, "*/video/media", VIDEO_MEDIA); publicMatcher.addURI(AUTHORITY, "*/video/media/#", VIDEO_MEDIA_ID)6278 publicMatcher.addURI(AUTHORITY, "*/video/media/#", VIDEO_MEDIA_ID); publicMatcher.addURI(AUTHORITY, "*/video/media/#/thumbnail", VIDEO_MEDIA_ID_THUMBNAIL)6279 publicMatcher.addURI(AUTHORITY, "*/video/media/#/thumbnail", VIDEO_MEDIA_ID_THUMBNAIL); publicMatcher.addURI(AUTHORITY, "*/video/thumbnails", VIDEO_THUMBNAILS)6280 publicMatcher.addURI(AUTHORITY, "*/video/thumbnails", VIDEO_THUMBNAILS); publicMatcher.addURI(AUTHORITY, "*/video/thumbnails/#", VIDEO_THUMBNAILS_ID)6281 publicMatcher.addURI(AUTHORITY, "*/video/thumbnails/#", VIDEO_THUMBNAILS_ID); 6282 publicMatcher.addURI(AUTHORITY, "*/media_scanner", MEDIA_SCANNER)6283 publicMatcher.addURI(AUTHORITY, "*/media_scanner", MEDIA_SCANNER); 6284 6285 // NOTE: technically hidden, since Uri is never exposed publicMatcher.addURI(AUTHORITY, "*/fs_id", FS_ID)6286 publicMatcher.addURI(AUTHORITY, "*/fs_id", FS_ID); 6287 // NOTE: technically hidden, since Uri is never exposed publicMatcher.addURI(AUTHORITY, "*/version", VERSION)6288 publicMatcher.addURI(AUTHORITY, "*/version", VERSION); 6289 hiddenMatcher.addURI(AUTHORITY, "*", VOLUMES_ID)6290 hiddenMatcher.addURI(AUTHORITY, "*", VOLUMES_ID); hiddenMatcher.addURI(AUTHORITY, null, VOLUMES)6291 hiddenMatcher.addURI(AUTHORITY, null, VOLUMES); 6292 6293 // Used by MTP implementation publicMatcher.addURI(AUTHORITY, "*/file", FILES)6294 publicMatcher.addURI(AUTHORITY, "*/file", FILES); publicMatcher.addURI(AUTHORITY, "*/file/#", FILES_ID)6295 publicMatcher.addURI(AUTHORITY, "*/file/#", FILES_ID); hiddenMatcher.addURI(AUTHORITY, "*/object", MTP_OBJECTS)6296 hiddenMatcher.addURI(AUTHORITY, "*/object", MTP_OBJECTS); hiddenMatcher.addURI(AUTHORITY, "*/object/#", MTP_OBJECTS_ID)6297 hiddenMatcher.addURI(AUTHORITY, "*/object/#", MTP_OBJECTS_ID); hiddenMatcher.addURI(AUTHORITY, "*/object/#/references", MTP_OBJECT_REFERENCES)6298 hiddenMatcher.addURI(AUTHORITY, "*/object/#/references", MTP_OBJECT_REFERENCES); 6299 6300 // Used only to trigger special logic for directories hiddenMatcher.addURI(AUTHORITY, "*/dir", FILES_DIRECTORY)6301 hiddenMatcher.addURI(AUTHORITY, "*/dir", FILES_DIRECTORY); 6302 publicMatcher.addURI(AUTHORITY, "*/downloads", DOWNLOADS)6303 publicMatcher.addURI(AUTHORITY, "*/downloads", DOWNLOADS); publicMatcher.addURI(AUTHORITY, "*/downloads/#", DOWNLOADS_ID)6304 publicMatcher.addURI(AUTHORITY, "*/downloads/#", DOWNLOADS_ID); 6305 } 6306 6307 /** 6308 * Set of columns that can be safely mutated by external callers; all other 6309 * columns are treated as read-only, since they reflect what the media 6310 * scanner found on disk, and any mutations would be overwritten the next 6311 * time the media was scanned. 6312 */ 6313 private static final ArraySet<String> sMutableColumns = new ArraySet<>(); 6314 6315 { 6316 sMutableColumns.add(MediaStore.MediaColumns.DATA); 6317 sMutableColumns.add(MediaStore.MediaColumns.RELATIVE_PATH); 6318 sMutableColumns.add(MediaStore.MediaColumns.DISPLAY_NAME); 6319 sMutableColumns.add(MediaStore.MediaColumns.IS_PENDING); 6320 sMutableColumns.add(MediaStore.MediaColumns.IS_TRASHED); 6321 sMutableColumns.add(MediaStore.MediaColumns.DATE_EXPIRES); 6322 sMutableColumns.add(MediaStore.MediaColumns.PRIMARY_DIRECTORY); 6323 sMutableColumns.add(MediaStore.MediaColumns.SECONDARY_DIRECTORY); 6324 6325 sMutableColumns.add(MediaStore.Audio.AudioColumns.BOOKMARK); 6326 6327 sMutableColumns.add(MediaStore.Video.VideoColumns.TAGS); 6328 sMutableColumns.add(MediaStore.Video.VideoColumns.CATEGORY); 6329 sMutableColumns.add(MediaStore.Video.VideoColumns.BOOKMARK); 6330 6331 sMutableColumns.add(MediaStore.Audio.Playlists.NAME); 6332 sMutableColumns.add(MediaStore.Audio.Playlists.Members.AUDIO_ID); 6333 sMutableColumns.add(MediaStore.Audio.Playlists.Members.PLAY_ORDER); 6334 6335 sMutableColumns.add(MediaStore.Files.FileColumns.MIME_TYPE); 6336 sMutableColumns.add(MediaStore.Files.FileColumns.MEDIA_TYPE); 6337 } 6338 6339 /** 6340 * Set of columns that affect placement of files on disk. 6341 */ 6342 private static final ArraySet<String> sPlacementColumns = new ArraySet<>(); 6343 6344 { 6345 sPlacementColumns.add(MediaStore.MediaColumns.DATA); 6346 sPlacementColumns.add(MediaStore.MediaColumns.RELATIVE_PATH); 6347 sPlacementColumns.add(MediaStore.MediaColumns.DISPLAY_NAME); 6348 sPlacementColumns.add(MediaStore.MediaColumns.MIME_TYPE); 6349 sPlacementColumns.add(MediaStore.MediaColumns.PRIMARY_DIRECTORY); 6350 sPlacementColumns.add(MediaStore.MediaColumns.SECONDARY_DIRECTORY); 6351 } 6352 6353 /** 6354 * List of abusive custom columns that we're willing to allow via 6355 * {@link SQLiteQueryBuilder#setProjectionGreylist(List)}. 6356 */ 6357 static final ArrayList<Pattern> sGreylist = new ArrayList<>(); 6358 addGreylistPattern(String pattern)6359 private static void addGreylistPattern(String pattern) { 6360 sGreylist.add(Pattern.compile(" *" + pattern + " *")); 6361 } 6362 6363 static { 6364 final String maybeAs = "( (as )?[_a-z0-9]+)?"; 6365 addGreylistPattern("(?i)[_a-z0-9]+" + maybeAs); 6366 addGreylistPattern("audio\\._id AS _id"); 6367 addGreylistPattern("(?i)(min|max|sum|avg|total|count|cast)\\(([_a-z0-9]+" + maybeAs + "|\\*)\\)" + maybeAs); 6368 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"); 6369 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\\)"); 6370 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\\)"); 6371 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\\)"); 6372 addGreylistPattern("\"content://media/[a-z]+/audio/media\""); 6373 addGreylistPattern("substr\\(_data, length\\(_data\\)-length\\(_display_name\\), 1\\) as filename_prevchar"); 6374 addGreylistPattern("\\*" + maybeAs); 6375 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"); 6376 } 6377 6378 @GuardedBy("sProjectionMapCache") 6379 private static final ArrayMap<Class<?>, ArrayMap<String, String>> 6380 sProjectionMapCache = new ArrayMap<>(); 6381 6382 /** 6383 * Return a projection map that represents the valid columns that can be 6384 * queried the given contract class. The mapping is built automatically 6385 * using the {@link Column} annotation, and is designed to ensure that we 6386 * always support public API commitments. 6387 */ getProjectionMap(Class<?> clazz)6388 static ArrayMap<String, String> getProjectionMap(Class<?> clazz) { 6389 synchronized (sProjectionMapCache) { 6390 ArrayMap<String, String> map = sProjectionMapCache.get(clazz); 6391 if (map == null) { 6392 map = new ArrayMap<>(); 6393 sProjectionMapCache.put(clazz, map); 6394 try { 6395 for (Field field : clazz.getFields()) { 6396 if (field.isAnnotationPresent(Column.class)) { 6397 final String column = (String) field.get(null); 6398 map.put(column, column); 6399 } 6400 } 6401 } catch (ReflectiveOperationException e) { 6402 throw new RuntimeException(e); 6403 } 6404 } 6405 return map; 6406 } 6407 } 6408 6409 /** 6410 * Simple attempt to balance the given SQL expression by adding parenthesis 6411 * when needed. 6412 * <p> 6413 * Since this is only used for recovering from abusive apps, we're not 6414 * interested in trying to build a fully valid SQL parser up in Java. It'll 6415 * give up when it encounters complex SQL, such as string literals. 6416 */ 6417 @VisibleForTesting maybeBalance(@ullable String sql)6418 static @Nullable String maybeBalance(@Nullable String sql) { 6419 if (sql == null) return null; 6420 6421 int count = 0; 6422 char literal = '\0'; 6423 for (int i = 0; i < sql.length(); i++) { 6424 final char c = sql.charAt(i); 6425 6426 if (c == '\'' || c == '"') { 6427 if (literal == '\0') { 6428 // Start literal 6429 literal = c; 6430 } else if (literal == c) { 6431 // End literal 6432 literal = '\0'; 6433 } 6434 } 6435 6436 if (literal == '\0') { 6437 if (c == '(') { 6438 count++; 6439 } else if (c == ')') { 6440 count--; 6441 } 6442 } 6443 } 6444 while (count > 0) { 6445 sql = sql + ")"; 6446 count--; 6447 } 6448 while (count < 0) { 6449 sql = "(" + sql; 6450 count++; 6451 } 6452 return sql; 6453 } 6454 containsAny(Set<T> a, Set<T> b)6455 static <T> boolean containsAny(Set<T> a, Set<T> b) { 6456 for (T i : b) { 6457 if (a.contains(i)) { 6458 return true; 6459 } 6460 } 6461 return false; 6462 } 6463 6464 /** 6465 * Gracefully recover from abusive callers that are smashing invalid 6466 * {@code GROUP BY} clauses into {@code WHERE} clauses. 6467 */ 6468 @VisibleForTesting recoverAbusiveGroupBy(Pair<String, String> selectionAndGroupBy)6469 static Pair<String, String> recoverAbusiveGroupBy(Pair<String, String> selectionAndGroupBy) { 6470 final String origSelection = selectionAndGroupBy.first; 6471 final String origGroupBy = selectionAndGroupBy.second; 6472 6473 final int index = (origSelection != null) 6474 ? origSelection.toUpperCase().indexOf(" GROUP BY ") : -1; 6475 if (index != -1) { 6476 String selection = origSelection.substring(0, index); 6477 String groupBy = origSelection.substring(index + " GROUP BY ".length()); 6478 6479 // Try balancing things out 6480 selection = maybeBalance(selection); 6481 groupBy = maybeBalance(groupBy); 6482 6483 // Yell if we already had a group by requested 6484 if (!TextUtils.isEmpty(origGroupBy)) { 6485 throw new IllegalArgumentException( 6486 "Abusive '" + groupBy + "' conflicts with requested '" + origGroupBy + "'"); 6487 } 6488 6489 Log.w(TAG, "Recovered abusive '" + selection + "' and '" + groupBy + "' from '" 6490 + origSelection + "'"); 6491 return Pair.create(selection, groupBy); 6492 } else { 6493 return selectionAndGroupBy; 6494 } 6495 } 6496 6497 @VisibleForTesting computeCommonPrefix(@onNull List<Uri> uris)6498 static @Nullable Uri computeCommonPrefix(@NonNull List<Uri> uris) { 6499 if (uris.isEmpty()) return null; 6500 6501 final Uri base = uris.get(0); 6502 final List<String> basePath = new ArrayList<>(base.getPathSegments()); 6503 for (int i = 1; i < uris.size(); i++) { 6504 final List<String> probePath = uris.get(i).getPathSegments(); 6505 for (int j = 0; j < basePath.size() && j < probePath.size(); j++) { 6506 if (!Objects.equals(basePath.get(j), probePath.get(j))) { 6507 // Trim away all remaining common elements 6508 while (basePath.size() > j) { 6509 basePath.remove(j); 6510 } 6511 } 6512 } 6513 6514 final int probeSize = probePath.size(); 6515 while (basePath.size() > probeSize) { 6516 basePath.remove(probeSize); 6517 } 6518 } 6519 6520 final Uri.Builder builder = base.buildUpon().path(null); 6521 for (int i = 0; i < basePath.size(); i++) { 6522 builder.appendPath(basePath.get(i)); 6523 } 6524 return builder.build(); 6525 } 6526 6527 @Deprecated getCallingPackageOrSelf()6528 private String getCallingPackageOrSelf() { 6529 return mCallingIdentity.get().getPackageName(); 6530 } 6531 6532 @Deprecated getCallingPackageTargetSdkVersion()6533 private int getCallingPackageTargetSdkVersion() { 6534 return mCallingIdentity.get().getTargetSdkVersion(); 6535 } 6536 6537 @Deprecated isCallingPackageAllowedHidden()6538 private boolean isCallingPackageAllowedHidden() { 6539 return isCallingPackageSystem(); 6540 } 6541 6542 @Deprecated isCallingPackageSystem()6543 private boolean isCallingPackageSystem() { 6544 return mCallingIdentity.get().hasPermission(PERMISSION_IS_SYSTEM); 6545 } 6546 6547 @Deprecated isCallingPackageLegacy()6548 private boolean isCallingPackageLegacy() { 6549 return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY); 6550 } 6551 6552 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)6553 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 6554 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); 6555 pw.printPair("mThumbSize", mThumbSize); 6556 pw.println(); 6557 pw.printPair("mAttachedVolumeNames", mAttachedVolumeNames); 6558 pw.println(); 6559 6560 pw.println(dump(mInternalDatabase, true)); 6561 pw.println(dump(mExternalDatabase, true)); 6562 } 6563 dump(DatabaseHelper dbh, boolean dumpDbLog)6564 private String dump(DatabaseHelper dbh, boolean dumpDbLog) { 6565 StringBuilder s = new StringBuilder(); 6566 s.append(dbh.mName); 6567 s.append(": "); 6568 SQLiteDatabase db = dbh.getReadableDatabase(); 6569 if (db == null) { 6570 s.append("null"); 6571 } else { 6572 s.append("version " + db.getVersion() + ", "); 6573 Cursor c = db.query("files", new String[] {"count(*)"}, null, null, null, null, null); 6574 try { 6575 if (c != null && c.moveToFirst()) { 6576 int num = c.getInt(0); 6577 s.append(num + " rows, "); 6578 } else { 6579 s.append("couldn't get row count, "); 6580 } 6581 } finally { 6582 IoUtils.closeQuietly(c); 6583 } 6584 if (dbh.mScanStartTime != 0) { 6585 s.append("scan started " + DateUtils.formatDateTime(getContext(), 6586 dbh.mScanStartTime / 1000, 6587 DateUtils.FORMAT_SHOW_DATE 6588 | DateUtils.FORMAT_SHOW_TIME 6589 | DateUtils.FORMAT_ABBREV_ALL)); 6590 long now = dbh.mScanStopTime; 6591 if (now < dbh.mScanStartTime) { 6592 now = SystemClock.currentTimeMicro(); 6593 } 6594 s.append(" (" + DateUtils.formatElapsedTime( 6595 (now - dbh.mScanStartTime) / 1000000) + ")"); 6596 if (dbh.mScanStopTime < dbh.mScanStartTime) { 6597 if (mMediaScannerVolume != null && 6598 dbh.mName.startsWith(mMediaScannerVolume)) { 6599 s.append(" (ongoing)"); 6600 } else { 6601 s.append(" (scanning " + mMediaScannerVolume + ")"); 6602 } 6603 } 6604 } 6605 if (dumpDbLog) { 6606 c = db.query("log", new String[] {"time", "message"}, 6607 null, null, null, null, "rowid"); 6608 try { 6609 if (c != null) { 6610 while (c.moveToNext()) { 6611 String when = c.getString(0); 6612 String msg = c.getString(1); 6613 s.append("\n" + when + " : " + msg); 6614 } 6615 } 6616 } finally { 6617 IoUtils.closeQuietly(c); 6618 } 6619 } else { 6620 s.append(": pid=" + android.os.Process.myPid()); 6621 s.append(", fingerprint=" + Build.FINGERPRINT); 6622 } 6623 } 6624 return s.toString(); 6625 } 6626 } 6627