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