Merge "Return ENOENT if app tries to access other packages fuse path" into rvc-dev
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 16a0892..84b2b10 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -57,7 +57,6 @@
 import android.os.storage.StorageManager;
 import android.os.storage.StorageVolume;
 import android.text.TextUtils;
-import android.text.format.DateUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
@@ -609,6 +608,15 @@
     public static final String QUERY_ARG_RELATED_URI = "android:query-arg-related-uri";
 
     /**
+     * Flag that can be used to enable movement of media items on disk through
+     * {@link ContentResolver#update} calls. This is typically true for
+     * third-party apps, but false for system components.
+     *
+     * @hide
+     */
+    public static final String QUERY_ARG_ALLOW_MOVEMENT = "android:query-arg-allow-movement";
+
+    /**
      * Specify how {@link MediaColumns#IS_PENDING} items should be filtered when
      * performing a {@link MediaStore} operation.
      * <p>
@@ -719,7 +727,7 @@
     /** @hide */
     @Deprecated
     public static boolean getIncludePending(@NonNull Uri uri) {
-        return parseBoolean(uri.getQueryParameter(MediaStore.PARAM_INCLUDE_PENDING));
+        return uri.getBooleanQueryParameter(MediaStore.PARAM_INCLUDE_PENDING, false);
     }
 
     /**
@@ -749,7 +757,7 @@
      * @see MediaStore#setRequireOriginal(Uri)
      */
     public static boolean getRequireOriginal(@NonNull Uri uri) {
-        return parseBoolean(uri.getQueryParameter(MediaStore.PARAM_REQUIRE_ORIGINAL));
+        return uri.getBooleanQueryParameter(MediaStore.PARAM_REQUIRE_ORIGINAL, false);
     }
 
     /**
@@ -3735,13 +3743,6 @@
         return volumeName;
     }
 
-    private static boolean parseBoolean(@Nullable String value) {
-        if (value == null) return false;
-        if ("1".equals(value)) return true;
-        if ("true".equalsIgnoreCase(value)) return true;
-        return false;
-    }
-
     /**
      * Uri for querying the state of the media scanner.
      */
diff --git a/legacy/src/com/android/providers/media/LegacyMediaProvider.java b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
index a505ee1..6e152d4 100644
--- a/legacy/src/com/android/providers/media/LegacyMediaProvider.java
+++ b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
@@ -20,15 +20,19 @@
 import static com.android.providers.media.DatabaseHelper.INTERNAL_DATABASE_NAME;
 
 import android.content.ContentProvider;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.OperationApplicationException;
 import android.content.pm.ProviderInfo;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.MediaStore;
 import android.provider.MediaStore.MediaColumns;
+import android.util.ArraySet;
 
 import androidx.annotation.NonNull;
 
