Merge "Revert "Add permission checks for setattr"" into rvc-dev
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 6c7f671..185d29b 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -992,6 +992,17 @@
         /**
          * Absolute filesystem path to the media item on disk.
          * <p>
+         * On Android 11, you can use this value when you access an existing
+         * media file using direct file paths. That's because this value has
+         * a valid file path. However, don't assume that the file is always
+         * available. Be prepared to handle any file-based I/O errors that
+         * could occur.
+         * <p>
+         * Don't use this value when you create or update a media file, even
+         * if you're on Android 11 and are using direct file paths. Instead,
+         * use the values of the {@link #DISPLAY_NAME} and
+         * {@link #RELATIVE_PATH} columns.
+         * <p>
          * Note that apps may not have filesystem permissions to directly access
          * this path. Instead of trying to open this path directly, apps should
          * use {@link ContentResolver#openFileDescriptor(Uri, String)} to gain
diff --git a/apex/testing/test_manifest.json b/apex/testing/test_manifest.json
index ffef8fb..8ec7fba 100644
--- a/apex/testing/test_manifest.json
+++ b/apex/testing/test_manifest.json
@@ -1,4 +1,4 @@
 {
   "name": "com.android.mediaprovider",
-  "version": 300000000
+  "version": 2147483647
 }
diff --git a/jni/Android.bp b/jni/Android.bp
index 5688e5c..280ddcd 100644
--- a/jni/Android.bp
+++ b/jni/Android.bp
@@ -21,6 +21,7 @@
         "jni_init.cpp",
         "com_android_providers_media_FuseDaemon.cpp",
         "FuseDaemon.cpp",
+        "FuseUtils.cpp",
         "MediaProviderWrapper.cpp",
         "ReaddirHelper.cpp",
         "RedactionInfo.cpp",
@@ -128,3 +129,39 @@
     sdk_version: "current",
     stl: "c++_static",
 }
+
+cc_test {
+    name: "FuseUtilsTest",
+    test_suites: ["device-tests", "mts"],
+    test_config: "FuseUtilsTest.xml",
+
+    compile_multilib: "both",
+    multilib: {
+        lib32: { suffix: "32", },
+        lib64: { suffix: "64", },
+    },
+
+    srcs: [
+        "FuseUtilsTest.cpp",
+        "FuseUtils.cpp",
+    ],
+
+    header_libs: [
+        "libnativehelper_header_only",
+    ],
+
+    local_include_dirs: ["include"],
+
+    static_libs: [
+        "libbase_ndk",
+    ],
+
+    shared_libs: [
+        "liblog",
+    ],
+
+    tidy: true,
+
+    sdk_version: "current",
+    stl: "c++_static",
+}
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index b23fc52..87d403f 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -61,6 +61,7 @@
 #include <vector>
 
 #include "MediaProviderWrapper.h"
+#include "libfuse_jni/FuseUtils.h"
 #include "libfuse_jni/ReaddirHelper.h"
 #include "libfuse_jni/RedactionInfo.h"
 #include "node-inl.h"
@@ -75,8 +76,9 @@
 using std::vector;
 
 // logging macros to avoid duplication.
-#define TRACE_NODE(__node) \
-    LOG(VERBOSE) << __FUNCTION__ << " : " << #__node << " = [" << get_name(__node) << "] "
+#define TRACE_NODE(__node, __req)                                                  \
+    LOG(VERBOSE) << __FUNCTION__ << " : " << #__node << " = [" << get_name(__node) \
+                 << "] (uid=" << __req->ctx.uid << ") "
 
 #define ATRACE_NAME(name) ScopedTrace ___tracer(name)
 #define ATRACE_CALL() ATRACE_NAME(__FUNCTION__)
@@ -108,9 +110,6 @@
     "^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb|sandbox)/([^/]+)(/?.*)?",
     std::regex_constants::icase);
 
-const std::regex ANDROID_DATA_OBB_PATH("^/storage/[^/]+/(?:[0-9]+/)?(?:Android)/?(?:data|obb)?$",
-                                       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,
@@ -220,9 +219,9 @@
     }
 
     std::mutex mutex_;
-    std::thread thread_;
-    std::queue<Message> queue_;
     std::condition_variable cv_;
+    std::queue<Message> queue_;
+    std::thread thread_;
 
     typedef std::multimap<size_t, int> Sizes;
     typedef std::map<int, Sizes::iterator> Files;
@@ -246,6 +245,13 @@
 
     inline bool IsRoot(const node* node) const { return node == root; }
 
+    inline string GetEffectiveRootPath() {
+        if (path.find("/storage/emulated", 0) == 0) {
+            return path + "/" + std::to_string(getuid() / PER_USER_RANGE);
+        }
+        return path;
+    }
+
     // Note that these two (FromInode / ToInode) conversion wrappers are required
     // 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
@@ -293,10 +299,8 @@
 
 static inline string get_name(node* n) {
     if (n) {
-        std::string name("node_path: " + n->BuildSafePath());
-        if (IS_OS_DEBUGABLE) {
-            name += " real_path: " + n->BuildPath();
-        }
+        std::string name = IS_OS_DEBUGABLE ? "real_path: " + n->BuildPath() + " " : "";
+        name += "node_path: " + n->BuildSafePath();
         return name;
     }
     return "?";
@@ -363,18 +367,36 @@
     return std::regex_match(path, PATTERN_OWNED_PATH);
 }
 
-static void invalidate_case_insensitive_dentry_matches(struct fuse* fuse, node* parent,
-                                                       const vector<string>& children) {
-    fuse_ino_t parent_ino = fuse->ToInode(parent);
-    std::thread t([=]() {
-        for (const string& child_name : children) {
-            if (fuse_lowlevel_notify_inval_entry(fuse->se, parent_ino, child_name.c_str(),
-                                                 child_name.size())) {
-                LOG(ERROR) << "Failed to invalidate dentry " << child_name;
-            }
-        }
-    });
-    t.detach();
+// See fuse_lowlevel.h fuse_lowlevel_notify_inval_entry for how to call this safetly without
+// deadlocking the kernel
+static void fuse_inval(fuse_session* se, fuse_ino_t parent_ino, fuse_ino_t child_ino,
+                       const string& child_name, const string& path) {
+    if (mediaprovider::fuse::containsMount(path, std::to_string(getuid() / PER_USER_RANGE))) {
+        LOG(WARNING) << "Ignoring attempt to invalidate dentry for FUSE mounts";
+        return;
+    }
+
+    if (fuse_lowlevel_notify_inval_entry(se, parent_ino, child_name.c_str(), child_name.size())) {
+        // Invalidating the dentry can fail if there's no dcache entry, however, there may still
+        // be cached attributes, so attempt to invalidate those by invalidating the inode
+        fuse_lowlevel_notify_inval_inode(se, child_ino, 0, 0);
+    }
+}
+
+static double get_timeout(struct fuse* fuse, const string& path, bool should_inval) {
+    string media_path = fuse->GetEffectiveRootPath() + "/Android/media";
+    if (should_inval || path.find(media_path, 0) == 0 || is_package_owned_path(path, fuse->path)) {
+        // We set dentry timeout to 0 for the following reasons:
+        // 1. Case-insensitive lookups need to invalidate other case-insensitive dentry matches
+        // 2. Installd might delete Android/media/<package> dirs when app data is cleared.
+        // This can leave a stale entry in the kernel dcache, and break subsequent creation of the
+        // dir via FUSE.
+        // 3. With app data isolation enabled, app A should not guess existence of app B from the
+        // Android/{data,obb}/<package> paths, hence we prevent the kernel from caching that
+        // information.
+        return 0;
+    }
+    return std::numeric_limits<double>::max();
 }
 
 static node* make_node_entry(fuse_req_t req, node* parent, const string& name, const string& path,
@@ -389,21 +411,30 @@
         return NULL;
     }
 
+    bool should_inval = false;
     node = parent->LookupChildByName(name, true /* acquire */);
     if (!node) {
         node = ::node::Create(parent, name, &fuse->lock, &fuse->tracker);
-    } else if (!std::regex_match(node->BuildPath(), ANDROID_DATA_OBB_PATH)) {
+    } else if (!mediaprovider::fuse::containsMount(path, std::to_string(getuid() / PER_USER_RANGE))) {
+        should_inval = true;
+        // Only invalidate a path if it does not contain mount.
         // Invalidate both names to ensure there's no dentry left in the kernel after the following
         // operations:
         // 1) touch foo, touch FOO, unlink *foo*
         // 2) touch foo, touch FOO, unlink *FOO*
         // Invalidating lookup_name fixes (1) and invalidating node_name fixes (2)
-        vector<string> children;
-        children.push_back(name);
-        children.push_back(node->GetName());
-        invalidate_case_insensitive_dentry_matches(fuse, parent, children);
+        // |should_inval| invalidates lookup_name by using 0 timeout below and we explicitly
+        // invalidate node_name if different case
+        // Note that we invalidate async otherwise we will deadlock the kernel
+        if (name != node->GetName()) {
+            std::thread t([=]() {
+                fuse_inval(fuse->se, fuse->ToInode(parent), fuse->ToInode(node), node->GetName(),
+                           path);
+            });
+            t.detach();
+        }
     }
-    TRACE_NODE(node);
+    TRACE_NODE(node, req);
 
     // This FS is not being exported via NFS so just a fixed generation number
     // for now. If we do need this, we need to increment the generation ID each
@@ -411,10 +442,10 @@
     // reuse inode numbers.
     e->generation = 0;
     e->ino = fuse->ToInode(node);
-    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();
+    e->entry_timeout = get_timeout(fuse, path, should_inval);
+    e->attr_timeout = is_package_owned_path(path, fuse->path) || should_inval
+                              ? 0
+                              : std::numeric_limits<double>::max();
 
     return node;
 }
@@ -459,6 +490,12 @@
         return true;
     }
 
+    if (path == "/storage/emulated") {
+        // Apps should never refer to /storage/emulated - they should be using the user-spcific
+        // subdirs, eg /storage/emulated/0
+        return false;
+    }
+
     std::smatch match;
     if (std::regex_match(path, match, PATTERN_OWNED_PATH)) {
         const std::string& pkg = match[1];
@@ -485,14 +522,16 @@
         return nullptr;
     }
     string parent_path = parent_node->BuildPath();
-    if (!is_app_accessible_path(fuse->mp, parent_path, req->ctx.uid)) {
+    // We should always allow lookups on the root, because failing them could cause
+    // bind mounts to be invalidated.
+    if (!fuse->IsRoot(parent_node) && !is_app_accessible_path(fuse->mp, parent_path, req->ctx.uid)) {
         *error_code = ENOENT;
         return nullptr;
     }
 
     string child_path = parent_path + "/" + name;
 
-    TRACE_NODE(parent_node);
+    TRACE_NODE(parent_node, req);
 
     std::smatch match;
     std::regex_search(child_path, match, storage_emulated_regex);
@@ -517,9 +556,9 @@
     }
 }
 
-static void do_forget(struct fuse* fuse, fuse_ino_t ino, uint64_t nlookup) {
+static void do_forget(fuse_req_t req, struct fuse* fuse, fuse_ino_t ino, uint64_t nlookup) {
     node* node = fuse->FromInode(ino);
-    TRACE_NODE(node);
+    TRACE_NODE(node, req);
     if (node) {
         // This is a narrowing conversion from an unsigned 64bit to a 32bit value. For
         // some reason we only keep 32 bit refcounts but the kernel issues
@@ -534,7 +573,7 @@
     node* node;
     struct fuse* fuse = get_fuse(req);
 
-    do_forget(fuse, ino, nlookup);
+    do_forget(req, fuse, ino, nlookup);
     fuse_reply_none(req);
 }
 
@@ -545,7 +584,7 @@
     struct fuse* fuse = get_fuse(req);
 
     for (int i = 0; i < count; i++) {
-        do_forget(fuse, forgets[i].ino, forgets[i].nlookup);
+        do_forget(req, fuse, forgets[i].ino, forgets[i].nlookup);
     }
     fuse_reply_none(req);
 }
@@ -565,7 +604,7 @@
         fuse_reply_err(req, ENOENT);
         return;
     }
-    TRACE_NODE(node);
+    TRACE_NODE(node, req);
 
     struct stat s;
     memset(&s, 0, sizeof(s));
@@ -596,7 +635,7 @@
     }
     struct timespec times[2];
 
-    TRACE_NODE(node);
+    TRACE_NODE(node, req);
 
     /* XXX: incomplete implementation on purpose.
      * chmod/chown should NEVER be implemented.*/
@@ -632,7 +671,7 @@
             }
         }
 
-        TRACE_NODE(node);
+        TRACE_NODE(node, req);
         if (utimensat(-1, path.c_str(), times, 0) < 0) {
             fuse_reply_err(req, errno);
             return;
@@ -676,7 +715,7 @@
         return;
     }
 
-    TRACE_NODE(parent_node);
+    TRACE_NODE(parent_node, req);
 
     const string child_path = parent_path + "/" + name;
 
@@ -714,7 +753,7 @@
         return;
     }
 
-    TRACE_NODE(parent_node);
+    TRACE_NODE(parent_node, req);
 
     const string child_path = parent_path + "/" + name;
 
@@ -755,7 +794,7 @@
         return;
     }
 
-    TRACE_NODE(parent_node);
+    TRACE_NODE(parent_node, req);
 
     const string child_path = parent_path + "/" + name;
 
@@ -766,7 +805,7 @@
     }
 
     node* child_node = parent_node->LookupChildByName(name, false /* acquire */);
-    TRACE_NODE(child_node);
+    TRACE_NODE(child_node, req);
     if (child_node) {
         child_node->SetDeleted();
     }
@@ -787,7 +826,7 @@
         fuse_reply_err(req, ENOENT);
         return;
     }
-    TRACE_NODE(parent_node);
+    TRACE_NODE(parent_node, req);
 
     const string child_path = parent_path + "/" + name;
 
@@ -803,7 +842,7 @@
     }
 
     node* child_node = parent_node->LookupChildByName(name, false /* acquire */);
-    TRACE_NODE(child_node);
+    TRACE_NODE(child_node, req);
     if (child_node) {
         child_node->SetDeleted();
     }
@@ -848,11 +887,11 @@
         return 0;
     }
 
-    TRACE_NODE(old_parent_node);
-    TRACE_NODE(new_parent_node);
+    TRACE_NODE(old_parent_node, req);
+    TRACE_NODE(new_parent_node, req);
 
     node* child_node = old_parent_node->LookupChildByName(name, true /* acquire */);
-    TRACE_NODE(child_node) << "old_child";
+    TRACE_NODE(child_node, req) << "old_child";
 
     const string old_child_path = child_node->BuildPath();
     const string new_child_path = new_parent_path + "/" + new_name;
@@ -864,7 +903,7 @@
     if (res == 0) {
         child_node->Rename(new_name, new_parent_node);
     }
-    TRACE_NODE(child_node) << "new_child";
+    TRACE_NODE(child_node, req) << "new_child";
 
     child_node->Release(1);
     return res;
@@ -922,7 +961,7 @@
         return;
     }
 
-    TRACE_NODE(node) << (is_requesting_write(fi->flags) ? "write" : "read");
+    TRACE_NODE(node, req) << (is_requesting_write(fi->flags) ? "write" : "read");
 
     if (fi->flags & O_DIRECT) {
         fi->flags &= ~O_DIRECT;
@@ -1153,7 +1192,7 @@
                      struct fuse_file_info* fi) {
     ATRACE_CALL();
     struct fuse* fuse = get_fuse(req);
-    TRACE_NODE(nullptr) << "noop";
+    TRACE_NODE(nullptr, req) << "noop";
     fuse_reply_err(req, 0);
 }
 
