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