Merge "Always close FUSE file descriptor when we're exiting the thread." into rvc-dev
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 27e61ec..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);
     }
 
     /**
@@ -896,11 +904,8 @@
         final ContentValues values = new ContentValues();
         if (value) {
             values.put(MediaColumns.IS_TRASHED, 1);
-            values.put(MediaColumns.DATE_EXPIRES,
-                    (System.currentTimeMillis() + DateUtils.WEEK_IN_MILLIS) / 1000);
         } else {
             values.put(MediaColumns.IS_TRASHED, 0);
-            values.putNull(MediaColumns.DATE_EXPIRES);
         }
         return createRequest(resolver, CREATE_TRASH_REQUEST_CALL, uris, values);
     }
@@ -1103,9 +1108,17 @@
          * The time the media item should be considered expired. Typically only
          * meaningful in the context of {@link #IS_PENDING} or
          * {@link #IS_TRASHED}.
+         * <p>
+         * The value stored in this column is automatically calculated when
+         * {@link #IS_PENDING} or {@link #IS_TRASHED} is changed. The default
+         * pending expiration is typically 7 days, and the default trashed
+         * expiration is typically 30 days.
+         * <p>
+         * Expired media items are automatically deleted once their expiration
+         * time has passed, typically during during the next device idle period.
          */
         @CurrentTimeSecondsLong
-        @Column(Cursor.FIELD_TYPE_INTEGER)
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
         public static final String DATE_EXPIRES = "date_expires";
 
         /**
@@ -2058,7 +2071,6 @@
                 values.put(MediaColumns.MIME_TYPE, "image/jpeg");
                 values.put(MediaColumns.DATE_ADDED, now / 1000);
                 values.put(MediaColumns.DATE_MODIFIED, now / 1000);
-                values.put(MediaColumns.DATE_EXPIRES, (now + DateUtils.DAY_IN_MILLIS) / 1000);
                 values.put(MediaColumns.IS_PENDING, 1);
 
                 final Uri uri = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
@@ -2070,7 +2082,6 @@
                     // Everything went well above, publish it!
                     values.clear();
                     values.put(MediaColumns.IS_PENDING, 0);
-                    values.putNull(MediaColumns.DATE_EXPIRES);
                     cr.update(uri, values, null, null);
                     return uri.toString();
                 } catch (Exception e) {
@@ -3732,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/deploy.sh b/deploy.sh
index 22524c0..e6edcce 100755
--- a/deploy.sh
+++ b/deploy.sh
@@ -8,6 +8,7 @@
 adb remount
 adb sync
 adb shell umount /apex/com.android.mediaprovider*
+adb shell setprop apexd.status '""'
 adb shell setprop ctl.restart apexd
 adb shell rm -rf /system/priv-app/MediaProvider
 adb shell rm -rf /system/priv-app/MediaProviderGoogle
diff --git a/jni/Android.bp b/jni/Android.bp
index 0ff937b..265f7fb 100644
--- a/jni/Android.bp
+++ b/jni/Android.bp
@@ -65,7 +65,14 @@
 
 cc_test {
     name: "fuse_node_test",
-    test_suites: ["device-tests"],
+    test_suites: ["device-tests", "mts"],
+
+    compile_multilib: "both",
+    multilib: {
+        lib32: { suffix: "32", },
+        lib64: { suffix: "64", },
+    },
+
     srcs: [
         "node_test.cpp",
         "node.cpp",
@@ -91,7 +98,14 @@
 
 cc_test {
     name: "RedactionInfoTest",
-    test_suites: ["device-tests"],
+    test_suites: ["device-tests", "mts"],
+
+    compile_multilib: "both",
+    multilib: {
+        lib32: { suffix: "32", },
+        lib64: { suffix: "64", },
+    },
+
     srcs: [
         "RedactionInfoTest.cpp",
         "RedactionInfo.cpp",
@@ -111,4 +125,4 @@
 
     sdk_version: "current",
     stl: "c++_static",
-}
\ No newline at end of file
+}
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index 01837bf..7f95186 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -96,10 +96,18 @@
 
 #define FUSE_UNKNOWN_INO 0xffffffff
 
+// Stolen from: android_filesystem_config.h
+#define AID_APP_START 10000
+
 constexpr size_t MAX_READ_SIZE = 128 * 1024;
 // Stolen from: UserHandle#getUserId
 constexpr int PER_USER_RANGE = 100000;
 
+// Regex copied from FileUtils.java in MediaProvider, but without media directory.
+const std::regex PATTERN_OWNED_PATH(
+    "^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb|sandbox)/([^/]+)(/?.*)?",
+    std::regex_constants::icase);
+
 /*
  * In order to avoid double caching with fuse, call fadvise on the file handles
  * in the underlying file system. However, if this is done on every read/write,
@@ -240,12 +248,29 @@
     // because fuse_lowlevel_ops documents that the root inode is always one
     // (see FUSE_ROOT_ID in fuse_lowlevel.h). There are no particular requirements
     // on any of the other inodes in the FS.
-    inline node* FromInode(__u64 inode) {
+    // Also, if ctx is not nullptr, it will also check if the path can be accessible
+    // by the caller, and return nullptr if it cannot access.
+    inline node* FromInode(__u64 inode, const struct fuse_ctx* ctx) {
         if (inode == FUSE_ROOT_ID) {
             return root;
         }
 
-        return node::FromInode(inode, &tracker);
+        node* result = node::FromInode(inode, &tracker);
+        if (ctx == nullptr || ctx->uid < AID_APP_START) {
+            return result;
+        }
+
+        string path = result->BuildPath();
+        std::smatch match;
+        if (std::regex_match(path, match, PATTERN_OWNED_PATH)) {
+            std::string const & pkg = match[1];
+            if (!mp->IsUidForPackage(pkg, ctx->uid)) {
+                PLOG(WARNING) << "Invalid file access from " << pkg << ": " << path;
+                return nullptr;
+            }
+        }
+
+        return result;
     }
 
     inline __u64 ToInode(node* node) const {
@@ -346,6 +371,13 @@
     return reinterpret_cast<struct fuse*>(fuse_req_userdata(req));
 }
 
+static bool is_package_owned_path(const string& path, const string& fuse_path) {
+    if (path.rfind(fuse_path, 0) != 0) {
+        return false;
+    }
+    return std::regex_match(path, PATTERN_OWNED_PATH);
+}
+
 static node* make_node_entry(fuse_req_t req, node* parent, const string& name, const string& path,
                              struct fuse_entry_param* e, int* error_code) {
     struct fuse* fuse = get_fuse(req);
@@ -370,8 +402,10 @@
     // reuse inode numbers.
     e->generation = 0;
     e->ino = fuse->ToInode(node);
-    e->entry_timeout = std::numeric_limits<double>::max();
-    e->attr_timeout = std::numeric_limits<double>::max();
+    e->entry_timeout = is_package_owned_path(path, fuse->path) ?
+            0 : std::numeric_limits<double>::max();
+    e->attr_timeout = is_package_owned_path(path, fuse->path) ?
+            0 : std::numeric_limits<double>::max();
 
     return node;
 }
@@ -415,7 +449,11 @@
                        struct fuse_entry_param* e, int* error_code) {
     struct fuse* fuse = get_fuse(req);
     const struct fuse_ctx* ctx = fuse_req_ctx(req);
-    node* parent_node = fuse->FromInode(parent);
+    node* parent_node = fuse->FromInode(parent, ctx);
+    if (!parent_node) {
+        *error_code = ENOENT;
+        return nullptr;
+    }
     string parent_path = parent_node->BuildPath();
     string child_path = parent_path + "/" + name;
 
@@ -445,7 +483,8 @@
 }
 
 static void do_forget(struct fuse* fuse, fuse_ino_t ino, uint64_t nlookup) {
-    node* node = fuse->FromInode(ino);
+    // Always allow to forget so we put ctx as nullptr.
+    node* node = fuse->FromInode(ino, nullptr);
     TRACE_NODE(node);
     if (node) {
         // This is a narrowing conversion from an unsigned 64bit to a 32bit value. For
@@ -482,18 +521,21 @@
     ATRACE_CALL();
     struct fuse* fuse = get_fuse(req);
     const struct fuse_ctx* ctx = fuse_req_ctx(req);
-    node* node = fuse->FromInode(ino);
+    node* node = fuse->FromInode(ino, ctx);
+    if (!node) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
     string path = node->BuildPath();
     TRACE_NODE(node);
 
-    if (!node) fuse_reply_err(req, ENOENT);
-
     struct stat s;
     memset(&s, 0, sizeof(s));
     if (lstat(path.c_str(), &s) < 0) {
         fuse_reply_err(req, errno);
     } else {
-        fuse_reply_attr(req, &s, std::numeric_limits<double>::max());
+        fuse_reply_attr(req, &s, is_package_owned_path(path, fuse->path) ?
+                0 : std::numeric_limits<double>::max());
     }
 }
 
@@ -505,16 +547,15 @@
     ATRACE_CALL();
     struct fuse* fuse = get_fuse(req);
     const struct fuse_ctx* ctx = fuse_req_ctx(req);
-    node* node = fuse->FromInode(ino);
-    string path = node->BuildPath();
-    struct timespec times[2];
-
-    TRACE_NODE(node);
-
+    node* node = fuse->FromInode(ino, ctx);
     if (!node) {
         fuse_reply_err(req, ENOENT);
         return;
     }
+    string path = node->BuildPath();
+    struct timespec times[2];
+
+    TRACE_NODE(node);
 
     /* XXX: incomplete implementation on purpose.
      * chmod/chown should NEVER be implemented.*/