@@ -1165,7 +1204,7 @@
 
     node* node = fuse->FromInode(ino);
     handle* h = reinterpret_cast<handle*>(fi->fh);
-    TRACE_NODE(node);
+    TRACE_NODE(node, req);
 
     fuse->fadviser.Close(h->fd);
     if (node) {
@@ -1220,7 +1259,7 @@
         return;
     }
 
-    TRACE_NODE(node);
+    TRACE_NODE(node, req);
 
     int status = fuse->mp->IsOpendirAllowed(path, ctx->uid);
     if (status) {
@@ -1270,7 +1309,7 @@
         return;
     }
 
-    TRACE_NODE(node);
+    TRACE_NODE(node, req);
     // Get all directory entries from MediaProvider on first readdir() call of
     // directory handle. h->next_off = 0 indicates that current readdir() call
     // is first readdir() call for the directory handle, Avoid multiple JNI calls
@@ -1307,7 +1346,7 @@
                 // Ignore lookup errors on
                 // 1. non-existing files returned from MediaProvider database.
                 // 2. path that doesn't match FuseDaemon UID and calling uid.
-                if (error_code == ENOENT || error_code == EPERM) continue;
+                if (error_code == ENOENT || error_code == EPERM || error_code == EACCES) continue;
                 fuse_reply_err(req, error_code);
                 return;
             }
@@ -1328,7 +1367,7 @@
             // When an entry is rejected, lookup called by readdir_plus will not be tracked by
             // kernel. Call forget on the rejected node to decrement the reference count.
             if (plus) {
-                do_forget(fuse, e.ino, 1);
+                do_forget(req, fuse, e.ino, 1);
             }
             break;
         }
@@ -1361,7 +1400,7 @@
     node* node = fuse->FromInode(ino);
 
     dirhandle* h = reinterpret_cast<dirhandle*>(fi->fh);
-    TRACE_NODE(node);
+    TRACE_NODE(node, req);
     if (node) {
         node->DestroyDirHandle(h);
     }
@@ -1416,7 +1455,7 @@
         fuse_reply_err(req, ENOENT);
         return;
     }
-    TRACE_NODE(node);
+    TRACE_NODE(node, req);
 
     // exists() checks are always allowed.
     if (mask == F_OK) {
@@ -1468,7 +1507,7 @@
         return;
     }
 
-    TRACE_NODE(parent_node);
+    TRACE_NODE(parent_node, req);
 
     const string child_path = parent_path + "/" + name;
 
@@ -1497,20 +1536,19 @@
         return;
     }
 
-    // File was inserted to MP database with default mime type/media type values. ScanFile will
-    // update the db columns with appropriate values. This is used for hidden file handling.
-    fuse->mp->ScanFile(child_path.c_str());
-
     int error_code = 0;
     struct fuse_entry_param e;
     node* node = make_node_entry(req, parent_node, name, child_path, &e, &error_code);
-    TRACE_NODE(node);
+    TRACE_NODE(node, req);
     if (!node) {
         CHECK(error_code != 0);
         fuse_reply_err(req, error_code);
         return;
     }
 
+    // Let MediaProvider know we've created a new file
+    fuse->mp->OnFileCreated(child_path);
+
     // TODO(b/147274248): Assume there will be no EXIF to redact.
     // This prevents crashing during reads but can be a security hole if a malicious app opens an fd
     // to the file before all the EXIF content is written. We could special case reads before the
@@ -1650,19 +1688,19 @@
     if (active.load(std::memory_order_acquire)) {
         string name;
         fuse_ino_t parent;
-
+        fuse_ino_t child;
         {
             std::lock_guard<std::recursive_mutex> guard(fuse->lock);
             const node* node = node::LookupAbsolutePath(fuse->root, path);
             if (node) {
                 name = node->GetName();
+                child = fuse->ToInode(const_cast<class node*>(node));
                 parent = fuse->ToInode(node->GetParent());
             }
         }
 
-        if (!name.empty() &&
-            fuse_lowlevel_notify_inval_entry(fuse->se, parent, name.c_str(), name.size())) {
-            LOG(WARNING) << "Failed to invalidate dentry for path";
+        if (!name.empty()) {
+            fuse_inval(fuse->se, parent, child, name, path);
         }
     } else {
         LOG(WARNING) << "FUSE daemon is inactive. Cannot invalidate dentry";
@@ -1677,11 +1715,11 @@
 }
 
 void FuseDaemon::Start(android::base::unique_fd fd, const std::string& path) {
+    android::base::SetDefaultTag(LOG_TAG);
+
     struct fuse_args args;
     struct fuse_cmdline_opts opts;
 
-    SetMinimumLogSeverity(android::base::VERBOSE);
-
     struct stat stat;
 
     if (lstat(path.c_str(), &stat)) {
diff --git a/jni/FuseUtils.cpp b/jni/FuseUtils.cpp
new file mode 100644
index 0000000..8d6c35a
--- /dev/null
+++ b/jni/FuseUtils.cpp
@@ -0,0 +1,40 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#define LOG_TAG "FuseUtils"
+
+#include "include/libfuse_jni/FuseUtils.h"
+
+#include <string>
+#include <vector>
+
+using std::string;
+
+namespace mediaprovider {
+namespace fuse {
+
+bool containsMount(const string& path, const string& userid) {
+    const string& prefix = "/storage/emulated/" + userid;
+    std::vector<string> suffixes = {"/Android", "/Android/data", "/Android/obb"};
+
+    if (path.find(prefix) != 0) {
+        return false;
+    }
+
+    const string& path_suffix = path.substr(prefix.length());
+    return std::find(suffixes.begin(), suffixes.end(), path_suffix) != suffixes.end();
+}
+
+}  // namespace fuse
+}  // namespace mediaprovider
diff --git a/jni/FuseUtilsTest.cpp b/jni/FuseUtilsTest.cpp
new file mode 100644
index 0000000..9d05bc7
--- /dev/null
+++ b/jni/FuseUtilsTest.cpp
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specic language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "FuseUtilsTest"
+
+#include "libfuse_jni/FuseUtils.h"
+
+#include <gtest/gtest.h>
+
+using namespace mediaprovider::fuse;
+
+TEST(FuseUtilsTest, testContainsMount_isTrueForAndroidDataObb) {
+    EXPECT_TRUE(containsMount("/storage/emulated/1234/Android", "1234"));
+    EXPECT_TRUE(containsMount("/storage/emulated/1234/Android/data", "1234"));
+    EXPECT_TRUE(containsMount("/storage/emulated/1234/Android/obb", "1234"));
+}
+
+TEST(FuseUtilsTest, testContainsMount) {
+    EXPECT_FALSE(containsMount("/random/path", "1234"));
+    EXPECT_FALSE(containsMount("/storage/abc-123", "1234"));
+}
+
+TEST(FuseUtilsTest, testContainsMount_isCaseSensitive) {
+    EXPECT_FALSE(containsMount("/storage/emulated/1234/android", "1234"));
+    EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/Data", "1234"));
+    EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/OBB", "1234"));
+}
+
+TEST(FuseUtilsTest, testContainsMount_isFalseForPathWithAdditionalSlash) {
+    EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/", "1234"));
+    EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/data/", "1234"));
+    EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/obb/", "1234"));
+
+    EXPECT_FALSE(containsMount("//storage/emulated/1234/Android", "1234"));
+    EXPECT_FALSE(containsMount("/storage/emulated/1234//Android/data", "1234"));
+}
+
+TEST(FuseUtilsTest, testContainsMount_isFalseForPathWithWrongUserid) {
+    EXPECT_FALSE(containsMount("/storage/emulated/11234/Android", "1234"));
+    EXPECT_FALSE(containsMount("/storage/emulated/0/Android/data", "1234"));
+    EXPECT_FALSE(containsMount("/storage/emulated/12345/Android/obb", "1234"));
+}
diff --git a/jni/FuseUtilsTest.xml b/jni/FuseUtilsTest.xml
new file mode 100644
index 0000000..46fccac
--- /dev/null
+++ b/jni/FuseUtilsTest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<!-- Note: this is derived from the autogenerated configuration. We require
+           root support. -->
+<configuration description="Runs FuseUtilsTest">
+    <option name="test-suite-tag" value="mts" />
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="FuseUtilsTest->/data/local/tmp/FuseUtilsTest" />
+        <option name="append-bitness" value="true" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+    <test class="com.android.tradefed.testtype.GTest" >
+        <option name="native-test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="FuseUtilsTest" />
+        <option name="runtime-hint" value="10m" />
+        <!-- test-timeout unit is ms, value = 10 min -->
+        <option name="native-test-timeout" value="600000" />
+    </test>
+
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.mediaprovider" />
+    </object>
+</configuration>
diff --git a/jni/MediaProviderWrapper.cpp b/jni/MediaProviderWrapper.cpp
index 3a85923..ac84ff7 100644
--- a/jni/MediaProviderWrapper.cpp
+++ b/jni/MediaProviderWrapper.cpp
@@ -217,6 +217,16 @@
     }
     return res;
 }
+
+void onFileCreatedInternal(JNIEnv* env, jobject media_provider_object,
+                           jmethodID mid_on_file_created, const string& path) {
+    ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
+
+    env->CallVoidMethod(media_provider_object, mid_on_file_created, j_path.get());
+    CheckForJniException(env);
+    return;
+}
+
 }  // namespace
 /*****************************************************************************************/
 /******************************* Public API Implementation *******************************/
@@ -266,6 +276,8 @@
                               /*is_static*/ false);
     mid_is_uid_for_package_ = CacheMethod(env, "isUidForPackage", "(Ljava/lang/String;I)Z",
                               /*is_static*/ false);
