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