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