+    mid_on_file_created_ = CacheMethod(env, "onFileCreated", "(Ljava/lang/String;)V",
+                                       /*is_static*/ false);
 }
 
 MediaProviderWrapper::~MediaProviderWrapper() {
@@ -292,7 +304,7 @@
 }
 
 int MediaProviderWrapper::InsertFile(const string& path, uid_t uid) {
-    if (shouldBypassMediaProvider(uid)) {
+    if (uid == ROOT_UID) {
         return 0;
     }
 
@@ -301,9 +313,8 @@
 }
 
 int MediaProviderWrapper::DeleteFile(const string& path, uid_t uid) {
-    if (shouldBypassMediaProvider(uid)) {
+    if (uid == ROOT_UID) {
         int res = unlink(path.c_str());
-        ScanFile(path);
         return res;
     }
 
@@ -391,7 +402,9 @@
 }
 
 int MediaProviderWrapper::Rename(const string& old_path, const string& new_path, uid_t uid) {
-    if (shouldBypassMediaProvider(uid)) {
+    // Rename from SHELL_UID should go through MediaProvider to update database rows, so only bypass
+    // MediaProvider for ROOT_UID.
+    if (uid == ROOT_UID) {
         int res = rename(old_path.c_str(), new_path.c_str());
         if (res != 0) res = -errno;
         return res;
@@ -401,6 +414,12 @@
     return renameInternal(env, media_provider_object_, mid_rename_, old_path, new_path, uid);
 }
 
+void MediaProviderWrapper::OnFileCreated(const string& path) {
+    JNIEnv* env = MaybeAttachCurrentThread();
+
+    return onFileCreatedInternal(env, media_provider_object_, mid_on_file_created_, path);
+}
+
 /*****************************************************************************************/
 /******************************** Private member functions *******************************/
 /*****************************************************************************************/
diff --git a/jni/MediaProviderWrapper.h b/jni/MediaProviderWrapper.h
index 9ad30ae..9d4fad9 100644
--- a/jni/MediaProviderWrapper.h
+++ b/jni/MediaProviderWrapper.h
@@ -157,6 +157,13 @@
     int Rename(const std::string& old_path, const std::string& new_path, uid_t uid);
 
     /**
+     * Called whenever a file has been created through FUSE.
+     *
+     * @param path path of the file that has been created.
+     */
+    void OnFileCreated(const std::string& path);
+
+    /**
      * Initializes per-process static variables associated with the lifetime of
      * a managed runtime.
      */
@@ -179,6 +186,7 @@
     jmethodID mid_get_files_in_dir_;
     jmethodID mid_rename_;
     jmethodID mid_is_uid_for_package_;
+    jmethodID mid_on_file_created_;
 
     /**
      * Auxiliary for caching MediaProvider methods.
diff --git a/jni/RedactionInfo.cpp b/jni/RedactionInfo.cpp
index d943c8a..17de22e 100644
--- a/jni/RedactionInfo.cpp
+++ b/jni/RedactionInfo.cpp
@@ -14,12 +14,8 @@
  * limitations under the License.
  */
 
-#define LOG_TAG "RedactionInfo"
-
 #include "include/libfuse_jni/RedactionInfo.h"
 
-#include <android-base/logging.h>
-
 using std::unique_ptr;
 using std::vector;
 
@@ -94,7 +90,6 @@
 
 unique_ptr<vector<RedactionRange>> RedactionInfo::getOverlappingRedactionRanges(size_t size,
                                                                                 off64_t off) const {
-    LOG(DEBUG) << "Computing redaction ranges for request: sz = " << size << " off = " << off;
     if (hasOverlapWithReadRequest(size, off)) {
         auto first_redaction = redaction_ranges_.end();
         auto last_redaction = redaction_ranges_.end();
@@ -112,12 +107,9 @@
             last_redaction = iter;
         }
         if (first_redaction != redaction_ranges_.end()) {
-            LOG(DEBUG) << "Returning " << (int)(last_redaction - first_redaction + 1)
-                       << " redaction ranges!";
             return std::make_unique<vector<RedactionRange>>(first_redaction, last_redaction + 1);
         }
     }
-    LOG(DEBUG) << "No relevant redaction ranges!";
     return std::make_unique<vector<RedactionRange>>();
 }
 }  // namespace fuse
diff --git a/jni/TEST_MAPPING b/jni/TEST_MAPPING
index a871459..5ee1bc6 100644
--- a/jni/TEST_MAPPING
+++ b/jni/TEST_MAPPING
@@ -1,6 +1,9 @@
 {
   "presubmit": [
     {
+      "name": "FuseUtilsTest"
+    },
+    {
       "name": "RedactionInfoTest"
     },
     {
diff --git a/jni/include/libfuse_jni/FuseUtils.h b/jni/include/libfuse_jni/FuseUtils.h
new file mode 100644
index 0000000..3528ca0
--- /dev/null
+++ b/jni/include/libfuse_jni/FuseUtils.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specic language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEDIAPROVIDER_JNI_UTILS_H_
+#define MEDIAPROVIDER_JNI_UTILS_H_
+
+#include <string>
+
+namespace mediaprovider {
+namespace fuse {
+
+/**
+ * Returns true if the given path is mounted for the given userid. Mounted paths
+ * are:
+ * "/storage/emulated/<userid>/Android"
+ * "/storage/emulated/<userid>/Android/data"
+ * "/storage/emulated/<userid>/Android/obb" *
+ */
+bool containsMount(const std::string& path, const std::string& userid);
+
+}  // namespace fuse
+}  // namespace mediaprovider
+
+#endif  // MEDIAPROVIDER_JNI_UTILS_H_
diff --git a/logging.sh b/logging.sh
index 43f0d86..fd2a3a5 100755
--- a/logging.sh
+++ b/logging.sh
@@ -20,10 +20,12 @@
     adb shell setprop log.tag.SQLiteQueryBuilder VERBOSE
     adb shell setprop log.tag.FuseDaemon VERBOSE
     adb shell setprop log.tag.libfuse VERBOSE
+    adb shell setprop persist.sys.fuse.log true
 else
     adb shell setprop log.tag.SQLiteQueryBuilder INFO
     adb shell setprop log.tag.FuseDaemon INFO
     adb shell setprop log.tag.libfuse INFO
+    adb shell setprop persist.sys.fuse.log false
 fi
 
 # Kill process to kick new settings into place
diff --git a/res/drawable/ic_warning.xml b/res/drawable/ic_warning.xml
new file mode 100644
index 0000000..2163203
--- /dev/null
+++ b/res/drawable/ic_warning.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24"
+        android:viewportWidth="24">
+    <path
+        android:fillColor="#1A73E8"
+        android:pathData="M12 5.99L19.53 19H4.47L12 5.99M12 2L1 21h22L12 2zm1 14h-2v2h2v-2zm0-6h-2v4h2v-4z" />
+</vector>
\ No newline at end of file
diff --git a/res/drawable/thumb_clip.xml b/res/drawable/thumb_clip.xml
index 0a05ecd..f075e41 100644
--- a/res/drawable/thumb_clip.xml
+++ b/res/drawable/thumb_clip.xml
@@ -16,5 +16,6 @@
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
         android:shape="rectangle">
+    <solid android:color="@color/thumb_gray_color"/>
     <corners android:radius="4dp" />
 </shape>
diff --git a/res/layout/cache_clearing_dialog.xml b/res/layout/cache_clearing_dialog.xml
index f5e9bde..06f97b8 100644
--- a/res/layout/cache_clearing_dialog.xml
+++ b/res/layout/cache_clearing_dialog.xml
@@ -21,13 +21,17 @@
         android:paddingStart="?android:attr/dialogPreferredPadding"
         android:paddingEnd="?android:attr/dialogPreferredPadding"
         android:orientation="vertical">
+    <ImageView
+        android:adjustViewBounds="true"
+        android:layout_gravity="bottom|center_horizontal"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_marginTop="20dp"
+        android:scaleType="fitCenter"
+        android:src="@drawable/ic_warning"
+        android:contentDescription="@null"/>
+
     <TextView
         android:id="@+id/dialog_title"
-        android:gravity="center"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:paddingTop="15dp"
-        android:textSize="20sp"
-        android:textColor="?android:attr/textColorPrimary"
-        android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Title" />
+        style="@style/CacheClearingAlertDialogTitle"/>
 </LinearLayout>
diff --git a/res/layout/permission_body.xml b/res/layout/permission_body.xml
index a4d1250..6282c7c 100644
--- a/res/layout/permission_body.xml
+++ b/res/layout/permission_body.xml
@@ -77,6 +77,7 @@
         android:layout_width="match_parent"
         android:layout_height="200dp"
         android:scaleType="centerCrop"
+        android:src="@color/thumb_gray_color"
         android:visibility="gone" />
 
     <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 8be0253..beace75 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -30,7 +30,7 @@
     <string name="grant_dialog_button_allow" msgid="1644287024501033471">"Autoriser"</string>
     <string name="grant_dialog_button_deny" msgid="6190589471415815741">"Refuser"</string>
     <plurals name="permission_more_thumb" formatted="false" msgid="4392079224649478923">
-      <item quantity="one">+<xliff:g id="COUNT_1">^1</xliff:g></item>
+      <item quantity="one">+ <xliff:g id="COUNT_1">^1</xliff:g></item>
       <item quantity="other">+ <xliff:g id="COUNT_1">^1</xliff:g></item>
     </plurals>
     <plurals name="permission_more_text" formatted="false" msgid="7291997297174507324">
@@ -43,67 +43,67 @@
     <string name="allow" msgid="8885707816848569619">"Autoriser"</string>
     <string name="deny" msgid="6040983710442068936">"Refuser"</string>
     <plurals name="permission_write_audio" formatted="false" msgid="8914759422381305478">
-      <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> audio files?</item>
+      <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> fichier audio ?</item>
       <item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> fichiers audio ?</item>
     </plurals>
     <plurals name="permission_write_video" formatted="false" msgid="1098082003326873084">
-      <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> videos?</item>
+      <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> vidéo ?</item>
       <item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> vidéos ?</item>
     </plurals>
     <plurals name="permission_write_image" formatted="false" msgid="748745548893845892">
-      <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> photos?</item>
+      <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> photo ?</item>
       <item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> photos ?</item>
     </plurals>
     <plurals name="permission_write_generic" formatted="false" msgid="3270172714743671779">
-      <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> items?</item>
+      <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> élément ?</item>
       <item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> éléments ?</item>
     </plurals>
     <plurals name="permission_trash_audio" formatted="false" msgid="8907813869381755423">
-      <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to move <xliff:g id="COUNT">^2</xliff:g> audio files to trash?</item>
+      <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à placer <xliff:g id="COUNT">^2</xliff:g> fichier audio dans la corbeille ?</item>
       <item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à placer <xliff:g id="COUNT">^2</xliff:g> fichiers audio dans la corbeille ?</item>
     </plurals>
     <plurals name="permission_trash_video" formatted="false" msgid="4672871911555787438">
-      <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to move <xliff:g id="COUNT">^2</xliff:g> videos to trash?</item>
+      <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à placer <xliff:g id="COUNT">^2</xliff:g> vidéo dans la corbeille ?</item>
       <item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à placer <xliff:g id="COUNT">^2</xliff:g> vidéos dans la corbeille ?</item>
     </plurals>
     <plurals name="permission_trash_image" formatted="false" msgid="6400475304599873227">
-      <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to move <xliff:g id="COUNT">^2</xliff:g> photos to trash?</item>
+      <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à placer <xliff:g id="COUNT">^2</xliff:g> photo dans la corbeille ?</item>
       <item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à placer <xliff:g id="COUNT">^2</xliff:g> photos dans la corbeille ?</item>
     </plurals>
     <plurals name="permission_trash_generic" formatted="false" msgid="3814167365075039711">
-      <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to move <xliff:g id="COUNT">^2</xliff:g> items to trash?</item>
+      <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à placer <xliff:g id="COUNT">^2</xliff:g> élément dans la corbeille ?</item>
       <item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à placer <xliff:g id="COUNT">^2</xliff:g> éléments dans la corbeille ?</item>
     </plurals>
     <plurals name="permission_untrash_audio" formatted="false" msgid="7795265980168966321">
-      <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to move <xliff:g id="COUNT">^2</xliff:g> audio files out of trash?</item>
+      <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à retirer <xliff:g id="COUNT">^2</xliff:g> fichier audio de la corbeille ?</item>
       <item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à retirer <xliff:g id="COUNT">^2</xliff:g> fichiers audio de la corbeille ?</item>
     </plurals>
     <plurals name="permission_untrash_video" formatted="false" msgid="332894888445508879">
-      <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to move <xliff:g id="COUNT">^2</xliff:g> videos out of trash?</item>
+      <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à retirer <xliff:g id="COUNT">^2</xliff:g> vidéo de la corbeille ?</item>
       <item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à retirer <xliff:g id="COUNT">^2</xliff:g> vidéos de la corbeille ?</item>
     </plurals>
     <plurals name="permission_untrash_image" formatted="false" msgid="7024071378733595056">
-      <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to move <xliff:g id="COUNT">^2</xliff:g> photos out of trash?</item>
+      <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à retirer <xliff:g id="COUNT">^2</xliff:g> photo de la corbeille ?</item>
       <item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à retirer <xliff:g id="COUNT">^2</xliff:g> photos de la corbeille ?</item>
     </plurals>
     <plurals name="permission_untrash_generic" formatted="false" msgid="6872817093731198374">
-      <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to move <xliff:g id="COUNT">^2</xliff:g> items out of trash?</item>
+      <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à retirer <xliff:g id="COUNT">^2</xliff:g> élément de la corbeille ?</item>
       <item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à retirer <xliff:g id="COUNT">^2</xliff:g> éléments de la corbeille ?</item>
     </plurals>
     <plurals name="permission_delete_audio" formatted="false" msgid="6848547621165184719">
-      <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to delete <xliff:g id="COUNT">^2</xliff:g> audio files?</item>
+      <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à supprimer <xliff:g id="COUNT">^2</xliff:g> fichier audio ?</item>
       <item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à supprimer <xliff:g id="COUNT">^2</xliff:g> fichiers audio ?</item>
     </plurals>
     <plurals name="permission_delete_video" formatted="false" msgid="1251942606336748563">
-      <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to delete <xliff:g id="COUNT">^2</xliff:g> videos?</item>
+      <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à supprimer <xliff:g id="COUNT">^2</xliff:g> vidéo ?</item>
       <item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à supprimer <xliff:g id="COUNT">^2</xliff:g> vidéos ?</item>
     </plurals>
     <plurals name="permission_delete_image" formatted="false" msgid="2303409455224710111">
-      <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to delete <xliff:g id="COUNT">^2</xliff:g> photos?</item>
+      <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à supprimer <xliff:g id="COUNT">^2</xliff:g> photo ?</item>
       <item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à supprimer <xliff:g id="COUNT">^2</xliff:g> photos ?</item>
     </plurals>
     <plurals name="permission_delete_generic" formatted="false" msgid="1412218850351841181">
-      <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to delete <xliff:g id="COUNT">^2</xliff:g> items?</item>
+      <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à supprimer <xliff:g id="COUNT">^2</xliff:g> élément ?</item>
       <item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à supprimer <xliff:g id="COUNT">^2</xliff:g> éléments ?</item>
     </plurals>
 </resources>
diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml
new file mode 100644
index 0000000..56d8417
--- /dev/null
+++ b/res/values-night/colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <color name="thumb_gray_color">#3c4043</color>
+</resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
new file mode 100644
index 0000000..28f31ee
--- /dev/null
+++ b/res/values/colors.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <color name="thumb_gray_color">#1f000000</color>
+</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 1e866cb..d910577 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -17,12 +17,14 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
     <style name="PickerDialogTheme" parent="@*android:style/Theme.DeviceDefault.Settings.Dialog">
+        <item name="android:windowNoTitle">true</item>
     </style>
 
     <style name="AlertDialogTheme"
-      parent="@*android:style/Theme.DeviceDefault.Light.Dialog.Alert" />
+      parent="@*android:style/Theme.DeviceDefault.Dialog.Alert.DayNight" />
 
-    <style name="CacheClearingAlertDialogTheme">
+    <style name="CacheClearingAlertDialogTheme"
+           parent="@*android:style/Theme.DeviceDefault.Dialog.Alert.DayNight">
         <item name="android:windowIsTranslucent">true</item>
         <item name="android:windowBackground">@android:color/transparent</item>
         <item name="android:windowContentOverlay">@null</item>
@@ -32,4 +34,12 @@
         <item name="android:alertDialogTheme">@style/AlertDialogTheme</item>
     </style>
 
+    <style name="CacheClearingAlertDialogTitle"
+           parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:gravity">center</item>
+        <item name="android:textSize">16sp</item>
+    </style>
+
 </resources>
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index f35ad83..84ed540 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -437,6 +437,12 @@
         public boolean successful;
 
         /**
+         * List of tasks that should be executed in a blocking fashion when this
+         * transaction has been successfully finished.
+         */
+        public final ArrayList<Runnable> blockingTasks = new ArrayList<>();
+
+        /**
          * Map from {@code flags} value to set of {@link Uri} that would have
          * been sent directly via {@link ContentResolver#notifyChange}, but are
          * instead being collected due to this ongoing transaction.
@@ -511,6 +517,10 @@
         mSchemaLock.readLock().unlock();
 
         if (state.successful) {
+            for (int i = 0; i < state.blockingTasks.size(); i++) {
+                state.blockingTasks.get(i).run();
+            }
+
             // We carefully "phase" our two sets of work here to ensure that we
             // completely finish dispatching all change notifications before we
             // process background tasks, to ensure that the background work
@@ -632,9 +642,23 @@
     }
 
     /**
-     * Post given task to be run in background. This enqueues the task if
-     * currently inside a transaction, and they'll be clustered and sent when
-     * the transaction completes.
+     * Post the given task to be run in a blocking fashion after any current
+     * transaction has finished. If there is no active transaction, the task is
+     * immediately executed.
+     */
+    public void postBlocking(@NonNull Runnable command) {
+        final TransactionState state = mTransactionState.get();
+        if (state != null) {
+            state.blockingTasks.add(command);
+        } else {
+            command.run();
+        }
+    }
+
+    /**
+     * Post the given task to be run in background after any current transaction
+     * has finished. If there is no active transaction, the task is immediately
+     * dispatched to run in the background.
      */
     public void postBackground(@NonNull Runnable command) {
         final TransactionState state = mTransactionState.get();
diff --git a/src/com/android/providers/media/LocalCallingIdentity.java b/src/com/android/providers/media/LocalCallingIdentity.java
index 9b13c38..6f554b4 100644
--- a/src/com/android/providers/media/LocalCallingIdentity.java
+++ b/src/com/android/providers/media/LocalCallingIdentity.java
@@ -22,13 +22,14 @@
 import static android.content.pm.PackageManager.PERMISSION_DENIED;
 
 import static com.android.providers.media.util.PermissionUtils.checkIsLegacyStorageGranted;
-import static com.android.providers.media.util.PermissionUtils.checkPermissionBackup;
-import static com.android.providers.media.util.PermissionUtils.checkPermissionManageExternalStorage;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionDelegator;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionManager;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionReadAudio;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionReadImages;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionReadStorage;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionReadVideo;
-import static com.android.providers.media.util.PermissionUtils.checkPermissionSystem;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionSelf;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionShell;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteAudio;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteImages;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteStorage;
@@ -132,7 +133,8 @@
         ident.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT;
         ident.targetSdkVersionResolved = true;
         ident.hasPermission = ~(PERMISSION_IS_LEGACY_GRANTED | PERMISSION_IS_LEGACY_WRITE
-                | PERMISSION_IS_LEGACY_READ | PERMISSION_IS_REDACTION_NEEDED);
+                | PERMISSION_IS_LEGACY_READ | PERMISSION_IS_REDACTION_NEEDED
+                | PERMISSION_IS_SHELL | PERMISSION_IS_DELEGATOR);
         ident.hasPermissionResolved = ~0;
         return ident;
     }
@@ -194,19 +196,22 @@
         return Build.VERSION_CODES.CUR_DEVELOPMENT;
     }
 
-    public static final int PERMISSION_IS_SYSTEM = 1 << 0;
-    public static final int PERMISSION_IS_LEGACY_WRITE = 1 << 1;
-    public static final int PERMISSION_IS_REDACTION_NEEDED = 1 << 2;
-    public static final int PERMISSION_READ_AUDIO = 1 << 3;
-    public static final int PERMISSION_READ_VIDEO = 1 << 4;
-    public static final int PERMISSION_READ_IMAGES = 1 << 5;
-    public static final int PERMISSION_WRITE_AUDIO = 1 << 6;
-    public static final int PERMISSION_WRITE_VIDEO = 1 << 7;
-    public static final int PERMISSION_WRITE_IMAGES = 1 << 8;
-    public static final int PERMISSION_IS_LEGACY_READ = 1 << 9;
-    public static final int PERMISSION_IS_LEGACY_GRANTED = 1 << 10;
-    public static final int PERMISSION_IS_BACKUP = 1 << 11;
-    public static final int PERMISSION_MANAGE_EXTERNAL_STORAGE = 1 << 12;
+    public static final int PERMISSION_IS_SELF = 1 << 0;
+    public static final int PERMISSION_IS_SHELL = 1 << 1;
+    public static final int PERMISSION_IS_MANAGER = 1 << 2;
+    public static final int PERMISSION_IS_DELEGATOR = 1 << 3;
+
+    public static final int PERMISSION_IS_REDACTION_NEEDED = 1 << 8;
+    public static final int PERMISSION_IS_LEGACY_GRANTED = 1 << 9;
+    public static final int PERMISSION_IS_LEGACY_READ = 1 << 10;
+    public static final int PERMISSION_IS_LEGACY_WRITE = 1 << 11;
+
+    public static final int PERMISSION_READ_AUDIO = 1 << 16;
+    public static final int PERMISSION_READ_VIDEO = 1 << 17;
+    public static final int PERMISSION_READ_IMAGES = 1 << 18;
+    public static final int PERMISSION_WRITE_AUDIO = 1 << 19;
+    public static final int PERMISSION_WRITE_VIDEO = 1 << 20;
+    public static final int PERMISSION_WRITE_IMAGES = 1 << 21;
 
     private int hasPermission;
     private int hasPermissionResolved;
@@ -230,22 +235,30 @@
         }
 
         switch (permission) {
-            case PERMISSION_IS_SYSTEM:
-                return isSystemInternal();
-            case PERMISSION_IS_BACKUP:
-                return isBackupInternal();
-            case PERMISSION_IS_LEGACY_GRANTED:
-                return isLegacyStorageGranted();
-            case PERMISSION_IS_LEGACY_WRITE:
-                return isLegacyWriteInternal();
-            case PERMISSION_IS_LEGACY_READ:
-                return isLegacyReadInternal();
+            case PERMISSION_IS_SELF:
+                return checkPermissionSelf(context, pid, uid);
+            case PERMISSION_IS_SHELL:
+                return checkPermissionShell(context, pid, uid);
+            case PERMISSION_IS_MANAGER:
+                return checkPermissionManager(context, pid, uid, getPackageName(), attributionTag);
+            case PERMISSION_IS_DELEGATOR:
+                return checkPermissionDelegator(context, pid, uid);
+
             case PERMISSION_IS_REDACTION_NEEDED:
                 return isRedactionNeededInternal();
+            case PERMISSION_IS_LEGACY_GRANTED:
+                return isLegacyStorageGranted();
+            case PERMISSION_IS_LEGACY_READ:
+                return isLegacyReadInternal();
+            case PERMISSION_IS_LEGACY_WRITE:
+                return isLegacyWriteInternal();
+
             case PERMISSION_READ_AUDIO:
-                return checkPermissionReadAudio(context, pid, uid, getPackageName(), attributionTag);
+                return checkPermissionReadAudio(
+                        context, pid, uid, getPackageName(), attributionTag);
             case PERMISSION_READ_VIDEO:
-                return checkPermissionReadVideo(context, pid, uid, getPackageName(), attributionTag);
+                return checkPermissionReadVideo(
+                        context, pid, uid, getPackageName(), attributionTag);
             case PERMISSION_READ_IMAGES:
                 return checkPermissionReadImages(
                         context, pid, uid, getPackageName(), attributionTag);
@@ -258,22 +271,11 @@
             case PERMISSION_WRITE_IMAGES:
                 return checkPermissionWriteImages(
                         context, pid, uid, getPackageName(), attributionTag);
-            case PERMISSION_MANAGE_EXTERNAL_STORAGE:
-                return checkPermissionManageExternalStorage(
-                        context, pid, uid, getPackageName(), attributionTag);
             default:
                 return false;
         }
     }
 
-    private boolean isSystemInternal() {
-        return checkPermissionSystem(context, pid, uid, getPackageName());
-    }
-
-    private boolean isBackupInternal() {
-        return checkPermissionBackup(context, pid, uid);
-    }
-
     private boolean isLegacyStorageGranted() {
         boolean defaultScopedStorage = CompatChanges.isChangeEnabled(
                 DEFAULT_SCOPED_STORAGE, getPackageName(), UserHandle.getUserHandleForUid(uid));
@@ -314,7 +316,7 @@
 
     /** System internals or callers holding permission have no redaction */
     private boolean isRedactionNeededInternal() {
-        if (hasPermission(PERMISSION_IS_SYSTEM)) {
+        if (hasPermission(PERMISSION_IS_SELF) || hasPermission(PERMISSION_IS_SHELL)) {
             return false;
         }
 
diff --git a/src/com/android/providers/media/MediaDocumentsProvider.java b/src/com/android/providers/media/MediaDocumentsProvider.java
index 38482b3..f26716d 100644
--- a/src/com/android/providers/media/MediaDocumentsProvider.java
+++ b/src/com/android/providers/media/MediaDocumentsProvider.java
@@ -76,6 +76,7 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 
 /**
  * Presents a {@link DocumentsContract} view of {@link MediaProvider} external
@@ -1059,13 +1060,15 @@
     private boolean isEmpty(Uri uri) {
         final ContentResolver resolver = getContext().getContentResolver();
         final long token = Binder.clearCallingIdentity();
-        Cursor cursor = null;
-        try {
-            cursor = resolver.query(uri, new String[] {
-                    BaseColumns._ID }, null, null, null);
-            return (cursor == null) || (cursor.getCount() == 0);
+        try (Cursor cursor = resolver.query(uri,
+                new String[] { "COUNT(_id)" }, null, null, null)) {
+            if (cursor.moveToFirst()) {
+                return cursor.getInt(0) == 0;
+            } else {
+                // No count information means we need to assume empty
+                return true;
+            }
         } finally {
-            FileUtils.closeQuietly(cursor);
             Binder.restoreCallingIdentity(token);
         }
     }
@@ -1172,13 +1175,15 @@
         final String[] PROJECTION = new String[] {
                 ImageColumns.BUCKET_ID,
                 ImageColumns.BUCKET_DISPLAY_NAME,
-                ImageColumns.DATE_MODIFIED };
+                ImageColumns.DATE_MODIFIED,
+                ImageColumns.VOLUME_NAME };
         final String SORT_ORDER = ImageColumns.BUCKET_ID + ", " + ImageColumns.DATE_MODIFIED
                 + " DESC";
 
         final int BUCKET_ID = 0;
         final int BUCKET_DISPLAY_NAME = 1;
         final int DATE_MODIFIED = 2;
+        final int VOLUME_NAME = 3;
     }
 
     private void includeImagesBucket(MatrixCursor result, Cursor cursor) {
@@ -1187,8 +1192,9 @@
 
         final RowBuilder row = result.newRow();
         row.add(Document.COLUMN_DOCUMENT_ID, docId);
-        row.add(Document.COLUMN_DISPLAY_NAME,
-            cleanUpMediaBucketName(cursor.getString(ImagesBucketQuery.BUCKET_DISPLAY_NAME)));
+        row.add(Document.COLUMN_DISPLAY_NAME, cleanUpMediaBucketName(
+                cursor.getString(ImagesBucketQuery.BUCKET_DISPLAY_NAME),
+                cursor.getString(ImagesBucketQuery.VOLUME_NAME)));
         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
         row.add(Document.COLUMN_LAST_MODIFIED,
                 cursor.getLong(ImagesBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
@@ -1232,13 +1238,15 @@
         final String[] PROJECTION = new String[] {
                 VideoColumns.BUCKET_ID,
                 VideoColumns.BUCKET_DISPLAY_NAME,
-                VideoColumns.DATE_MODIFIED };
+                VideoColumns.DATE_MODIFIED,
+                VideoColumns.VOLUME_NAME };
         final String SORT_ORDER = VideoColumns.BUCKET_ID + ", " + VideoColumns.DATE_MODIFIED
                 + " DESC";
 
         final int BUCKET_ID = 0;
         final int BUCKET_DISPLAY_NAME = 1;
         final int DATE_MODIFIED = 2;
+        final int VOLUME_NAME = 3;
     }
 
     private void includeVideosBucket(MatrixCursor result, Cursor cursor) {
@@ -1247,8 +1255,9 @@
 
         final RowBuilder row = result.newRow();
         row.add(Document.COLUMN_DOCUMENT_ID, docId);
-        row.add(Document.COLUMN_DISPLAY_NAME,
-            cleanUpMediaBucketName(cursor.getString(VideosBucketQuery.BUCKET_DISPLAY_NAME)));
+        row.add(Document.COLUMN_DISPLAY_NAME, cleanUpMediaBucketName(
+                cursor.getString(VideosBucketQuery.BUCKET_DISPLAY_NAME),
+                cursor.getString(VideosBucketQuery.VOLUME_NAME)));
         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
         row.add(Document.COLUMN_LAST_MODIFIED,
                 cursor.getLong(VideosBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
@@ -1292,13 +1301,15 @@
         final String[] PROJECTION = new String[] {
                 FileColumns.BUCKET_ID,
                 FileColumns.BUCKET_DISPLAY_NAME,
-                FileColumns.DATE_MODIFIED };
+                FileColumns.DATE_MODIFIED,
+                FileColumns.VOLUME_NAME };
         final String SORT_ORDER = FileColumns.BUCKET_ID + ", " + FileColumns.DATE_MODIFIED
                 + " DESC";
 
         final int BUCKET_ID = 0;
         final int BUCKET_DISPLAY_NAME = 1;
         final int DATE_MODIFIED = 2;
+        final int VOLUME_NAME = 3;
     }
 
     private void includeDocumentsBucket(MatrixCursor result, Cursor cursor) {
@@ -1307,8 +1318,9 @@
 
         final RowBuilder row = result.newRow();
         row.add(Document.COLUMN_DOCUMENT_ID, docId);
-        row.add(Document.COLUMN_DISPLAY_NAME,
-                cleanUpMediaBucketName(cursor.getString(DocumentsBucketQuery.BUCKET_DISPLAY_NAME)));
+        row.add(Document.COLUMN_DISPLAY_NAME, cleanUpMediaBucketName(
+                cursor.getString(DocumentsBucketQuery.BUCKET_DISPLAY_NAME),
+                cursor.getString(DocumentsBucketQuery.VOLUME_NAME)));
         row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
         row.add(Document.COLUMN_LAST_MODIFIED,
                 cursor.getLong(DocumentsBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
@@ -1495,10 +1507,13 @@
         return getContext().getResources().getString(R.string.unknown);
     }
 
-    private String cleanUpMediaBucketName(String bucketDisplayName) {
+    private String cleanUpMediaBucketName(String bucketDisplayName, String volumeName) {
         if (!TextUtils.isEmpty(bucketDisplayName)) {
             return bucketDisplayName;
+        } else if (!Objects.equals(volumeName, MediaStore.VOLUME_EXTERNAL_PRIMARY)) {
+            return volumeName;
+        } else {
+            return getContext().getResources().getString(R.string.unknown);
         }
-        return getContext().getResources().getString(R.string.unknown);
     }
 }
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 6d10191..0af4bb9 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -36,13 +36,14 @@
 
 import static com.android.providers.media.DatabaseHelper.EXTERNAL_DATABASE_NAME;
 import static com.android.providers.media.DatabaseHelper.INTERNAL_DATABASE_NAME;
-import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_BACKUP;
+import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_DELEGATOR;
 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_GRANTED;
 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_READ;
 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE;
+import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_MANAGER;
 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED;
-import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SYSTEM;
-import static com.android.providers.media.LocalCallingIdentity.PERMISSION_MANAGE_EXTERNAL_STORAGE;
+import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SELF;
+import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SHELL;
 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_AUDIO;
 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_IMAGES;
 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_VIDEO;
@@ -66,7 +67,6 @@
 import static com.android.providers.media.util.FileUtils.sanitizePath;
 import static com.android.providers.media.util.Logging.LOGV;
 import static com.android.providers.media.util.Logging.TAG;
-import static com.android.providers.media.util.PermissionUtils.checkPermissionManageExternalStorage;
 
 import android.app.AppOpsManager;
 import android.app.AppOpsManager.OnOpActiveChangedListener;
@@ -126,8 +126,8 @@
 import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.UserHandle;
-import android.os.storage.StorageManager.StorageVolumeCallback;
 import android.os.storage.StorageManager;
+import android.os.storage.StorageManager.StorageVolumeCallback;
 import android.os.storage.StorageVolume;
 import android.preference.PreferenceManager;
 import android.provider.BaseColumns;
@@ -156,6 +156,7 @@
 import android.util.LongSparseArray;
 import android.util.Size;
 import android.util.SparseArray;
+import android.webkit.MimeTypeMap;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.Keep;
@@ -541,14 +542,6 @@
 
     private final void updateQuotaTypeForUri(@NonNull Uri uri, int mediaType) {
         Trace.beginSection("updateQuotaTypeForUri");
-        try {
-            updateQuotaTypeForUriInternal(uri, mediaType);
-        } finally {
-            Trace.endSection();
-        }
-    }
-
-    private final void updateQuotaTypeForUriInternal(@NonNull Uri uri, int mediaType) {
         File file;
         try {
             file = queryForDataFile(uri, null);
@@ -556,10 +549,24 @@
                 // This can happen if an item is inserted in MediaStore before it is created
                 return;
             }
+
+            if (mediaType == FileColumns.MEDIA_TYPE_NONE) {
+                // This might be because the file is hidden; but we still want to
+                // attribute its quota to the correct type, so get the type from
+                // the extension instead.
+                mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(file));
+            }
+
+            updateQuotaTypeForFileInternal(file, mediaType);
         } catch (FileNotFoundException e) {
             // Ignore
             return;
+        } finally {
+            Trace.endSection();
         }
+    }
+
+    private final void updateQuotaTypeForFileInternal(File file, int mediaType) {
         try {
             switch (mediaType) {
                 case FileColumns.MEDIA_TYPE_AUDIO:
@@ -922,6 +929,17 @@
                 null /* all packages */, mModeListener);
         mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO,
                 null /* all packages */, mModeListener);
+        try {
+            // Here we are forced to depend on the non-public API of AppOpsManager. If
+            // OPSTR_NO_ISOLATED_STORAGE app op is not defined in AppOpsManager, then this call will
+            // throw an IllegalArgumentException during MediaProvider startup. In combination with
+            // MediaProvider's CTS tests it should give us guarantees that OPSTR_NO_ISOLATED_STORAGE
+            // is defined.
+            mAppOpsManager.startWatchingMode(PermissionUtils.OPSTR_NO_ISOLATED_STORAGE,
+                    null /* all packages */, mModeListener);
+        } catch (IllegalArgumentException e) {
+            Log.w(TAG, "Failed to start watching " + PermissionUtils.OPSTR_NO_ISOLATED_STORAGE, e);
+        }
         return true;
     }
 
@@ -1113,14 +1131,31 @@
     }
 
     /**
+     * Called when a new file is created through FUSE
+     *
+     * @param file path of the file that was created
+     *
+     * Called from JNI in jni/MediaProviderWrapper.cpp
+     */
+    @Keep
+    public void onFileCreatedForFuse(String path) {
+        // Make sure we update the quota type of the file
+        BackgroundThread.getExecutor().execute(() -> {
+            File file = new File(path);
+            int mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(file));
+            updateQuotaTypeForFileInternal(file, mediaType);
+        });
+    }
+
+    /**
      * Returns true if the app denoted by the given {@code uid} and {@code packageName} is allowed
      * to clear other apps' cache directories.
      */
     static boolean hasPermissionToClearCaches(Context context, ApplicationInfo ai) {
         PermissionUtils.setOpDescription("clear app cache");
         try {
-            return checkPermissionManageExternalStorage(context, /*pid*/ -1, ai.uid, ai.packageName,
-                    /*attributionTag*/ null);
+            return PermissionUtils.checkPermissionManager(context, /* pid */ -1, ai.uid,
+                    ai.packageName, /* attributionTag */ null);
         } finally {
             PermissionUtils.clearOpDescription();
         }
@@ -2222,7 +2257,8 @@
         if (c != null) {
             // As a performance optimization, only configure notifications when
             // resulting cursor will leave our process
-            if (mCallingIdentity.get().pid != android.os.Process.myPid()) {
+            final boolean callerIsRemote = mCallingIdentity.get().pid != android.os.Process.myPid();
+            if (callerIsRemote && !isFuseThread()) {
                 c.setNotificationUri(getContext().getContentResolver(), uri);
             }
 
@@ -2424,13 +2460,55 @@
         if (!TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) {
             FileUtils.computeValuesFromData(values, isFuseThread());
         }
-        // Extract the MIME type from the display name if we couldn't resolve it from the raw path
-        if (!TextUtils.isEmpty(values.getAsString(MediaColumns.DISPLAY_NAME))) {
-            final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME);
 
-            if (TextUtils.isEmpty(values.getAsString(MediaColumns.MIME_TYPE))) {
-                values.put(
-                        MediaColumns.MIME_TYPE, MimeUtils.resolveMimeType(new File(displayName)));
+        final boolean isTargetSdkROrHigher =
+                getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R;
+        final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME);
+        final String mimeTypeFromExt = TextUtils.isEmpty(displayName) ? null :
+                MimeUtils.resolveMimeType(new File(displayName));
+
+        if (TextUtils.isEmpty(values.getAsString(MediaColumns.MIME_TYPE))) {
+            if (isTargetSdkROrHigher) {
+                // Extract the MIME type from the display name if we couldn't resolve it from the
+                // raw path
+                if (mimeTypeFromExt != null) {
+                    values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt);
+                } else {
+                    // We couldn't resolve mimeType, it means that both display name and MIME type
+                    // were missing in values, so we use defaultMimeType.
+                    values.put(MediaColumns.MIME_TYPE, defaultMimeType);
+                }
+            } else if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
+                values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt);
+            } else {
+                // We don't use mimeTypeFromExt to preserve legacy behavior.
+                values.put(MediaColumns.MIME_TYPE, defaultMimeType);
+            }
+        }
+
+        String mimeType = values.getAsString(MediaColumns.MIME_TYPE);
+        if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
+            // We allow any mimeType for generic uri with default media type as MEDIA_TYPE_NONE.
+        } else if (mimeType != null &&
+                MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) == null) {
+            if (mimeTypeFromExt != null &&
+                    defaultMediaType == MimeUtils.resolveMediaType(mimeTypeFromExt)) {
+                // If mimeType from extension matches the defaultMediaType of uri, we use mimeType
+                // from file extension as mimeType. This is an effort to guess the mimeType when we
+                // get unsupported mimeType.
+                // Note: We can't force defaultMimeType because when we force defaultMimeType, we
+                // will force the file extension as well. For example, if DISPLAY_NAME=Foo.png and
+                // mimeType="image/*". If we force mimeType to be "image/jpeg", we append the file
+                // name with the new file extension i.e., "Foo.png.jpg" where as the expected file
+                // name was "Foo.png"
+                values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt);
+            } else if (isTargetSdkROrHigher) {
+                // We are here because given mimeType is unsupported also we couldn't guess valid
+                // mimeType from file extension.
+                throw new IllegalArgumentException("Unsupported MIME type " + mimeType);
+            } else {
+                // We can't throw error for legacy apps, so we try to use defaultMimeType.
+                values.put(MediaColumns.MIME_TYPE, defaultMimeType);
             }
         }
 
