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