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