@@ -2443,12 +2521,10 @@
         final int format = formatObject == null ? 0 : formatObject.intValue();
         if (format == MtpConstants.FORMAT_ASSOCIATION) {
             values.putNull(MediaColumns.MIME_TYPE);
-        } else if (TextUtils.isEmpty(values.getAsString(MediaColumns.MIME_TYPE))) {
-            values.put(MediaColumns.MIME_TYPE, defaultMimeType);
         }
 
+        mimeType = values.getAsString(MediaColumns.MIME_TYPE);
         // Sanity check MIME type against table
-        final String mimeType = values.getAsString(MediaColumns.MIME_TYPE);
         if (mimeType != null) {
             final int actualMediaType = MimeUtils.resolveMediaType(mimeType);
             if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
@@ -2577,7 +2653,7 @@
 
             // Allow apps with MANAGE_EXTERNAL_STORAGE to create files anywhere
             if (!validPath) {
-                validPath = isCallingPackageExternalStorageManager();
+                validPath = isCallingPackageManager();
             }
 
             // Allow system gallery to create image/video files.
@@ -2604,6 +2680,10 @@
                 throw new IllegalStateException("Failed to create directory: " + res);
             }
             values.put(MediaColumns.DATA, res.getAbsolutePath());
+            // buildFile may have changed the file name, compute values to extract new DISPLAY_NAME.
+            // Note: We can't extract displayName from res.getPath() because for pending & trashed
+            // files DISPLAY_NAME will not be same as file name.
+            FileUtils.computeValuesFromData(values, isFuseThread());
         } else {
             assertFileColumnsSane(match, uri, values);
         }
