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