@@ -38,7 +42,9 @@
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Objects;
+import java.util.Set;
 
 /**
  * Very limited subset of {@link MediaProvider} which only surfaces
@@ -106,13 +112,41 @@
     }
 
     @Override
-    public Uri insert(Uri uri, ContentValues values) {
+    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
+                throws OperationApplicationException {
+        // Open transactions on databases for requested volumes
+        final Set<DatabaseHelper> transactions = new ArraySet<>();
         try {
-            final File file = new File(values.getAsString(MediaColumns.DATA));
-            file.getParentFile().mkdirs();
-            file.createNewFile();
-        } catch (IOException e) {
-            throw new IllegalStateException(e);
+            for (ContentProviderOperation op : operations) {
+                final DatabaseHelper helper = getDatabaseForUri(op.getUri());
+                if (!transactions.contains(helper)) {
+                    helper.beginTransaction();
+                    transactions.add(helper);
+                }
+            }
+
+            final ContentProviderResult[] result = super.applyBatch(operations);
+            for (DatabaseHelper helper : transactions) {
+                helper.setTransactionSuccessful();
+            }
+            return result;
+        } finally {
+            for (DatabaseHelper helper : transactions) {
+                helper.endTransaction();
+            }
+        }
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        if (!uri.getBooleanQueryParameter("silent", false)) {
+            try {
+                final File file = new File(values.getAsString(MediaColumns.DATA));
+                file.getParentFile().mkdirs();
+                file.createNewFile();
+            } catch (IOException e) {
+                throw new IllegalStateException(e);
+            }
         }
 
         final DatabaseHelper helper = getDatabaseForUri(uri);
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 47be764..2d36456 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -23,7 +23,7 @@
     <string name="unknown" msgid="2059049215682829375">"અજાણ"</string>
     <string name="root_images" msgid="5861633549189045666">"છબીઓ"</string>
     <string name="root_videos" msgid="8792703517064649453">"વિડિઓઝ"</string>
-    <string name="root_audio" msgid="3505830755201326018">"ઑડિઓ"</string>
+    <string name="root_audio" msgid="3505830755201326018">"ઑડિયો"</string>
     <string name="root_documents" msgid="3829103301363849237">"દસ્તાવેજો"</string>
     <string name="permission_required" msgid="1460820436132943754">"આ આઇટમમાં ફેરફાર કરવા માટે અથવા તેને ડિલીટ કરવા માટે પરવાનગી હોવી જરૂરી છે."</string>
     <string name="permission_required_action" msgid="706370952366113539">"આગળ વધો"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 09eac9f..80999fa 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -21,7 +21,7 @@
     <string name="app_label" msgid="9035307001052716210">"Przechowywanie multimediów"</string>
     <string name="artist_label" msgid="8105600993099120273">"Wykonawca"</string>
     <string name="unknown" msgid="2059049215682829375">"Nieznany"</string>
-    <string name="root_images" msgid="5861633549189045666">"Grafika"</string>
+    <string name="root_images" msgid="5861633549189045666">"Obrazy"</string>
     <string name="root_videos" msgid="8792703517064649453">"Filmy"</string>
     <string name="root_audio" msgid="3505830755201326018">"Dźwięk"</string>
     <string name="root_documents" msgid="3829103301363849237">"Dokumenty"</string>
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index afa3d63..a61075b 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -69,7 +69,6 @@
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Field;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Objects;
 import java.util.Set;
@@ -112,6 +111,13 @@
     long mScanStopTime;
 
     /**
+     * Flag indicating that this database should invoke
+     * {@link #migrateFromLegacy} to migrate from a legacy database, typically
+     * only set when this database is starting from scratch.
+     */
+    boolean mMigrateFromLegacy;
+
+    /**
      * Lock used to guard against deadlocks in SQLite; the write lock is used to
      * guard any schema changes, and the read lock is used for all other
      * database operations.
@@ -242,7 +248,7 @@
         db.setCustomScalarFunction("_INSERT", (arg) -> {
             if (arg != null && mFilesListener != null
                     && !mSchemaLock.isWriteLockedByCurrentThread()) {
-                final String[] split = arg.split(":");
+                final String[] split = arg.split(":", 4);
                 final String volumeName = split[0];
                 final long id = Long.parseLong(split[1]);
                 final int mediaType = Integer.parseInt(split[2]);
@@ -261,7 +267,7 @@
         db.setCustomScalarFunction("_UPDATE", (arg) -> {
             if (arg != null && mFilesListener != null
                     && !mSchemaLock.isWriteLockedByCurrentThread()) {
-                final String[] split = arg.split(":");
+                final String[] split = arg.split(":", 10);
                 final String volumeName = split[0];
                 final long oldId = Long.parseLong(split[1]);
                 final int oldMediaType = Integer.parseInt(split[2]);
@@ -271,8 +277,7 @@
                 final boolean newIsDownload = Integer.parseInt(split[6]) != 0;
                 final String oldOwnerPackage = split[7];
                 final String newOwnerPackage = split[8];
-                // Path can include ':',  assume rest of split[9..length] is path.
-                final String oldPath = String.join(":", Arrays.copyOfRange(split, 9, split.length));
+                final String oldPath = split[9];
 
                 Trace.beginSection("_UPDATE");
                 try {
@@ -288,14 +293,13 @@
         db.setCustomScalarFunction("_DELETE", (arg) -> {
             if (arg != null && mFilesListener != null
                     && !mSchemaLock.isWriteLockedByCurrentThread()) {
-                final String[] split = arg.split(":");
+                final String[] split = arg.split(":", 6);
                 final String volumeName = split[0];
                 final long id = Long.parseLong(split[1]);
                 final int mediaType = Integer.parseInt(split[2]);
                 final boolean isDownload = Integer.parseInt(split[3]) != 0;
                 final String ownerPackage = split[4];
-                // Path can include ':',  assume rest of split[5..length] is path.
-                final String path = String.join(":", Arrays.copyOfRange(split, 5, split.length));
+                final String path = split[5];
 
                 Trace.beginSection("_DELETE");
                 try {
@@ -353,6 +357,22 @@
         }
     }
 
+    @Override
+    public void onOpen(final SQLiteDatabase db) {
+        Log.v(TAG, "onOpen() for " + mName);
+        if (mMigrateFromLegacy) {
+            // Clear flag, since we should only attempt once
+            mMigrateFromLegacy = false;
+
+            mSchemaLock.writeLock().lock();
+            try {
+                migrateFromLegacy(db);
+            } finally {
+                mSchemaLock.writeLock().unlock();
+            }
+        }
+    }
+
     @GuardedBy("mProjectionMapCache")
     private final ArrayMap<Class<?>, ArrayMap<String, String>>
             mProjectionMapCache = new ArrayMap<>();
@@ -760,7 +780,7 @@
         // Since this code is used by both the legacy and modern providers, we
         // only want to migrate when we're running as the modern provider
         if (!mLegacyProvider) {
-            migrateFromLegacy(db);
+            mMigrateFromLegacy = true;
         }
     }
 
@@ -789,8 +809,8 @@
             extras.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE);
             extras.putInt(MediaStore.QUERY_ARG_MATCH_FAVORITE, MediaStore.MATCH_INCLUDE);
 
-            db.execSQL("SAVEPOINT before_migrate");
-            Log.d(TAG, "Starting migration from legacy provider for " + mName);
+            db.beginTransaction();
+            Log.d(TAG, "Starting migration from legacy provider");
             mMigrationListener.onStarted(client, mVolumeName);
             try (Cursor c = client.query(queryUri, sMigrateColumns.toArray(new String[0]),
                     extras, null)) {
@@ -809,17 +829,19 @@
 
                     // When migrating pending or trashed files, we might need to
                     // rename them on disk to match new schema
-                    FileUtils.computeDataFromValues(values,
-                            new File(FileUtils.extractVolumePath(data)));
-                    final String recomputedData = values.getAsString(MediaColumns.DATA);
-                    if (!Objects.equals(data, recomputedData)) {
-                        try {
-                            Os.rename(data, recomputedData);
-                        } catch (ErrnoException e) {
-                            // We only have one shot to migrate data, so log and
-                            // keep marching forward
-                            Log.w(TAG, "Failed to rename " + values + "; continuing");
-                            FileUtils.computeValuesFromData(values);
+                    final String volumePath = FileUtils.extractVolumePath(data);
+                    if (volumePath != null) {
+                        FileUtils.computeDataFromValues(values, new File(volumePath));
+                        final String recomputedData = values.getAsString(MediaColumns.DATA);
+                        if (!Objects.equals(data, recomputedData)) {
+                            try {
+                                Os.rename(data, recomputedData);
+                            } catch (ErrnoException e) {
+                                // We only have one shot to migrate data, so log and
+                                // keep marching forward
+                                Log.w(TAG, "Failed to rename " + values + "; continuing");
+                                FileUtils.computeValuesFromData(values);
+                            }
                         }
                     }
 
@@ -828,18 +850,28 @@
                         // keep marching forward
                         Log.w(TAG, "Failed to insert " + values + "; continuing");
                     }
+
+                    // To avoid SQLITE_NOMEM errors, we need to periodically
+                    // flush the current transaction and start another one
+                    if ((c.getPosition() % 1_000) == 0) {
+                        db.setTransactionSuccessful();
+                        db.endTransaction();
+                        db.beginTransaction();
+                    }
                 }
 
-                db.execSQL("RELEASE before_migrate");
-                Log.d(TAG, "Finished migration from legacy provider for " + mName);
-                mMigrationListener.onFinished(client, mVolumeName);
+                Log.d(TAG, "Finished migration from legacy provider");
             } catch (Exception e) {
                 // We have to guard ourselves against any weird behavior of the
                 // legacy provider by trying to catch everything
-                db.execSQL("ROLLBACK TO before_migrate");
                 Log.wtf(TAG, "Failed migration from legacy provider", e);
-                mMigrationListener.onFinished(client, mVolumeName);
             }
+
+            // We tried our best above to migrate everything we could, and we
+            // only have one possible shot, so mark everything successful
+            db.setTransactionSuccessful();
+            db.endTransaction();
+            mMigrationListener.onFinished(client, mVolumeName);
         }
     }
 
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 84a0a56..40325cb 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -2445,7 +2445,7 @@
         values.put(FileColumns.VOLUME_NAME, extractVolumeName(path));
         values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path));
         values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path));
-        values.put(FileColumns.IS_DOWNLOAD, isDownload(path));
+        values.put(FileColumns.IS_DOWNLOAD, isDownload(path) ? 1 : 0);
         File file = new File(path);
         if (file.exists()) {
             values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000);
@@ -2746,12 +2746,13 @@
             try {
                 return qb.insert(helper, values);
             } catch (SQLiteConstraintException e) {
-                final long rowId = getIdIfPathExistsForCallingPackage(qb, helper, path);
+                SQLiteQueryBuilder qbForUpsert = getQueryBuilderForUpsert(path);
+                final long rowId = getIdIfPathExistsForCallingPackage(qbForUpsert, helper, path);
                 // Apps sometimes create a file via direct path and then insert it into
                 // MediaStore via ContentResolver. The former should create a database entry,
                 // so we have to treat the latter as an upsert.
                 // TODO(b/149917493) Perform all INSERT operations as UPSERT.
-                if (rowId != -1 && qb.update(helper, values, "_id=?",
+                if (rowId != -1 && qbForUpsert.update(helper, values, "_id=?",
                         new String[]{Long.toString(rowId)}) == 1) {
                     return rowId;
                 }
@@ -2782,6 +2783,28 @@
         return -1;
     }
 
+    /**
+     * @return {@link SQLiteQueryBuilder} for upsert with Files uri. This disables strict columns
+     * check to allow upsert to update any column with Files uri.
+     */
+    private SQLiteQueryBuilder getQueryBuilderForUpsert(@NonNull String path) {
+        final Uri uri = Files.getContentUriForPath(path);
+        final boolean allowHidden = isCallingPackageAllowedHidden();
+        // When Fuse inserts a file to database it doesn't set is_download column. When app tries
+        // insert with Downloads uri, upsert fails because getIdIfPathExistsForCallingPackage can't
+        // find a row ID with is_download=1. Use Files uri to query & update any existing row
+        // irrespective of is_download=1.
+        SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, matchUri(uri, allowHidden), uri,
+                Bundle.EMPTY, null);
+
+        // We won't be able to update columns that are not part of projection map of Files table. We
+        // have already checked strict columns in previous insert operation which failed with
+        // exception. Any malicious column usage would have got caught in insert operation, hence we
+        // can safely disable strict column check for upsert.
+        qb.setStrictColumns(false);
+        return qb;
+    }
+
     private void maybePut(@NonNull ContentValues values, @NonNull String key,
             @Nullable String value) {
         if (value != null) {
@@ -2792,7 +2815,7 @@
     private boolean maybeMarkAsDownload(@NonNull ContentValues values) {
         final String path = values.getAsString(MediaColumns.DATA);
         if (path != null && isDownload(path)) {
-            values.put(FileColumns.IS_DOWNLOAD, true);
+            values.put(FileColumns.IS_DOWNLOAD, 1);
             return true;
         }
         return false;
@@ -3108,7 +3131,7 @@
 
             case DOWNLOADS:
                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
-                initialValues.put(FileColumns.IS_DOWNLOAD, true);
+                initialValues.put(FileColumns.IS_DOWNLOAD, 1);
                 rowId = insertFile(qb, helper, match, uri, extras, initialValues,
                         FileColumns.MEDIA_TYPE_NONE, false);
                 if (rowId > 0) {
@@ -3192,14 +3215,6 @@
         }
     }
 
-    @VisibleForTesting
-    static boolean parseBoolean(String value) {
-        if (value == null) return false;
-        if ("1".equals(value)) return true;
-        if ("true".equalsIgnoreCase(value)) return true;
-        return false;
-    }
-
     @Deprecated
     private String getSharedPackages(String callingPackage) {
         final String[] sharedPackageNames = mCallingIdentity.get().getSharedPackageNames();
@@ -3248,7 +3263,7 @@
         }
 
         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
-        if (parseBoolean(uri.getQueryParameter("distinct"))) {
+        if (uri.getBooleanQueryParameter("distinct", false)) {
             qb.setDistinct(true);
         }
         qb.setStrict(true);
@@ -4556,7 +4571,7 @@
                 final Uri playlistUri = ContentUris.withAppendedId(
                         MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId);
 
-                if (parseBoolean(uri.getQueryParameter("move"))) {
+                if (uri.getBooleanQueryParameter("move", false)) {
                     // Convert explicit request into query; sigh, moveItem()
                     // uses zero-based indexing instead of one-based indexing
                     final int from = Integer.parseInt(uri.getPathSegments().get(5)) + 1;
@@ -4713,16 +4728,14 @@
                 break;
         }
 
-        // TODO: remove this as part of fixing b/151768142
-        final boolean isCallingPackageSystem = isCallingPackageSystem()
-                && !"com.android.systemui".equals(getCallingPackageOrSelf());
-
         // If we're touching columns that would change placement of a file,
         // blend in current values and recalculate path
+        final boolean allowMovement = extras.getBoolean(MediaStore.QUERY_ARG_ALLOW_MOVEMENT,
+                !isCallingPackageSystem());
         if (containsAny(initialValues.keySet(), sPlacementColumns)
                 && !initialValues.containsKey(MediaColumns.DATA)
-                && !isCallingPackageSystem
-                && !isThumbnail) {
+                && !isThumbnail
+                && allowMovement) {
             Trace.beginSection("movement");
 
             // We only support movement under well-defined collections
diff --git a/src/com/android/providers/media/PermissionActivity.java b/src/com/android/providers/media/PermissionActivity.java
index 0c398dd..0696ef5 100644
--- a/src/com/android/providers/media/PermissionActivity.java
+++ b/src/com/android/providers/media/PermissionActivity.java
@@ -20,6 +20,7 @@
 import static com.android.providers.media.MediaProvider.IMAGES_MEDIA_ID;
 import static com.android.providers.media.MediaProvider.VIDEO_MEDIA_ID;
 import static com.android.providers.media.MediaProvider.collectUris;
+import static com.android.providers.media.util.DatabaseUtils.getAsBoolean;
 import static com.android.providers.media.util.Logging.TAG;
 
 import android.app.Activity;
@@ -198,6 +199,7 @@
                             for (Uri uri : uris) {
                                 ops.add(ContentProviderOperation.newUpdate(uri)
                                         .withValues(values)
+                                        .withExtra(MediaStore.QUERY_ARG_ALLOW_MOVEMENT, true)
                                         .withExceptionAllowed(true)
                                         .build());
                             }
@@ -290,10 +292,10 @@
             case MediaStore.CREATE_WRITE_REQUEST_CALL:
                 return VERB_WRITE;
             case MediaStore.CREATE_TRASH_REQUEST_CALL:
-                return (values.getAsInteger(MediaColumns.IS_TRASHED) != 0)
+                return getAsBoolean(values, MediaColumns.IS_TRASHED, false)
                         ? VERB_TRASH : VERB_UNTRASH;
             case MediaStore.CREATE_FAVORITE_REQUEST_CALL:
-                return (values.getAsInteger(MediaColumns.IS_FAVORITE) != 0)
+                return getAsBoolean(values, MediaColumns.IS_FAVORITE, false)
                         ? VERB_FAVORITE : VERB_UNFAVORITE;
             case MediaStore.CREATE_DELETE_REQUEST_CALL:
                 return VERB_DELETE;
diff --git a/src/com/android/providers/media/util/DatabaseUtils.java b/src/com/android/providers/media/util/DatabaseUtils.java
index ef33b04..a5ab700 100644
--- a/src/com/android/providers/media/util/DatabaseUtils.java
+++ b/src/com/android/providers/media/util/DatabaseUtils.java
@@ -48,7 +48,9 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
+import java.util.Locale;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
@@ -532,10 +534,27 @@
         return sb.toString();
     }
 
+    public static boolean parseBoolean(@Nullable Object value, boolean def) {
+        if (value instanceof Boolean) {
+            return (Boolean) value;
+        } else if (value instanceof Number) {
+            return ((Number) value).intValue() != 0;
+        } else if (value instanceof String) {
+            final String stringValue = ((String) value).toLowerCase(Locale.ROOT);
+            return (!"false".equals(stringValue) && !"0".equals(stringValue));
+        } else {
+            return def;
+        }
+    }
+
+    public static boolean getAsBoolean(@NonNull Bundle extras,
+            @NonNull String key, boolean def) {
+        return parseBoolean(extras.get(key), def);
+    }
+
     public static boolean getAsBoolean(@NonNull ContentValues values,
             @NonNull String key, boolean def) {
-        final Integer value = values.getAsInteger(key);
-        return (value != null) ? (value != 0) : def;
+        return parseBoolean(values.get(key), def);
     }
 
     public static long getAsLong(@NonNull ContentValues values,
diff --git a/src/com/android/providers/media/util/FileUtils.java b/src/com/android/providers/media/util/FileUtils.java
index d96b465..6b0cd80 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -35,6 +35,7 @@
 
 import static com.android.providers.media.util.DatabaseUtils.getAsBoolean;
 import static com.android.providers.media.util.DatabaseUtils.getAsLong;
+import static com.android.providers.media.util.DatabaseUtils.parseBoolean;
 import static com.android.providers.media.util.Logging.TAG;
 
 import android.content.ClipDescription;
@@ -957,18 +958,18 @@
 
         // Only define the field when this modification is actually adjusting
         // one of the flags that should influence the expiration
-        final Integer pending = values.getAsInteger(MediaColumns.IS_PENDING);
+        final Object pending = values.get(MediaColumns.IS_PENDING);
         if (pending != null) {
-            if (pending != 0) {
+            if (parseBoolean(pending, false)) {
                 values.put(MediaColumns.DATE_EXPIRES,
                         (System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000);
             } else {
                 values.putNull(MediaColumns.DATE_EXPIRES);
             }
         }
-        final Integer trashed = values.getAsInteger(MediaColumns.IS_TRASHED);
+        final Object trashed = values.get(MediaColumns.IS_TRASHED);
         if (trashed != null) {
-            if (trashed != 0) {
+            if (parseBoolean(trashed, false)) {
                 values.put(MediaColumns.DATE_EXPIRES,
                         (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000);
             } else {
@@ -986,7 +987,6 @@
         // Worst case we have to assume no bucket details
         values.remove(MediaColumns.VOLUME_NAME);
         values.remove(MediaColumns.RELATIVE_PATH);
-        values.remove(MediaColumns.IS_DOWNLOAD);
         values.remove(MediaColumns.IS_PENDING);
         values.remove(MediaColumns.IS_TRASHED);
         values.remove(MediaColumns.DATE_EXPIRES);
@@ -1002,8 +1002,6 @@
 
         values.put(MediaColumns.VOLUME_NAME, extractVolumeName(data));
         values.put(MediaColumns.RELATIVE_PATH, extractRelativePath(data));
-        values.put(MediaColumns.IS_DOWNLOAD, isDownload(data));
-
         final String displayName = extractDisplayName(data);
         final Matcher matcher = FileUtils.PATTERN_EXPIRES_FILE.matcher(displayName);
         if (matcher.matches()) {
diff --git a/tests/client/Android.bp b/tests/client/Android.bp
index 9d5be41..0d29bbf 100644
--- a/tests/client/Android.bp
+++ b/tests/client/Android.bp
@@ -21,6 +21,7 @@
     static_libs: [
         "androidx.test.rules",
         "mockito-target",
+        "truth-prebuilt",
     ],
 
     certificate: "media",
diff --git a/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java b/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
index fa2de15..71f79a1 100644
--- a/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
+++ b/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
@@ -24,6 +24,7 @@
 
 import android.app.UiAutomation;
 import android.content.ContentProviderClient;
+import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
@@ -49,6 +50,8 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.google.common.truth.Truth;
+
 import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
@@ -61,6 +64,7 @@
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -80,6 +84,11 @@
     private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
     private static final long POLLING_SLEEP_MILLIS = 100;
 
+    /**
+     * Number of media items to insert for {@link #testLegacy_Extreme()}.
+     */
+    private static final int EXTREME_COUNT = 10_000;
+
     private Uri mExternalAudio;
     private Uri mExternalVideo;
     private Uri mExternalImages;
@@ -182,6 +191,67 @@
         doLegacy(mExternalDownloads, values);
     }
 
+    /**
+     * Verify that a legacy database with thousands of media entries can be
+     * successfully migrated.
+     */
+    @Test
+    public void testLegacy_Extreme() throws Exception {
+        final Context context = InstrumentationRegistry.getTargetContext();
+        final UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
+        final ProviderInfo legacyProvider = context.getPackageManager()
+                .resolveContentProvider(MediaStore.AUTHORITY_LEGACY, 0);
+        final ProviderInfo modernProvider = context.getPackageManager()
+                .resolveContentProvider(MediaStore.AUTHORITY, 0);
+
+        // Only continue if we have both providers to test against
+        Assume.assumeNotNull(legacyProvider);
+        Assume.assumeNotNull(modernProvider);
+
+        // Wait until everything calms down
+        MediaStore.waitForIdle(context.getContentResolver());
+
+        // Clear data on the legacy provider so that we create a database
+        executeShellCommand("pm clear " + legacyProvider.applicationInfo.packageName, ui);
+
+        // Create thousands of items in the legacy provider
+        try (ContentProviderClient legacy = context.getContentResolver()
+                .acquireContentProviderClient(MediaStore.AUTHORITY_LEGACY)) {
+            // We're purposefully "silent" to avoid creating the raw file on
+            // disk, since otherwise this test would take several minutes
+            final Uri insertTarget = rewriteToLegacy(
+                    mExternalImages.buildUpon().appendQueryParameter("silent", "true").build());
+
+            final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+            for (int i = 0; i < EXTREME_COUNT; i++) {
+                ops.add(ContentProviderOperation.newInsert(insertTarget)
+                        .withValues(generateValues(FileColumns.MEDIA_TYPE_IMAGE, "image/png",
+                                Environment.DIRECTORY_PICTURES))
+                        .build());
+
+                if ((ops.size() > 1_000) || (i == (EXTREME_COUNT - 1))) {
+                    Log.v(TAG, "Inserting items...");
+                    legacy.applyBatch(MediaStore.AUTHORITY_LEGACY, ops);
+                    ops.clear();
+                }
+            }
+        }
+
+        // Clear data on the modern provider so that the initial scan recovers
+        // metadata from the legacy provider
+        executeShellCommand("pm clear " + modernProvider.applicationInfo.packageName, ui);
+        pollForExternalStorageState();
+
+        // Confirm that details from legacy provider have migrated
+        try (ContentProviderClient modern = context.getContentResolver()
+                .acquireContentProviderClient(MediaStore.AUTHORITY)) {
+            try (Cursor cursor = modern.query(mExternalImages, null, null, null)) {
+                Truth.assertThat(cursor.getCount()).isAtLeast(EXTREME_COUNT);
+            }
+        }
+    }
+
     private void doLegacy(Uri collectionUri, ContentValues values) throws Exception {
         final Context context = InstrumentationRegistry.getTargetContext();
         final UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation();
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index 0f0ed64..6e9fda9 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -741,18 +741,6 @@
     }
 
     @Test
-    public void testParseBoolean() throws Exception {
-        assertTrue(MediaProvider.parseBoolean("TRUE"));
-        assertTrue(MediaProvider.parseBoolean("true"));
-        assertTrue(MediaProvider.parseBoolean("1"));
-
-        assertFalse(MediaProvider.parseBoolean("FALSE"));
-        assertFalse(MediaProvider.parseBoolean("false"));
-        assertFalse(MediaProvider.parseBoolean("0"));
-        assertFalse(MediaProvider.parseBoolean(null));
-    }
-
-    @Test
     public void testIsDownload() throws Exception {
         assertTrue(isDownload("/storage/emulated/0/Download/colors.png"));
         assertTrue(isDownload("/storage/emulated/0/Download/test.pdf"));
diff --git a/tests/src/com/android/providers/media/scan/DrmTest.java b/tests/src/com/android/providers/media/scan/DrmTest.java
index 038a93e..c7ed122 100644
--- a/tests/src/com/android/providers/media/scan/DrmTest.java
+++ b/tests/src/com/android/providers/media/scan/DrmTest.java
@@ -97,6 +97,7 @@
 
     @Test
     public void testForwardLock_Audio() throws Exception {
+        Assume.assumeTrue(isForwardLockSupported());
         doForwardLock("audio/mpeg", R.raw.test_audio, (values) -> {
             assertEquals(1_045L, (long) values.getAsLong(FileColumns.DURATION));
             assertEquals(FileColumns.MEDIA_TYPE_AUDIO,
@@ -106,6 +107,7 @@
 
     @Test
     public void testForwardLock_Video() throws Exception {
+        Assume.assumeTrue(isForwardLockSupported());
         doForwardLock("video/mp4", R.raw.test_video, (values) -> {
             assertEquals(40_000L, (long) values.getAsLong(FileColumns.DURATION));
             assertEquals(FileColumns.MEDIA_TYPE_VIDEO,
@@ -115,6 +117,7 @@
 
     @Test
     public void testForwardLock_Image() throws Exception {
+        Assume.assumeTrue(isForwardLockSupported());
         doForwardLock("image/jpeg", R.raw.test_image, (values) -> {
             // ExifInterface currently doesn't know how to scan DRM images, so
             // the best we can do is verify the base test metadata
@@ -125,6 +128,7 @@
 
     @Test
     public void testForwardLock_Binary() throws Exception {
+        Assume.assumeTrue(isForwardLockSupported());
         doForwardLock("application/octet-stream", R.raw.test_image, null);
     }
 
@@ -134,6 +138,8 @@
      */
     @Test
     public void testForwardLock_130680734() throws Exception {
+        Assume.assumeTrue(isForwardLockSupported());
+
         final ContentValues values = new ContentValues();
         values.put(MediaColumns.DISPLAY_NAME, "temp" + System.nanoTime() + ".fl");
         values.put(MediaColumns.MIME_TYPE, MIME_FORWARD_LOCKED);
@@ -185,8 +191,6 @@
 
     private void doForwardLock(String mimeType, int resId,
             @Nullable Consumer<ContentValues> verifier) throws Exception {
-        Assume.assumeTrue(isForwardLockSupported());
-
         InputStream dmStream = createDmStream(mimeType, resId);
 
         File flPath = new File(mContext.getExternalMediaDirs()[0],
diff --git a/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java b/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java
index 3a0ea75..d4ff968 100644
--- a/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java
@@ -32,6 +32,7 @@
 import static android.database.DatabaseUtils.escapeForLike;
 
 import static com.android.providers.media.util.DatabaseUtils.maybeBalance;
+import static com.android.providers.media.util.DatabaseUtils.parseBoolean;
 import static com.android.providers.media.util.DatabaseUtils.recoverAbusiveLimit;
 import static com.android.providers.media.util.DatabaseUtils.recoverAbusiveSortOrder;
 import static com.android.providers.media.util.DatabaseUtils.resolveQueryArgs;
@@ -368,6 +369,24 @@
                 escapeForLike("/path/to/fi%le.bin"));
     }
 
+    @Test
+    public void testParseBoolean() throws Exception {
+        assertTrue(parseBoolean("TRUE", false));
+        assertTrue(parseBoolean("true", false));
+        assertTrue(parseBoolean("1", false));
+        assertTrue(parseBoolean(1, false));
+        assertTrue(parseBoolean(true, false));
+
+        assertFalse(parseBoolean("FALSE", true));
+        assertFalse(parseBoolean("false", true));
+        assertFalse(parseBoolean("0", true));
+        assertFalse(parseBoolean(0, true));
+        assertFalse(parseBoolean(false, true));
+
+        assertFalse(parseBoolean(null, false));
+        assertTrue(parseBoolean(null, true));
+    }
+
     private static Pair<String, String> recoverAbusiveGroupBy(
             Pair<String, String> selectionAndGroupBy) {
         final Bundle queryArgs = new Bundle();