@@ -2914,10 +2994,12 @@
 
         if (mimeType != null) {
             values.put(FileColumns.MIME_TYPE, mimeType);
-            if (isCallingPackageSystem() && values.containsKey(FileColumns.MEDIA_TYPE)) {
+            if (isCallingPackageSelf() && values.containsKey(FileColumns.MEDIA_TYPE)) {
                 // Leave FileColumns.MEDIA_TYPE untouched if the caller is ModernMediaScanner and
                 // FileColumns.MEDIA_TYPE is already populated.
-            } else{
+            } else if (path != null && shouldFileBeHidden(new File(path))) {
+                values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE);
+            } else {
                 values.put(FileColumns.MEDIA_TYPE, MimeUtils.resolveMediaType(mimeType));
             }
         } else {
@@ -3038,7 +3120,7 @@
         // names of givenOwnerPackage. If givenOwnerPackage is not CallingIdentity, since
         // DownloadProvider can upsert a row on behalf of app, we should include all shared packages
         // of givenOwnerPackage.
-        if (givenOwnerPackage != null && isCallingPackageSystem() &&
+        if (givenOwnerPackage != null && isCallingPackageDelegator() &&
                 !isCallingIdentitySharedPackageName(givenOwnerPackage)) {
             // Allow DownloadProvider to Upsert if givenOwnerPackage is owner of the db row.
             packages.addAll(Arrays.asList(getSharedPackagesForPackage(givenOwnerPackage)));
@@ -3202,7 +3284,7 @@
             for (String column : sDataColumns.keySet()) {
                 if (!initialValues.containsKey(column)) continue;
 
-                if (isCallingPackageSystem() || isCallingPackageLegacyWrite()) {
+                if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) {
                     // Mutation allowed
                 } else {
                     Log.w(TAG, "Ignoring mutation of  " + column + " from "
@@ -3213,7 +3295,7 @@
 
             path = initialValues.getAsString(MediaStore.MediaColumns.DATA);
 
-            if (!isCallingPackageSystem()) {
+            if (!isCallingPackageSelf()) {
                 initialValues.remove(FileColumns.IS_DOWNLOAD);
             }
 
@@ -3225,14 +3307,23 @@
                 initialValues.putNull(ImageColumns.LONGITUDE);
             }
 
-            if (isCallingPackageSystem() || isCallingPackageBackup()) {
-                // When media inserted by ourselves during a scan, or by a
-                // backup app, the best we can do is guess ownership based on
-                // path when it's not explicitly provided
+            if (isCallingPackageSelf() || isCallingPackageShell()) {
+                // When media inserted by ourselves during a scan, or by the
+                // shell, the best we can do is guess ownership based on path
+                // when it's not explicitly provided
                 ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME);
                 if (TextUtils.isEmpty(ownerPackageName)) {
                     ownerPackageName = extractPathOwnerPackageName(path);
                 }
+            } else if (isCallingPackageDelegator()) {
+                // When caller is a delegator, we handle ownership as a hybrid
+                // of the two other cases: we're willing to accept any ownership
+                // transfer attempted during insert, but we fall back to using
+                // the Binder identity if they don't request a specific owner
+                ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME);
+                if (TextUtils.isEmpty(ownerPackageName)) {
+                    ownerPackageName = getCallingPackageOrSelf();
+                }
             } else {
                 // Remote callers have no direct control over owner column; we force
                 // it be whoever is creating the content.
@@ -3327,7 +3418,7 @@
                 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000);
                 // Playlist names are stored as display names, but leave
                 // values untouched if the caller is ModernMediaScanner
-                if (!isCallingPackageSystem()) {
+                if (!isCallingPackageSelf()) {
                     if (values.containsKey(Playlists.NAME)) {
                         values.put(MediaColumns.DISPLAY_NAME, values.getAsString(Playlists.NAME));
                     }
@@ -3545,7 +3636,7 @@
             qb.setDistinct(true);
         }
         qb.setStrict(true);
-        if (isCallingPackageSystem()) {
+        if (isCallingPackageSelf()) {
             // When caller is system, such as the media scanner, we're willing
             // to let them access any columns they want
         } else {
@@ -4294,16 +4385,15 @@
             }
 
             if (deletedDownloadIds.size() > 0) {
-                final long token = Binder.clearCallingIdentity();
-                try {
+                // Do this on a background thread, since we don't want to make binder
+                // calls as part of a FUSE call.
+                helper.postBackground(() -> {
                     getContext().getSystemService(DownloadManager.class)
                             .onMediaStoreDownloadsDeleted(deletedDownloadIds);
-                } finally {
-                    Binder.restoreCallingIdentity(token);
-                }
+                });
             }
 
-            if (isFilesTable && !isCallingPackageSystem()) {
+            if (isFilesTable && !isCallingPackageSelf()) {
                 Metrics.logDeletion(volumeName, mCallingIdentity.get().uid,
                         getCallingPackageOrSelf(), count);
             }
@@ -4522,13 +4612,7 @@
         final ClipData clipData = extras.getParcelable(MediaStore.EXTRA_CLIP_DATA);
         final List<Uri> uris = collectUris(clipData);
 
-        final String volumeName = MediaStore.getVolumeName(uris.get(0));
         for (Uri uri : uris) {
-            // Require that everything is on the same volume
-            if (!Objects.equals(volumeName, MediaStore.getVolumeName(uri))) {
-                throw new IllegalArgumentException("All requested items must be on same volume");
-            }
-
             final int match = matchUri(uri, false);
             switch (match) {
                 case IMAGES_MEDIA_ID:
@@ -4946,7 +5030,7 @@
             for (String column : sDataColumns.keySet()) {
                 if (!initialValues.containsKey(column)) continue;
 
-                if (isCallingPackageSystem() || isCallingPackageLegacyWrite()) {
+                if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) {
                     // Mutation allowed
                 } else {
                     Log.w(TAG, "Ignoring mutation of  " + column + " from "
@@ -4955,12 +5039,44 @@
                 }
             }
 
-            if (!isCallingPackageSystem()) {
-                Trace.beginSection("filter");
+            // Enforce allowed ownership transfers
+            if (initialValues.containsKey(MediaColumns.OWNER_PACKAGE_NAME)) {
+                if (isCallingPackageSelf() || isCallingPackageShell()) {
+                    // When the caller is the media scanner or the shell, we let
+                    // them change ownership however they see fit; nothing to do
+                } else if (isCallingPackageDelegator()) {
+                    // When the caller is a delegator, allow them to shift
+                    // ownership only when current owner, or when ownerless
+                    final String currentOwner;
+                    final String proposedOwner = initialValues
+                            .getAsString(MediaColumns.OWNER_PACKAGE_NAME);
+                    final Uri genericUri = MediaStore.Files.getContentUri(volumeName,
+                            ContentUris.parseId(uri));
+                    try (Cursor c = queryForSingleItem(genericUri,
+                            new String[] { MediaColumns.OWNER_PACKAGE_NAME }, null, null, null)) {
+                        currentOwner = c.getString(0);
+                    } catch (FileNotFoundException e) {
+                        throw new IllegalStateException(e);
+                    }
+                    final boolean transferAllowed = (currentOwner == null)
+                            || Arrays.asList(getSharedPackagesForPackage(getCallingPackageOrSelf()))
+                                    .contains(currentOwner);
+                    if (transferAllowed) {
+                        Log.v(TAG, "Ownership transfer from " + currentOwner + " to "
+                                + proposedOwner + " allowed");
+                    } else {
+                        Log.w(TAG, "Ownership transfer from " + currentOwner + " to "
+                                + proposedOwner + " blocked");
+                        initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME);
+                    }
+                } else {
+                    // Otherwise no ownership changes are allowed
+                    initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME);
+                }
+            }
 
-                // Remote callers have no direct control over owner column; we
-                // force it be whoever is creating the content.
-                initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME);
+            if (!isCallingPackageSelf()) {
+                Trace.beginSection("filter");
 
                 // We default to filtering mutable columns, except when we know
                 // the single item being updated is pending; when it's finally
@@ -5036,7 +5152,7 @@
             case AUDIO_PLAYLISTS_ID:
                 // Playlist names are stored as display names, but leave
                 // values untouched if the caller is ModernMediaScanner
-                if (!isCallingPackageSystem()) {
+                if (!isCallingPackageSelf()) {
                     if (initialValues.containsKey(Playlists.NAME)) {
                         initialValues.put(MediaColumns.DISPLAY_NAME,
                                 initialValues.getAsString(Playlists.NAME));
@@ -5051,7 +5167,7 @@
         // 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());
+                !isCallingPackageSelf());
         if (containsAny(initialValues.keySet(), sPlacementColumns)
                 && !initialValues.containsKey(MediaColumns.DATA)
                 && !isThumbnail
@@ -5073,7 +5189,9 @@
             }
 
             final LocalCallingIdentity token = clearLocalCallingIdentity();
-            try (Cursor c = queryForSingleItem(uri,
+            final Uri genericUri = MediaStore.Files.getContentUri(volumeName,
+                    ContentUris.parseId(uri));
+            try (Cursor c = queryForSingleItem(genericUri,
                     sPlacementColumns.toArray(new String[0]), userWhere, userWhereArgs, null)) {
                 for (int i = 0; i < c.getColumnCount(); i++) {
                     final String column = c.getColumnName(i);
@@ -5121,7 +5239,11 @@
                     invalidateFuseDentry(beforePath);
                     invalidateFuseDentry(afterPath);
                 } catch (ErrnoException e) {
-                    throw new IllegalStateException(e);
+                    if (e.errno == OsConstants.ENOENT) {
+                        Log.d(TAG, "Missing file at " + beforePath + "; continuing anyway");
+                    } else {
+                        throw new IllegalStateException(e);
+                    }
                 }
                 initialValues.put(MediaColumns.DATA, afterPath);
 
@@ -5150,7 +5272,7 @@
 
         // If we're already doing this update from an internal scan, no need to
         // kick off another no-op scan
-        if (isCallingPackageSystem()) {
+        if (isCallingPackageSelf()) {
             triggerScan = false;
         }
 
@@ -5213,7 +5335,15 @@
                     if (triggerScan) {
                         try (Cursor c = queryForSingleItem(updatedUri,
                                 new String[] { FileColumns.DATA }, null, null, null)) {
-                            mMediaScanner.scanFile(new File(c.getString(0)), REASON_DEMAND);
+                            final File file = new File(c.getString(0));
+                            helper.postBlocking(() -> {
+                                final LocalCallingIdentity tokenInner = clearLocalCallingIdentity();
+                                try {
+                                    mMediaScanner.scanFile(file, REASON_DEMAND);
+                                } finally {
+                                    restoreLocalCallingIdentity(tokenInner);
+                                }
+                            });
                         } catch (Exception e) {
                             Log.w(TAG, "Failed to update metadata for " + updatedUri, e);
                         }
@@ -6063,7 +6193,7 @@
             return true;
         }
 
-        if (isCallingPackageExternalStorageManager()) {
+        if (isCallingPackageManager()) {
             return true;
         }
 
@@ -6536,17 +6666,30 @@
             final String mimeType = MimeUtils.resolveMimeType(new File(path));
 
             if (shouldBypassFuseRestrictions(/*forWrite*/ true, path)) {
-                // Ignore insert errors for apps that bypass scoped storage restriction.
+                final boolean callerRequestingLegacy = isCallingPackageRequestingLegacy();
                 if (!fileExists(path)) {
                     // If app has already inserted the db row, inserting the row again might set
                     // IS_PENDING=1. We shouldn't overwrite existing entry as part of FUSE
                     // operation, hence, insert the db row only when it doesn't exist.
                     try {
-                        insertFileForFuse(path, FileUtils.getContentUriForPath(path), mimeType,
-                                /*useData*/ isCallingPackageRequestingLegacy());
+                        insertFileForFuse(path, FileUtils.getContentUriForPath(path),
+                                mimeType, /*useData*/ callerRequestingLegacy);
                     } catch (Exception ignored) {
                     }
+                } else {
+                    // Upon creating a file via FUSE, if a row matching the path already exists
+                    // but a file doesn't exist on the filesystem, we transfer ownership to the
+                    // app attempting to create the file. If we don't update ownership, then the
+                    // app that inserted the original row may be able to observe the contents of
+                    // written file even though they don't hold the right permissions to do so.
+                    if (callerRequestingLegacy) {
+                        final String owner = getCallingPackageOrSelf();
+                        if (owner != null && !updateOwnerForPath(path, owner)) {
+                            return OsConstants.EPERM;
+                        }
+                    }
                 }
+
                 return 0;
             }
 
@@ -6575,6 +6718,23 @@
         }
     }
 
+    private boolean updateOwnerForPath(@NonNull String path, @NonNull String newOwner) {
+        final DatabaseHelper helper;
+        try {
+            helper = getDatabaseForUri(FileUtils.getContentUriForPath(path));
+        } catch (VolumeNotFoundException e) {
+            // Cannot happen, as this is a path that we already resolved.
+            throw new AssertionError("Path must already be resolved", e);
+        }
+
+        ContentValues values = new ContentValues(1);
+        values.put(FileColumns.OWNER_PACKAGE_NAME, newOwner);
+
+        return helper.runWithoutTransaction((db) -> {
+            return db.update("files", values, "_data=?", new String[] { path });
+        }) == 1;
+    }
+
     private static int deleteFileUnchecked(@NonNull String path) {
         final File toDelete = new File(path);
         if (toDelete.delete()) {
@@ -6761,12 +6921,12 @@
 
     private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) {
         // System internals can work with all media
-        if (isCallingPackageSystem()) {
+        if (isCallingPackageSelf() || isCallingPackageShell()) {
             return true;
         }
 
         // Apps that have permission to manage external storage can work with all files
-        if (isCallingPackageExternalStorageManager()) {
+        if (isCallingPackageManager()) {
             return true;
         }
 
@@ -7372,6 +7532,7 @@
         sMutableColumns.add(MediaStore.MediaColumns.IS_PENDING);
         sMutableColumns.add(MediaStore.MediaColumns.IS_TRASHED);
         sMutableColumns.add(MediaStore.MediaColumns.IS_FAVORITE);
+        sMutableColumns.add(MediaStore.MediaColumns.OWNER_PACKAGE_NAME);
 
         sMutableColumns.add(MediaStore.Audio.AudioColumns.BOOKMARK);
 
@@ -7383,6 +7544,9 @@
         sMutableColumns.add(MediaStore.Audio.Playlists.Members.AUDIO_ID);
         sMutableColumns.add(MediaStore.Audio.Playlists.Members.PLAY_ORDER);
 
+        sMutableColumns.add(MediaStore.DownloadColumns.DOWNLOAD_URI);
+        sMutableColumns.add(MediaStore.DownloadColumns.REFERER_URI);
+
         sMutableColumns.add(MediaStore.Files.FileColumns.MIME_TYPE);
         sMutableColumns.add(MediaStore.Files.FileColumns.MEDIA_TYPE);
     }
@@ -7476,28 +7640,34 @@
     }
 
     @Deprecated
-    private int getCallingPackageTargetSdkVersion() {
+    @VisibleForTesting
+    public int getCallingPackageTargetSdkVersion() {
         return mCallingIdentity.get().getTargetSdkVersion();
     }
 
     @Deprecated
     private boolean isCallingPackageAllowedHidden() {
-        return isCallingPackageSystem();
+        return isCallingPackageSelf();
     }
 
     @Deprecated
-    private boolean isCallingPackageSystem() {
-        return mCallingIdentity.get().hasPermission(PERMISSION_IS_SYSTEM);
+    private boolean isCallingPackageSelf() {
+        return mCallingIdentity.get().hasPermission(PERMISSION_IS_SELF);
     }
 
     @Deprecated
-    private boolean isCallingPackageBackup() {
-        return mCallingIdentity.get().hasPermission(PERMISSION_IS_BACKUP);
+    private boolean isCallingPackageShell() {
+        return mCallingIdentity.get().hasPermission(PERMISSION_IS_SHELL);
     }
 
     @Deprecated
-    private boolean isCallingPackageLegacyWrite() {
-        return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_WRITE);
+    private boolean isCallingPackageManager() {
+        return mCallingIdentity.get().hasPermission(PERMISSION_IS_MANAGER);
+    }
+
+    @Deprecated
+    private boolean isCallingPackageDelegator() {
+        return mCallingIdentity.get().hasPermission(PERMISSION_IS_DELEGATOR);
     }
 
     @Deprecated
@@ -7505,11 +7675,11 @@
         return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_READ);
     }
 
-    private boolean isCallingPackageExternalStorageManager() {
-        return mCallingIdentity.get().hasPermission(PERMISSION_MANAGE_EXTERNAL_STORAGE);
+    @Deprecated
+    private boolean isCallingPackageLegacyWrite() {
+        return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_WRITE);
     }
 
-
     @Override
     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
         writer.println("mThumbSize=" + mThumbSize);
diff --git a/src/com/android/providers/media/PermissionActivity.java b/src/com/android/providers/media/PermissionActivity.java
index 3f97464..041d89f 100644
--- a/src/com/android/providers/media/PermissionActivity.java
+++ b/src/com/android/providers/media/PermissionActivity.java
@@ -25,6 +25,7 @@
 
 import android.app.Activity;
 import android.app.AlertDialog;
+import android.app.ProgressDialog;
 import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
 import android.content.ContentValues;
@@ -43,6 +44,7 @@
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
+import android.os.Handler;
 import android.provider.MediaStore;
 import android.provider.MediaStore.MediaColumns;
 import android.text.TextUtils;
@@ -95,8 +97,11 @@
     private String volumeName;
     private ApplicationInfo appInfo;
 
+    private ProgressDialog progressDialog;
     private TextView titleView;
 
+    private static final Long LEAST_SHOW_PROGRESS_TIME_MS = 300L;
+
     private static final String VERB_WRITE = "write";
     private static final String VERB_TRASH = "trash";
     private static final String VERB_UNTRASH = "untrash";
@@ -174,9 +179,17 @@
         titleView = (TextView) findViewByPredicate(dialog.getWindow().getDecorView(), (view) -> {
             return (view instanceof TextView) && view.isImportantForAccessibility();
         });
+
+        progressDialog = new ProgressDialog(this);
     }
 
     private void onPositiveAction(DialogInterface dialog, int which) {
+        // Disable the buttons
+        ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
+        ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(false);
+
+        progressDialog.show();
+        final long startTime = System.currentTimeMillis();
         new AsyncTask<Void, Void, Void>() {
             @Override
             protected Void doInBackground(Void... params) {
@@ -226,7 +239,18 @@
             @Override
             protected void onPostExecute(Void result) {
                 setResult(Activity.RESULT_OK);
-                finish();
+                // Don't dismiss the progress dialog too quick, it will cause bad UX.
+                final long duration = System.currentTimeMillis() - startTime;
+                if (duration > LEAST_SHOW_PROGRESS_TIME_MS) {
+                    progressDialog.dismiss();
+                    finish();
+                } else {
+                    Handler handler = new Handler(getMainLooper());
+                    handler.postDelayed(() -> {
+                        progressDialog.dismiss();
+                        finish();
+                    }, LEAST_SHOW_PROGRESS_TIME_MS - duration);
+                }
             }
         }.execute();
     }
@@ -423,13 +447,40 @@
             final List<Uri> uris = params[0];
             final List<Description> res = new ArrayList<>();
 
+            // If the size is zero, return the res directly.
+            if (uris.isEmpty()) {
+                return res;
+            }
+
             // Default information that we'll load for each item
             int loadFlags = Description.LOAD_THUMBNAIL | Description.LOAD_CONTENT_DESCRIPTION;
             int neededThumbs = MAX_THUMBS;
 
             // If we're only asking for single item, load the full image
             if (uris.size() == 1) {
+                // Set visible to the thumb_full to avoid the size
+                // changed of the dialog in full decoding.
+                final ImageView thumbFull = bodyView.requireViewById(R.id.thumb_full);
+                thumbFull.setVisibility(View.VISIBLE);
                 loadFlags |= Description.LOAD_FULL;
+            } else {
+                // If the size equals 2, we will remove thumb1 later.
+                // Set visible to the thumb2 and thumb3 first to avoid
+                // the size changed of the dialog.
+                ImageView thumb = bodyView.requireViewById(R.id.thumb2);
+                thumb.setVisibility(View.VISIBLE);
+                thumb = bodyView.requireViewById(R.id.thumb3);
+                thumb.setVisibility(View.VISIBLE);
+                // If the count of thumbs equals to MAX_THUMBS, set visible to thumb1.
+                if (uris.size() == MAX_THUMBS) {
+                    thumb = bodyView.requireViewById(R.id.thumb1);
+                    thumb.setVisibility(View.VISIBLE);
+                } else if (uris.size() > MAX_THUMBS) {
+                    // If the count is larger than MAX_THUMBS, set visible to
+                    // thumb_more_container.
+                    final View container = bodyView.requireViewById(R.id.thumb_more_container);
+                    container.setVisibility(View.VISIBLE);
+                }
             }
 
             for (Uri uri : uris) {
diff --git a/src/com/android/providers/media/util/FileUtils.java b/src/com/android/providers/media/util/FileUtils.java
index 60770ff..4515c6f 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -957,13 +957,29 @@
     @VisibleForTesting
     public static @Nullable String extractRelativePathForDirectory(@Nullable String directoryPath) {
         if (directoryPath == null) return null;
+
+        if (directoryPath.equals("/storage/emulated") ||
+                directoryPath.equals("/storage/emulated/")) {
+            // This path is not reachable for MediaProvider.
+            return null;
+        }
+
+        // We are extracting relative path for the directory itself, we add "/" so that we can use
+        // same PATTERN_RELATIVE_PATH to match relative path for directory. For example, relative
+        // path of '/storage/<volume_name>' is null where as relative path for directory is "/", for
+        // PATTERN_RELATIVE_PATH to match '/storage/<volume_name>', it should end with "/".
+        if (!directoryPath.endsWith("/")) {
+            // Relative path for directory should end with "/".
+            directoryPath += "/";
+        }
+
         final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(directoryPath);
         if (matcher.find()) {
-            if (matcher.end() == directoryPath.length() - 1) {
+            if (matcher.end() == directoryPath.length()) {
                 // This is the top-level directory, so relative path is "/"
                 return "/";
             }
-            return directoryPath.substring(matcher.end()) + "/";
+            return directoryPath.substring(matcher.end());
         }
         return null;
     }
diff --git a/src/com/android/providers/media/util/IsoInterface.java b/src/com/android/providers/media/util/IsoInterface.java
index d91a1a5..5de4b6b 100644
--- a/src/com/android/providers/media/util/IsoInterface.java
+++ b/src/com/android/providers/media/util/IsoInterface.java
@@ -49,6 +49,7 @@
     private static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE);
 
     public static final int BOX_FTYP = 0x66747970;
+    public static final int BOX_HDLR = 0x68646c72;
     public static final int BOX_UUID = 0x75756964;
     public static final int BOX_META = 0x6d657461;
     public static final int BOX_XMP = 0x584d505f;
@@ -99,6 +100,7 @@
         public UUID uuid;
         public byte[] data;
         public List<Box> children;
+        public int headerSize;
 
         public Box(int type, long[] range) {
             this.type = type;
@@ -133,44 +135,75 @@
     private static @Nullable Box parseNextBox(@NonNull FileDescriptor fd, long end,
             @NonNull String prefix) throws ErrnoException, IOException {
         final long pos = Os.lseek(fd, 0, OsConstants.SEEK_CUR);
-        if (pos == end) {
+
+        int headerSize = 8;
+        if (end - pos < headerSize) {
             return null;
         }
 
-        final long len = Integer.toUnsignedLong(readInt(fd));
-        if (len <= 0 || pos + len > end) {
-            Log.w(TAG, "Invalid box at " + pos + " of length " + len
-                    + " reached beyond end of parent " + end);
-            return null;
-        }
-
-        // Skip past legacy data on 'meta' box
+        long len = Integer.toUnsignedLong(readInt(fd));
         final int type = readInt(fd);
-        if (type == BOX_META) {
-            readInt(fd);
+
+        if (len == 0) {
+            // Length 0 means the box extends to the end of the file.
+            len = end - pos;
+        } else if (len == 1) {
+            // Actually 64-bit box length.
+            headerSize += 8;
+            long high = readInt(fd);
+            long low = readInt(fd);
+            len = (high << 32L) | (low & 0xffffffffL);
+        }
+
+        if (len < headerSize || pos + len > end) {
+            Log.w(TAG, "Invalid box at " + pos + " of length " + len
+                    + ". End of parent " + end);
+            return null;
         }
 
         final Box box = new Box(type, new long[] { pos, len });
-        if (LOGV) {
-            Log.v(TAG, prefix + "Found box " + typeToString(type)
-                    + " at " + pos + " length " + len);
-        }
+        box.headerSize = headerSize;
 
         // Parse UUID box
         if (type == BOX_UUID) {
+            box.headerSize += 16;
             box.uuid = readUuid(fd);
             if (LOGV) {
                 Log.v(TAG, prefix + "  UUID " + box.uuid);
             }
 
-            box.data = new byte[(int) (len - 8 - 16)];
+            if (len > Integer.MAX_VALUE) {
+                Log.w(TAG, "Skipping abnormally large uuid box");
+                return null;
+            }
+
+            box.data = new byte[(int) (len - box.headerSize)];
             Os.read(fd, box.data, 0, box.data.length);
+        } else if (type == BOX_XMP) {
+            if (len > Integer.MAX_VALUE) {
+                Log.w(TAG, "Skipping abnormally large xmp box");
+                return null;
+            }
+            box.data = new byte[(int) (len - box.headerSize)];
+            Os.read(fd, box.data, 0, box.data.length);
+        } else if (type == BOX_META && len != headerSize) {
+            // The format of this differs in ISO and QT encoding:
+            // (iso) [1 byte version + 3 bytes flags][4 byte size of next atom]
+            // (qt)  [4 byte size of next atom      ][4 byte hdlr atom type   ]
+            // In case of (iso) we need to skip the next 4 bytes before parsing
+            // the children.
+            readInt(fd);
+            int maybeBoxType = readInt(fd);
+            if (maybeBoxType != BOX_HDLR) {
+                // ISO, skip 4 bytes.
+                box.headerSize += 4;
+            }
+            Os.lseek(fd, pos + box.headerSize, OsConstants.SEEK_SET);
         }
 
-        // Parse XMP box
-        if (type == BOX_XMP) {
-            box.data = new byte[(int) (len - 8)];
-            Os.read(fd, box.data, 0, box.data.length);
+        if (LOGV) {
+            Log.v(TAG, prefix + "Found box " + typeToString(type)
+                    + " at " + pos + " hdr " + box.headerSize + " length " + len);
         }
 
         // Recursively parse any children boxes
@@ -248,7 +281,7 @@
         LongArray res = new LongArray();
         for (Box box : mFlattened) {
             if (box.type == type) {
-                res.add(box.range[0] + 8);
+                res.add(box.range[0] + box.headerSize);
                 res.add(box.range[0] + box.range[1]);
             }
         }
@@ -259,7 +292,7 @@
         LongArray res = new LongArray();
         for (Box box : mFlattened) {
             if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) {
-                res.add(box.range[0] + 8 + 16);
+                res.add(box.range[0] + box.headerSize);
                 res.add(box.range[0] + box.range[1]);
             }
         }
diff --git a/src/com/android/providers/media/util/MimeUtils.java b/src/com/android/providers/media/util/MimeUtils.java
index a932fd9..09417bd 100644
--- a/src/com/android/providers/media/util/MimeUtils.java
+++ b/src/com/android/providers/media/util/MimeUtils.java
@@ -43,7 +43,8 @@
      */
     public static boolean startsWithIgnoreCase(@Nullable String target, @Nullable String other) {
         if (target == null || other == null) return false;
-        return target.regionMatches(true, 0, other, 0, Math.min(target.length(), other.length()));
+        if (other.length() > target.length()) return false;
+        return target.regionMatches(true, 0, other, 0, other.length());
     }
 
     /**
diff --git a/src/com/android/providers/media/util/PermissionUtils.java b/src/com/android/providers/media/util/PermissionUtils.java
index 181dbb3..fc10b78 100644
--- a/src/com/android/providers/media/util/PermissionUtils.java
+++ b/src/com/android/providers/media/util/PermissionUtils.java
@@ -19,6 +19,7 @@
 import static android.Manifest.permission.BACKUP;
 import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
 import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
+import static android.Manifest.permission.UPDATE_DEVICE_STATS;
 import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
 import static android.app.AppOpsManager.MODE_ALLOWED;
 import static android.app.AppOpsManager.OPSTR_LEGACY_STORAGE;
@@ -31,21 +32,22 @@
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 
 import android.app.AppOpsManager;
+import android.app.DownloadManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
-import android.content.pm.ProviderInfo;
-import android.provider.MediaStore;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 public class PermissionUtils {
+
+    public static final String OPSTR_NO_ISOLATED_STORAGE = "android:no_isolated_storage";
+
     // 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;
-
     private static ThreadLocal<String> sOpDescription = new ThreadLocal<>();
 
     public static void setOpDescription(@Nullable String description) {
@@ -54,24 +56,50 @@
 
     public static void clearOpDescription() { sOpDescription.set(null); }
 
-    public static boolean checkPermissionSystem(
-            @NonNull 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
-                || isLegacyMediaProvider(context, uid);
+    public static boolean checkPermissionSelf(@NonNull Context context, int pid, int uid) {
+        return android.os.Process.myUid() == uid;
     }
 
-    public static boolean checkPermissionBackup(@NonNull Context context, int pid, int uid) {
-        return context.checkPermission(BACKUP, pid, uid) == PERMISSION_GRANTED;
+    public static boolean checkPermissionShell(@NonNull Context context, int pid, int uid) {
+        switch (uid) {
+            case android.os.Process.ROOT_UID:
+            case android.os.Process.SHELL_UID:
+                return true;
+            default:
+                return false;
+        }
     }
 
-    public static boolean checkPermissionManageExternalStorage(@NonNull Context context, int pid,
+    /**
+     * Check if the given package has been granted the "file manager" role on
+     * the device, which should grant them certain broader access.
+     */
+    public static boolean checkPermissionManager(@NonNull Context context, int pid,
             int uid, @NonNull String packageName, @Nullable String attributionTag) {
-        return checkPermissionForDataDelivery(context, MANAGE_EXTERNAL_STORAGE, pid, uid,
+        if (checkPermissionForDataDelivery(context, MANAGE_EXTERNAL_STORAGE, pid, uid,
                 packageName, attributionTag,
-                generateAppOpMessage(packageName,sOpDescription.get()));
+                generateAppOpMessage(packageName,sOpDescription.get()))) {
+            return true;
+        }
+        // Fallback to OPSTR_NO_ISOLATED_STORAGE app op.
+        return checkNoIsolatedStorageGranted(context, uid, packageName, attributionTag);
+    }
+
+    /**
+     * Check if the given package has the ability to "delegate" the ownership of
+     * media items that they own to other apps, typically when they've finished
+     * performing operations on behalf of those apps.
+     * <p>
+     * One use-case for this is backup/restore apps, where the app restoring the
+     * content needs to shift the ownership back to the app that originally
+     * owned that media.
+     * <p>
+     * Another use-case is {@link DownloadManager}, which shifts ownership of
+     * finished downloads to the app that originally requested them.
+     */
+    public static boolean checkPermissionDelegator(@NonNull Context context, int pid, int uid) {
+        return (context.checkPermission(BACKUP, pid, uid) == PERMISSION_GRANTED)
+                || (context.checkPermission(UPDATE_DEVICE_STATS, pid, uid) == PERMISSION_GRANTED);
     }
 
     public static boolean checkPermissionWriteStorage(@NonNull Context context, int pid, int uid,
@@ -157,6 +185,15 @@
                 generateAppOpMessage(packageName, sOpDescription.get()));
     }
 
+    @VisibleForTesting
+    static boolean checkNoIsolatedStorageGranted(@NonNull Context context, int uid,
+            @NonNull String packageName, @Nullable String attributionTag) {
+        final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
+        int ret = appOps.noteOpNoThrow(OPSTR_NO_ISOLATED_STORAGE, uid, packageName, attributionTag,
+                generateAppOpMessage(packageName, "am instrument --no-isolated-storage"));
+        return ret == AppOpsManager.MODE_ALLOWED;
+    }
+
     /**
      * Generates a message to be used with the different {@link AppOpsManager#noteOp} variations.
      * If the supplied description is {@code null}, the returned message will be {@code null}.
@@ -210,21 +247,6 @@
         }
     }
 
-    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.
-            ProviderInfo pi = context.getPackageManager()
-                    .resolveContentProvider(MediaStore.AUTHORITY_LEGACY, 0);
-            if (pi == null) {
-                return false;
-            }
-
-            sLegacyMediaProviderUid = pi.applicationInfo.uid;
-        }
-        return (uid == sLegacyMediaProviderUid);
-    }
-
     /**
      * Checks whether a given package in a UID and PID has a given permission
      * and whether the app op that corresponds to this permission is allowed.
diff --git a/src/com/android/providers/media/util/SQLiteQueryBuilder.java b/src/com/android/providers/media/util/SQLiteQueryBuilder.java
index 27c0a25..e95d28c 100644
--- a/src/com/android/providers/media/util/SQLiteQueryBuilder.java
+++ b/src/com/android/providers/media/util/SQLiteQueryBuilder.java
@@ -76,6 +76,9 @@
     private static final Pattern sPattern156832140 = Pattern.compile(
             "(?i)%com\\.gopro\\.smarty%");
 
+    private static final Pattern sCustomCollatorPattern = Pattern.compile(
+            "(?i)custom_[a-zA-Z]+");
+
     private Map<String, String> mProjectionMap = null;
     private Collection<Pattern> mProjectionGreylist = null;
 
@@ -786,6 +789,7 @@
     private void enforceStrictToken(@NonNull String token) {
         if (TextUtils.isEmpty(token)) return;
         if (isTableOrColumn(token)) return;
+        if (isCustomCollator(token)) return;
         if (SQLiteTokenizer.isFunction(token)) return;
         if (SQLiteTokenizer.isType(token)) return;
 
@@ -1078,6 +1082,10 @@
         return computeSingleProjection(token) != null;
     }
 
+    private boolean isCustomCollator(String token) {
+        return sCustomCollatorPattern.matcher(token).matches();
+    }
+
     /** {@hide} */
     public @Nullable String computeWhere(@Nullable String selection) {
         final boolean hasInternal = !TextUtils.isEmpty(mWhereClause);
diff --git a/tests/client/Android.bp b/tests/client/Android.bp
index 0d29bbf..26a0a0b 100644
--- a/tests/client/Android.bp
+++ b/tests/client/Android.bp
@@ -20,6 +20,7 @@
 
     static_libs: [
         "androidx.test.rules",
+        "collector-device-lib-platform",
         "mockito-target",
         "truth-prebuilt",
     ],
diff --git a/tests/client/src/com/android/providers/media/client/DelegatorTest.java b/tests/client/src/com/android/providers/media/client/DelegatorTest.java
new file mode 100644
index 0000000..042223c
--- /dev/null
+++ b/tests/client/src/com/android/providers/media/client/DelegatorTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.client;
+
+import static com.android.providers.media.client.LegacyProviderMigrationTest.executeShellCommand;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.provider.MediaStore.MediaColumns;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Verify media ownership delegation behaviors for an app holding
+ * {@code UPDATE_DEVICE_STATS} permission.
+ */
+@RunWith(AndroidJUnit4.class)
+public class DelegatorTest {
+    private static final String TAG = "DelegatorTest";
+
+    /**
+     * To confirm behaviors, we need to pick an app installed on all devices
+     * which has no permissions, and the best candidate is the "Easter Egg" app.
+     */
+    private static final String PERMISSIONLESS_APP = "com.android.egg";
+
+    private ContentResolver mResolver;
+
+    private Uri mExternalAudio = MediaStore.Audio.Media
+            .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+
+    @Before
+    public void setUp() throws Exception {
+        mResolver = InstrumentationRegistry.getTargetContext().getContentResolver();
+    }
+
+    /**
+     * Delegation allows us to push ownership of items we own to someone else.
+     */
+    @Test
+    public void testPushAllowed() throws Exception {
+        final Uri uri = createAudio();
+
+        assertEquals(InstrumentationRegistry.getTargetContext().getPackageName(), getOwner(uri));
+
+        final ContentValues values = new ContentValues();
+        values.put(MediaColumns.OWNER_PACKAGE_NAME, PERMISSIONLESS_APP);
+        mResolver.update(uri, values, null);
+
+        assertEquals(PERMISSIONLESS_APP, getOwner(uri));
+    }
+
+    /**
+     * Delegation allows us to push orphaned items to someone else.
+     */
+    @Test
+    public void testOrphanedAllowed() throws Exception {
+        final Uri uri = createAudio();
+        clearOwner(uri);
+
+        assertEquals(null, getOwner(uri));
+
+        final ContentValues values = new ContentValues();
+        values.put(MediaColumns.OWNER_PACKAGE_NAME, PERMISSIONLESS_APP);
+        mResolver.update(uri, values, null);
+
+        assertEquals(PERMISSIONLESS_APP, getOwner(uri));
+    }
+
+    /**
+     * However, attempting to steal items belonging to someone else is blocked.
+     */
+    @Test
+    public void testPullBlocked() throws Exception {
+        final Uri uri = createAudio();
+        setOwner(uri, PERMISSIONLESS_APP);
+
+        assertEquals(PERMISSIONLESS_APP, getOwner(uri));
+
+        final ContentValues values = new ContentValues();
+        values.put(MediaColumns.OWNER_PACKAGE_NAME,
+                InstrumentationRegistry.getTargetContext().getPackageName());
+        mResolver.update(uri, values, null);
+
+        assertEquals(PERMISSIONLESS_APP, getOwner(uri));
+    }
+
+    private Uri createAudio() throws IOException {
+        final ContentValues values = new ContentValues();
+        values.put(MediaColumns.DISPLAY_NAME, "Song " + System.nanoTime());
+        values.put(MediaColumns.MIME_TYPE, "audio/mpeg");
+
+        final Uri uri = mResolver.insert(mExternalAudio, values);
+        try (OutputStream out = mResolver.openOutputStream(uri)) {
+        }
+        return uri;
+    }
+
+    private String getOwner(Uri uri) throws Exception {
+        try (Cursor cursor = mResolver.query(uri,
+                new String[] { MediaColumns.OWNER_PACKAGE_NAME }, null, null)) {
+            assertTrue(cursor.moveToFirst());
+            return cursor.getString(0);
+        }
+    }
+
+    public static void setOwner(Uri uri, String packageName) throws Exception {
+        executeShellCommand("content update"
+                + " --user " + InstrumentationRegistry.getTargetContext().getUserId()
+                + " --uri " + uri
+                + " --bind owner_package_name:s:" + packageName,
+                InstrumentationRegistry.getInstrumentation().getUiAutomation());
+    }
+
+    public static void clearOwner(Uri uri) throws Exception {
+        executeShellCommand("content update"
+                + " --user " + InstrumentationRegistry.getTargetContext().getUserId()
+                + " --uri " + uri
+                + " --bind owner_package_name:n:",
+                InstrumentationRegistry.getInstrumentation().getUiAutomation());
+    }
+}
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 cb642b2..0c1fc65 100644
--- a/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
+++ b/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
@@ -65,6 +65,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.InterruptedIOException;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.concurrent.TimeUnit;
@@ -359,6 +360,21 @@
 
     public static String executeShellCommand(String command, UiAutomation uiAutomation)
             throws IOException {
+        int attempt = 0;
+        while (attempt++ < 5) {
+            try {
+                return executeShellCommandInternal(command, uiAutomation);
+            } catch (InterruptedIOException e) {
+                // Hmm, we had trouble executing the shell command; the best we
+                // can do is try again a few more times
+                Log.v(TAG, "Trouble executing " + command + "; trying again", e);
+            }
+        }
+        throw new IOException("Failed to execute " + command);
+    }
+
+    public static String executeShellCommandInternal(String command, UiAutomation uiAutomation)
+            throws IOException {
         Log.v(TAG, "$ " + command);
         ParcelFileDescriptor pfd = uiAutomation.executeShellCommand(command.toString());
         BufferedReader br = null;
diff --git a/tests/client/src/com/android/providers/media/client/PerformanceTest.java b/tests/client/src/com/android/providers/media/client/PerformanceTest.java
index 9d89465..1e1abe7 100644
--- a/tests/client/src/com/android/providers/media/client/PerformanceTest.java
+++ b/tests/client/src/com/android/providers/media/client/PerformanceTest.java
@@ -16,6 +16,7 @@
 
 package com.android.providers.media.client;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 import android.content.ContentProviderOperation;
@@ -25,6 +26,7 @@
 import android.database.ContentObserver;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Environment;
 import android.os.SystemClock;
 import android.provider.MediaStore;
 import android.provider.MediaStore.MediaColumns;
@@ -36,9 +38,12 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.File;
+import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -232,6 +237,62 @@
         MediaStore.waitForIdle(resolver);
     }
 
+    @Test
+    public void testDirOperations_10() throws Exception {
+        Timer createTimer = new Timer("mkdir");
+        Timer readTimer = new Timer("readdir");
+        Timer deleteTimer = new Timer("rmdir");
+        for (int i = 0; i < COUNT_REPEAT; i++ ){
+            doDirOperations(10, createTimer, readTimer, deleteTimer);
+        }
+        createTimer.dumpResults();
+        readTimer.dumpResults();
+        deleteTimer.dumpResults();
+    }
+
+    @Test
+    public void testDirOperations_100() throws Exception {
+        Timer createTimer = new Timer("mkdir");
+        Timer readTimer = new Timer("readdir");
+        Timer deleteTimer = new Timer("rmdir");
+        for (int i = 0; i < COUNT_REPEAT; i++ ){
+            doDirOperations(100, createTimer, readTimer, deleteTimer);
+        }
+        createTimer.dumpResults();
+        readTimer.dumpResults();
+        deleteTimer.dumpResults();
+    }
+
+    private void doDirOperations(int size, Timer createTimer, Timer readTimer, Timer deleteTimer)
+            throws Exception {
+        createTimer.start();
+        File testDir = new File(new File(Environment.getExternalStorageDirectory(),
+                "Download"), "test_dir_" + System.nanoTime());
+        testDir.mkdir();
+        List<File> files = new ArrayList<>();
+        for (int i = 0; i < size; i++) {
+            File file = new File(testDir, "file_" + System.nanoTime());
+            assertTrue(file.createNewFile());
+            files.add(file);
+        }
+        createTimer.stop();
+
+        try {
+            readTimer.start();
+            File[] result = testDir.listFiles();
+            readTimer.stop();
+            assertEquals(size, result.length);
+
+        } finally {
+            deleteTimer.start();
+            for (File file : files) {
+                assertTrue(file.delete());
+            }
+            assertTrue(testDir.delete());
+            deleteTimer.stop();
+        }
+    }
+
     private static Set<Uri> asSet(Collection<Uri> uris) {
         return new HashSet<>(uris);
     }
diff --git a/tests/res/raw/testvideo_meta.mp4 b/tests/res/raw/testvideo_meta.mp4
new file mode 100644
index 0000000..e83c61d
--- /dev/null
+++ b/tests/res/raw/testvideo_meta.mp4
Binary files differ
diff --git a/tests/src/com/android/providers/media/LocalCallingIdentityTest.java b/tests/src/com/android/providers/media/LocalCallingIdentityTest.java
index 64411d3..e30ed92 100644
--- a/tests/src/com/android/providers/media/LocalCallingIdentityTest.java
+++ b/tests/src/com/android/providers/media/LocalCallingIdentityTest.java
@@ -67,19 +67,22 @@
         assertEquals(Arrays.asList(pm.getPackagesForUid(android.os.Process.myUid())),
                 Arrays.asList(ident.getSharedPackageNames()));
 
-        assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_SYSTEM));
-        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE));
+        assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_SELF));
+        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_SHELL));
+        assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_MANAGER));
+        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_DELEGATOR));
+
         assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED));
+        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_GRANTED));
+        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_READ));
+        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE));
+
         assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_READ_AUDIO));
         assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_READ_VIDEO));
         assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_READ_IMAGES));
         assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_WRITE_AUDIO));
         assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_WRITE_VIDEO));
         assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_WRITE_IMAGES));
