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