1 /* 2 * Copyright (C) 2019 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 com.android.providers.media.DatabaseBackupAndRecovery.getXattr; 20 import static com.android.providers.media.DatabaseBackupAndRecovery.setXattr; 21 import static com.android.providers.media.util.DatabaseUtils.bindList; 22 import static com.android.providers.media.util.Logging.LOGV; 23 import static com.android.providers.media.util.Logging.TAG; 24 25 import android.annotation.SuppressLint; 26 import android.content.ContentProviderClient; 27 import android.content.ContentResolver; 28 import android.content.ContentUris; 29 import android.content.ContentValues; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.pm.PackageInfo; 33 import android.content.pm.PackageManager; 34 import android.content.pm.ProviderInfo; 35 import android.database.Cursor; 36 import android.database.sqlite.SQLiteDatabase; 37 import android.database.sqlite.SQLiteOpenHelper; 38 import android.mtp.MtpConstants; 39 import android.net.Uri; 40 import android.os.Bundle; 41 import android.os.Environment; 42 import android.os.RemoteException; 43 import android.os.SystemClock; 44 import android.os.SystemProperties; 45 import android.os.Trace; 46 import android.os.UserHandle; 47 import android.provider.MediaStore; 48 import android.provider.MediaStore.Audio; 49 import android.provider.MediaStore.Downloads; 50 import android.provider.MediaStore.Files.FileColumns; 51 import android.provider.MediaStore.Images; 52 import android.provider.MediaStore.MediaColumns; 53 import android.provider.MediaStore.Video; 54 import android.system.ErrnoException; 55 import android.system.Os; 56 import android.system.OsConstants; 57 import android.text.format.DateUtils; 58 import android.util.ArrayMap; 59 import android.util.ArraySet; 60 import android.util.Log; 61 import android.util.SparseArray; 62 63 import androidx.annotation.GuardedBy; 64 import androidx.annotation.NonNull; 65 import androidx.annotation.Nullable; 66 import androidx.annotation.VisibleForTesting; 67 68 import com.android.modules.utils.BackgroundThread; 69 import com.android.providers.media.dao.FileRow; 70 import com.android.providers.media.playlist.Playlist; 71 import com.android.providers.media.util.DatabaseUtils; 72 import com.android.providers.media.util.FileUtils; 73 import com.android.providers.media.util.ForegroundThread; 74 import com.android.providers.media.util.Logging; 75 import com.android.providers.media.util.MimeUtils; 76 77 import com.google.common.collect.Iterables; 78 79 import java.io.File; 80 import java.io.FileNotFoundException; 81 import java.io.FilenameFilter; 82 import java.io.IOException; 83 import java.util.ArrayList; 84 import java.util.Collection; 85 import java.util.HashSet; 86 import java.util.List; 87 import java.util.Locale; 88 import java.util.Objects; 89 import java.util.Optional; 90 import java.util.Set; 91 import java.util.UUID; 92 import java.util.concurrent.atomic.AtomicBoolean; 93 import java.util.concurrent.atomic.AtomicLong; 94 import java.util.concurrent.locks.ReentrantReadWriteLock; 95 import java.util.function.Function; 96 import java.util.function.UnaryOperator; 97 import java.util.regex.Matcher; 98 99 /** 100 * Wrapper class for a specific database (associated with one particular 101 * external card, or with internal storage). Can open the actual database 102 * on demand, create and upgrade the schema, etc. 103 */ 104 public class DatabaseHelper extends SQLiteOpenHelper implements AutoCloseable { 105 @VisibleForTesting 106 static final String TEST_UPGRADE_DB = "test_upgrade"; 107 @VisibleForTesting 108 static final String TEST_DOWNGRADE_DB = "test_downgrade"; 109 @VisibleForTesting 110 public static final String TEST_CLEAN_DB = "test_clean"; 111 112 /** 113 * Prefix of key name of xattr used to set next row id for internal DB. 114 */ 115 static final String INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX = "user.intdbnextrowid"; 116 117 /** 118 * Key name of xattr used to set next row id for internal DB. 119 */ 120 static final String INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY = 121 INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX.concat( 122 String.valueOf(UserHandle.myUserId())); 123 124 /** 125 * Prefix of key name of xattr used to set next row id for external DB. 126 */ 127 static final String EXTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX = "user.extdbnextrowid"; 128 129 /** 130 * Key name of xattr used to set next row id for external DB. 131 */ 132 static final String EXTERNAL_DB_NEXT_ROW_ID_XATTR_KEY = 133 EXTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX.concat( 134 String.valueOf(UserHandle.myUserId())); 135 136 /** 137 * Prefix of key name of xattr used to set session id for internal DB. 138 */ 139 static final String INTERNAL_DB_SESSION_ID_XATTR_KEY_PREFIX = "user.intdbsessionid"; 140 141 /** 142 * Key name of xattr used to set session id for internal DB. 143 */ 144 static final String INTERNAL_DB_SESSION_ID_XATTR_KEY = 145 INTERNAL_DB_SESSION_ID_XATTR_KEY_PREFIX.concat( 146 String.valueOf(UserHandle.myUserId())); 147 148 /** 149 * Prefix of key name of xattr used to set session id for external DB. 150 */ 151 static final String EXTERNAL_DB_SESSION_ID_XATTR_KEY_PREFIX = "user.extdbsessionid"; 152 153 /** 154 * Key name of xattr used to set session id for external DB. 155 */ 156 static final String EXTERNAL_DB_SESSION_ID_XATTR_KEY = 157 EXTERNAL_DB_SESSION_ID_XATTR_KEY_PREFIX.concat( 158 String.valueOf(UserHandle.myUserId())); 159 160 /** Indicates a billion value used when next row id is not present in respective xattr. */ 161 private static final Long NEXT_ROW_ID_DEFAULT_BILLION_VALUE = Double.valueOf( 162 Math.pow(10, 9)).longValue(); 163 164 private static final Long INVALID_ROW_ID = -1L; 165 166 /** 167 * Path used for setting next row id and database session id for each user profile. Storing here 168 * because media provider does not have required permission on path /data/media/<user-id> for 169 * work profiles. 170 * For devices with adoptable storage support, opting for adoptable storage will not delete 171 * /data/media/0 directory. 172 */ 173 static final String DATA_MEDIA_XATTR_DIRECTORY_PATH = "/data/media/0"; 174 175 static final String INTERNAL_DATABASE_NAME = "internal.db"; 176 static final String EXTERNAL_DATABASE_NAME = "external.db"; 177 178 /** 179 * Raw SQL clause that can be used to obtain the current generation, which 180 * is designed to be populated into {@link MediaColumns#GENERATION_ADDED} or 181 * {@link MediaColumns#GENERATION_MODIFIED}. 182 */ 183 public static final String CURRENT_GENERATION_CLAUSE = "SELECT generation FROM local_metadata"; 184 185 private static final int NOTIFY_BATCH_SIZE = 256; 186 187 final Context mContext; 188 final String mName; 189 final int mVersion; 190 final String mVolumeName; 191 final boolean mEarlyUpgrade; 192 final boolean mLegacyProvider; 193 private final ProjectionHelper mProjectionHelper; 194 final @Nullable OnSchemaChangeListener mSchemaListener; 195 final @Nullable OnFilesChangeListener mFilesListener; 196 final @Nullable OnLegacyMigrationListener mMigrationListener; 197 final @Nullable UnaryOperator<String> mIdGenerator; 198 final Set<String> mFilterVolumeNames = new ArraySet<>(); 199 private final String mMigrationFileName; 200 long mScanStartTime; 201 long mScanStopTime; 202 private boolean mEnableNextRowIdRecovery; 203 private final DatabaseBackupAndRecovery mDatabaseBackupAndRecovery; 204 205 /** 206 * Unfortunately we can have multiple instances of DatabaseHelper, causing 207 * onUpgrade() to be called multiple times if those instances happen to run in 208 * parallel. To prevent that, keep track of which databases we've already upgraded. 209 * 210 */ 211 static final Set<String> sDatabaseUpgraded = new HashSet<>(); 212 static final Object sLock = new Object(); 213 /** 214 * Lock used to guard against deadlocks in SQLite; the write lock is used to 215 * guard any schema changes, and the read lock is used for all other 216 * database operations. 217 * <p> 218 * As a concrete example: consider the case where the primary database 219 * connection is performing a schema change inside a transaction, while a 220 * secondary connection is waiting to begin a transaction. When the primary 221 * database connection changes the schema, it attempts to close all other 222 * database connections, which then deadlocks. 223 */ 224 private final ReentrantReadWriteLock mSchemaLock = new ReentrantReadWriteLock(); 225 226 private static Object sMigrationLockInternal = new Object(); 227 private static Object sMigrationLockExternal = new Object(); 228 229 /** 230 * Object used to synchronise sequence of next row id in database. 231 */ 232 private static final Object sRecoveryLock = new Object(); 233 234 /** Stores cached value of next row id of the database which optimises new id inserts. */ 235 private AtomicLong mNextRowIdBackup = new AtomicLong(INVALID_ROW_ID); 236 237 /** Indicates whether the database is recovering from a rollback or not. */ 238 private AtomicBoolean mIsRecovering = new AtomicBoolean(false); 239 240 public interface OnSchemaChangeListener { onSchemaChange(@onNull String volumeName, int versionFrom, int versionTo, long itemCount, long durationMillis, String databaseUuid)241 void onSchemaChange(@NonNull String volumeName, int versionFrom, int versionTo, 242 long itemCount, long durationMillis, String databaseUuid); 243 } 244 245 public interface OnFilesChangeListener { onInsert(@onNull DatabaseHelper helper, @NonNull FileRow insertedRow)246 void onInsert(@NonNull DatabaseHelper helper, @NonNull FileRow insertedRow); 247 onUpdate(@onNull DatabaseHelper helper, @NonNull FileRow oldRow, @NonNull FileRow newRow)248 void onUpdate(@NonNull DatabaseHelper helper, @NonNull FileRow oldRow, 249 @NonNull FileRow newRow); 250 251 /** Method invoked on database row delete. */ onDelete(@onNull DatabaseHelper helper, @NonNull FileRow deletedRow)252 void onDelete(@NonNull DatabaseHelper helper, @NonNull FileRow deletedRow); 253 } 254 255 public interface OnLegacyMigrationListener { onStarted(ContentProviderClient client, String volumeName)256 void onStarted(ContentProviderClient client, String volumeName); 257 onProgress(ContentProviderClient client, String volumeName, long progress, long total)258 void onProgress(ContentProviderClient client, String volumeName, 259 long progress, long total); 260 onFinished(ContentProviderClient client, String volumeName)261 void onFinished(ContentProviderClient client, String volumeName); 262 } 263 DatabaseHelper(Context context, String name, boolean earlyUpgrade, boolean legacyProvider, ProjectionHelper projectionHelper, @Nullable OnSchemaChangeListener schemaListener, @Nullable OnFilesChangeListener filesListener, @NonNull OnLegacyMigrationListener migrationListener, @Nullable UnaryOperator<String> idGenerator, boolean enableNextRowIdRecovery, DatabaseBackupAndRecovery databaseBackupAndRecovery)264 public DatabaseHelper(Context context, String name, 265 boolean earlyUpgrade, boolean legacyProvider, 266 ProjectionHelper projectionHelper, 267 @Nullable OnSchemaChangeListener schemaListener, 268 @Nullable OnFilesChangeListener filesListener, 269 @NonNull OnLegacyMigrationListener migrationListener, 270 @Nullable UnaryOperator<String> idGenerator, boolean enableNextRowIdRecovery, 271 DatabaseBackupAndRecovery databaseBackupAndRecovery) { 272 this(context, name, getDatabaseVersion(context), earlyUpgrade, legacyProvider, 273 projectionHelper, schemaListener, filesListener, 274 migrationListener, idGenerator, enableNextRowIdRecovery, databaseBackupAndRecovery); 275 } 276 DatabaseHelper(Context context, String name, int version, boolean earlyUpgrade, boolean legacyProvider, ProjectionHelper projectionHelper, @Nullable OnSchemaChangeListener schemaListener, @Nullable OnFilesChangeListener filesListener, @NonNull OnLegacyMigrationListener migrationListener, @Nullable UnaryOperator<String> idGenerator, boolean enableNextRowIdRecovery, DatabaseBackupAndRecovery databaseBackupAndRecovery)277 public DatabaseHelper(Context context, String name, int version, 278 boolean earlyUpgrade, boolean legacyProvider, 279 ProjectionHelper projectionHelper, 280 @Nullable OnSchemaChangeListener schemaListener, 281 @Nullable OnFilesChangeListener filesListener, 282 @NonNull OnLegacyMigrationListener migrationListener, 283 @Nullable UnaryOperator<String> idGenerator, boolean enableNextRowIdRecovery, 284 DatabaseBackupAndRecovery databaseBackupAndRecovery) { 285 super(context, name, null, version); 286 mContext = context; 287 mName = name; 288 mVersion = version; 289 if (isInternal()) { 290 mVolumeName = MediaStore.VOLUME_INTERNAL; 291 } else if (isExternal()) { 292 mVolumeName = MediaStore.VOLUME_EXTERNAL; 293 } else { 294 throw new IllegalStateException("Db must be internal/external"); 295 } 296 mEarlyUpgrade = earlyUpgrade; 297 mLegacyProvider = legacyProvider; 298 mProjectionHelper = projectionHelper; 299 mSchemaListener = schemaListener; 300 mFilesListener = filesListener; 301 mMigrationListener = migrationListener; 302 mIdGenerator = idGenerator; 303 mMigrationFileName = "." + mVolumeName; 304 this.mEnableNextRowIdRecovery = enableNextRowIdRecovery; 305 this.mDatabaseBackupAndRecovery = databaseBackupAndRecovery; 306 307 // Configure default filters until we hear differently 308 if (isInternal()) { 309 mFilterVolumeNames.add(MediaStore.VOLUME_INTERNAL); 310 } else if (isExternal()) { 311 mFilterVolumeNames.add(MediaStore.VOLUME_EXTERNAL_PRIMARY); 312 } 313 314 setWriteAheadLoggingEnabled(true); 315 } 316 317 /** 318 * Configure the set of {@link MediaColumns#VOLUME_NAME} that we should use 319 * for filtering query results. 320 * <p> 321 * This is typically set to the list of storage volumes which are currently 322 * mounted, so that we don't leak cached indexed metadata from volumes which 323 * are currently ejected. 324 */ setFilterVolumeNames(@onNull Set<String> filterVolumeNames)325 public void setFilterVolumeNames(@NonNull Set<String> filterVolumeNames) { 326 synchronized (mFilterVolumeNames) { 327 // Skip update if identical, to help avoid database churn 328 if (mFilterVolumeNames.equals(filterVolumeNames)) { 329 return; 330 } 331 332 mFilterVolumeNames.clear(); 333 mFilterVolumeNames.addAll(filterVolumeNames); 334 } 335 336 // Recreate all views to apply this filter 337 final SQLiteDatabase db = super.getWritableDatabase(); 338 mSchemaLock.writeLock().lock(); 339 db.beginTransaction(); 340 try { 341 createLatestViews(db); 342 db.setTransactionSuccessful(); 343 } finally { 344 db.endTransaction(); 345 mSchemaLock.writeLock().unlock(); 346 } 347 } 348 349 @Override getReadableDatabase()350 public SQLiteDatabase getReadableDatabase() { 351 throw new UnsupportedOperationException("All database operations must be routed through" 352 + " runWithTransaction() or runWithoutTransaction() to avoid deadlocks"); 353 } 354 355 @Override getWritableDatabase()356 public SQLiteDatabase getWritableDatabase() { 357 throw new UnsupportedOperationException("All database operations must be routed through" 358 + " runWithTransaction() or runWithoutTransaction() to avoid deadlocks"); 359 } 360 361 @VisibleForTesting getWritableDatabaseForTest()362 SQLiteDatabase getWritableDatabaseForTest() { 363 // Tests rely on creating multiple instances of DatabaseHelper to test upgrade 364 // scenarios; so clear this state before returning databases to test. 365 synchronized (sLock) { 366 sDatabaseUpgraded.clear(); 367 } 368 return super.getWritableDatabase(); 369 } 370 371 @Override onConfigure(SQLiteDatabase db)372 public void onConfigure(SQLiteDatabase db) { 373 Log.v(TAG, "onConfigure() for " + mName); 374 375 if (isExternal()) { 376 db.setForeignKeyConstraintsEnabled(true); 377 } 378 379 db.setCustomScalarFunction("_INSERT", (arg) -> { 380 if (arg != null && mFilesListener != null 381 && !mSchemaLock.isWriteLockedByCurrentThread()) { 382 final String[] split = arg.split(":", 11); 383 final String volumeName = split[0]; 384 final long id = Long.parseLong(split[1]); 385 final int mediaType = Integer.parseInt(split[2]); 386 final boolean isDownload = Integer.parseInt(split[3]) != 0; 387 final boolean isPending = Integer.parseInt(split[4]) != 0; 388 final boolean isTrashed = Integer.parseInt(split[5]) != 0; 389 final boolean isFavorite = Integer.parseInt(split[6]) != 0; 390 final int userId = Integer.parseInt(split[7]); 391 final String dateExpires = split[8]; 392 final String ownerPackageName = split[9]; 393 final String path = split[10]; 394 395 FileRow insertedRow = FileRow.newBuilder(id) 396 .setVolumeName(volumeName) 397 .setMediaType(mediaType) 398 .setIsDownload(isDownload) 399 .setIsPending(isPending) 400 .setIsTrashed(isTrashed) 401 .setIsFavorite(isFavorite) 402 .setUserId(userId) 403 .setDateExpires(dateExpires) 404 .setOwnerPackageName(ownerPackageName) 405 .setPath(path) 406 .build(); 407 Trace.beginSection(traceSectionName("_INSERT")); 408 try { 409 mFilesListener.onInsert(DatabaseHelper.this, insertedRow); 410 } finally { 411 Trace.endSection(); 412 } 413 } 414 return null; 415 }); 416 db.setCustomScalarFunction("_UPDATE", (arg) -> { 417 if (arg != null && mFilesListener != null 418 && !mSchemaLock.isWriteLockedByCurrentThread()) { 419 final String[] split = arg.split(":", 22); 420 final String volumeName = split[0]; 421 final long oldId = Long.parseLong(split[1]); 422 final int oldMediaType = Integer.parseInt(split[2]); 423 final boolean oldIsDownload = Integer.parseInt(split[3]) != 0; 424 final long newId = Long.parseLong(split[4]); 425 final int newMediaType = Integer.parseInt(split[5]); 426 final boolean newIsDownload = Integer.parseInt(split[6]) != 0; 427 final boolean oldIsTrashed = Integer.parseInt(split[7]) != 0; 428 final boolean newIsTrashed = Integer.parseInt(split[8]) != 0; 429 final boolean oldIsPending = Integer.parseInt(split[9]) != 0; 430 final boolean newIsPending = Integer.parseInt(split[10]) != 0; 431 final boolean oldIsFavorite = Integer.parseInt(split[11]) != 0; 432 final boolean newIsFavorite = Integer.parseInt(split[12]) != 0; 433 final int oldSpecialFormat = Integer.parseInt(split[13]); 434 final int newSpecialFormat = Integer.parseInt(split[14]); 435 final String oldOwnerPackage = split[15]; 436 final String newOwnerPackage = split[16]; 437 final int oldUserId = Integer.parseInt(split[17]); 438 final int newUserId = Integer.parseInt(split[18]); 439 final String oldDateExpires = split[19]; 440 final String newDateExpires = split[20]; 441 final String oldPath = split[21]; 442 443 FileRow oldRow = FileRow.newBuilder(oldId) 444 .setVolumeName(volumeName) 445 .setMediaType(oldMediaType) 446 .setIsDownload(oldIsDownload) 447 .setIsTrashed(oldIsTrashed) 448 .setIsPending(oldIsPending) 449 .setIsFavorite(oldIsFavorite) 450 .setSpecialFormat(oldSpecialFormat) 451 .setOwnerPackageName(oldOwnerPackage) 452 .setUserId(oldUserId) 453 .setDateExpires(oldDateExpires) 454 .setPath(oldPath) 455 .build(); 456 FileRow newRow = FileRow.newBuilder(newId) 457 .setVolumeName(volumeName) 458 .setMediaType(newMediaType) 459 .setIsDownload(newIsDownload) 460 .setIsTrashed(newIsTrashed) 461 .setIsPending(newIsPending) 462 .setIsFavorite(newIsFavorite) 463 .setSpecialFormat(newSpecialFormat) 464 .setOwnerPackageName(newOwnerPackage) 465 .setUserId(newUserId) 466 .setDateExpires(newDateExpires) 467 .build(); 468 469 Trace.beginSection(traceSectionName("_UPDATE")); 470 try { 471 mFilesListener.onUpdate(DatabaseHelper.this, oldRow, newRow); 472 } finally { 473 Trace.endSection(); 474 } 475 } 476 return null; 477 }); 478 db.setCustomScalarFunction("_DELETE", (arg) -> { 479 if (arg != null && mFilesListener != null 480 && !mSchemaLock.isWriteLockedByCurrentThread()) { 481 final String[] split = arg.split(":", 6); 482 final String volumeName = split[0]; 483 final long id = Long.parseLong(split[1]); 484 final int mediaType = Integer.parseInt(split[2]); 485 final boolean isDownload = Integer.parseInt(split[3]) != 0; 486 final String ownerPackage = split[4]; 487 final String path = split[5]; 488 489 FileRow deletedRow = FileRow.newBuilder(id) 490 .setVolumeName(volumeName) 491 .setMediaType(mediaType) 492 .setIsDownload(isDownload) 493 .setOwnerPackageName(ownerPackage) 494 .setPath(path) 495 .build(); 496 Trace.beginSection(traceSectionName("_DELETE")); 497 try { 498 mFilesListener.onDelete(DatabaseHelper.this, deletedRow); 499 } finally { 500 Trace.endSection(); 501 } 502 } 503 return null; 504 }); 505 db.setCustomScalarFunction("_GET_ID", (arg) -> { 506 if (mIdGenerator != null && !mSchemaLock.isWriteLockedByCurrentThread()) { 507 Trace.beginSection(traceSectionName("_GET_ID")); 508 try { 509 return mIdGenerator.apply(arg); 510 } finally { 511 Trace.endSection(); 512 } 513 } 514 return null; 515 }); 516 } 517 518 @Override onCreate(final SQLiteDatabase db)519 public void onCreate(final SQLiteDatabase db) { 520 Log.v(TAG, "onCreate() for " + mName); 521 mSchemaLock.writeLock().lock(); 522 try { 523 updateDatabase(db, 0, mVersion); 524 } finally { 525 mSchemaLock.writeLock().unlock(); 526 } 527 } 528 529 @Override onUpgrade(final SQLiteDatabase db, final int oldV, final int newV)530 public void onUpgrade(final SQLiteDatabase db, final int oldV, final int newV) { 531 Log.v(TAG, "onUpgrade() for " + mName + " from " + oldV + " to " + newV); 532 mSchemaLock.writeLock().lock(); 533 try { 534 synchronized (sLock) { 535 if (sDatabaseUpgraded.contains(mName)) { 536 Log.v(TAG, "Skipping onUpgrade() for " + mName + 537 " because it was already upgraded."); 538 return; 539 } else { 540 sDatabaseUpgraded.add(mName); 541 } 542 } 543 updateDatabase(db, oldV, newV); 544 } finally { 545 mSchemaLock.writeLock().unlock(); 546 } 547 } 548 549 @Override onDowngrade(final SQLiteDatabase db, final int oldV, final int newV)550 public void onDowngrade(final SQLiteDatabase db, final int oldV, final int newV) { 551 Log.v(TAG, "onDowngrade() for " + mName + " from " + oldV + " to " + newV); 552 mSchemaLock.writeLock().lock(); 553 try { 554 downgradeDatabase(db, oldV, newV); 555 } finally { 556 mSchemaLock.writeLock().unlock(); 557 } 558 // In case of a bad MP release which decreases the DB version, we would end up downgrading 559 // database. We are explicitly setting a new session id on database to trigger recovery 560 // in onOpen() call. 561 setXattr(db.getPath(), getSessionIdXattrKeyForDatabase(), UUID.randomUUID().toString()); 562 } 563 564 @Override onOpen(final SQLiteDatabase db)565 public void onOpen(final SQLiteDatabase db) { 566 Log.v(TAG, "onOpen() for " + mName); 567 // Recovering before migration from legacy because recovery process will clear up data to 568 // read from xattrs once ids are persisted in xattrs. 569 tryRecoverDatabase(db); 570 tryRecoverRowIdSequence(db); 571 tryMigrateFromLegacy(db); 572 } 573 tryRecoverDatabase(SQLiteDatabase db)574 private void tryRecoverDatabase(SQLiteDatabase db) { 575 String volumeName = 576 isInternal() ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL_PRIMARY; 577 if (!mDatabaseBackupAndRecovery.isStableUrisEnabled(volumeName)) { 578 return; 579 } 580 581 synchronized (sRecoveryLock) { 582 // Read last used session id from /data/media/0. 583 Optional<String> lastUsedSessionIdFromExternalStoragePathXattr = getXattr( 584 getExternalStorageDbXattrPath(), getSessionIdXattrKeyForDatabase()); 585 if (!lastUsedSessionIdFromExternalStoragePathXattr.isPresent()) { 586 // First time scenario will have no session id at /data/media/0. 587 // Set next row id in External Storage to handle rollback in future. 588 backupNextRowId(NEXT_ROW_ID_DEFAULT_BILLION_VALUE); 589 updateSessionIdInDatabaseAndExternalStorage(db); 590 return; 591 } 592 593 Optional<Long> nextRowIdFromXattrOptional = getNextRowIdFromXattr(); 594 // Check if session is same as last used. 595 if (isLastUsedDatabaseSession(db) && nextRowIdFromXattrOptional.isPresent()) { 596 // Same session id present as xattr on DB and External Storage 597 Log.i(TAG, String.format(Locale.ROOT, 598 "No database change across sequential open calls for %s.", mName)); 599 mNextRowIdBackup.set(nextRowIdFromXattrOptional.get()); 600 updateSessionIdInDatabaseAndExternalStorage(db); 601 return; 602 } 603 604 605 MediaProviderStatsLog.write( 606 MediaProviderStatsLog.MEDIA_PROVIDER_DATABASE_ROLLBACK_REPORTED, isInternal() 607 ? 608 MediaProviderStatsLog.MEDIA_PROVIDER_DATABASE_ROLLBACK_REPORTED__DATABASE_NAME__INTERNAL 609 : 610 MediaProviderStatsLog.MEDIA_PROVIDER_DATABASE_ROLLBACK_REPORTED__DATABASE_NAME__EXTERNAL); 611 Log.w(TAG, String.format(Locale.ROOT, "%s database inconsistency identified.", mName)); 612 // Delete old data and create new schema. 613 recreateLatestSchema(db); 614 // Recover data from backup 615 // Ensure we do not back up in case of recovery. 616 mIsRecovering.set(true); 617 try { 618 mDatabaseBackupAndRecovery.recoverData(db, volumeName); 619 } catch (Exception exception) { 620 Log.e(TAG, "Error in recovering data", exception); 621 } finally { 622 updateNextRowIdInDatabaseAndExternalStorage(db); 623 mIsRecovering.set(false); 624 mDatabaseBackupAndRecovery.resetLastBackedUpGenerationNumber(volumeName); 625 updateSessionIdInDatabaseAndExternalStorage(db); 626 } 627 } 628 } 629 getExternalStorageDbXattrPath()630 protected String getExternalStorageDbXattrPath() { 631 return DATA_MEDIA_XATTR_DIRECTORY_PATH; 632 } 633 634 @GuardedBy("sRecoveryLock") recreateLatestSchema(SQLiteDatabase db)635 private void recreateLatestSchema(SQLiteDatabase db) { 636 mSchemaLock.writeLock().lock(); 637 try { 638 createLatestSchema(db); 639 } finally { 640 mSchemaLock.writeLock().unlock(); 641 } 642 } 643 tryRecoverRowIdSequence(SQLiteDatabase db)644 private void tryRecoverRowIdSequence(SQLiteDatabase db) { 645 if (isInternal()) { 646 // Database row id recovery for internal is handled in tryRecoverDatabase() 647 return; 648 } 649 650 if (!isNextRowIdBackupEnabled()) { 651 Log.d(TAG, "Skipping row id recovery as backup is not enabled."); 652 return; 653 } 654 655 if (mDatabaseBackupAndRecovery.isStableUrisEnabled(MediaStore.VOLUME_EXTERNAL_PRIMARY)) { 656 // Row id change would have been taken care by tryRecoverDatabase method 657 return; 658 } 659 660 synchronized (sRecoveryLock) { 661 boolean isLastUsedDatabaseSession = isLastUsedDatabaseSession(db); 662 Optional<Long> nextRowIdFromXattrOptional = getNextRowIdFromXattr(); 663 if (isLastUsedDatabaseSession && nextRowIdFromXattrOptional.isPresent()) { 664 Log.i(TAG, String.format(Locale.ROOT, 665 "No database change across sequential open calls for %s.", mName)); 666 mNextRowIdBackup.set(nextRowIdFromXattrOptional.get()); 667 updateSessionIdInDatabaseAndExternalStorage(db); 668 return; 669 } 670 671 Log.w(TAG, String.format(Locale.ROOT, 672 "%s database inconsistent: isLastUsedDatabaseSession:%b, " 673 + "nextRowIdOptionalPresent:%b", mName, isLastUsedDatabaseSession, 674 nextRowIdFromXattrOptional.isPresent())); 675 676 // This could be a rollback, clear all media grants 677 clearMediaGrantsTable(db); 678 679 // TODO(b/222313219): Add an assert to ensure that next row id xattr is always 680 // present when DB session id matches across sequential open calls. 681 updateNextRowIdInDatabaseAndExternalStorage(db); 682 updateSessionIdInDatabaseAndExternalStorage(db); 683 } 684 } 685 clearMediaGrantsTable(SQLiteDatabase db)686 private void clearMediaGrantsTable(SQLiteDatabase db) { 687 mSchemaLock.writeLock().lock(); 688 try { 689 updateAddMediaGrantsTable(db); 690 } finally { 691 mSchemaLock.writeLock().unlock(); 692 } 693 } 694 695 @GuardedBy("sRecoveryLock") isLastUsedDatabaseSession(SQLiteDatabase db)696 private boolean isLastUsedDatabaseSession(SQLiteDatabase db) { 697 Optional<String> lastUsedSessionIdFromDatabasePathXattr = getXattr(db.getPath(), 698 getSessionIdXattrKeyForDatabase()); 699 Optional<String> lastUsedSessionIdFromExternalStoragePathXattr = getXattr( 700 getExternalStorageDbXattrPath(), getSessionIdXattrKeyForDatabase()); 701 702 return lastUsedSessionIdFromDatabasePathXattr.isPresent() 703 && lastUsedSessionIdFromExternalStoragePathXattr.isPresent() 704 && lastUsedSessionIdFromDatabasePathXattr.get().equals( 705 lastUsedSessionIdFromExternalStoragePathXattr.get()); 706 } 707 708 @GuardedBy("sRecoveryLock") updateSessionIdInDatabaseAndExternalStorage(SQLiteDatabase db)709 private void updateSessionIdInDatabaseAndExternalStorage(SQLiteDatabase db) { 710 final String uuid = UUID.randomUUID().toString(); 711 boolean setOnDatabase = setXattr(db.getPath(), getSessionIdXattrKeyForDatabase(), uuid); 712 boolean setOnExternalStorage = setXattr(getExternalStorageDbXattrPath(), 713 getSessionIdXattrKeyForDatabase(), uuid); 714 if (setOnDatabase && setOnExternalStorage) { 715 Log.i(TAG, String.format(Locale.ROOT, "SessionId set to %s on paths %s and %s.", uuid, 716 db.getPath(), getExternalStorageDbXattrPath())); 717 } 718 } 719 tryMigrateFromLegacy(SQLiteDatabase db)720 private void tryMigrateFromLegacy(SQLiteDatabase db) { 721 final Object migrationLock; 722 if (isInternal()) { 723 migrationLock = sMigrationLockInternal; 724 } else if (isExternal()) { 725 migrationLock = sMigrationLockExternal; 726 } else { 727 throw new IllegalStateException("Db migration only supported for internal/external db"); 728 } 729 730 final File migration = new File(mContext.getFilesDir(), mMigrationFileName); 731 // Another thread entering migration block will be blocked until the 732 // migration is complete from current thread. 733 synchronized (migrationLock) { 734 if (!migration.exists()) { 735 Log.v(TAG, "onOpen() finished for " + mName); 736 return; 737 } 738 739 mSchemaLock.writeLock().lock(); 740 try { 741 // Temporarily drop indexes to improve migration performance 742 makePristineIndexes(db); 743 migrateFromLegacy(db); 744 createAllLatestIndexes(db); 745 } finally { 746 mSchemaLock.writeLock().unlock(); 747 // Clear flag, since we should only attempt once 748 migration.delete(); 749 Log.v(TAG, "onOpen() finished for " + mName); 750 } 751 } 752 } 753 754 /** 755 * Local state related to any transaction currently active on a specific 756 * thread, such as collecting the set of {@link Uri} that should be notified 757 * upon transaction success. 758 * <p> 759 * We suppress Error Prone here because there are multiple 760 * {@link DatabaseHelper} instances within the process, and state needs to 761 * be tracked uniquely per-helper. 762 */ 763 @SuppressWarnings("ThreadLocalUsage") 764 private final ThreadLocal<TransactionState> mTransactionState = new ThreadLocal<>(); 765 766 private static class TransactionState { 767 /** 768 * Flag indicating if this transaction has been marked as being 769 * successful. 770 */ 771 public boolean successful; 772 773 /** 774 * List of tasks that should be executed in a blocking fashion when this 775 * transaction has been successfully finished. 776 */ 777 public final ArrayList<Runnable> blockingTasks = new ArrayList<>(); 778 779 /** 780 * Map from {@code flags} value to set of {@link Uri} that would have 781 * been sent directly via {@link ContentResolver#notifyChange}, but are 782 * instead being collected due to this ongoing transaction. 783 */ 784 public final SparseArray<ArraySet<Uri>> notifyChanges = new SparseArray<>(); 785 786 /** 787 * List of tasks that should be enqueued onto {@link BackgroundThread} 788 * after any {@link #notifyChanges} have been dispatched. We keep this 789 * as a separate pass to ensure that we don't risk running in parallel 790 * with other more important tasks. 791 */ 792 public final ArrayList<Runnable> backgroundTasks = new ArrayList<>(); 793 } 794 isTransactionActive()795 public boolean isTransactionActive() { 796 return (mTransactionState.get() != null); 797 } 798 beginTransaction()799 public void beginTransaction() { 800 Trace.beginSection(traceSectionName("transaction")); 801 Trace.beginSection(traceSectionName("beginTransaction")); 802 try { 803 beginTransactionInternal(); 804 } finally { 805 // Only end the "beginTransaction" section. We'll end the "transaction" section in 806 // endTransaction(). 807 Trace.endSection(); 808 } 809 } 810 beginTransactionInternal()811 private void beginTransactionInternal() { 812 if (mTransactionState.get() != null) { 813 throw new IllegalStateException("Nested transactions not supported"); 814 } 815 mTransactionState.set(new TransactionState()); 816 817 final SQLiteDatabase db = super.getWritableDatabase(); 818 mSchemaLock.readLock().lock(); 819 db.beginTransaction(); 820 db.execSQL("UPDATE local_metadata SET generation=generation+1;"); 821 } 822 setTransactionSuccessful()823 public void setTransactionSuccessful() { 824 final TransactionState state = mTransactionState.get(); 825 if (state == null) { 826 throw new IllegalStateException("No transaction in progress"); 827 } 828 state.successful = true; 829 830 final SQLiteDatabase db = super.getWritableDatabase(); 831 db.setTransactionSuccessful(); 832 } 833 endTransaction()834 public void endTransaction() { 835 Trace.beginSection(traceSectionName("endTransaction")); 836 try { 837 endTransactionInternal(); 838 } finally { 839 Trace.endSection(); 840 // End "transaction" section, which we started in beginTransaction(). 841 Trace.endSection(); 842 } 843 } 844 endTransactionInternal()845 private void endTransactionInternal() { 846 final TransactionState state = mTransactionState.get(); 847 if (state == null) { 848 throw new IllegalStateException("No transaction in progress"); 849 } 850 mTransactionState.remove(); 851 852 final SQLiteDatabase db = super.getWritableDatabase(); 853 db.endTransaction(); 854 mSchemaLock.readLock().unlock(); 855 856 if (state.successful) { 857 for (int i = 0; i < state.blockingTasks.size(); i++) { 858 state.blockingTasks.get(i).run(); 859 } 860 // We carefully "phase" our two sets of work here to ensure that we 861 // completely finish dispatching all change notifications before we 862 // process background tasks, to ensure that the background work 863 // doesn't steal resources from the more important foreground work 864 ForegroundThread.getExecutor().execute(() -> { 865 for (int i = 0; i < state.notifyChanges.size(); i++) { 866 notifyChangeInternal(state.notifyChanges.valueAt(i), 867 state.notifyChanges.keyAt(i)); 868 } 869 870 // Now that we've finished with all our important work, we can 871 // finally kick off any internal background tasks 872 for (int i = 0; i < state.backgroundTasks.size(); i++) { 873 BackgroundThread.getExecutor().execute(state.backgroundTasks.get(i)); 874 } 875 }); 876 } 877 } 878 879 /** 880 * Execute the given operation inside a transaction. If the calling thread 881 * is not already in an active transaction, this method will wrap the given 882 * runnable inside a new transaction. 883 */ runWithTransaction(@onNull Function<SQLiteDatabase, T> op)884 public @NonNull <T> T runWithTransaction(@NonNull Function<SQLiteDatabase, T> op) { 885 // We carefully acquire the database here so that any schema changes can 886 // be applied before acquiring the read lock below 887 final SQLiteDatabase db = super.getWritableDatabase(); 888 889 if (mTransactionState.get() != null) { 890 // Already inside a transaction, so we can run directly 891 return op.apply(db); 892 } else { 893 // Not inside a transaction, so we need to make one 894 beginTransaction(); 895 try { 896 final T res = op.apply(db); 897 setTransactionSuccessful(); 898 return res; 899 } finally { 900 endTransaction(); 901 } 902 } 903 } 904 905 /** 906 * Execute the given operation regardless of the calling thread being in an 907 * active transaction or not. 908 */ runWithoutTransaction(@onNull Function<SQLiteDatabase, T> op)909 public @NonNull <T> T runWithoutTransaction(@NonNull Function<SQLiteDatabase, T> op) { 910 // We carefully acquire the database here so that any schema changes can 911 // be applied before acquiring the read lock below 912 final SQLiteDatabase db = super.getWritableDatabase(); 913 914 if (mTransactionState.get() != null) { 915 // Already inside a transaction, so we can run directly 916 return op.apply(db); 917 } else { 918 // We still need to acquire a schema read lock 919 mSchemaLock.readLock().lock(); 920 try { 921 return op.apply(db); 922 } finally { 923 mSchemaLock.readLock().unlock(); 924 } 925 } 926 } 927 notifyInsert(@onNull Uri uri)928 public void notifyInsert(@NonNull Uri uri) { 929 notifyChange(uri, ContentResolver.NOTIFY_INSERT); 930 } 931 notifyUpdate(@onNull Uri uri)932 public void notifyUpdate(@NonNull Uri uri) { 933 notifyChange(uri, ContentResolver.NOTIFY_UPDATE); 934 } 935 notifyDelete(@onNull Uri uri)936 public void notifyDelete(@NonNull Uri uri) { 937 notifyChange(uri, ContentResolver.NOTIFY_DELETE); 938 } 939 940 /** 941 * Notify that the given {@link Uri} has changed. This enqueues the 942 * notification if currently inside a transaction, and they'll be 943 * clustered and sent when the transaction completes. 944 */ notifyChange(@onNull Uri uri, int flags)945 public void notifyChange(@NonNull Uri uri, int flags) { 946 if (LOGV) Log.v(TAG, "Notifying " + uri); 947 948 // Also sync change to the network. 949 final int notifyFlags = flags | ContentResolver.NOTIFY_SYNC_TO_NETWORK; 950 951 final TransactionState state = mTransactionState.get(); 952 if (state != null) { 953 ArraySet<Uri> set = state.notifyChanges.get(notifyFlags); 954 if (set == null) { 955 set = new ArraySet<>(); 956 state.notifyChanges.put(notifyFlags, set); 957 } 958 set.add(uri); 959 } else { 960 ForegroundThread.getExecutor().execute(() -> { 961 notifySingleChangeInternal(uri, notifyFlags); 962 }); 963 } 964 } 965 notifySingleChangeInternal(@onNull Uri uri, int flags)966 private void notifySingleChangeInternal(@NonNull Uri uri, int flags) { 967 Trace.beginSection(traceSectionName("notifySingleChange")); 968 try { 969 mContext.getContentResolver().notifyChange(uri, null, flags); 970 } finally { 971 Trace.endSection(); 972 } 973 } 974 notifyChangeInternal(@onNull Collection<Uri> uris, int flags)975 private void notifyChangeInternal(@NonNull Collection<Uri> uris, int flags) { 976 Trace.beginSection(traceSectionName("notifyChange")); 977 try { 978 for (List<Uri> partition : Iterables.partition(uris, NOTIFY_BATCH_SIZE)) { 979 mContext.getContentResolver().notifyChange(partition, null, flags); 980 } 981 } finally { 982 Trace.endSection(); 983 } 984 } 985 986 /** 987 * Post the given task to be run in a blocking fashion after any current 988 * transaction has finished. If there is no active transaction, the task is 989 * immediately executed. 990 */ postBlocking(@onNull Runnable command)991 public void postBlocking(@NonNull Runnable command) { 992 final TransactionState state = mTransactionState.get(); 993 if (state != null) { 994 state.blockingTasks.add(command); 995 } else { 996 command.run(); 997 } 998 } 999 1000 /** 1001 * Post the given task to be run in background after any current transaction 1002 * has finished. If there is no active transaction, the task is immediately 1003 * dispatched to run in the background. 1004 */ postBackground(@onNull Runnable command)1005 public void postBackground(@NonNull Runnable command) { 1006 final TransactionState state = mTransactionState.get(); 1007 if (state != null) { 1008 state.backgroundTasks.add(command); 1009 } else { 1010 BackgroundThread.getExecutor().execute(command); 1011 } 1012 } 1013 1014 /** 1015 * This method cleans up any files created by android.media.MiniThumbFile, removed after P. 1016 * It's triggered during database update only, in order to run only once. 1017 */ deleteLegacyThumbnailData()1018 private static void deleteLegacyThumbnailData() { 1019 File directory = new File(Environment.getExternalStorageDirectory(), "/DCIM/.thumbnails"); 1020 1021 final FilenameFilter filter = (dir, filename) -> filename.startsWith(".thumbdata"); 1022 final File[] files = directory.listFiles(filter); 1023 for (File f : (files != null) ? files : new File[0]) { 1024 if (!f.delete()) { 1025 Log.e(TAG, "Failed to delete legacy thumbnail data " + f.getAbsolutePath()); 1026 } 1027 } 1028 } 1029 1030 @Deprecated getDatabaseVersion(Context context)1031 public static int getDatabaseVersion(Context context) { 1032 // We now use static versions defined internally instead of the 1033 // versionCode from the manifest 1034 return VERSION_LATEST; 1035 } 1036 1037 @VisibleForTesting makePristineSchema(SQLiteDatabase db)1038 static void makePristineSchema(SQLiteDatabase db) { 1039 // We are dropping all tables and recreating new schema. This 1040 // is a clear indication of major change in MediaStore version. 1041 // Hence reset the Uuid whenever we change the schema. 1042 resetAndGetUuid(db); 1043 1044 // drop all triggers 1045 Cursor c = db.query("sqlite_master", new String[] {"name"}, "type is 'trigger'", 1046 null, null, null, null); 1047 while (c.moveToNext()) { 1048 if (c.getString(0).startsWith("sqlite_")) continue; 1049 db.execSQL("DROP TRIGGER IF EXISTS " + c.getString(0)); 1050 } 1051 c.close(); 1052 1053 // drop all views 1054 c = db.query("sqlite_master", new String[] {"name"}, "type is 'view'", 1055 null, null, null, null); 1056 while (c.moveToNext()) { 1057 if (c.getString(0).startsWith("sqlite_")) continue; 1058 db.execSQL("DROP VIEW IF EXISTS " + c.getString(0)); 1059 } 1060 c.close(); 1061 1062 // drop all indexes 1063 c = db.query("sqlite_master", new String[] {"name"}, "type is 'index'", 1064 null, null, null, null); 1065 while (c.moveToNext()) { 1066 if (c.getString(0).startsWith("sqlite_")) continue; 1067 db.execSQL("DROP INDEX IF EXISTS " + c.getString(0)); 1068 } 1069 c.close(); 1070 1071 // drop all tables 1072 c = db.query("sqlite_master", new String[] {"name"}, "type is 'table'", 1073 null, null, null, null); 1074 while (c.moveToNext()) { 1075 if (c.getString(0).startsWith("sqlite_")) continue; 1076 db.execSQL("DROP TABLE IF EXISTS " + c.getString(0)); 1077 } 1078 c.close(); 1079 } 1080 createLatestSchema(SQLiteDatabase db)1081 private void createLatestSchema(SQLiteDatabase db) { 1082 // We're about to start all ID numbering from scratch, so revoke any 1083 // outstanding permission grants to ensure we don't leak data 1084 try { 1085 final PackageInfo pkg = mContext.getPackageManager().getPackageInfo( 1086 mContext.getPackageName(), PackageManager.GET_PROVIDERS); 1087 if (pkg != null && pkg.providers != null) { 1088 for (ProviderInfo provider : pkg.providers) { 1089 mContext.revokeUriPermission(Uri.parse("content://" + provider.authority), 1090 Intent.FLAG_GRANT_READ_URI_PERMISSION 1091 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 1092 } 1093 } 1094 } catch (Exception e) { 1095 Log.w(TAG, "Failed to revoke permissions", e); 1096 } 1097 1098 makePristineSchema(db); 1099 1100 db.execSQL("CREATE TABLE local_metadata (generation INTEGER DEFAULT 0)"); 1101 db.execSQL("INSERT INTO local_metadata VALUES (0)"); 1102 1103 db.execSQL("CREATE TABLE android_metadata (locale TEXT)"); 1104 db.execSQL("CREATE TABLE thumbnails (_id INTEGER PRIMARY KEY,_data TEXT,image_id INTEGER," 1105 + "kind INTEGER,width INTEGER,height INTEGER)"); 1106 db.execSQL("CREATE TABLE album_art (album_id INTEGER PRIMARY KEY,_data TEXT)"); 1107 db.execSQL("CREATE TABLE videothumbnails (_id INTEGER PRIMARY KEY,_data TEXT," 1108 + "video_id INTEGER,kind INTEGER,width INTEGER,height INTEGER)"); 1109 db.execSQL("CREATE TABLE files (_id INTEGER PRIMARY KEY AUTOINCREMENT," 1110 + "_data TEXT UNIQUE COLLATE NOCASE,_size INTEGER,format INTEGER,parent INTEGER," 1111 + "date_added INTEGER,date_modified INTEGER,mime_type TEXT,title TEXT," 1112 + "description TEXT,_display_name TEXT,picasa_id TEXT,orientation INTEGER," 1113 + "latitude DOUBLE,longitude DOUBLE,datetaken INTEGER,mini_thumb_magic INTEGER," 1114 + "bucket_id TEXT,bucket_display_name TEXT,isprivate INTEGER,title_key TEXT," 1115 + "artist_id INTEGER,album_id INTEGER,composer TEXT,track INTEGER," 1116 + "year INTEGER CHECK(year!=0),is_ringtone INTEGER,is_music INTEGER," 1117 + "is_alarm INTEGER,is_notification INTEGER,is_podcast INTEGER,album_artist TEXT," 1118 + "duration INTEGER,bookmark INTEGER,artist TEXT,album TEXT,resolution TEXT," 1119 + "tags TEXT,category TEXT,language TEXT,mini_thumb_data TEXT,name TEXT," 1120 + "media_type INTEGER,old_id INTEGER,is_drm INTEGER," 1121 + "width INTEGER, height INTEGER, title_resource_uri TEXT," 1122 + "owner_package_name TEXT DEFAULT NULL," 1123 + "color_standard INTEGER, color_transfer INTEGER, color_range INTEGER," 1124 + "_hash BLOB DEFAULT NULL, is_pending INTEGER DEFAULT 0," 1125 + "is_download INTEGER DEFAULT 0, download_uri TEXT DEFAULT NULL," 1126 + "referer_uri TEXT DEFAULT NULL, is_audiobook INTEGER DEFAULT 0," 1127 + "date_expires INTEGER DEFAULT NULL,is_trashed INTEGER DEFAULT 0," 1128 + "group_id INTEGER DEFAULT NULL,primary_directory TEXT DEFAULT NULL," 1129 + "secondary_directory TEXT DEFAULT NULL,document_id TEXT DEFAULT NULL," 1130 + "instance_id TEXT DEFAULT NULL,original_document_id TEXT DEFAULT NULL," 1131 + "relative_path TEXT DEFAULT NULL,volume_name TEXT DEFAULT NULL," 1132 + "artist_key TEXT DEFAULT NULL,album_key TEXT DEFAULT NULL," 1133 + "genre TEXT DEFAULT NULL,genre_key TEXT DEFAULT NULL,genre_id INTEGER," 1134 + "author TEXT DEFAULT NULL, bitrate INTEGER DEFAULT NULL," 1135 + "capture_framerate REAL DEFAULT NULL, cd_track_number TEXT DEFAULT NULL," 1136 + "compilation INTEGER DEFAULT NULL, disc_number TEXT DEFAULT NULL," 1137 + "is_favorite INTEGER DEFAULT 0, num_tracks INTEGER DEFAULT NULL," 1138 + "writer TEXT DEFAULT NULL, exposure_time TEXT DEFAULT NULL," 1139 + "f_number TEXT DEFAULT NULL, iso INTEGER DEFAULT NULL," 1140 + "scene_capture_type INTEGER DEFAULT NULL, generation_added INTEGER DEFAULT 0," 1141 + "generation_modified INTEGER DEFAULT 0, xmp BLOB DEFAULT NULL," 1142 + "_transcode_status INTEGER DEFAULT 0, _video_codec_type TEXT DEFAULT NULL," 1143 + "_modifier INTEGER DEFAULT 0, is_recording INTEGER DEFAULT 0," 1144 + "redacted_uri_id TEXT DEFAULT NULL, _user_id INTEGER DEFAULT " 1145 + UserHandle.myUserId() + ", _special_format INTEGER DEFAULT NULL)"); 1146 db.execSQL("CREATE TABLE log (time DATETIME, message TEXT)"); 1147 db.execSQL("CREATE TABLE deleted_media (_id INTEGER PRIMARY KEY AUTOINCREMENT," 1148 + "old_id INTEGER UNIQUE, generation_modified INTEGER NOT NULL)"); 1149 1150 if (isExternal()) { 1151 db.execSQL("CREATE TABLE audio_playlists_map (_id INTEGER PRIMARY KEY," 1152 + "audio_id INTEGER NOT NULL,playlist_id INTEGER NOT NULL," 1153 + "play_order INTEGER NOT NULL)"); 1154 updateAddMediaGrantsTable(db); 1155 } 1156 1157 createLatestViews(db); 1158 createLatestTriggers(db); 1159 createAllLatestIndexes(db); 1160 1161 // Since this code is used by both the legacy and modern providers, we 1162 // only want to migrate when we're running as the modern provider 1163 if (!mLegacyProvider) { 1164 try { 1165 new File(mContext.getFilesDir(), mMigrationFileName).createNewFile(); 1166 } catch (IOException e) { 1167 Log.e(TAG, "Failed to create a migration file: ." + mVolumeName, e); 1168 } 1169 } 1170 } 1171 createAllLatestIndexes(SQLiteDatabase db)1172 private void createAllLatestIndexes(SQLiteDatabase db) { 1173 createLatestIndexes(db); 1174 if (isExternal()) { 1175 createMediaGrantsIndex(db); 1176 } 1177 } 1178 updateAddMediaGrantsTable(SQLiteDatabase db)1179 private static void updateAddMediaGrantsTable(SQLiteDatabase db) { 1180 db.execSQL("DROP INDEX IF EXISTS media_grants.generation_granted"); 1181 db.execSQL("DROP TABLE IF EXISTS media_grants"); 1182 db.execSQL( 1183 "CREATE TABLE media_grants (" 1184 + "owner_package_name TEXT," 1185 + "file_id INTEGER," 1186 + "package_user_id INTEGER," 1187 + "generation_granted INTEGER DEFAULT 0," 1188 + "UNIQUE(owner_package_name, file_id, package_user_id)" 1189 + " ON CONFLICT IGNORE " 1190 + "FOREIGN KEY (file_id)" 1191 + " REFERENCES files(_id)" 1192 + " ON DELETE CASCADE" 1193 + ")"); 1194 createMediaGrantsIndex(db); 1195 } 1196 createMediaGrantsIndex(SQLiteDatabase db)1197 private static void createMediaGrantsIndex(SQLiteDatabase db) { 1198 db.execSQL( 1199 "CREATE INDEX generation_granted_index ON media_grants" 1200 + "(generation_granted)"); 1201 } 1202 1203 /** 1204 * Migrate important information from {@link MediaStore#AUTHORITY_LEGACY}, 1205 * if present on this device. We only do this once during early database 1206 * creation, to help us preserve information like {@link MediaColumns#_ID} 1207 * and {@link MediaColumns#IS_FAVORITE}. 1208 */ migrateFromLegacy(SQLiteDatabase db)1209 private void migrateFromLegacy(SQLiteDatabase db) { 1210 // TODO: focus this migration on secondary volumes once we have separate 1211 // databases for each volume; for now only migrate primary storage 1212 1213 try (ContentProviderClient client = mContext.getContentResolver() 1214 .acquireContentProviderClient(MediaStore.AUTHORITY_LEGACY)) { 1215 if (client == null) { 1216 Log.d(TAG, "No legacy provider available for migration"); 1217 return; 1218 } 1219 1220 final Uri queryUri = MediaStore 1221 .rewriteToLegacy(MediaStore.Files.getContentUri(mVolumeName)); 1222 1223 final Bundle extras = new Bundle(); 1224 extras.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE); 1225 extras.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE); 1226 extras.putInt(MediaStore.QUERY_ARG_MATCH_FAVORITE, MediaStore.MATCH_INCLUDE); 1227 1228 db.beginTransaction(); 1229 Log.d(TAG, "Starting migration from legacy provider"); 1230 if (mMigrationListener != null) { 1231 mMigrationListener.onStarted(client, mVolumeName); 1232 } 1233 try (Cursor c = client.query(queryUri, sMigrateColumns.toArray(new String[0]), 1234 extras, null)) { 1235 final ContentValues values = new ContentValues(); 1236 while (c.moveToNext()) { 1237 values.clear(); 1238 1239 // Start by deriving all values from migrated data column, 1240 // then overwrite with other migrated columns 1241 final String data = c.getString(c.getColumnIndex(MediaColumns.DATA)); 1242 values.put(MediaColumns.DATA, data); 1243 FileUtils.computeValuesFromData(values, /*isForFuse*/ false); 1244 final String volumeNameFromPath = values.getAsString(MediaColumns.VOLUME_NAME); 1245 for (String column : sMigrateColumns) { 1246 DatabaseUtils.copyFromCursorToContentValues(column, c, values); 1247 } 1248 final String volumeNameMigrated = values.getAsString(MediaColumns.VOLUME_NAME); 1249 // While upgrading from P OS or below, VOLUME_NAME can be NULL in legacy 1250 // database. When VOLUME_NAME is NULL, extract VOLUME_NAME from 1251 // MediaColumns.DATA 1252 if (volumeNameMigrated == null || volumeNameMigrated.isEmpty()) { 1253 values.put(MediaColumns.VOLUME_NAME, volumeNameFromPath); 1254 } 1255 1256 final String volumePath = FileUtils.extractVolumePath(data); 1257 1258 // Handle playlist files which may need special handling if 1259 // there are no "real" playlist files. 1260 final int mediaType = c.getInt(c.getColumnIndex(FileColumns.MEDIA_TYPE)); 1261 if (isExternal() && volumePath != null && 1262 mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 1263 File playlistFile = new File(data); 1264 1265 if (!playlistFile.exists()) { 1266 if (LOGV) Log.v(TAG, "Migrating playlist file " + playlistFile); 1267 1268 // Migrate virtual playlists to a "real" playlist file. 1269 // Also change playlist file name and path to adapt to new 1270 // default primary directory. 1271 String playlistFilePath = data; 1272 try { 1273 playlistFilePath = migratePlaylistFiles(client, 1274 c.getLong(c.getColumnIndex(FileColumns._ID))); 1275 // Either migration didn't happen or is not necessary because 1276 // playlist file already exists 1277 if (playlistFilePath == null) playlistFilePath = data; 1278 } catch (Exception e) { 1279 // We only have one shot to migrate data, so log and 1280 // keep marching forward. 1281 Log.w(TAG, "Couldn't migrate playlist file " + data); 1282 } 1283 1284 values.put(FileColumns.DATA, playlistFilePath); 1285 FileUtils.computeValuesFromData(values, /*isForFuse*/ false); 1286 } 1287 } 1288 1289 // When migrating pending or trashed files, we might need to 1290 // rename them on disk to match new schema 1291 if (volumePath != null) { 1292 final String oldData = values.getAsString(MediaColumns.DATA); 1293 FileUtils.computeDataFromValues(values, new File(volumePath), 1294 /*isForFuse*/ false); 1295 final String recomputedData = values.getAsString(MediaColumns.DATA); 1296 if (!Objects.equals(oldData, recomputedData)) { 1297 try { 1298 renameWithRetry(oldData, recomputedData); 1299 } catch (IOException e) { 1300 // We only have one shot to migrate data, so log and 1301 // keep marching forward 1302 Log.w(TAG, "Failed to rename " + values + "; continuing", e); 1303 FileUtils.computeValuesFromData(values, /*isForFuse*/ false); 1304 } 1305 } 1306 } 1307 1308 if (db.insert("files", null, values) == -1) { 1309 // We only have one shot to migrate data, so log and 1310 // keep marching forward 1311 Log.w(TAG, "Failed to insert " + values + "; continuing"); 1312 } 1313 1314 // To avoid SQLITE_NOMEM errors, we need to periodically 1315 // flush the current transaction and start another one 1316 if ((c.getPosition() % 2_000) == 0) { 1317 db.setTransactionSuccessful(); 1318 db.endTransaction(); 1319 db.beginTransaction(); 1320 1321 // And announce that we're actively making progress 1322 final int progress = c.getPosition(); 1323 final int total = c.getCount(); 1324 Log.v(TAG, "Migrated " + progress + " of " + total + "..."); 1325 if (mMigrationListener != null) { 1326 mMigrationListener.onProgress(client, mVolumeName, progress, total); 1327 } 1328 } 1329 } 1330 1331 Log.d(TAG, "Finished migration from legacy provider"); 1332 } catch (Exception e) { 1333 // We have to guard ourselves against any weird behavior of the 1334 // legacy provider by trying to catch everything 1335 Log.w(TAG, "Failed migration from legacy provider", e); 1336 } 1337 1338 // We tried our best above to migrate everything we could, and we 1339 // only have one possible shot, so mark everything successful 1340 db.setTransactionSuccessful(); 1341 db.endTransaction(); 1342 if (mMigrationListener != null) { 1343 mMigrationListener.onFinished(client, mVolumeName); 1344 } 1345 } 1346 1347 } 1348 1349 @Nullable migratePlaylistFiles(ContentProviderClient client, long playlistId)1350 private String migratePlaylistFiles(ContentProviderClient client, long playlistId) 1351 throws IllegalStateException { 1352 final String selection = FileColumns.MEDIA_TYPE + "=" + FileColumns.MEDIA_TYPE_PLAYLIST 1353 + " AND " + FileColumns._ID + "=" + playlistId; 1354 final String[] projection = new String[]{ 1355 FileColumns._ID, 1356 FileColumns.DATA, 1357 MediaColumns.MIME_TYPE, 1358 MediaStore.Audio.PlaylistsColumns.NAME, 1359 }; 1360 final Uri queryUri = MediaStore 1361 .rewriteToLegacy(MediaStore.Files.getContentUri(mVolumeName)); 1362 1363 try (Cursor cursor = client.query(queryUri, projection, selection, null, null)) { 1364 if (!cursor.moveToFirst()) { 1365 throw new IllegalStateException("Couldn't find database row for playlist file" 1366 + playlistId); 1367 } 1368 1369 final String data = cursor.getString(cursor.getColumnIndex(MediaColumns.DATA)); 1370 File playlistFile = new File(data); 1371 if (playlistFile.exists()) { 1372 throw new IllegalStateException("Playlist file exists " + data); 1373 } 1374 1375 String mimeType = cursor.getString(cursor.getColumnIndex(MediaColumns.MIME_TYPE)); 1376 // Sometimes, playlists in Q may have mimeType as 1377 // "application/octet-stream". Ensure that playlist rows have the 1378 // right playlist mimeType. These rows will be committed to a file 1379 // and hence they should have correct playlist mimeType for 1380 // Playlist#write to identify the right child playlist class. 1381 if (!MimeUtils.isPlaylistMimeType(mimeType)) { 1382 // Playlist files should always have right mimeType, default to 1383 // audio/mpegurl when mimeType doesn't match playlist media_type. 1384 mimeType = "audio/mpegurl"; 1385 } 1386 1387 // If the directory is Playlists/ change the directory to Music/ 1388 // since defaultPrimary for playlists is Music/. This helps 1389 // resolve any future app-compat issues around renaming playlist 1390 // files. 1391 File parentFile = playlistFile.getParentFile(); 1392 if (parentFile.getName().equalsIgnoreCase("Playlists")) { 1393 parentFile = new File(parentFile.getParentFile(), Environment.DIRECTORY_MUSIC); 1394 } 1395 final String playlistName = cursor.getString( 1396 cursor.getColumnIndex(MediaStore.Audio.PlaylistsColumns.NAME)); 1397 1398 try { 1399 // Build playlist file path with a file extension that matches 1400 // playlist mimeType. 1401 playlistFile = FileUtils.buildUniqueFile(parentFile, mimeType, playlistName); 1402 } catch(FileNotFoundException e) { 1403 Log.e(TAG, "Couldn't create unique file for " + playlistFile + 1404 ", using actual playlist file name", e); 1405 } 1406 1407 final long rowId = cursor.getLong(cursor.getColumnIndex(FileColumns._ID)); 1408 final Uri playlistMemberUri = MediaStore.rewriteToLegacy( 1409 MediaStore.Audio.Playlists.Members.getContentUri(mVolumeName, rowId)); 1410 createPlaylistFile(client, playlistMemberUri, playlistFile); 1411 return playlistFile.getAbsolutePath(); 1412 } catch (RemoteException e) { 1413 throw new IllegalStateException(e); 1414 } 1415 } 1416 1417 /** 1418 * Creates "real" playlist files on disk from the playlist data from the database. 1419 */ createPlaylistFile(ContentProviderClient client, @NonNull Uri playlistMemberUri, @NonNull File playlistFile)1420 private void createPlaylistFile(ContentProviderClient client, @NonNull Uri playlistMemberUri, 1421 @NonNull File playlistFile) throws IllegalStateException { 1422 final String[] projection = new String[] { 1423 MediaStore.Audio.Playlists.Members.AUDIO_ID, 1424 MediaStore.Audio.Playlists.Members.PLAY_ORDER, 1425 }; 1426 1427 final Playlist playlist = new Playlist(); 1428 // Migrating music->playlist association. 1429 try (Cursor c = client.query(playlistMemberUri, projection, null, null, 1430 Audio.Playlists.Members.DEFAULT_SORT_ORDER)) { 1431 while (c.moveToNext()) { 1432 // Write these values to the playlist file 1433 final long audioId = c.getLong(0); 1434 final int playOrder = c.getInt(1); 1435 1436 final Uri audioFileUri = MediaStore.rewriteToLegacy(ContentUris.withAppendedId( 1437 MediaStore.Files.getContentUri(mVolumeName), audioId)); 1438 final String audioFilePath = queryForData(client, audioFileUri); 1439 if (audioFilePath == null) { 1440 // This shouldn't happen, we should always find audio file 1441 // unless audio file is removed, and database has stale db 1442 // row. However this shouldn't block creating playlist 1443 // files; 1444 Log.e(TAG, "Couldn't find audio file for " + audioId + ", continuing.."); 1445 continue; 1446 } 1447 playlist.add(playOrder, playlistFile.toPath().getParent(). 1448 relativize(new File(audioFilePath).toPath())); 1449 } 1450 1451 try { 1452 writeToPlaylistFileWithRetry(playlistFile, playlist); 1453 } catch (IOException e) { 1454 // We only have one shot to migrate data, so log and 1455 // keep marching forward. 1456 Log.w(TAG, "Couldn't migrate playlist file " + playlistFile); 1457 } 1458 } catch (RemoteException e) { 1459 throw new IllegalStateException(e); 1460 } 1461 } 1462 1463 /** 1464 * Return the {@link MediaColumns#DATA} field for the given {@code uri}. 1465 */ queryForData(ContentProviderClient client, @NonNull Uri uri)1466 private String queryForData(ContentProviderClient client, @NonNull Uri uri) { 1467 try (Cursor c = client.query(uri, new String[] {FileColumns.DATA}, Bundle.EMPTY, null)) { 1468 if (c.moveToFirst()) { 1469 return c.getString(0); 1470 } 1471 } catch (Exception e) { 1472 Log.w(TAG, "Exception occurred while querying for data file for " + uri, e); 1473 } 1474 return null; 1475 } 1476 1477 /** 1478 * Set of columns that should be migrated from the legacy provider, 1479 * including core information to identify each media item, followed by 1480 * columns that can be edited by users. (We omit columns here that are 1481 * marked as "readOnly" in the {@link MediaStore} annotations, since those 1482 * will be regenerated by the first scan after upgrade.) 1483 */ 1484 private static final ArraySet<String> sMigrateColumns = new ArraySet<>(); 1485 1486 { 1487 sMigrateColumns.add(MediaStore.MediaColumns._ID); 1488 sMigrateColumns.add(MediaStore.MediaColumns.DATA); 1489 sMigrateColumns.add(MediaStore.MediaColumns.VOLUME_NAME); 1490 sMigrateColumns.add(MediaStore.Files.FileColumns.MEDIA_TYPE); 1491 1492 sMigrateColumns.add(MediaStore.MediaColumns.DATE_ADDED); 1493 sMigrateColumns.add(MediaStore.MediaColumns.DATE_EXPIRES); 1494 sMigrateColumns.add(MediaStore.MediaColumns.IS_PENDING); 1495 sMigrateColumns.add(MediaStore.MediaColumns.IS_TRASHED); 1496 sMigrateColumns.add(MediaStore.MediaColumns.IS_FAVORITE); 1497 sMigrateColumns.add(MediaStore.MediaColumns.OWNER_PACKAGE_NAME); 1498 1499 sMigrateColumns.add(MediaStore.MediaColumns.ORIENTATION); 1500 sMigrateColumns.add(MediaStore.Files.FileColumns.PARENT); 1501 1502 sMigrateColumns.add(MediaStore.Audio.AudioColumns.BOOKMARK); 1503 1504 sMigrateColumns.add(MediaStore.Video.VideoColumns.TAGS); 1505 sMigrateColumns.add(MediaStore.Video.VideoColumns.CATEGORY); 1506 sMigrateColumns.add(MediaStore.Video.VideoColumns.BOOKMARK); 1507 1508 // This also migrates MediaStore.Images.ImageColumns.IS_PRIVATE 1509 // as they both have the same value "isprivate". 1510 sMigrateColumns.add(MediaStore.Video.VideoColumns.IS_PRIVATE); 1511 1512 sMigrateColumns.add(MediaStore.DownloadColumns.DOWNLOAD_URI); 1513 sMigrateColumns.add(MediaStore.DownloadColumns.REFERER_URI); 1514 } 1515 makePristineViews(SQLiteDatabase db)1516 private static void makePristineViews(SQLiteDatabase db) { 1517 // drop all views 1518 Cursor c = db.query("sqlite_master", new String[] {"name"}, "type is 'view'", 1519 null, null, null, null); 1520 while (c.moveToNext()) { 1521 db.execSQL("DROP VIEW IF EXISTS " + c.getString(0)); 1522 } 1523 c.close(); 1524 } 1525 createLatestViews(SQLiteDatabase db)1526 private void createLatestViews(SQLiteDatabase db) { 1527 makePristineViews(db); 1528 1529 if (!mProjectionHelper.hasColumnAnnotation()) { 1530 Log.w(TAG, "No column annotation provided; not creating views"); 1531 return; 1532 } 1533 1534 final String filterVolumeNames; 1535 synchronized (mFilterVolumeNames) { 1536 filterVolumeNames = bindList(mFilterVolumeNames.toArray()); 1537 } 1538 1539 if (isExternal()) { 1540 db.execSQL("CREATE VIEW audio_playlists AS SELECT " 1541 + getColumnsForCollection(Audio.Playlists.class) 1542 + " FROM files WHERE media_type=4"); 1543 } 1544 1545 db.execSQL("CREATE VIEW searchhelpertitle AS SELECT * FROM audio ORDER BY title_key"); 1546 db.execSQL("CREATE VIEW search AS SELECT _id,'artist' AS mime_type,artist,NULL AS album," 1547 + "NULL AS title,artist AS text1,NULL AS text2,number_of_albums AS data1," 1548 + "number_of_tracks AS data2,artist_key AS match," 1549 + "'content://media/external/audio/artists/'||_id AS suggest_intent_data," 1550 + "1 AS grouporder FROM artist_info WHERE (artist!='<unknown>')" 1551 + " UNION ALL SELECT _id,'album' AS mime_type,artist,album," 1552 + "NULL AS title,album AS text1,artist AS text2,NULL AS data1," 1553 + "NULL AS data2,artist_key||' '||album_key AS match," 1554 + "'content://media/external/audio/albums/'||_id AS suggest_intent_data," 1555 + "2 AS grouporder FROM album_info" 1556 + " WHERE (album!='<unknown>')" 1557 + " UNION ALL SELECT searchhelpertitle._id AS _id,mime_type,artist,album,title," 1558 + "title AS text1,artist AS text2,NULL AS data1," 1559 + "NULL AS data2,artist_key||' '||album_key||' '||title_key AS match," 1560 + "'content://media/external/audio/media/'||searchhelpertitle._id" 1561 + " AS suggest_intent_data," 1562 + "3 AS grouporder FROM searchhelpertitle WHERE (title != '')"); 1563 1564 db.execSQL("CREATE VIEW audio AS SELECT " 1565 + getColumnsForCollection(Audio.Media.class) 1566 + " FROM files WHERE media_type=2"); 1567 db.execSQL("CREATE VIEW video AS SELECT " 1568 + getColumnsForCollection(Video.Media.class) 1569 + " FROM files WHERE media_type=3"); 1570 db.execSQL("CREATE VIEW images AS SELECT " 1571 + getColumnsForCollection(Images.Media.class) 1572 + " FROM files WHERE media_type=1"); 1573 db.execSQL("CREATE VIEW downloads AS SELECT " 1574 + getColumnsForCollection(Downloads.class) 1575 + " FROM files WHERE is_download=1"); 1576 1577 db.execSQL("CREATE VIEW audio_artists AS SELECT " 1578 + " artist_id AS " + Audio.Artists._ID 1579 + ", MIN(artist) AS " + Audio.Artists.ARTIST 1580 + ", artist_key AS " + Audio.Artists.ARTIST_KEY 1581 + ", COUNT(DISTINCT album_id) AS " + Audio.Artists.NUMBER_OF_ALBUMS 1582 + ", COUNT(DISTINCT _id) AS " + Audio.Artists.NUMBER_OF_TRACKS 1583 + " FROM audio" 1584 + " WHERE is_music=1 AND is_pending=0 AND is_trashed=0" 1585 + " AND volume_name IN " + filterVolumeNames 1586 + " GROUP BY artist_id"); 1587 1588 db.execSQL("CREATE VIEW audio_artists_albums AS SELECT " 1589 + " album_id AS " + Audio.Albums._ID 1590 + ", album_id AS " + Audio.Albums.ALBUM_ID 1591 + ", MIN(album) AS " + Audio.Albums.ALBUM 1592 + ", album_key AS " + Audio.Albums.ALBUM_KEY 1593 + ", artist_id AS " + Audio.Albums.ARTIST_ID 1594 + ", artist AS " + Audio.Albums.ARTIST 1595 + ", artist_key AS " + Audio.Albums.ARTIST_KEY 1596 + ", (SELECT COUNT(*) FROM audio WHERE " + Audio.Albums.ALBUM_ID 1597 + " = TEMP.album_id) AS " + Audio.Albums.NUMBER_OF_SONGS 1598 + ", COUNT(DISTINCT _id) AS " + Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST 1599 + ", MIN(year) AS " + Audio.Albums.FIRST_YEAR 1600 + ", MAX(year) AS " + Audio.Albums.LAST_YEAR 1601 + ", NULL AS " + Audio.Albums.ALBUM_ART 1602 + " FROM audio TEMP" 1603 + " WHERE is_music=1 AND is_pending=0 AND is_trashed=0" 1604 + " AND volume_name IN " + filterVolumeNames 1605 + " GROUP BY album_id, artist_id"); 1606 1607 db.execSQL("CREATE VIEW audio_albums AS SELECT " 1608 + " album_id AS " + Audio.Albums._ID 1609 + ", album_id AS " + Audio.Albums.ALBUM_ID 1610 + ", MIN(album) AS " + Audio.Albums.ALBUM 1611 + ", album_key AS " + Audio.Albums.ALBUM_KEY 1612 + ", artist_id AS " + Audio.Albums.ARTIST_ID 1613 + ", artist AS " + Audio.Albums.ARTIST 1614 + ", artist_key AS " + Audio.Albums.ARTIST_KEY 1615 + ", COUNT(DISTINCT _id) AS " + Audio.Albums.NUMBER_OF_SONGS 1616 + ", COUNT(DISTINCT _id) AS " + Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST 1617 + ", MIN(year) AS " + Audio.Albums.FIRST_YEAR 1618 + ", MAX(year) AS " + Audio.Albums.LAST_YEAR 1619 + ", NULL AS " + Audio.Albums.ALBUM_ART 1620 + " FROM audio" 1621 + " WHERE is_music=1 AND is_pending=0 AND is_trashed=0" 1622 + " AND volume_name IN " + filterVolumeNames 1623 + " GROUP BY album_id"); 1624 1625 db.execSQL("CREATE VIEW audio_genres AS SELECT " 1626 + " genre_id AS " + Audio.Genres._ID 1627 + ", MIN(genre) AS " + Audio.Genres.NAME 1628 + " FROM audio" 1629 + " WHERE is_pending=0 AND is_trashed=0 AND volume_name IN " + filterVolumeNames 1630 + " GROUP BY genre_id"); 1631 } 1632 getColumnsForCollection(Class<?> collection)1633 private String getColumnsForCollection(Class<?> collection) { 1634 return String.join(",", mProjectionHelper.getProjectionMap(collection).keySet()) 1635 + ",_modifier"; 1636 } 1637 makePristineTriggers(SQLiteDatabase db)1638 private static void makePristineTriggers(SQLiteDatabase db) { 1639 // drop all triggers 1640 Cursor c = db.query("sqlite_master", new String[] {"name"}, "type is 'trigger'", 1641 null, null, null, null); 1642 while (c.moveToNext()) { 1643 if (c.getString(0).startsWith("sqlite_")) continue; 1644 db.execSQL("DROP TRIGGER IF EXISTS " + c.getString(0)); 1645 } 1646 c.close(); 1647 } 1648 createLatestTriggers(SQLiteDatabase db)1649 private static void createLatestTriggers(SQLiteDatabase db) { 1650 makePristineTriggers(db); 1651 1652 final String insertArg = 1653 "new.volume_name||':'||new._id||':'||new.media_type||':'||new" 1654 + ".is_download||':'||new.is_pending||':'||new.is_trashed||':'||new" 1655 + ".is_favorite||':'||new._user_id||':'||ifnull(new.date_expires,'null')" 1656 + "||':'||ifnull(new.owner_package_name,'null')||':'||new._data"; 1657 final String updateArg = 1658 "old.volume_name||':'||old._id||':'||old.media_type||':'||old.is_download" 1659 + "||':'||new._id||':'||new.media_type||':'||new.is_download" 1660 + "||':'||old.is_trashed||':'||new.is_trashed" 1661 + "||':'||old.is_pending||':'||new.is_pending" 1662 + "||':'||ifnull(old.is_favorite,0)" 1663 + "||':'||ifnull(new.is_favorite,0)" 1664 + "||':'||ifnull(old._special_format,0)" 1665 + "||':'||ifnull(new._special_format,0)" 1666 + "||':'||ifnull(old.owner_package_name,'null')" 1667 + "||':'||ifnull(new.owner_package_name,'null')" 1668 + "||':'||ifnull(old._user_id,0)" 1669 + "||':'||ifnull(new._user_id,0)" 1670 + "||':'||ifnull(old.date_expires,'null')" 1671 + "||':'||ifnull(new.date_expires,'null')" 1672 + "||':'||old._data"; 1673 final String deleteArg = 1674 "old.volume_name||':'||old._id||':'||old.media_type||':'||old.is_download" 1675 + "||':'||ifnull(old.owner_package_name,'null')||':'||old._data"; 1676 1677 db.execSQL("CREATE TRIGGER files_insert AFTER INSERT ON files" 1678 + " BEGIN SELECT _INSERT(" + insertArg + "); END"); 1679 db.execSQL("CREATE TRIGGER files_update AFTER UPDATE ON files" 1680 + " BEGIN SELECT _UPDATE(" + updateArg + "); END"); 1681 db.execSQL("CREATE TRIGGER files_delete AFTER DELETE ON files" 1682 + " BEGIN SELECT _DELETE(" + deleteArg + "); END"); 1683 } 1684 makePristineIndexes(SQLiteDatabase db)1685 private static void makePristineIndexes(SQLiteDatabase db) { 1686 // drop all indexes 1687 Cursor c = db.query("sqlite_master", new String[] {"name"}, "type is 'index'", 1688 null, null, null, null); 1689 while (c.moveToNext()) { 1690 if (c.getString(0).startsWith("sqlite_")) continue; 1691 db.execSQL("DROP INDEX IF EXISTS " + c.getString(0)); 1692 } 1693 c.close(); 1694 } 1695 createLatestIndexes(SQLiteDatabase db)1696 private static void createLatestIndexes(SQLiteDatabase db) { 1697 makePristineIndexes(db); 1698 1699 db.execSQL("CREATE INDEX image_id_index on thumbnails(image_id)"); 1700 db.execSQL("CREATE INDEX video_id_index on videothumbnails(video_id)"); 1701 db.execSQL("CREATE INDEX album_id_idx ON files(album_id)"); 1702 db.execSQL("CREATE INDEX artist_id_idx ON files(artist_id)"); 1703 db.execSQL("CREATE INDEX genre_id_idx ON files(genre_id)"); 1704 db.execSQL("CREATE INDEX bucket_index on files(bucket_id,media_type,datetaken, _id)"); 1705 db.execSQL("CREATE INDEX bucket_name on files(bucket_id,media_type,bucket_display_name)"); 1706 db.execSQL("CREATE INDEX format_index ON files(format)"); 1707 db.execSQL("CREATE INDEX media_type_index ON files(media_type)"); 1708 db.execSQL("CREATE INDEX parent_index ON files(parent)"); 1709 db.execSQL("CREATE INDEX path_index ON files(_data)"); 1710 db.execSQL("CREATE INDEX sort_index ON files(datetaken ASC, _id ASC)"); 1711 db.execSQL("CREATE INDEX title_idx ON files(title)"); 1712 db.execSQL("CREATE INDEX titlekey_index ON files(title_key)"); 1713 db.execSQL("CREATE INDEX date_modified_index ON files(date_modified)"); 1714 db.execSQL("CREATE INDEX generation_modified_index ON files(generation_modified)"); 1715 } 1716 updateCollationKeys(SQLiteDatabase db)1717 private static void updateCollationKeys(SQLiteDatabase db) { 1718 // Delete albums and artists, then clear the modification time on songs, which 1719 // will cause the media scanner to rescan everything, rebuilding the artist and 1720 // album tables along the way, while preserving playlists. 1721 // We need this rescan because ICU also changed, and now generates different 1722 // collation keys 1723 db.execSQL("DELETE from albums"); 1724 db.execSQL("DELETE from artists"); 1725 db.execSQL("UPDATE files SET date_modified=0;"); 1726 } 1727 updateAddTitleResource(SQLiteDatabase db)1728 private static void updateAddTitleResource(SQLiteDatabase db) { 1729 // Add the column used for title localization, and force a rescan of any 1730 // ringtones, alarms and notifications that may be using it. 1731 db.execSQL("ALTER TABLE files ADD COLUMN title_resource_uri TEXT"); 1732 db.execSQL("UPDATE files SET date_modified=0" 1733 + " WHERE (is_alarm IS 1) OR (is_ringtone IS 1) OR (is_notification IS 1)"); 1734 } 1735 updateAddOwnerPackageName(SQLiteDatabase db)1736 private static void updateAddOwnerPackageName(SQLiteDatabase db) { 1737 db.execSQL("ALTER TABLE files ADD COLUMN owner_package_name TEXT DEFAULT NULL"); 1738 1739 // Derive new column value based on well-known paths 1740 try (Cursor c = db.query("files", new String[] { FileColumns._ID, FileColumns.DATA }, 1741 FileColumns.DATA + " REGEXP '" + FileUtils.PATTERN_OWNED_PATH.pattern() + "'", 1742 null, null, null, null, null)) { 1743 Log.d(TAG, "Updating " + c.getCount() + " entries with well-known owners"); 1744 1745 final Matcher m = FileUtils.PATTERN_OWNED_PATH.matcher(""); 1746 final ContentValues values = new ContentValues(); 1747 1748 while (c.moveToNext()) { 1749 final long id = c.getLong(0); 1750 final String data = c.getString(1); 1751 m.reset(data); 1752 if (m.matches()) { 1753 final String packageName = m.group(1); 1754 values.clear(); 1755 values.put(FileColumns.OWNER_PACKAGE_NAME, packageName); 1756 db.update("files", values, "_id=" + id, null); 1757 } 1758 } 1759 } 1760 } 1761 updateAddColorSpaces(SQLiteDatabase db)1762 private static void updateAddColorSpaces(SQLiteDatabase db) { 1763 // Add the color aspects related column used for HDR detection etc. 1764 db.execSQL("ALTER TABLE files ADD COLUMN color_standard INTEGER;"); 1765 db.execSQL("ALTER TABLE files ADD COLUMN color_transfer INTEGER;"); 1766 db.execSQL("ALTER TABLE files ADD COLUMN color_range INTEGER;"); 1767 } 1768 updateAddHashAndPending(SQLiteDatabase db)1769 private static void updateAddHashAndPending(SQLiteDatabase db) { 1770 db.execSQL("ALTER TABLE files ADD COLUMN _hash BLOB DEFAULT NULL;"); 1771 db.execSQL("ALTER TABLE files ADD COLUMN is_pending INTEGER DEFAULT 0;"); 1772 } 1773 updateAddDownloadInfo(SQLiteDatabase db)1774 private static void updateAddDownloadInfo(SQLiteDatabase db) { 1775 db.execSQL("ALTER TABLE files ADD COLUMN is_download INTEGER DEFAULT 0;"); 1776 db.execSQL("ALTER TABLE files ADD COLUMN download_uri TEXT DEFAULT NULL;"); 1777 db.execSQL("ALTER TABLE files ADD COLUMN referer_uri TEXT DEFAULT NULL;"); 1778 } 1779 updateAddAudiobook(SQLiteDatabase db)1780 private static void updateAddAudiobook(SQLiteDatabase db) { 1781 db.execSQL("ALTER TABLE files ADD COLUMN is_audiobook INTEGER DEFAULT 0;"); 1782 } 1783 updateAddRecording(SQLiteDatabase db)1784 private static void updateAddRecording(SQLiteDatabase db) { 1785 db.execSQL("ALTER TABLE files ADD COLUMN is_recording INTEGER DEFAULT 0;"); 1786 // We add the column is_recording, rescan all music files 1787 db.execSQL("UPDATE files SET date_modified=0 WHERE is_music=1;"); 1788 } 1789 updateAddRedactedUriId(SQLiteDatabase db)1790 private static void updateAddRedactedUriId(SQLiteDatabase db) { 1791 db.execSQL("ALTER TABLE files ADD COLUMN redacted_uri_id TEXT DEFAULT NULL;"); 1792 } 1793 updateClearLocation(SQLiteDatabase db)1794 private static void updateClearLocation(SQLiteDatabase db) { 1795 db.execSQL("UPDATE files SET latitude=NULL, longitude=NULL;"); 1796 } 1797 updateSetIsDownload(SQLiteDatabase db)1798 private static void updateSetIsDownload(SQLiteDatabase db) { 1799 db.execSQL("UPDATE files SET is_download=1 WHERE _data REGEXP '" 1800 + FileUtils.PATTERN_DOWNLOADS_FILE + "'"); 1801 } 1802 updateAddExpiresAndTrashed(SQLiteDatabase db)1803 private static void updateAddExpiresAndTrashed(SQLiteDatabase db) { 1804 db.execSQL("ALTER TABLE files ADD COLUMN date_expires INTEGER DEFAULT NULL;"); 1805 db.execSQL("ALTER TABLE files ADD COLUMN is_trashed INTEGER DEFAULT 0;"); 1806 } 1807 updateAddGroupId(SQLiteDatabase db)1808 private static void updateAddGroupId(SQLiteDatabase db) { 1809 db.execSQL("ALTER TABLE files ADD COLUMN group_id INTEGER DEFAULT NULL;"); 1810 } 1811 updateAddDirectories(SQLiteDatabase db)1812 private static void updateAddDirectories(SQLiteDatabase db) { 1813 db.execSQL("ALTER TABLE files ADD COLUMN primary_directory TEXT DEFAULT NULL;"); 1814 db.execSQL("ALTER TABLE files ADD COLUMN secondary_directory TEXT DEFAULT NULL;"); 1815 } 1816 updateAddXmpMm(SQLiteDatabase db)1817 private static void updateAddXmpMm(SQLiteDatabase db) { 1818 db.execSQL("ALTER TABLE files ADD COLUMN document_id TEXT DEFAULT NULL;"); 1819 db.execSQL("ALTER TABLE files ADD COLUMN instance_id TEXT DEFAULT NULL;"); 1820 db.execSQL("ALTER TABLE files ADD COLUMN original_document_id TEXT DEFAULT NULL;"); 1821 } 1822 updateAddPath(SQLiteDatabase db)1823 private static void updateAddPath(SQLiteDatabase db) { 1824 db.execSQL("ALTER TABLE files ADD COLUMN relative_path TEXT DEFAULT NULL;"); 1825 } 1826 updateAddVolumeName(SQLiteDatabase db)1827 private static void updateAddVolumeName(SQLiteDatabase db) { 1828 db.execSQL("ALTER TABLE files ADD COLUMN volume_name TEXT DEFAULT NULL;"); 1829 } 1830 updateDirsMimeType(SQLiteDatabase db)1831 private static void updateDirsMimeType(SQLiteDatabase db) { 1832 db.execSQL("UPDATE files SET mime_type=NULL WHERE format=" 1833 + MtpConstants.FORMAT_ASSOCIATION); 1834 } 1835 updateRelativePath(SQLiteDatabase db)1836 private static void updateRelativePath(SQLiteDatabase db) { 1837 db.execSQL("UPDATE files" 1838 + " SET " + MediaColumns.RELATIVE_PATH + "=" + MediaColumns.RELATIVE_PATH + "||'/'" 1839 + " WHERE " + MediaColumns.RELATIVE_PATH + " IS NOT NULL" 1840 + " AND " + MediaColumns.RELATIVE_PATH + " NOT LIKE '%/';"); 1841 } 1842 updateAddTranscodeSatus(SQLiteDatabase db)1843 private static void updateAddTranscodeSatus(SQLiteDatabase db) { 1844 db.execSQL("ALTER TABLE files ADD COLUMN _transcode_status INTEGER DEFAULT 0;"); 1845 } 1846 updateAddSpecialFormat(SQLiteDatabase db)1847 private static void updateAddSpecialFormat(SQLiteDatabase db) { 1848 db.execSQL("ALTER TABLE files ADD COLUMN _special_format INTEGER DEFAULT NULL;"); 1849 } 1850 updateSpecialFormatToNotDetected(SQLiteDatabase db)1851 private static void updateSpecialFormatToNotDetected(SQLiteDatabase db) { 1852 db.execSQL("UPDATE files SET _special_format=NULL WHERE _special_format=0"); 1853 } 1854 updateAddVideoCodecType(SQLiteDatabase db)1855 private static void updateAddVideoCodecType(SQLiteDatabase db) { 1856 db.execSQL("ALTER TABLE files ADD COLUMN _video_codec_type TEXT DEFAULT NULL;"); 1857 } 1858 updateClearDirectories(SQLiteDatabase db)1859 private static void updateClearDirectories(SQLiteDatabase db) { 1860 db.execSQL("UPDATE files SET primary_directory=NULL, secondary_directory=NULL;"); 1861 } 1862 updateRestructureAudio(SQLiteDatabase db)1863 private static void updateRestructureAudio(SQLiteDatabase db) { 1864 db.execSQL("ALTER TABLE files ADD COLUMN artist_key TEXT DEFAULT NULL;"); 1865 db.execSQL("ALTER TABLE files ADD COLUMN album_key TEXT DEFAULT NULL;"); 1866 db.execSQL("ALTER TABLE files ADD COLUMN genre TEXT DEFAULT NULL;"); 1867 db.execSQL("ALTER TABLE files ADD COLUMN genre_key TEXT DEFAULT NULL;"); 1868 db.execSQL("ALTER TABLE files ADD COLUMN genre_id INTEGER;"); 1869 1870 db.execSQL("DROP TABLE IF EXISTS artists;"); 1871 db.execSQL("DROP TABLE IF EXISTS albums;"); 1872 db.execSQL("DROP TABLE IF EXISTS audio_genres;"); 1873 db.execSQL("DROP TABLE IF EXISTS audio_genres_map;"); 1874 1875 db.execSQL("CREATE INDEX genre_id_idx ON files(genre_id)"); 1876 1877 db.execSQL("DROP INDEX IF EXISTS album_idx"); 1878 db.execSQL("DROP INDEX IF EXISTS albumkey_index"); 1879 db.execSQL("DROP INDEX IF EXISTS artist_idx"); 1880 db.execSQL("DROP INDEX IF EXISTS artistkey_index"); 1881 1882 // Since we're radically changing how the schema is defined, the 1883 // simplest path forward is to rescan all audio files 1884 db.execSQL("UPDATE files SET date_modified=0 WHERE media_type=2;"); 1885 } 1886 updateAddMetadata(SQLiteDatabase db)1887 private static void updateAddMetadata(SQLiteDatabase db) { 1888 db.execSQL("ALTER TABLE files ADD COLUMN author TEXT DEFAULT NULL;"); 1889 db.execSQL("ALTER TABLE files ADD COLUMN bitrate INTEGER DEFAULT NULL;"); 1890 db.execSQL("ALTER TABLE files ADD COLUMN capture_framerate REAL DEFAULT NULL;"); 1891 db.execSQL("ALTER TABLE files ADD COLUMN cd_track_number TEXT DEFAULT NULL;"); 1892 db.execSQL("ALTER TABLE files ADD COLUMN compilation INTEGER DEFAULT NULL;"); 1893 db.execSQL("ALTER TABLE files ADD COLUMN disc_number TEXT DEFAULT NULL;"); 1894 db.execSQL("ALTER TABLE files ADD COLUMN is_favorite INTEGER DEFAULT 0;"); 1895 db.execSQL("ALTER TABLE files ADD COLUMN num_tracks INTEGER DEFAULT NULL;"); 1896 db.execSQL("ALTER TABLE files ADD COLUMN writer TEXT DEFAULT NULL;"); 1897 db.execSQL("ALTER TABLE files ADD COLUMN exposure_time TEXT DEFAULT NULL;"); 1898 db.execSQL("ALTER TABLE files ADD COLUMN f_number TEXT DEFAULT NULL;"); 1899 db.execSQL("ALTER TABLE files ADD COLUMN iso INTEGER DEFAULT NULL;"); 1900 } 1901 updateAddSceneCaptureType(SQLiteDatabase db)1902 private static void updateAddSceneCaptureType(SQLiteDatabase db) { 1903 db.execSQL("ALTER TABLE files ADD COLUMN scene_capture_type INTEGER DEFAULT NULL;"); 1904 } 1905 updateMigrateLogs(SQLiteDatabase db)1906 private static void updateMigrateLogs(SQLiteDatabase db) { 1907 // Migrate any existing logs to new system 1908 try (Cursor c = db.query("log", new String[] { "time", "message" }, 1909 null, null, null, null, null)) { 1910 while (c.moveToNext()) { 1911 final String time = c.getString(0); 1912 final String message = c.getString(1); 1913 Logging.logPersistent("Historical log " + time + " " + message); 1914 } 1915 } 1916 db.execSQL("DELETE FROM log;"); 1917 } 1918 updateAddLocalMetadata(SQLiteDatabase db)1919 private static void updateAddLocalMetadata(SQLiteDatabase db) { 1920 db.execSQL("CREATE TABLE local_metadata (generation INTEGER DEFAULT 0)"); 1921 db.execSQL("INSERT INTO local_metadata VALUES (0)"); 1922 } 1923 updateAddGeneration(SQLiteDatabase db)1924 private static void updateAddGeneration(SQLiteDatabase db) { 1925 db.execSQL("ALTER TABLE files ADD COLUMN generation_added INTEGER DEFAULT 0;"); 1926 db.execSQL("ALTER TABLE files ADD COLUMN generation_modified INTEGER DEFAULT 0;"); 1927 } 1928 updateAddXmp(SQLiteDatabase db)1929 private static void updateAddXmp(SQLiteDatabase db) { 1930 db.execSQL("ALTER TABLE files ADD COLUMN xmp BLOB DEFAULT NULL;"); 1931 } 1932 updateAudioAlbumId(SQLiteDatabase db)1933 private static void updateAudioAlbumId(SQLiteDatabase db) { 1934 // We change the logic for generating album id, rescan all audio files 1935 db.execSQL("UPDATE files SET date_modified=0 WHERE media_type=2;"); 1936 } 1937 updateAddModifier(SQLiteDatabase db)1938 private static void updateAddModifier(SQLiteDatabase db) { 1939 db.execSQL("ALTER TABLE files ADD COLUMN _modifier INTEGER DEFAULT 0;"); 1940 // For existing files, set default value as _MODIFIER_MEDIA_SCAN 1941 db.execSQL("UPDATE files SET _modifier=3;"); 1942 } 1943 updateAddDeletedMediaTable(SQLiteDatabase db)1944 private static void updateAddDeletedMediaTable(SQLiteDatabase db) { 1945 db.execSQL("CREATE TABLE deleted_media (_id INTEGER PRIMARY KEY AUTOINCREMENT," 1946 + "old_id INTEGER UNIQUE, generation_modified INTEGER NOT NULL)"); 1947 } 1948 1949 /** 1950 * Alters existing database media_grants table to have an additional column to store 1951 * generation_granted. 1952 */ updateAddGenerationGranted(SQLiteDatabase db)1953 private static void updateAddGenerationGranted(SQLiteDatabase db) { 1954 db.execSQL("ALTER TABLE media_grants ADD COLUMN " + MediaGrants.GENERATION_GRANTED 1955 + " INTEGER DEFAULT 0"); 1956 createMediaGrantsIndex(db); 1957 } 1958 updateAddDateModifiedAndGenerationModifiedIndexes(SQLiteDatabase db)1959 private static void updateAddDateModifiedAndGenerationModifiedIndexes(SQLiteDatabase db) { 1960 db.execSQL("CREATE INDEX date_modified_index ON files(date_modified)"); 1961 db.execSQL("CREATE INDEX generation_modified_index ON files(generation_modified)"); 1962 } 1963 updateUserId(SQLiteDatabase db)1964 private void updateUserId(SQLiteDatabase db) { 1965 db.execSQL(String.format(Locale.ROOT, 1966 "ALTER TABLE files ADD COLUMN _user_id INTEGER DEFAULT %d;", 1967 UserHandle.myUserId())); 1968 } 1969 recomputeDataValues(SQLiteDatabase db)1970 private static void recomputeDataValues(SQLiteDatabase db) { 1971 try (Cursor c = db.query("files", new String[] { FileColumns._ID, FileColumns.DATA }, 1972 null, null, null, null, null, null)) { 1973 Log.d(TAG, "Recomputing " + c.getCount() + " data values"); 1974 1975 final ContentValues values = new ContentValues(); 1976 while (c.moveToNext()) { 1977 values.clear(); 1978 final long id = c.getLong(0); 1979 final String data = c.getString(1); 1980 values.put(FileColumns.DATA, data); 1981 FileUtils.computeValuesFromData(values, /*isForFuse*/ false); 1982 values.remove(FileColumns.DATA); 1983 if (!values.isEmpty()) { 1984 db.update("files", values, "_id=" + id, null); 1985 } 1986 } 1987 } 1988 } 1989 recomputeMediaTypeValues(SQLiteDatabase db)1990 private static void recomputeMediaTypeValues(SQLiteDatabase db) { 1991 // Only update the files with MEDIA_TYPE_NONE. 1992 final String selection = FileColumns.MEDIA_TYPE + "=?"; 1993 final String[] selectionArgs = new String[]{String.valueOf(FileColumns.MEDIA_TYPE_NONE)}; 1994 1995 ArrayMap<Long, Integer> newMediaTypes = new ArrayMap<>(); 1996 try (Cursor c = db.query("files", new String[] { FileColumns._ID, FileColumns.MIME_TYPE }, 1997 selection, selectionArgs, null, null, null, null)) { 1998 Log.d(TAG, "Recomputing " + c.getCount() + " MediaType values"); 1999 2000 // Accumulate all the new MEDIA_TYPE updates. 2001 while (c.moveToNext()) { 2002 final long id = c.getLong(0); 2003 final String mimeType = c.getString(1); 2004 // Only update Document and Subtitle media type 2005 if (MimeUtils.isSubtitleMimeType(mimeType)) { 2006 newMediaTypes.put(id, FileColumns.MEDIA_TYPE_SUBTITLE); 2007 } else if (MimeUtils.isDocumentMimeType(mimeType)) { 2008 newMediaTypes.put(id, FileColumns.MEDIA_TYPE_DOCUMENT); 2009 } 2010 } 2011 } 2012 // Now, update all the new MEDIA_TYPE values. 2013 final ContentValues values = new ContentValues(); 2014 for (long id: newMediaTypes.keySet()) { 2015 values.clear(); 2016 values.put(FileColumns.MEDIA_TYPE, newMediaTypes.get(id)); 2017 db.update("files", values, "_id=" + id, null); 2018 } 2019 } 2020 2021 static final int VERSION_R = 1115; 2022 static final int VERSION_S = 1209; 2023 static final int VERSION_T = 1308; 2024 // Leave some gaps in database version tagging to allow T schema changes 2025 // to go independent of U schema changes. 2026 static final int VERSION_U = 1409; 2027 public static final int VERSION_LATEST = VERSION_U; 2028 2029 /** 2030 * This method takes care of updating all the tables in the database to the 2031 * current version, creating them if necessary. 2032 * This method can only update databases at schema 700 or higher, which was 2033 * used by the KitKat release. Older database will be cleared and recreated. 2034 * @param db Database 2035 */ updateDatabase(SQLiteDatabase db, int fromVersion, int toVersion)2036 private void updateDatabase(SQLiteDatabase db, int fromVersion, int toVersion) { 2037 final long startTime = SystemClock.elapsedRealtime(); 2038 2039 if (fromVersion < 700) { 2040 // Anything older than KK is recreated from scratch 2041 createLatestSchema(db); 2042 } else { 2043 boolean recomputeDataValues = false; 2044 if (fromVersion < 800) { 2045 updateCollationKeys(db); 2046 } 2047 if (fromVersion < 900) { 2048 updateAddTitleResource(db); 2049 } 2050 if (fromVersion < 1000) { 2051 updateAddOwnerPackageName(db); 2052 } 2053 if (fromVersion < 1003) { 2054 updateAddColorSpaces(db); 2055 } 2056 if (fromVersion < 1004) { 2057 updateAddHashAndPending(db); 2058 } 2059 if (fromVersion < 1005) { 2060 updateAddDownloadInfo(db); 2061 } 2062 if (fromVersion < 1006) { 2063 updateAddAudiobook(db); 2064 } 2065 if (fromVersion < 1007) { 2066 updateClearLocation(db); 2067 } 2068 if (fromVersion < 1008) { 2069 updateSetIsDownload(db); 2070 } 2071 if (fromVersion < 1009) { 2072 // This database version added "secondary_bucket_id", but that 2073 // column name was refactored in version 1013 below, so this 2074 // update step is no longer needed. 2075 } 2076 if (fromVersion < 1010) { 2077 updateAddExpiresAndTrashed(db); 2078 } 2079 if (fromVersion < 1012) { 2080 recomputeDataValues = true; 2081 } 2082 if (fromVersion < 1013) { 2083 updateAddGroupId(db); 2084 updateAddDirectories(db); 2085 recomputeDataValues = true; 2086 } 2087 if (fromVersion < 1014) { 2088 updateAddXmpMm(db); 2089 } 2090 if (fromVersion < 1015) { 2091 // Empty version bump to ensure views are recreated 2092 } 2093 if (fromVersion < 1016) { 2094 // Empty version bump to ensure views are recreated 2095 } 2096 if (fromVersion < 1017) { 2097 updateSetIsDownload(db); 2098 recomputeDataValues = true; 2099 } 2100 if (fromVersion < 1018) { 2101 updateAddPath(db); 2102 recomputeDataValues = true; 2103 } 2104 if (fromVersion < 1019) { 2105 // Only trigger during "external", so that it runs only once. 2106 if (isExternal()) { 2107 deleteLegacyThumbnailData(); 2108 } 2109 } 2110 if (fromVersion < 1020) { 2111 updateAddVolumeName(db); 2112 recomputeDataValues = true; 2113 } 2114 if (fromVersion < 1021) { 2115 // Empty version bump to ensure views are recreated 2116 } 2117 if (fromVersion < 1022) { 2118 updateDirsMimeType(db); 2119 } 2120 if (fromVersion < 1023) { 2121 updateRelativePath(db); 2122 } 2123 if (fromVersion < 1100) { 2124 // Empty version bump to ensure triggers are recreated 2125 } 2126 if (fromVersion < 1101) { 2127 updateClearDirectories(db); 2128 } 2129 if (fromVersion < 1102) { 2130 updateRestructureAudio(db); 2131 } 2132 if (fromVersion < 1103) { 2133 updateAddMetadata(db); 2134 } 2135 if (fromVersion < 1104) { 2136 // Empty version bump to ensure views are recreated 2137 } 2138 if (fromVersion < 1105) { 2139 recomputeDataValues = true; 2140 } 2141 if (fromVersion < 1106) { 2142 updateMigrateLogs(db); 2143 } 2144 if (fromVersion < 1107) { 2145 updateAddSceneCaptureType(db); 2146 } 2147 if (fromVersion < 1108) { 2148 updateAddLocalMetadata(db); 2149 } 2150 if (fromVersion < 1109) { 2151 updateAddGeneration(db); 2152 } 2153 if (fromVersion < 1110) { 2154 // Empty version bump to ensure triggers are recreated 2155 } 2156 if (fromVersion < 1111) { 2157 recomputeMediaTypeValues(db); 2158 } 2159 if (fromVersion < 1112) { 2160 updateAddXmp(db); 2161 } 2162 if (fromVersion < 1113) { 2163 // Empty version bump to ensure triggers are recreated 2164 } 2165 if (fromVersion < 1114) { 2166 // Empty version bump to ensure triggers are recreated 2167 } 2168 if (fromVersion < 1115) { 2169 updateAudioAlbumId(db); 2170 } 2171 if (fromVersion < 1200) { 2172 updateAddTranscodeSatus(db); 2173 } 2174 if (fromVersion < 1201) { 2175 updateAddVideoCodecType(db); 2176 } 2177 if (fromVersion < 1202) { 2178 updateAddModifier(db); 2179 } 2180 if (fromVersion < 1203) { 2181 // Empty version bump to ensure views are recreated 2182 } 2183 if (fromVersion < 1204) { 2184 // Empty version bump to ensure views are recreated 2185 } 2186 if (fromVersion < 1205) { 2187 updateAddRecording(db); 2188 } 2189 if (fromVersion < 1206) { 2190 // Empty version bump to ensure views are recreated 2191 } 2192 if (fromVersion < 1207) { 2193 updateAddRedactedUriId(db); 2194 } 2195 if (fromVersion < 1208) { 2196 updateUserId(db); 2197 } 2198 if (fromVersion < 1209) { 2199 // Empty version bump to ensure views are recreated 2200 } 2201 if (fromVersion < 1301) { 2202 updateAddDeletedMediaTable(db); 2203 } 2204 if (fromVersion < 1302) { 2205 updateAddSpecialFormat(db); 2206 } 2207 if (fromVersion < 1303) { 2208 // Empty version bump to ensure views are recreated 2209 } 2210 if (fromVersion < 1304) { 2211 updateSpecialFormatToNotDetected(db); 2212 } 2213 if (fromVersion < 1305) { 2214 // Empty version bump to ensure views are recreated 2215 } 2216 if (fromVersion < 1306) { 2217 // Empty version bump to ensure views are recreated 2218 } 2219 if (fromVersion < 1307) { 2220 // This is to ensure Animated Webp files are tagged 2221 updateSpecialFormatToNotDetected(db); 2222 } 2223 if (fromVersion < 1308) { 2224 // Empty version bump to ensure triggers are recreated 2225 } 2226 if (fromVersion < 1400) { 2227 // Empty version bump to ensure triggers are recreated 2228 } 2229 if (fromVersion < 1404) { 2230 // Empty version bump to ensure triggers are recreated 2231 } 2232 2233 if (fromVersion < 1406) { 2234 // Empty version bump to ensure triggers are recreated 2235 } 2236 2237 if (fromVersion < 1408) { 2238 if (isExternal()) { 2239 if (fromVersion == 1407) { 2240 // media_grants table was added as part of version 1407, hence when 2241 // the fromVersion is 1407 the existing table needs to be altered to 2242 // introduce the new column and index. 2243 updateAddGenerationGranted(db); 2244 } else { 2245 // The media_grants table needs to be created with the latest schema 2246 // and index as it does not exist for fromVersion below 1407. 2247 updateAddMediaGrantsTable(db); 2248 } 2249 } 2250 } 2251 2252 if (fromVersion < 1409) { 2253 updateAddDateModifiedAndGenerationModifiedIndexes(db); 2254 } 2255 2256 // If this is the legacy database, it's not worth recomputing data 2257 // values locally, since they'll be recomputed after the migration 2258 if (mLegacyProvider) { 2259 recomputeDataValues = false; 2260 } 2261 2262 if (recomputeDataValues) { 2263 recomputeDataValues(db); 2264 } 2265 } 2266 2267 // Always recreate latest views and triggers during upgrade; they're 2268 // cheap and it's an easy way to ensure they're defined consistently 2269 createLatestViews(db); 2270 createLatestTriggers(db); 2271 2272 getOrCreateUuid(db); 2273 2274 final long elapsedMillis = (SystemClock.elapsedRealtime() - startTime); 2275 if (mSchemaListener != null) { 2276 mSchemaListener.onSchemaChange(mVolumeName, fromVersion, toVersion, 2277 getItemCount(db), elapsedMillis, getOrCreateUuid(db)); 2278 } 2279 } 2280 downgradeDatabase(SQLiteDatabase db, int fromVersion, int toVersion)2281 private void downgradeDatabase(SQLiteDatabase db, int fromVersion, int toVersion) { 2282 final long startTime = SystemClock.elapsedRealtime(); 2283 2284 // The best we can do is wipe and start over 2285 createLatestSchema(db); 2286 2287 final long elapsedMillis = (SystemClock.elapsedRealtime() - startTime); 2288 if (mSchemaListener != null) { 2289 mSchemaListener.onSchemaChange(mVolumeName, fromVersion, toVersion, 2290 getItemCount(db), elapsedMillis, getOrCreateUuid(db)); 2291 } 2292 } 2293 2294 private static final String XATTR_UUID = "user.uuid"; 2295 2296 /** 2297 * Return a UUID for the given database. If the database is deleted or 2298 * otherwise corrupted, then a new UUID will automatically be generated. 2299 */ getOrCreateUuid(@onNull SQLiteDatabase db)2300 public static @NonNull String getOrCreateUuid(@NonNull SQLiteDatabase db) { 2301 try { 2302 return new String(Os.getxattr(db.getPath(), XATTR_UUID)); 2303 } catch (ErrnoException e) { 2304 if (e.errno == OsConstants.ENODATA) { 2305 // Doesn't exist yet, so generate and persist a UUID 2306 return resetAndGetUuid(db); 2307 } else { 2308 throw new RuntimeException(e); 2309 } 2310 } 2311 } 2312 resetAndGetUuid(SQLiteDatabase db)2313 private static @NonNull String resetAndGetUuid(SQLiteDatabase db) { 2314 final String uuid = UUID.randomUUID().toString(); 2315 try { 2316 Os.setxattr(db.getPath(), XATTR_UUID, uuid.getBytes(), 0); 2317 } catch (ErrnoException e) { 2318 throw new RuntimeException(e); 2319 } 2320 return uuid; 2321 } 2322 2323 private static final long PASSTHROUGH_WAIT_TIMEOUT = 10 * DateUtils.SECOND_IN_MILLIS; 2324 2325 /** 2326 * When writing to playlist files during migration, the underlying 2327 * pass-through view of storage may not be mounted yet, so we're willing 2328 * to retry several times before giving up. 2329 * The retry logic is mainly added to avoid test flakiness. 2330 */ writeToPlaylistFileWithRetry(@onNull File playlistFile, @NonNull Playlist playlist)2331 private static void writeToPlaylistFileWithRetry(@NonNull File playlistFile, 2332 @NonNull Playlist playlist) throws IOException { 2333 final long start = SystemClock.elapsedRealtime(); 2334 while (true) { 2335 if (SystemClock.elapsedRealtime() - start > PASSTHROUGH_WAIT_TIMEOUT) { 2336 throw new IOException("Passthrough failed to mount"); 2337 } 2338 2339 try { 2340 playlistFile.getParentFile().mkdirs(); 2341 playlistFile.createNewFile(); 2342 playlist.write(playlistFile); 2343 return; 2344 } catch (IOException e) { 2345 Log.i(TAG, "Failed to migrate playlist file, retrying " + e); 2346 } 2347 Log.i(TAG, "Waiting for passthrough to be mounted..."); 2348 SystemClock.sleep(100); 2349 } 2350 } 2351 2352 /** 2353 * When renaming files during migration, the underlying pass-through view of 2354 * storage may not be mounted yet, so we're willing to retry several times 2355 * before giving up. 2356 */ renameWithRetry(@onNull String oldPath, @NonNull String newPath)2357 private static void renameWithRetry(@NonNull String oldPath, @NonNull String newPath) 2358 throws IOException { 2359 final long start = SystemClock.elapsedRealtime(); 2360 while (true) { 2361 if (SystemClock.elapsedRealtime() - start > PASSTHROUGH_WAIT_TIMEOUT) { 2362 throw new IOException("Passthrough failed to mount"); 2363 } 2364 2365 try { 2366 Os.rename(oldPath, newPath); 2367 return; 2368 } catch (ErrnoException e) { 2369 Log.i(TAG, "Failed to rename: " + e); 2370 } 2371 2372 Log.i(TAG, "Waiting for passthrough to be mounted..."); 2373 SystemClock.sleep(100); 2374 } 2375 } 2376 2377 /** 2378 * Return the current generation that will be populated into 2379 * {@link MediaColumns#GENERATION_ADDED} or 2380 * {@link MediaColumns#GENERATION_MODIFIED}. 2381 */ getGeneration(@onNull SQLiteDatabase db)2382 public static long getGeneration(@NonNull SQLiteDatabase db) { 2383 return android.database.DatabaseUtils.longForQuery(db, 2384 CURRENT_GENERATION_CLAUSE + ";", null); 2385 } 2386 2387 /** 2388 * Return total number of items tracked inside this database. This includes 2389 * only real media items, and does not include directories. 2390 */ getItemCount(@onNull SQLiteDatabase db)2391 public static long getItemCount(@NonNull SQLiteDatabase db) { 2392 return android.database.DatabaseUtils.longForQuery(db, 2393 "SELECT COUNT(_id) FROM files WHERE " + FileColumns.MIME_TYPE + " IS NOT NULL", 2394 null); 2395 } 2396 isInternal()2397 public boolean isInternal() { 2398 return mName.equals(INTERNAL_DATABASE_NAME); 2399 } 2400 isExternal()2401 public boolean isExternal() { 2402 // Matches test dbs as external 2403 switch (mName) { 2404 case EXTERNAL_DATABASE_NAME: 2405 return true; 2406 case TEST_UPGRADE_DB: 2407 return true; 2408 case TEST_DOWNGRADE_DB: 2409 return true; 2410 case TEST_CLEAN_DB: 2411 return true; 2412 default: 2413 return false; 2414 } 2415 } 2416 2417 @SuppressLint("DefaultLocale") 2418 @GuardedBy("sRecoveryLock") updateNextRowIdInDatabaseAndExternalStorage(SQLiteDatabase db)2419 private void updateNextRowIdInDatabaseAndExternalStorage(SQLiteDatabase db) { 2420 Optional<Long> nextRowIdOptional = getNextRowIdFromXattr(); 2421 // Use a billion as the next row id if not found on external storage. 2422 long nextRowId = nextRowIdOptional.orElse(NEXT_ROW_ID_DEFAULT_BILLION_VALUE); 2423 2424 backupNextRowId(nextRowId); 2425 // Insert and delete a row to update sqlite_sequence counter 2426 db.execSQL(String.format(Locale.ROOT, "INSERT INTO files(_ID) VALUES (%d)", nextRowId)); 2427 db.execSQL(String.format(Locale.ROOT, "DELETE FROM files WHERE _ID=%d", nextRowId)); 2428 Log.i(TAG, String.format(Locale.ROOT, "Updated sqlite counter of Files table of %s to %d.", 2429 mName, nextRowId)); 2430 } 2431 2432 /** 2433 * Backs up next row id value in xattr to {@code nextRowId} + BackupFrequency. Also updates 2434 * respective in-memory next row id cached value. 2435 */ backupNextRowId(long nextRowId)2436 protected void backupNextRowId(long nextRowId) { 2437 long backupId = nextRowId + getNextRowIdBackupFrequency(); 2438 boolean setOnExternalStorage = setXattr(DATA_MEDIA_XATTR_DIRECTORY_PATH, 2439 getNextRowIdXattrKeyForDatabase(), 2440 String.valueOf(backupId)); 2441 if (setOnExternalStorage) { 2442 mNextRowIdBackup.set(backupId); 2443 Log.i(TAG, String.format(Locale.ROOT, "Backed up next row id as:%d on path:%s for %s.", 2444 backupId, DATA_MEDIA_XATTR_DIRECTORY_PATH, mName)); 2445 } 2446 } 2447 getNextRowIdFromXattr()2448 protected Optional<Long> getNextRowIdFromXattr() { 2449 try { 2450 return Optional.of(Long.parseLong(new String( 2451 Os.getxattr(DATA_MEDIA_XATTR_DIRECTORY_PATH, 2452 getNextRowIdXattrKeyForDatabase())))); 2453 } catch (Exception e) { 2454 Log.e(TAG, String.format(Locale.ROOT, "Xattr:%s not found on external storage: %s", 2455 getNextRowIdXattrKeyForDatabase(), e)); 2456 return Optional.empty(); 2457 } 2458 } 2459 getNextRowIdXattrKeyForDatabase()2460 protected String getNextRowIdXattrKeyForDatabase() { 2461 if (isInternal()) { 2462 return INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY; 2463 } else if (isExternal()) { 2464 return EXTERNAL_DB_NEXT_ROW_ID_XATTR_KEY; 2465 } 2466 throw new RuntimeException( 2467 String.format(Locale.ROOT, "Next row id xattr key not defined for database:%s.", 2468 mName)); 2469 } 2470 getSessionIdXattrKeyForDatabase()2471 private String getSessionIdXattrKeyForDatabase() { 2472 if (isInternal()) { 2473 return INTERNAL_DB_SESSION_ID_XATTR_KEY; 2474 } else if (isExternal()) { 2475 return EXTERNAL_DB_SESSION_ID_XATTR_KEY; 2476 } 2477 throw new RuntimeException( 2478 String.format(Locale.ROOT, "Session id xattr key not defined for database:%s.", 2479 mName)); 2480 } 2481 getNextRowId()2482 protected Optional<Long> getNextRowId() { 2483 if (mNextRowIdBackup.get() == INVALID_ROW_ID) { 2484 return getNextRowIdFromXattr(); 2485 } 2486 2487 return Optional.of(mNextRowIdBackup.get()); 2488 } 2489 isNextRowIdBackupEnabled()2490 boolean isNextRowIdBackupEnabled() { 2491 if (!mEnableNextRowIdRecovery) { 2492 return false; 2493 } 2494 2495 if (mVersion < VERSION_R) { 2496 // Do not back up next row id if DB version is less than R. This is unlikely to hit 2497 // as we will backport row id backup changes till Android R. 2498 Log.v(TAG, "Skipping next row id backup for android versions less than R."); 2499 return false; 2500 } 2501 2502 if (!(new File(DATA_MEDIA_XATTR_DIRECTORY_PATH)).exists()) { 2503 Log.w(TAG, String.format(Locale.ROOT, 2504 "Skipping row id recovery as path:%s does not exist.", 2505 DATA_MEDIA_XATTR_DIRECTORY_PATH)); 2506 return false; 2507 } 2508 2509 return SystemProperties.getBoolean("persist.sys.fuse.backup.nextrowid_enabled", 2510 true); 2511 } 2512 getNextRowIdBackupFrequency()2513 public static int getNextRowIdBackupFrequency() { 2514 return SystemProperties.getInt("persist.sys.fuse.backup.nextrowid_backup_frequency", 2515 1000); 2516 } 2517 isDatabaseRecovering()2518 boolean isDatabaseRecovering() { 2519 return mIsRecovering.get(); 2520 } 2521 traceSectionName(@onNull String method)2522 private String traceSectionName(@NonNull String method) { 2523 return "DH[" + getDatabaseName() + "]." + method; 2524 } 2525 } 2526