-        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_READ));
-        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_GRANTED));
-        assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_BACKUP));
-        assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_MANAGE_EXTERNAL_STORAGE));
     }
 
     @Test
@@ -95,18 +98,21 @@
         assertEquals(Arrays.asList(MediaProviderTest.PERMISSIONLESS_APP),
                 Arrays.asList(ident.getSharedPackageNames()));
 
-        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_SYSTEM));
-        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE));
+        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_SELF));
+        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_SHELL));
+        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_MANAGER));
+        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_DELEGATOR));
+
         assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED));
+        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_GRANTED));
+        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_READ));
+        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE));
+
         assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_READ_AUDIO));
         assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_READ_VIDEO));
         assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_READ_IMAGES));
         assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_WRITE_AUDIO));
         assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_WRITE_VIDEO));
         assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_WRITE_IMAGES));
-        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_READ));
-        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_GRANTED));
-        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_BACKUP));
-        assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_MANAGE_EXTERNAL_STORAGE));
     }
 }
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index 56a9424..7e898f2 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -17,9 +17,12 @@
 package com.android.providers.media;
 
 import static com.android.providers.media.scan.MediaScannerTest.stage;
+import static com.android.providers.media.util.FileUtils.extractRelativePathForDirectory;
 import static com.android.providers.media.util.FileUtils.isDownload;
 import static com.android.providers.media.util.FileUtils.isDownloadDir;
 
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -503,6 +506,73 @@
     }
 
     @Test