@@ -559,12 +600,13 @@
     }
 
     lstat(path.c_str(), attr);
-    fuse_reply_attr(req, attr, std::numeric_limits<double>::max());
+    fuse_reply_attr(req, attr, is_package_owned_path(path, fuse->path) ?
+            0 : std::numeric_limits<double>::max());
 }
 
 static void pf_canonical_path(fuse_req_t req, fuse_ino_t ino)
 {
-    node* node = get_fuse(req)->FromInode(ino);
+    node* node = get_fuse(req)->FromInode(ino, fuse_req_ctx(req));
 
     if (node) {
         // TODO(b/147482155): Check that uid has access to |path| and its contents
@@ -582,15 +624,15 @@
     ATRACE_CALL();
     struct fuse* fuse = get_fuse(req);
     const struct fuse_ctx* ctx = fuse_req_ctx(req);
-    node* parent_node = fuse->FromInode(parent);
-    string parent_path = parent_node->BuildPath();
-
-    TRACE_NODE(parent_node);
-
+    node* parent_node = fuse->FromInode(parent, ctx);
     if (!parent_node) {
         fuse_reply_err(req, ENOENT);
         return;
     }
+    string parent_path = parent_node->BuildPath();
+
+    TRACE_NODE(parent_node);
+
     const string child_path = parent_path + "/" + name;
 
     mode = (mode & (~0777)) | 0664;
@@ -616,7 +658,11 @@
     ATRACE_CALL();
     struct fuse* fuse = get_fuse(req);
     const struct fuse_ctx* ctx = fuse_req_ctx(req);
-    node* parent_node = fuse->FromInode(parent);
+    node* parent_node = fuse->FromInode(parent, ctx);
+    if (!parent_node) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
     const string parent_path = parent_node->BuildPath();
 
     TRACE_NODE(parent_node);
@@ -649,7 +695,11 @@
     ATRACE_CALL();
     struct fuse* fuse = get_fuse(req);
     const struct fuse_ctx* ctx = fuse_req_ctx(req);
-    node* parent_node = fuse->FromInode(parent);
+    node* parent_node = fuse->FromInode(parent, ctx);
+    if (!parent_node) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
     const string parent_path = parent_node->BuildPath();
 
     TRACE_NODE(parent_node);
@@ -675,7 +725,11 @@
     ATRACE_CALL();
     struct fuse* fuse = get_fuse(req);
     const struct fuse_ctx* ctx = fuse_req_ctx(req);
-    node* parent_node = fuse->FromInode(parent);
+    node* parent_node = fuse->FromInode(parent, ctx);
+    if (!parent_node) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
     const string parent_path = parent_node->BuildPath();
 
     TRACE_NODE(parent_node);
@@ -718,9 +772,11 @@
         return EINVAL;
     }
 
-    node* old_parent_node = fuse->FromInode(parent);
+    node* old_parent_node = fuse->FromInode(parent, ctx);
+    if (!old_parent_node) return ENOENT;
     const string old_parent_path = old_parent_node->BuildPath();
-    node* new_parent_node = fuse->FromInode(new_parent);
+    node* new_parent_node = fuse->FromInode(new_parent, ctx);
+    if (!new_parent_node) return ENOENT;
     const string new_parent_path = new_parent_node->BuildPath();
 
     if (!old_parent_node || !new_parent_node) {
@@ -793,15 +849,14 @@
     ATRACE_CALL();
     struct fuse* fuse = get_fuse(req);
     const struct fuse_ctx* ctx = fuse_req_ctx(req);
-    node* node = fuse->FromInode(ino);
-    const string path = node->BuildPath();
-
-    TRACE_NODE(node) << (is_requesting_write(fi->flags) ? "write" : "read");
-
+    node* node = fuse->FromInode(ino, ctx);
     if (!node) {
         fuse_reply_err(req, ENOENT);
         return;
     }
+    const string path = node->BuildPath();
+
+    TRACE_NODE(node) << (is_requesting_write(fi->flags) ? "write" : "read");
 
     if (fi->flags & O_DIRECT) {
         fi->flags &= ~O_DIRECT;
@@ -1042,7 +1097,7 @@
     ATRACE_CALL();
     struct fuse* fuse = get_fuse(req);
 
-    node* node = fuse->FromInode(ino);
+    node* node = fuse->FromInode(ino, fuse_req_ctx(req));
     handle* h = reinterpret_cast<handle*>(fi->fh);
     TRACE_NODE(node);
 
@@ -1088,15 +1143,14 @@
     ATRACE_CALL();
     struct fuse* fuse = get_fuse(req);
     const struct fuse_ctx* ctx = fuse_req_ctx(req);
-    node* node = fuse->FromInode(ino);
-    const string path = node->BuildPath();
-
-    TRACE_NODE(node);
-
+    node* node = fuse->FromInode(ino, ctx);
     if (!node) {
         fuse_reply_err(req, ENOENT);
         return;
     }
+    const string path = node->BuildPath();
+
+    TRACE_NODE(node);
 
     int status = fuse->mp->IsOpendirAllowed(path, ctx->uid);
     if (status) {
@@ -1135,7 +1189,11 @@
     struct fuse_entry_param e;
     size_t entry_size = 0;
 
-    node* node = fuse->FromInode(ino);
+    node* node = fuse->FromInode(ino, fuse_req_ctx(req));
+    if (!node) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
     const string path = node->BuildPath();
 
     TRACE_NODE(node);
@@ -1223,7 +1281,7 @@
     ATRACE_CALL();
     struct fuse* fuse = get_fuse(req);
 
-    node* node = fuse->FromInode(ino);
+    node* node = fuse->FromInode(ino, fuse_req_ctx(req));
     dirhandle* h = reinterpret_cast<dirhandle*>(fi->fh);
     TRACE_NODE(node);
     if (node) {
@@ -1271,7 +1329,11 @@
     struct fuse* fuse = get_fuse(req);
     const struct fuse_ctx* ctx = fuse_req_ctx(req);
 
-    node* node = fuse->FromInode(ino);
+    node* node = fuse->FromInode(ino, ctx);
+    if (!node) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
     const string path = node->BuildPath();
     TRACE_NODE(node);
 
@@ -1287,7 +1349,11 @@
     ATRACE_CALL();
     struct fuse* fuse = get_fuse(req);
     const struct fuse_ctx* ctx = fuse_req_ctx(req);
-    node* parent_node = fuse->FromInode(parent);
+    node* parent_node = fuse->FromInode(parent, ctx);
+    if (!parent_node) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
     const string parent_path = parent_node->BuildPath();
 
     TRACE_NODE(parent_node);
diff --git a/jni/MediaProviderWrapper.cpp b/jni/MediaProviderWrapper.cpp
index 999396a..361b738 100644
--- a/jni/MediaProviderWrapper.cpp
+++ b/jni/MediaProviderWrapper.cpp
@@ -146,6 +146,18 @@
     return res;
 }
 
+bool isUidForPackageInternal(JNIEnv* env, jobject media_provider_object,
+                             jmethodID mid_is_uid_for_package, const string& pkg, uid_t uid) {
+    ScopedLocalRef<jstring> j_pkg(env, env->NewStringUTF(pkg.c_str()));
+    bool res = env->CallBooleanMethod(media_provider_object, mid_is_uid_for_package, j_pkg.get(),
+            uid);
+
+    if (CheckForJniException(env)) {
+        return false;
+    }
+    return res;
+}
+
 std::vector<std::shared_ptr<DirectoryEntry>> getFilesInDirectoryInternal(
         JNIEnv* env, jobject media_provider_object, jmethodID mid_get_files_in_dir, uid_t uid,
         const string& path) {
@@ -252,7 +264,8 @@
                         /*is_static*/ false);
     mid_rename_ = CacheMethod(env, "rename", "(Ljava/lang/String;Ljava/lang/String;I)I",
                               /*is_static*/ false);
-
+    mid_is_uid_for_package_ = CacheMethod(env, "isUidForPackage", "(Ljava/lang/String;I)Z",
+                              /*is_static*/ false);
 }
 
 MediaProviderWrapper::~MediaProviderWrapper() {
@@ -368,6 +381,15 @@
     return isOpendirAllowedInternal(env, media_provider_object_, mid_is_opendir_allowed_, path, uid);
 }
 
+bool MediaProviderWrapper::IsUidForPackage(const string& pkg, uid_t uid) {
+    if (shouldBypassMediaProvider(uid)) {
+        return true;
+    }
+
+    JNIEnv* env = MaybeAttachCurrentThread();
+    return isUidForPackageInternal(env, media_provider_object_, mid_is_uid_for_package_, pkg, uid);
+}
+
 int MediaProviderWrapper::Rename(const string& old_path, const string& new_path, uid_t uid) {
     if (shouldBypassMediaProvider(uid)) {
         int res = rename(old_path.c_str(), new_path.c_str());
diff --git a/jni/MediaProviderWrapper.h b/jni/MediaProviderWrapper.h
index 016696f..ff160ea 100644
--- a/jni/MediaProviderWrapper.h
+++ b/jni/MediaProviderWrapper.h
@@ -136,6 +136,15 @@
     int IsOpendirAllowed(const std::string& path, uid_t uid);
 
     /**
+     * Determines if the given package name matches its uid.
+     *
+     * @param pkg the package name of the app
+     * @param uid UID of the app
+     * @return true if it matches, otherwise return false.
+     */
+    bool IsUidForPackage(const std::string& pkg, uid_t uid);
+
+    /**
      * Renames a file or directory to new path.
      *
      * @param old_path path of the file or directory to be renamed.
@@ -169,6 +178,7 @@
     jmethodID mid_is_opendir_allowed_;
     jmethodID mid_get_files_in_dir_;
     jmethodID mid_rename_;
+    jmethodID mid_is_uid_for_package_;
 
     /**
      * Auxiliary for caching MediaProvider methods.
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/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 51a5d6c..77cc361 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -22,7 +22,7 @@
     <string name="artist_label" msgid="8105600993099120273">"Artist"</string>
     <string name="unknown" msgid="2059049215682829375">"Okänd"</string>
     <string name="root_images" msgid="5861633549189045666">"Bilder"</string>
-    <string name="root_videos" msgid="8792703517064649453">"Videoklipp"</string>
+    <string name="root_videos" msgid="8792703517064649453">"Videor"</string>
     <string name="root_audio" msgid="3505830755201326018">"Ljud"</string>
     <string name="root_documents" msgid="3829103301363849237">"Dokument"</string>
     <string name="permission_required" msgid="1460820436132943754">"Behörighet krävs för att ändra eller radera objektet."</string>
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index 55ec897..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.
@@ -135,7 +141,7 @@
         public void onUpdate(@NonNull DatabaseHelper helper, @NonNull String volumeName,
                 long oldId, int oldMediaType, boolean oldIsDownload,
                 long newId, int newMediaType, boolean newIsDownload,
-                String ownerPackage, String oldPath);
+                String oldOwnerPackage, String newOwnerPackage, String oldPath);
         public void onDelete(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id,
                 int mediaType, boolean isDownload, String ownerPackage, String path);
     }
@@ -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]);
@@ -269,15 +275,15 @@
                 final long newId = Long.parseLong(split[4]);
                 final int newMediaType = Integer.parseInt(split[5]);
                 final boolean newIsDownload = Integer.parseInt(split[6]) != 0;
-                final String ownerPackage = split[7];
-                // Path can include ':',  assume rest of split[8..length] is path.
-                final String oldPath = String.join(":", Arrays.copyOfRange(split, 8, split.length));
+                final String oldOwnerPackage = split[7];
+                final String newOwnerPackage = split[8];
+                final String oldPath = split[9];
 
                 Trace.beginSection("_UPDATE");
                 try {
                     mFilesListener.onUpdate(DatabaseHelper.this, volumeName, oldId,
                             oldMediaType, oldIsDownload, newId, newMediaType, newIsDownload,
-                            ownerPackage, oldPath);
+                            oldOwnerPackage, newOwnerPackage, oldPath);
                 } finally {
                     Trace.endSection();
                 }
@@ -287,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 {
@@ -352,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<>();
@@ -759,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;
         }
     }
 
@@ -788,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)) {
@@ -808,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);
+                            }
                         }
                     }
 
@@ -827,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);
         }
     }
 
@@ -989,7 +1022,8 @@
         final String updateArg =
                 "old.volume_name||':'||old._id||':'||old.media_type||':'||old.is_download"
                         + "||':'||new._id||':'||new.media_type||':'||new.is_download"
-                        + "||':'||ifnull(old.owner_package_name,'null')||':'||old._data";
+                        + "||':'||ifnull(old.owner_package_name,'null')"
+                        + "||':'||ifnull(new.owner_package_name,'null')||':'||old._data";
         final String deleteArg =
                 "old.volume_name||':'||old._id||':'||old.media_type||':'||old.is_download"
                         + "||':'||ifnull(old.owner_package_name,'null')||':'||old._data";
@@ -1247,7 +1281,7 @@
     static final int VERSION_O = 800;
     static final int VERSION_P = 900;
     static final int VERSION_Q = 1023;
-    static final int VERSION_R = 1113;
+    static final int VERSION_R = 1114;
     static final int VERSION_LATEST = VERSION_R;
 
     /**
@@ -1387,6 +1421,9 @@
             if (fromVersion < 1113) {
                 // Empty version bump to ensure triggers are recreated
             }
+            if (fromVersion < 1114) {
+                // Empty version bump to ensure triggers are recreated
+            }
 
             if (recomputeDataValues) {
                 recomputeDataValues(db, internal);
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 3fcf718..4d2f2d5 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -60,6 +60,7 @@
 import static com.android.providers.media.util.FileUtils.extractTopLevelDir;
 import static com.android.providers.media.util.FileUtils.extractVolumeName;
 import static com.android.providers.media.util.FileUtils.getAbsoluteSanitizedPath;
+import static com.android.providers.media.util.FileUtils.isDataOrObbPath;
 import static com.android.providers.media.util.FileUtils.isDownload;
 import static com.android.providers.media.util.FileUtils.sanitizePath;
 import static com.android.providers.media.util.Logging.LOGV;
@@ -119,6 +120,7 @@
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.ParcelFileDescriptor.OnCloseListener;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.SystemProperties;
@@ -564,9 +566,10 @@
         public void onUpdate(@NonNull DatabaseHelper helper, @NonNull String volumeName,
                 long oldId, int oldMediaType, boolean oldIsDownload,
                 long newId, int newMediaType, boolean newIsDownload,
-                String ownerPackage, String oldPath) {
+                String oldOwnerPackage, String newOwnerPackage, String oldPath) {
             final boolean isDownload = oldIsDownload || newIsDownload;
-            handleUpdatedRowForFuse(oldPath, ownerPackage, oldId, newId);
+            handleUpdatedRowForFuse(oldPath, oldOwnerPackage, oldId, newId);
+            handleOwnerPackageNameChange(oldPath, oldOwnerPackage, newOwnerPackage);
             acceptWithExpansion(helper::notifyUpdate, volumeName, oldId, oldMediaType, isDownload);
 
             if (newMediaType != oldMediaType) {
@@ -1276,6 +1279,13 @@
                 return new String[] {""};
             }
 
+            // Do not allow apps to list Android/data or Android/obb dirs. Installer and
+            // MOUNT_EXTERNAL_ANDROID_WRITABLE apps won't be blocked by this, as their OBB dirs
+            // are mounted to lowerfs directly.
+            if (isDataOrObbPath(path)) {
+                return new String[] {""};
+            }
+
             if (shouldBypassFuseRestrictions(/*forWrite*/ false, path)) {
                 return new String[] {"/"};
             }
@@ -1342,6 +1352,55 @@
                 mimeType.startsWith(supportedPrimaryMimeType));
     }
 
+    /**
+     * Removes owner package for the renamed path if the calling package doesn't own the db row
+     *
+     * When oldPath is renamed to newPath, if newPath exists in the database, and caller is not the
+     * owner of the file, owner package is set to 'null'. This prevents previous owner of newPath
+     * from accessing renamed file.
+     * @return {@code true} if
+     * <ul>
+     * <li> there is no corresponding database row for given {@code path}
+     * <li> shared calling package is the owner of the database row
+     * <li> owner package name is already set to 'null'
+     * <li> updating owner package name to 'null' was successful.
+     * </ul>
+     * Returns {@code false} otherwise.
+     */
+    private boolean maybeRemoveOwnerPackageForFuseRename(@NonNull DatabaseHelper helper,
+            @NonNull String path) {
+
+        final Uri uri = Files.getContentUriForPath(path);
+        final int match = matchUri(uri, isCallingPackageAllowedHidden());
+        final String ownerPackageName;
+        final String selection = MediaColumns.DATA + " =? AND "
+                + MediaColumns.OWNER_PACKAGE_NAME + " != 'null'";
+        final String[] selectionArgs = new String[] {path};
+
+        final SQLiteQueryBuilder qbForQuery =
+                getQueryBuilder(TYPE_QUERY, match, uri, Bundle.EMPTY, null);
+        try (Cursor c = qbForQuery.query(helper, new String[] {FileColumns.OWNER_PACKAGE_NAME},
+                selection, selectionArgs, null, null, null, null, null)) {
+            if (!c.moveToFirst()) {
+                // We don't need to remove owner_package from db row if path doesn't exist in
+                // database or owner_package is already set to 'null'
+                return true;
+            }
+            ownerPackageName = c.getString(0);
+            if (isCallingIdentitySharedPackageName(ownerPackageName)) {
+                // We don't need to remove owner_package from db row if calling package is the owner
+                // of the database row
+                return true;
+            }
+        }
+
+        final SQLiteQueryBuilder qbForUpdate =
+                getQueryBuilder(TYPE_UPDATE, match, uri, Bundle.EMPTY, null);
+        ContentValues values = new ContentValues();
+        values.put(FileColumns.OWNER_PACKAGE_NAME, "null");
+        return qbForUpdate.update(helper, values, selection, selectionArgs) == 1;
+    }
+
     private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper,
             @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values) {
         return updateDatabaseForFuseRename(helper, oldPath, newPath, values, Bundle.EMPTY);
@@ -1623,26 +1682,34 @@
         if (!isMimeTypeSupportedInPath(newPath, newMimeType)) {
             return OsConstants.EPERM;
         }
-        return renameFileUncheckedForFuse(oldPath, newPath);
+        return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ false) ;
     }
 
     private int renameFileUncheckedForFuse(String oldPath, String newPath) {
-        final String newMimeType = MimeUtils.resolveMimeType(new File(newPath));
+        return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ true) ;
+    }
+
+    private int renameFileForFuse(String oldPath, String newPath, boolean bypassRestrictions) {
         final DatabaseHelper helper;
         try {
             helper = getDatabaseForUri(Files.getContentUriForPath(oldPath));
         } catch (VolumeNotFoundException e) {
-            throw new IllegalStateException("Volume not found while trying to update database for"
-                + oldPath + ". Rename failed due to database update error", e);
+            throw new IllegalStateException("Failed to update database row with " + oldPath, e);
         }
 
         helper.beginTransaction();
         try {
+            final String newMimeType = MimeUtils.resolveMimeType(new File(newPath));
             final String oldMimeType = MimeUtils.resolveMimeType(new File(oldPath));
             if (!updateDatabaseForFuseRename(helper, oldPath, newPath,
                     getContentValuesForFuseRename(newPath, oldMimeType, newMimeType))) {
-                Log.e(TAG, "Calling package doesn't have write permission to rename file.");
-                return OsConstants.EPERM;
+                if (!bypassRestrictions) {
+                    Log.e(TAG, "Calling package doesn't have write permission to rename file.");
+                    return OsConstants.EPERM;
+                } else if (!maybeRemoveOwnerPackageForFuseRename(helper, newPath)) {
+                    Log.wtf(TAG, "Couldn't clear owner package name for " + newPath);
+                    return OsConstants.EPERM;
+                }
             }
 
             // Try renaming oldPath to newPath in lower file system.
@@ -2378,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);
@@ -2679,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;
                 }
