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