+    public void testBuildData_InvalidSecondaryTypes() throws Exception {
+        assertEndsWith("/Pictures/foo.png",
+                buildFile(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
+                        null, "foo.png", "image/*"));
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            buildFile(MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
+                    null, "foo", "video/*");
+        });
+        assertThrows(IllegalArgumentException.class, () -> {
+            buildFile(MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
+                    null, "foo.mp4", "audio/*");
+        });
+    }
+
+    @Test
+    public void testBuildData_EmptyTypes() throws Exception {
+        Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+        assertEndsWith("/Pictures/foo.png",
+                buildFile(uri, null, "foo.png", ""));
+
+        uri = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+        assertEndsWith(".mp4",
+                buildFile(uri, null, "", ""));
+    }
+
+    @Test
+    public void testEnsureFileColumns_InvalidMimeType_targetSdkQ() throws Exception {
+        final MediaProvider provider = new MediaProvider() {
+            @Override
+            public boolean isFuseThread() {
+                return false;
+            }
+
+            @Override
+            public int getCallingPackageTargetSdkVersion() {
+                return Build.VERSION_CODES.Q;
+            }
+        };
+
+        final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+        final ContentValues values = new ContentValues();
+
+        values.put(MediaColumns.DISPLAY_NAME, "pngimage.png");
+        provider.ensureFileColumns(uri, values);
+        assertMimetype(values, "image/jpeg");
+        assertDisplayName(values, "pngimage.png.jpg");
+
+        values.clear();
+        values.put(MediaColumns.DISPLAY_NAME, "pngimage.png");
+        values.put(MediaColumns.MIME_TYPE, "");
+        provider.ensureFileColumns(uri, values);
+        assertMimetype(values, "image/jpeg");
+        assertDisplayName(values, "pngimage.png.jpg");
+
+        values.clear();
+        values.put(MediaColumns.MIME_TYPE, "");
+        provider.ensureFileColumns(uri, values);
+        assertMimetype(values, "image/jpeg");
+
+        values.clear();
+        values.put(MediaColumns.DISPLAY_NAME, "foo.foo");
+        provider.ensureFileColumns(uri, values);
+        assertMimetype(values, "image/jpeg");
+        assertDisplayName(values, "foo.foo.jpg");
+    }
+
     @Ignore("Enable as part of b/142561358")
     public void testBuildData_Charset() throws Exception {
         final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
@@ -885,6 +955,11 @@
             public boolean isFuseThread() {
                 return false;
             }
+
+            @Override
+            public int getCallingPackageTargetSdkVersion() {
+                return Build.VERSION_CODES.CUR_DEVELOPMENT;
+            }
         };
         provider.ensureFileColumns(uri, values);
 