@@ -2715,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) {
@@ -2725,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;
@@ -2835,6 +2925,9 @@
             // IDs are forever; nobody should be editing them
             initialValues.remove(MediaColumns._ID);
 
+            // Expiration times are hard-coded; let's derive them
+            FileUtils.computeDateExpires(initialValues);
+
             // Ignore or augment incoming raw filesystem paths
             for (String column : sDataColumns.keySet()) {
                 if (!initialValues.containsKey(column)) continue;
@@ -3038,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) {
@@ -3122,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();
@@ -3178,7 +3263,7 @@
         }
 
         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
-        if (parseBoolean(uri.getQueryParameter("distinct"))) {
+        if (uri.getBooleanQueryParameter("distinct", false)) {
             qb.setDistinct(true);
         }
         qb.setStrict(true);
@@ -3186,11 +3271,8 @@
             // When caller is system, such as the media scanner, we're willing
             // to let them access any columns they want
         } else {
-            // TODO: re-enable regardless of target SDK in b/146518586
-            if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) {
-                qb.setStrictColumns(true);
-                qb.setStrictGrammar(true);
-            }
+            qb.setStrictColumns(true);
+            qb.setStrictGrammar(true);
         }
 
         final String callingPackage = getCallingPackageOrSelf();
@@ -3925,19 +4007,19 @@
      */
     private int deleteRecursive(SQLiteQueryBuilder qb, DatabaseHelper helper, String userWhere,
             String[] userWhereArgs) {
-        synchronized (mDirectoryCache) {
-            mDirectoryCache.clear();
+        return (int) helper.runWithTransaction((db) -> {
+            synchronized (mDirectoryCache) {
+                mDirectoryCache.clear();
+            }
 
-            return (int) helper.runWithTransaction((db) -> {
-                int n = 0;
-                int total = 0;
-                do {
-                    n = qb.delete(helper, userWhere, userWhereArgs);
-                    total += n;
-                } while (n > 0);
-                return total;
-            });
-        }
+            int n = 0;
+            int total = 0;
+            do {
+                n = qb.delete(helper, userWhere, userWhereArgs);
+                total += n;
+            } while (n > 0);
+            return total;
+        });
     }
 
     @Override
