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