@@ -893,29 +968,39 @@
 
     @Test
     public void testRelativePathForInvalidDirectories() throws Exception {
-        for (String data : new String[] {
-            "/storage/IMG1024.JPG",
-            "/data/media/IMG1024.JPG",
-            "IMG1024.JPG",
-            "storage/emulated/",
+        for (String path : new String[] {
+                "/storage/emulated",
+                "/storage",
+                "/data/media/Foo.jpg",
+                "Foo.jpg",
+                "storage/Foo"
         }) {
-            assertEquals(FileUtils.extractRelativePathForDirectory(data), null);
+            assertEquals(null, FileUtils.extractRelativePathForDirectory(path));
         }
     }
 
     @Test
     public void testRelativePathForValidDirectories() throws Exception {
-        for (Pair<String, String> top : Arrays.asList(
-                Pair.create("/storage/emulated/0", new String("/")),
-                Pair.create("/storage/emulated/0/DCIM", "DCIM/"),
-                Pair.create("/storage/emulated/0/DCIM/Camera", "DCIM/Camera/"),
-                Pair.create("/storage/emulated/0/Android/media/com.example/Foo",
-                        "Android/media/com.example/Foo/"),
-                Pair.create("/storage/0000-0000/DCIM/Camera", "DCIM/Camera/"))) {
-            assertEquals(top.second, FileUtils.extractRelativePathForDirectory(top.first));
+        for (String prefix : new String[] {
+                "/storage/emulated/0",
+                "/storage/emulated/10",
+                "/storage/ABCD-1234"
+        }) {
+            assertRelativePathForDirectory(prefix, "/");
+            assertRelativePathForDirectory(prefix + "/DCIM", "DCIM/");
+            assertRelativePathForDirectory(prefix + "/DCIM/Camera", "DCIM/Camera/");
+            assertRelativePathForDirectory(prefix + "/Z", "Z/");
+            assertRelativePathForDirectory(prefix + "/Android/media/com.example/Foo",
+                    "Android/media/com.example/Foo/");
         }
     }
 
+    private static void assertRelativePathForDirectory(String directoryPath, String relativePath) {
+        assertWithMessage("extractRelativePathForDirectory(" + directoryPath + ") :")
+                .that(extractRelativePathForDirectory(directoryPath))
+                .isEqualTo(relativePath);
+    }
+
     private static ContentValues computeDataValues(String path) {
         final ContentValues values = new ContentValues();
         values.put(MediaColumns.DATA, path);
@@ -948,6 +1033,10 @@
         assertEquals(type, values.get(MediaColumns.MIME_TYPE));
     }
 
+    private static void assertDisplayName(ContentValues values, String type) {
+        assertEquals(type, values.get(MediaColumns.DISPLAY_NAME));
+    }
+
     private static boolean isGreylistMatch(String raw) {
         for (Pattern p : MediaProvider.sGreylist) {
             if (p.matcher(raw).matches()) {
diff --git a/tests/src/com/android/providers/media/util/IsoInterfaceTest.java b/tests/src/com/android/providers/media/util/IsoInterfaceTest.java
index 227a8fd..7783957 100644
--- a/tests/src/com/android/providers/media/util/IsoInterfaceTest.java
+++ b/tests/src/com/android/providers/media/util/IsoInterfaceTest.java
@@ -84,6 +84,28 @@
         assertEquals("3F9DD7A46B26513A7C35272F0D623A06", xmp.getOriginalDocumentId());
     }
 
+    @Test
+    public void testIsoMeta() throws Exception {
+        final IsoInterface isoMeta = IsoInterface.fromFile(stageFile(R.raw.test_video_xmp));
+        final long[] hdlrRanges = isoMeta.getBoxRanges(IsoInterface.BOX_HDLR);
+
+        // There are 3 hdlr boxes, the 3rd is inside the meta box. Check it was parsed correctly.
+        assertEquals(3 * 2, hdlrRanges.length);
+        assertEquals(30145, hdlrRanges[2 * 2 + 0]);
+        assertEquals(30170, hdlrRanges[2 * 2 + 1]);
+    }
+
+    @Test
+    public void testQtMeta() throws Exception {
+        final IsoInterface qtMeta = IsoInterface.fromFile(stageFile(R.raw.testvideo_meta));
+        final long[] hdlrRanges = qtMeta.getBoxRanges(IsoInterface.BOX_HDLR);
+
+        // There are 3 hdlr boxes, the 1st is inside the meta box. Check it was parsed correctly.
+        assertEquals(3 * 2, hdlrRanges.length);
+        assertEquals(16636, hdlrRanges[2 * 0 + 0]);
+        assertEquals(16661, hdlrRanges[2 * 0 + 1]);
+    }
+
     private static File stageFile(int resId) throws Exception {
         final Context context = InstrumentationRegistry.getContext();
         final File file = File.createTempFile("test", ".mp4");
diff --git a/tests/src/com/android/providers/media/util/MimeUtilsTest.java b/tests/src/com/android/providers/media/util/MimeUtilsTest.java
index abf8ff0..b57b5c5 100644
--- a/tests/src/com/android/providers/media/util/MimeUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/MimeUtilsTest.java
@@ -57,12 +57,12 @@
         assertTrue(startsWithIgnoreCase("image/jpg", "image/"));
         assertTrue(startsWithIgnoreCase("Image/Jpg", "image/"));
 
-        assertFalse(equalIgnoreCase("image/", "image/jpg"));
+        assertFalse(startsWithIgnoreCase("image/", "image/jpg"));
 
-        assertFalse(equalIgnoreCase("image/jpg", "audio/"));
-        assertFalse(equalIgnoreCase("image/jpg", null));
-        assertFalse(equalIgnoreCase(null, "audio/"));
-        assertFalse(equalIgnoreCase(null, null));
+        assertFalse(startsWithIgnoreCase("image/jpg", "audio/"));
+        assertFalse(startsWithIgnoreCase("image/jpg", null));
+        assertFalse(startsWithIgnoreCase(null, "audio/"));
+        assertFalse(startsWithIgnoreCase(null, null));
     }
 
     @Test
diff --git a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
index 5c54e3f..1eba379 100644
--- a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
@@ -16,13 +16,15 @@
 
 package com.android.providers.media.util;
 
-import static com.android.providers.media.util.PermissionUtils.checkPermissionBackup;
-import static com.android.providers.media.util.PermissionUtils.checkPermissionManageExternalStorage;
+import static com.android.providers.media.util.PermissionUtils.checkNoIsolatedStorageGranted;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionDelegator;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionManager;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionReadAudio;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionReadImages;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionReadStorage;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionReadVideo;
-import static com.android.providers.media.util.PermissionUtils.checkPermissionSystem;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionSelf;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionShell;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteAudio;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteImages;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteStorage;
@@ -57,9 +59,10 @@
         final int uid = android.os.Process.myUid();
         final String packageName = context.getPackageName();
 
-        assertTrue(checkPermissionSystem(context, pid, uid, packageName));
-        assertFalse(checkPermissionBackup(context, pid, uid));
-        assertFalse(checkPermissionManageExternalStorage(context, pid, uid, packageName, null));
+        assertTrue(checkPermissionSelf(context, pid, uid));
+        assertFalse(checkPermissionShell(context, pid, uid));
+        assertFalse(checkPermissionManager(context, pid, uid, packageName, null));
+        assertFalse(checkPermissionDelegator(context, pid, uid));
 
         assertTrue(checkPermissionReadStorage(context, pid, uid, packageName, null));
         assertTrue(checkPermissionWriteStorage(context, pid, uid, packageName, null));
@@ -71,4 +74,15 @@
         assertTrue(checkPermissionReadImages(context, pid, uid, packageName, null));
         assertFalse(checkPermissionWriteImages(context, pid, uid, packageName, null));
     }
+
+    /**
+     * Test that {@code android:no_isolated_storage} app op is by default denied.
+     */
+    @Test
+    public void testNoIsolatedStorageIsByDefaultDenied() throws Exception {
+        final Context context = InstrumentationRegistry.getContext();
+        final int uid = android.os.Process.myUid();
+        final String packageName = context.getPackageName();
+        assertFalse(checkNoIsolatedStorageGranted(context, uid, packageName, null));
+    }
 }
diff --git a/tests/src/com/android/providers/media/util/SQLiteQueryBuilderTest.java b/tests/src/com/android/providers/media/util/SQLiteQueryBuilderTest.java
index 2d304d3..9af8e5d 100644
--- a/tests/src/com/android/providers/media/util/SQLiteQueryBuilderTest.java
+++ b/tests/src/com/android/providers/media/util/SQLiteQueryBuilderTest.java
@@ -740,6 +740,17 @@
                 "Month", "Month In (1,2)", null, null);
     }
 
+    @Test
+    public void testStrictCustomCollator() {
+        final SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
+        final HashMap<String, String> map = new HashMap<>();
+        map.put("bucket_id", "bucket_id");
+        builder.setProjectionMap(map);
+
+        final String sortOrder = "bucket_id COLLATE custom_zh ASC";
+        builder.enforceStrictGrammar(null, null, null, sortOrder, null);
+    }
+
     private void assertStrictInsertValid(ContentValues values) {
         mStrictBuilder.insert(mDatabase, values);
     }