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