@@ -4158,8 +4240,7 @@
                 break;
             case MediaStore.CREATE_TRASH_REQUEST_CALL:
                 allowedColumns = Arrays.asList(
-                        MediaColumns.IS_TRASHED,
-                        MediaColumns.DATE_EXPIRES);
+                        MediaColumns.IS_TRASHED);
                 break;
             default:
                 allowedColumns = Arrays.asList();
@@ -4291,7 +4372,7 @@
             // doesn't exist we fall through to create it below
             final File thumbFile = getThumbnailFile(uri);
             try {
-                return ParcelFileDescriptor.open(thumbFile,
+                return FileUtils.openSafely(thumbFile,
                         ParcelFileDescriptor.MODE_READ_ONLY);
             } catch (FileNotFoundException ignored) {
             }
@@ -4314,9 +4395,9 @@
                 // once for remote reading. Both FDs point at the same
                 // underlying inode on disk, so they're stable across renames
                 // to avoid race conditions between threads.
-                thumbWrite = ParcelFileDescriptor.open(thumbTempFile,
+                thumbWrite = FileUtils.openSafely(thumbTempFile,
                         ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_CREATE);
-                thumbRead = ParcelFileDescriptor.open(thumbTempFile,
+                thumbRead = FileUtils.openSafely(thumbTempFile,
                         ParcelFileDescriptor.MODE_READ_ONLY);
 
                 final Bitmap thumbnail = getThumbnailBitmap(uri, signal);
@@ -4490,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;
@@ -4543,6 +4624,9 @@
             // IDs are forever; nobody should be editing them
             initialValues.remove(MediaColumns._ID);
 
+            // Expiration times are hard-coded; let's derive them
+            FileUtils.computeDateExpires(initialValues);
+
             // Ignore or augment incoming raw filesystem paths
             for (String column : sDataColumns.keySet()) {
                 if (!initialValues.containsKey(column)) continue;
@@ -4644,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
@@ -4734,15 +4816,6 @@
         // Make sure any updated paths look sane
         assertFileColumnsSane(match, uri, initialValues);
 
-        // if the media type is being changed, check if it's being changed from image or video
-        // to something else
-        if (initialValues.containsKey(FileColumns.MEDIA_TYPE)) {
-            final int newMediaType = initialValues.getAsInteger(FileColumns.MEDIA_TYPE);
-
-            // If we're changing media types, invalidate any thumbnails
-            triggerInvalidate = true;
-        }
-
         if (initialValues.containsKey(FileColumns.DATA)) {
             // If we're changing paths, invalidate any thumbnails
             triggerInvalidate = true;
@@ -4767,11 +4840,7 @@
 
         final ContentValues values = new ContentValues(initialValues);
         switch (match) {
-            case AUDIO_MEDIA_ID: {
-                computeAudioLocalizedValues(values);
-                computeAudioKeyValues(values);
-                // fall-through
-            }
+            case AUDIO_MEDIA_ID:
             case AUDIO_PLAYLISTS_ID:
             case VIDEO_MEDIA_ID:
             case IMAGES_MEDIA_ID:
@@ -4782,6 +4851,17 @@
             }
         }
 
+        if (initialValues.containsKey(FileColumns.MEDIA_TYPE)) {
+            final int mediaType = initialValues.getAsInteger(FileColumns.MEDIA_TYPE);
+            switch (mediaType) {
+                case FileColumns.MEDIA_TYPE_AUDIO: {
+                    computeAudioLocalizedValues(values);
+                    computeAudioKeyValues(values);
+                    break;
+                }
+            }
+        }
+
         count = qb.update(helper, values, userWhere, userWhereArgs);
 
         // If the caller tried (and failed) to update metadata, the file on disk
@@ -5208,6 +5288,16 @@
         mCallingIdentity.get().addDeletedRowId(path, rowId);
     }
 
+    private void handleOwnerPackageNameChange(@NonNull String oldPath,
+            @NonNull String oldOwnerPackage, @NonNull String newOwnerPackage) {
+        if (Objects.equals(oldOwnerPackage, newOwnerPackage)) {
+            return;
+        }
+        // Invalidate saved owned ID's of the previous owner of the renamed path, this prevents old
+        // owner from gaining access to replaced file.
+        invalidateLocalCallingIdentityCache(oldOwnerPackage, "owner_package_changed:" + oldPath);
+    }
+
     /**
      * Return the {@link MediaColumns#DATA} field for the given {@code Uri}.
      */
@@ -5416,7 +5506,7 @@
                     // If fuse is enabled, we can provide an fd that points to the fuse
                     // file system and handle redaction in the fuse handler when the caller reads.
                     Log.i(TAG, "Redacting with new FUSE for " + filePath);
-                    pfd = ParcelFileDescriptor.open(getFuseFile(file), modeBits);
+                    pfd = FileUtils.openSafely(getFuseFile(file), modeBits);
                 } else {
                     // TODO(b/135341978): Remove this and associated code
                     // when fuse is on by default.
@@ -5434,7 +5524,7 @@
                     daemon = getFuseDaemonForFile(file);
                 } catch (FileNotFoundException ignored) {
                 }
-                ParcelFileDescriptor lowerFsFd = ParcelFileDescriptor.open(file, modeBits);
+                ParcelFileDescriptor lowerFsFd = FileUtils.openSafely(file, modeBits);
                 boolean forRead = (modeBits & ParcelFileDescriptor.MODE_READ_ONLY) != 0;
                 boolean shouldOpenWithFuse = daemon != null
                         && daemon.shouldOpenWithFuse(filePath, forRead, lowerFsFd.getFd());
@@ -5445,7 +5535,7 @@
                     // resulting from cache inconsistencies between the upper and lower
                     // filesystem caches
                     Log.w(TAG, "Using FUSE for " + filePath);
-                    pfd = ParcelFileDescriptor.open(getFuseFile(file), modeBits);
+                    pfd = FileUtils.openSafely(getFuseFile(file), modeBits);
                     try {
                         lowerFsFd.close();
                     } catch (IOException e) {
@@ -6210,6 +6300,17 @@
         }
     }
 
+    @Keep
+    public boolean isUidForPackageForFuse(@NonNull String packageName, int uid) {
+        final LocalCallingIdentity token =
+                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+        try {
+            return isCallingIdentitySharedPackageName(packageName);
+        } finally {
+            restoreLocalCallingIdentity(token);
+        }
+    }
+
     private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) {
         // System internals can work with all media
         if (isCallingPackageSystem()) {
@@ -6822,7 +6923,6 @@
         sMutableColumns.add(MediaStore.MediaColumns.IS_PENDING);
         sMutableColumns.add(MediaStore.MediaColumns.IS_TRASHED);
         sMutableColumns.add(MediaStore.MediaColumns.IS_FAVORITE);
-        sMutableColumns.add(MediaStore.MediaColumns.DATE_EXPIRES);
 
         sMutableColumns.add(MediaStore.Audio.AudioColumns.BOOKMARK);
 
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 ee5d7e1..e711d0c 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -25,16 +25,21 @@
 import static android.system.OsConstants.F_OK;
 import static android.system.OsConstants.O_ACCMODE;
 import static android.system.OsConstants.O_APPEND;
+import static android.system.OsConstants.O_CLOEXEC;
 import static android.system.OsConstants.O_CREAT;
+import static android.system.OsConstants.O_NOFOLLOW;
 import static android.system.OsConstants.O_RDONLY;
 import static android.system.OsConstants.O_RDWR;
 import static android.system.OsConstants.O_TRUNC;
 import static android.system.OsConstants.O_WRONLY;
 import static android.system.OsConstants.R_OK;
+import static android.system.OsConstants.S_IRWXG;
+import static android.system.OsConstants.S_IRWXU;
 import static android.system.OsConstants.W_OK;
 
 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;
@@ -42,9 +47,13 @@
 import android.content.Context;
 import android.net.Uri;
 import android.os.Environment;
+import android.os.ParcelFileDescriptor;
 import android.os.storage.StorageManager;
 import android.provider.MediaStore;
 import android.provider.MediaStore.MediaColumns;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
 import android.util.Log;
@@ -55,6 +64,7 @@
 import androidx.annotation.VisibleForTesting;
 
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
@@ -79,10 +89,39 @@
 import java.util.regex.Pattern;
 
 public class FileUtils {
+    /**
+     * Drop-in replacement for {@link ParcelFileDescriptor#open(File, int)}
+     * which adds security features like {@link OsConstants#O_CLOEXEC} and
+     * {@link OsConstants#O_NOFOLLOW}.
+     */
+    public static @NonNull ParcelFileDescriptor openSafely(@NonNull File file, int pfdFlags)
+            throws FileNotFoundException {
+        final int posixFlags = translateModePfdToPosix(pfdFlags) | O_CLOEXEC | O_NOFOLLOW;
+        try {
+            final FileDescriptor fd = Os.open(file.getAbsolutePath(), posixFlags,
+                    S_IRWXU | S_IRWXG);
+            try {
+                return ParcelFileDescriptor.dup(fd);
+            } finally {
+                closeQuietly(fd);
+            }
+        } catch (IOException | ErrnoException e) {
+            throw new FileNotFoundException(e.getMessage());
+        }
+    }
+
     public static void closeQuietly(@Nullable AutoCloseable closeable) {
         android.os.FileUtils.closeQuietly(closeable);
     }
 
+    public static void closeQuietly(@Nullable FileDescriptor fd) {
+        if (fd == null) return;
+        try {
+            Os.close(fd);
+        } catch (ErrnoException ignored) {
+        }
+    }
+
     public static long copy(@NonNull InputStream in, @NonNull OutputStream out) throws IOException {
         return android.os.FileUtils.copy(in, out);
     }
@@ -804,13 +843,13 @@
      * Default duration that {@link MediaColumns#IS_PENDING} items should be
      * preserved for until automatically cleaned by {@link #runIdleMaintenance}.
      */
-    public static final long DEFAULT_DURATION_PENDING = DateUtils.WEEK_IN_MILLIS;
+    public static final long DEFAULT_DURATION_PENDING = 7 * DateUtils.DAY_IN_MILLIS;
 
     /**
      * Default duration that {@link MediaColumns#IS_TRASHED} items should be
      * preserved for until automatically cleaned by {@link #runIdleMaintenance}.
      */
-    public static final long DEFAULT_DURATION_TRASHED = DateUtils.WEEK_IN_MILLIS;
+    public static final long DEFAULT_DURATION_TRASHED = 30 * DateUtils.DAY_IN_MILLIS;
 
     public static boolean isDownload(@NonNull String path) {
         return PATTERN_DOWNLOADS_FILE.matcher(path).matches();
@@ -831,7 +870,13 @@
      * and which captures the package name as the first group.
      */
     public static final Pattern PATTERN_OWNED_PATH = Pattern.compile(
-            "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|media|obb|sandbox)/([^/]+)(/.*)?");
+            "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|media|obb|sandbox)/([^/]+)(/?.*)?");
+
+    /**
+     * Regex that matches Android/obb or Android/data path.
+     */
+    public static final Pattern PATTERN_DATA_OR_OBB_PATH = Pattern.compile(
+            "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb)/?$");
 
     /**
      * Regex that matches paths for {@link MediaColumns#RELATIVE_PATH}; it
@@ -920,6 +965,15 @@
     }
 
     /**
+     * Returns true if relative path is Android/data or Android/obb path.
+     */
+    public static boolean isDataOrObbPath(String path) {
+        if (path == null) return false;
+        final Matcher m = PATTERN_DATA_OR_OBB_PATH.matcher(path);
+        return m.matches();
+    }
+
+    /**
      * Returns the name of the top level directory, or null if the path doesn't go through the
      * external storage directory.
      */
@@ -933,6 +987,36 @@
     }
 
     /**
+     * Compute the value of {@link MediaColumns#DATE_EXPIRES} based on other
+     * columns being modified by this operation.
+     */
+    public static void computeDateExpires(@NonNull ContentValues values) {
+        // External apps have no ability to change this field
+        values.remove(MediaColumns.DATE_EXPIRES);
+
+        // Only define the field when this modification is actually adjusting
+        // one of the flags that should influence the expiration
+        final Object pending = values.get(MediaColumns.IS_PENDING);
+        if (pending != null) {
+            if (parseBoolean(pending, false)) {
+                values.put(MediaColumns.DATE_EXPIRES,
+                        (System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000);
+            } else {
+                values.putNull(MediaColumns.DATE_EXPIRES);
+            }
+        }
+        final Object trashed = values.get(MediaColumns.IS_TRASHED);
+        if (trashed != null) {
+            if (parseBoolean(trashed, false)) {
+                values.put(MediaColumns.DATE_EXPIRES,
+                        (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000);
+            } else {
+                values.putNull(MediaColumns.DATE_EXPIRES);
+            }
+        }
+    }
+
+    /**
      * Compute several scattered {@link MediaColumns} values from
      * {@link MediaColumns#DATA}. This method performs no enforcement of
      * argument validity.
@@ -941,7 +1025,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);
@@ -957,8 +1040,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/src/com/android/providers/media/util/PermissionUtils.java b/src/com/android/providers/media/util/PermissionUtils.java
index c076914..387ced0 100644
--- a/src/com/android/providers/media/util/PermissionUtils.java
+++ b/src/com/android/providers/media/util/PermissionUtils.java
@@ -35,16 +35,22 @@
 import android.annotation.NonNull;
 import android.app.AppOpsManager;
 import android.content.Context;
+import android.provider.MediaStore;
 
 public class PermissionUtils {
     // Callers must hold both the old and new permissions, so that we can
     // handle obscure cases like when an app targets Q but was installed on
     // a device that was originally running on P before being upgraded to Q.
 
+    private static volatile int sLegacyMediaProviderUid = -1;
+
     public static boolean checkPermissionSystem(Context context,
             int pid, int uid, String packageName) {
+        // Apps sharing legacy MediaProvider's uid like DownloadProvider and MTP are treated as
+        // system.
         return uid == android.os.Process.SYSTEM_UID || uid == android.os.Process.myUid()
-                || uid == android.os.Process.SHELL_UID || uid == android.os.Process.ROOT_UID;
+                || uid == android.os.Process.SHELL_UID || uid == android.os.Process.ROOT_UID
+                || isLegacyMediaProvider(context, uid);
     }
 
     public static boolean checkPermissionBackup(Context context, int pid, int uid) {
@@ -209,4 +215,15 @@
                 throw new IllegalStateException(op + " has unknown mode " + mode);
         }
     }
+
+    private static boolean isLegacyMediaProvider(Context context, int uid) {
+        if (sLegacyMediaProviderUid == -1) {
+            // Uid stays constant while legacy Media Provider stays installed. Cache legacy
+            // MediaProvider's uid for the first time.
+            sLegacyMediaProviderUid = context.getPackageManager()
+                    .resolveContentProvider(MediaStore.AUTHORITY_LEGACY, 0)
+                    .applicationInfo.uid;
+        }
+        return (uid == sLegacyMediaProviderUid);
+    }
 }
diff --git a/src/com/android/providers/media/util/RedactingFileDescriptor.java b/src/com/android/providers/media/util/RedactingFileDescriptor.java
index a0bec59..bf2524f 100644
--- a/src/com/android/providers/media/util/RedactingFileDescriptor.java
+++ b/src/com/android/providers/media/util/RedactingFileDescriptor.java
@@ -47,15 +47,10 @@
     private volatile long[] mRedactRanges;
     private volatile long[] mFreeOffsets;
 
-    private FileDescriptor mInner = null;
+    private ParcelFileDescriptor mInner = null;
     private ParcelFileDescriptor mOuter = null;
 
-    public static void closeQuietly(FileDescriptor fd) {
-        try {
-            Os.close(fd);
-        } catch (ErrnoException ignored) {
-        }
-    }
+    private FileDescriptor mInnerFd = null;
 
     private RedactingFileDescriptor(
             Context context, File file, int mode, long[] redactRanges, long[] freeOffsets)
@@ -64,17 +59,13 @@
         mFreeOffsets = freeOffsets;
 
         try {
-            try {
-                mInner = Os.open(file.getAbsolutePath(),
-                        FileUtils.translateModePfdToPosix(mode), 0);
-                mOuter = context.getSystemService(StorageManager.class)
-                        .openProxyFileDescriptor(mode, mCallback,
-                                new Handler(Looper.getMainLooper()));
-            } catch (ErrnoException e) {
-                throw e.rethrowAsIOException();
-            }
+            mInner = FileUtils.openSafely(file, mode);
+            mInnerFd = mInner.getFileDescriptor();
+            mOuter = context.getSystemService(StorageManager.class)
+                    .openProxyFileDescriptor(mode, mCallback,
+                            new Handler(Looper.getMainLooper()));
         } catch (IOException e) {
-            closeQuietly(mInner);
+            FileUtils.closeQuietly(mInner);
             FileUtils.closeQuietly(mOuter);
             throw e;
         }
@@ -158,7 +149,7 @@
     private final ProxyFileDescriptorCallback mCallback = new ProxyFileDescriptorCallback() {
         @Override
         public long onGetSize() throws ErrnoException {
-            return Os.fstat(mInner).st_size;
+            return Os.fstat(mInnerFd).st_size;
         }
 
         @Override
@@ -166,7 +157,7 @@
             int n = 0;
             while (n < size) {
                 try {
-                    final int res = Os.pread(mInner, data, n, size - n, offset + n);
+                    final int res = Os.pread(mInnerFd, data, n, size - n, offset + n);
                     if (res == 0) {
                         break;
                     } else {
@@ -203,7 +194,7 @@
             int n = 0;
             while (n < size) {
                 try {
-                    final int res = Os.pwrite(mInner, data, n, size - n, offset + n);
+                    final int res = Os.pwrite(mInnerFd, data, n, size - n, offset + n);
                     if (res == 0) {
                         break;
                     } else {
@@ -222,12 +213,12 @@
 
         @Override
         public void onFsync() throws ErrnoException {
-            Os.fsync(mInner);
+            Os.fsync(mInnerFd);
         }
 
         @Override
         public void onRelease() {
-            closeQuietly(mInner);
+            FileUtils.closeQuietly(mInner);
         }
     };
 }
diff --git a/tests/Android.bp b/tests/Android.bp
index e79f576..ee5143d 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -9,6 +9,7 @@
         "device-tests",
         "mts",
     ],
+    compile_multilib: "both",
 
     manifest: "AndroidManifest.xml",
 
diff --git a/tests/client/Android.bp b/tests/client/Android.bp
index 65b45cd..0d29bbf 100644
--- a/tests/client/Android.bp
+++ b/tests/client/Android.bp
@@ -4,6 +4,7 @@
         "device-tests",
         "mts",
     ],
+    compile_multilib: "both",
 
     manifest: "AndroidManifest.xml",
 
@@ -20,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/jni/FuseDaemonTest/Android.bp b/tests/jni/FuseDaemonTest/Android.bp
index 1208e1d..e3e92b0 100644
--- a/tests/jni/FuseDaemonTest/Android.bp
+++ b/tests/jni/FuseDaemonTest/Android.bp
@@ -47,6 +47,7 @@
     manifest: "AndroidManifest.xml",
     srcs: ["src/**/*.java"],
     static_libs: ["androidx.test.rules", "truth-prebuilt", "tests-fusedaemon-lib"],
+    compile_multilib: "both",
     test_suites: ["general-tests", "mts"],
     sdk_version: "test_current",
     java_resources: [
@@ -61,6 +62,7 @@
     manifest: "legacy/AndroidManifest.xml",
     srcs: ["legacy/src/**/*.java"],
     static_libs: ["androidx.test.rules", "truth-prebuilt",  "tests-fusedaemon-lib"],
+    compile_multilib: "both",
     test_suites: ["general-tests", "mts"],
     sdk_version: "test_current",
     target_sdk_version: "29",
diff --git a/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/ReaddirTestHelper.java b/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/ReaddirTestHelper.java
index 074231b..29986bc 100644
--- a/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/ReaddirTestHelper.java
+++ b/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/ReaddirTestHelper.java
@@ -18,6 +18,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.DirectoryIteratorException;
 import java.nio.file.DirectoryStream;
 import java.nio.file.DirectoryStream.Filter;
 import java.nio.file.Files;
@@ -69,14 +70,15 @@
     public static ArrayList<String> readDirectory(String directoryPath, Filter<Path> filter) {
         ArrayList<String> directoryEntries = new ArrayList<String>();
         File dir = new File(directoryPath);
+
         try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dir.toPath(),
                 filter)) {
             for (Path de: directoryStream) {
                 directoryEntries.add(de.getFileName().toString());
             }
-        } catch (IOException x) {
+        } catch (IOException | DirectoryIteratorException x) {
             Log.e(TAG, "IOException occurred while readding directory entries from " +
-                  directoryPath);
+                  directoryPath, x);
         }
         return directoryEntries;
     }
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/PermissionActivityTest.java b/tests/src/com/android/providers/media/PermissionActivityTest.java
index e751a90..7bbce62 100644
--- a/tests/src/com/android/providers/media/PermissionActivityTest.java
+++ b/tests/src/com/android/providers/media/PermissionActivityTest.java
@@ -19,15 +19,22 @@
 import android.app.Instrumentation;
 import android.content.ClipData;
 import android.content.ContentValues;
+import android.content.Context;
 import android.content.Intent;
+import android.net.Uri;
+import android.os.Environment;
 import android.provider.MediaStore;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.providers.media.scan.MediaScannerTest;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.File;
+
 /**
  * We already have solid coverage of this logic in {@code CtsProviderTestCases},
  * but the coverage system currently doesn't measure that, so we add the bare
@@ -45,11 +52,18 @@
         activity.startActivityForResult(createIntent(), 42);
     }
 
-    private static Intent createIntent() {
+    private static Intent createIntent() throws Exception {
+        final Context context = InstrumentationRegistry.getContext();
+
+        final File dir = Environment
+                .getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
+        final File file = MediaScannerTest.stage(R.raw.test_image,
+                new File(dir, "test" + System.nanoTime() + ".jpg"));
+        final Uri uri = MediaStore.scanFile(context.getContentResolver(), file);
+
         final Intent intent = new Intent(MediaStore.CREATE_WRITE_REQUEST_CALL, null,
-                InstrumentationRegistry.getContext(), PermissionActivity.class);
-        intent.putExtra(MediaStore.EXTRA_CLIP_DATA, ClipData.newRawUri("",
-                MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY, 42)));
+                context, PermissionActivity.class);
+        intent.putExtra(MediaStore.EXTRA_CLIP_DATA, ClipData.newRawUri("", uri));
         intent.putExtra(MediaStore.EXTRA_CONTENT_VALUES, new ContentValues());
         return intent;
     }
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();
diff --git a/tests/src/com/android/providers/media/util/FileUtilsTest.java b/tests/src/com/android/providers/media/util/FileUtilsTest.java
index 35f789e..da8ddce 100644
--- a/tests/src/com/android/providers/media/util/FileUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/FileUtilsTest.java
@@ -51,6 +51,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -61,6 +62,9 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.google.common.collect.Range;
+import com.google.common.truth.Truth;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -548,6 +552,63 @@
         assertEquals("/", values.get(MediaColumns.RELATIVE_PATH));
     }
 
+    @Test
+    public void testComputeDateExpires_None() throws Exception {
+        final ContentValues values = new ContentValues();
+        values.put(MediaColumns.DATE_EXPIRES, 1577836800L);
+
+        FileUtils.computeDateExpires(values);
+        assertFalse(values.containsKey(MediaColumns.DATE_EXPIRES));
+    }
+
+    @Test
+    public void testComputeDateExpires_Pending_Set() throws Exception {
+        final ContentValues values = new ContentValues();
+        values.put(MediaColumns.IS_PENDING, 1);
+        values.put(MediaColumns.DATE_EXPIRES, 1577836800L);
+
+        FileUtils.computeDateExpires(values);
+        final long target = (System.currentTimeMillis()
+                + FileUtils.DEFAULT_DURATION_PENDING) / 1_000;
+        Truth.assertThat(values.getAsLong(MediaColumns.DATE_EXPIRES))
+                .isIn(Range.closed(target - 5, target + 5));
+    }
+
+    @Test
+    public void testComputeDateExpires_Pending_Clear() throws Exception {
+        final ContentValues values = new ContentValues();
+        values.put(MediaColumns.IS_PENDING, 0);
+        values.put(MediaColumns.DATE_EXPIRES, 1577836800L);
+
+        FileUtils.computeDateExpires(values);
+        assertTrue(values.containsKey(MediaColumns.DATE_EXPIRES));
+        assertNull(values.get(MediaColumns.DATE_EXPIRES));
+    }
+
+    @Test
+    public void testComputeDateExpires_Trashed_Set() throws Exception {
+        final ContentValues values = new ContentValues();
+        values.put(MediaColumns.IS_TRASHED, 1);
+        values.put(MediaColumns.DATE_EXPIRES, 1577836800L);
+
+        FileUtils.computeDateExpires(values);
+        final long target = (System.currentTimeMillis()
+                + FileUtils.DEFAULT_DURATION_TRASHED) / 1_000;
+        Truth.assertThat(values.getAsLong(MediaColumns.DATE_EXPIRES))
+                .isIn(Range.closed(target - 5, target + 5));
+    }
+
+    @Test
+    public void testComputeDateExpires_Trashed_Clear() throws Exception {
+        final ContentValues values = new ContentValues();
+        values.put(MediaColumns.IS_TRASHED, 0);
+        values.put(MediaColumns.DATE_EXPIRES, 1577836800L);
+
+        FileUtils.computeDateExpires(values);
+        assertTrue(values.containsKey(MediaColumns.DATE_EXPIRES));
+        assertNull(values.get(MediaColumns.DATE_EXPIRES));
+    }
+
     private static File touch(File dir, String name) throws IOException {
         final File res = new File(dir, name);
         res.createNewFile();