Merge "Const correctness handle / RedactionInfo."
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 3c0b842..41eb152 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -157,9 +157,10 @@
     /** {@hide} */
     public static final String CREATE_DELETE_REQUEST_CALL = "create_delete_request";
 
-
     /** {@hide} */
     public static final String GET_VERSION_CALL = "get_version";
+    /** {@hide} */
+    public static final String GET_GENERATION_CALL = "get_generation";
 
     /** {@hide} */
     @Deprecated
@@ -1196,6 +1197,40 @@
         @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
         public static final String IS_DOWNLOAD = "is_download";
 
+        /**
+         * Generation number at which metadata for this media item was first
+         * inserted. This is useful for apps that are attempting to quickly
+         * identify exactly which media items have been added since a previous
+         * point in time. Generation numbers are monotonically increasing over
+         * time, and can be safely arithmetically compared.
+         * <p>
+         * Detecting media additions using generation numbers is more robust
+         * than using {@link #DATE_ADDED}, since those values may change in
+         * unexpected ways when apps use {@link File#setLastModified(long)} or
+         * when the system clock is set incorrectly.
+         *
+         * @see MediaStore#getGeneration(Context, String)
+         */
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        public static final String GENERATION_ADDED = "generation_added";
+
+        /**
+         * Generation number at which metadata for this media item was last
+         * changed. This is useful for apps that are attempting to quickly
+         * identify exactly which media items have changed since a previous
+         * point in time. Generation numbers are monotonically increasing over
+         * time, and can be safely arithmetically compared.
+         * <p>
+         * Detecting media changes using generation numbers is more robust than
+         * using {@link #DATE_MODIFIED}, since those values may change in
+         * unexpected ways when apps use {@link File#setLastModified(long)} or
+         * when the system clock is set incorrectly.
+         *
+         * @see MediaStore#getGeneration(Context, String)
+         */
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        public static final String GENERATION_MODIFIED = "generation_modified";
+
         // =======================================
         // ==== MediaMetadataRetriever values ====
         // =======================================
@@ -3634,6 +3669,39 @@
     }
 
     /**
+     * Return the latest generation value for the given volume.
+     * <p>
+     * Generation numbers are useful for apps that are attempting to quickly
+     * identify exactly which media items have been added or changed since a
+     * previous point in time. Generation numbers are monotonically increasing
+     * over time, and can be safely arithmetically compared.
+     * <p>
+     * Detecting media changes using generation numbers is more robust than
+     * using {@link MediaColumns#DATE_ADDED} or
+     * {@link MediaColumns#DATE_MODIFIED}, since those values may change in
+     * unexpected ways when apps use {@link File#setLastModified(long)} or when
+     * the system clock is set incorrectly.
+     *
+     * @param volumeName specific volume to obtain an generation value for. Must
+     *            be one of the values returned from
+     *            {@link #getExternalVolumeNames(Context)}.
+     * @see MediaColumns#GENERATION_ADDED
+     * @see MediaColumns#GENERATION_MODIFIED
+     */
+    public static long getGeneration(@NonNull Context context, @NonNull String volumeName) {
+        return getGeneration(context.getContentResolver(), volumeName);
+    }
+
+    /** {@hide} */
+    public static long getGeneration(@NonNull ContentResolver resolver,
+            @NonNull String volumeName) {
+        final Bundle in = new Bundle();
+        in.putString(Intent.EXTRA_TEXT, volumeName);
+        final Bundle out = resolver.call(AUTHORITY, GET_GENERATION_CALL, null, in);
+        return out.getLong(Intent.EXTRA_INDEX);
+    }
+
+    /**
      * Return a {@link DocumentsProvider} Uri that is an equivalent to the given
      * {@link MediaStore} Uri.
      * <p>
diff --git a/jni/Android.bp b/jni/Android.bp
index 3fe9c56..0ff937b 100644
--- a/jni/Android.bp
+++ b/jni/Android.bp
@@ -88,3 +88,27 @@
     sdk_version: "current",
     stl: "c++_static",
 }
+
+cc_test {
+    name: "RedactionInfoTest",
+    test_suites: ["device-tests"],
+    srcs: [
+        "RedactionInfoTest.cpp",
+        "RedactionInfo.cpp",
+    ],
+
+    local_include_dirs: ["include"],
+
+    static_libs: [
+        "libbase_ndk",
+    ],
+
+    shared_libs: [
+        "liblog",
+    ],
+
+    tidy: true,
+
+    sdk_version: "current",
+    stl: "c++_static",
+}
\ No newline at end of file
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index 5dfbb3f..8c96088 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -14,6 +14,7 @@
 
 #define ATRACE_TAG ATRACE_TAG_APP
 #define LOG_TAG "FuseDaemon"
+#define LIBFUSE_LOG_TAG "libfuse"
 
 #include "FuseDaemon.h"
 
@@ -74,7 +75,9 @@
 
 // logging macros to avoid duplication.
 #define TRACE LOG(DEBUG)
+#define TRACE_VERBOSE LOG(VERBOSE)
 #define TRACE_FUSE(__fuse) TRACE << "[" << __fuse->path << "] "
+#define TRACE_FUSE_VERBOSE(__fuse) TRACE_VERBOSE << "[" << __fuse->path << "] "
 
 #define ATRACE_NAME(name) ScopedTrace ___tracer(name)
 #define ATRACE_CALL() ATRACE_NAME(__FUNCTION__)
@@ -319,7 +322,7 @@
  */
 static int set_file_lock(int fd, bool for_read, const std::string& path) {
     std::string lock_str = (for_read ? "read" : "write");
-    TRACE << "Setting " << lock_str << " lock for path " << path;
+    TRACE_VERBOSE << "Setting " << lock_str << " lock for path " << path;
 
     struct flock fl{};
     fl.l_type = for_read ? F_RDLCK : F_WRLCK;
@@ -330,7 +333,7 @@
         PLOG(ERROR) << "Failed to set " << lock_str << " lock on path " << path;
         return res;
     }
-    TRACE << "Successfully set " << lock_str << " lock on path " << path;
+    TRACE_VERBOSE << "Successfully set " << lock_str << " lock on path " << path;
     return res;
 }
 
@@ -344,7 +347,7 @@
  * Returns true if fd may have a lock, false otherwise
  */
 static bool is_file_locked(int fd, const std::string& path) {
-    TRACE << "Checking if file is locked " << path;
+    TRACE_VERBOSE << "Checking if file is locked " << path;
 
     struct flock fl{};
     fl.l_type = F_WRLCK;
@@ -357,7 +360,7 @@
         return true;
     }
     bool locked = fl.l_type != F_UNLCK;
-    TRACE << "File " << path << " is " << (locked ? "locked" : "unlocked");
+    TRACE_VERBOSE << "File " << path << " is " << (locked ? "locked" : "unlocked");
     return locked;
 }
 
@@ -453,8 +456,8 @@
     node* parent_node = fuse->FromInode(parent);
     string parent_path = parent_node->BuildPath();
 
-    TRACE_FUSE(fuse) << "LOOKUP " << name << " @ " << parent << " (" << safe_name(parent_node)
-                     << ")";
+    TRACE_FUSE_VERBOSE(fuse) << "LOOKUP " << name << " @ " << parent << " ("
+                             << safe_name(parent_node) << ")";
 
     string child_path = parent_path + "/" + name;
 
@@ -523,7 +526,6 @@
     const struct fuse_ctx* ctx = fuse_req_ctx(req);
     node* node = fuse->FromInode(ino);
     string path = node->BuildPath();
-
     TRACE_FUSE(fuse) << "GETATTR @ " << ino << " (" << safe_name(node) << ")";
 
     if (!node) fuse_reply_err(req, ENOENT);
@@ -601,12 +603,19 @@
     lstat(path.c_str(), attr);
     fuse_reply_attr(req, attr, 10);
 }
-/*
-static void pf_readlink(fuse_req_t req, fuse_ino_t ino)
+
+static void pf_canonical_path(fuse_req_t req, fuse_ino_t ino)
 {
-    cout << "TODO:" << __func__;
+    node* node = get_fuse(req)->FromInode(ino);
+
+    if (node) {
+        // TODO(b/147482155): Check that uid has access to |path| and its contents
+        fuse_reply_canonical_path(req, node->BuildPath().c_str());
+        return;
+    }
+    fuse_reply_err(req, ENOENT);
 }
-*/
+
 static void pf_mknod(fuse_req_t req,
                      fuse_ino_t parent,
                      const char* name,
@@ -985,7 +994,6 @@
     ATRACE_CALL();
     handle* h = reinterpret_cast<handle*>(fi->fh);
     struct fuse* fuse = get_fuse(req);
-    TRACE_FUSE(fuse) << "READ";
 
     fuse->fadviser.Record(h->fd, size);
 
@@ -1306,7 +1314,7 @@
 
     node* node = fuse->FromInode(ino);
     const string path = node->BuildPath();
-    TRACE_FUSE(fuse) << "ACCESS " << path;
+    TRACE_FUSE_VERBOSE(fuse) << "ACCESS " << path;
 
     int res = access(path.c_str(), F_OK);
     fuse_reply_err(req, res ? errno : 0);
@@ -1422,7 +1430,7 @@
     .destroy = pf_destroy,
     .lookup = pf_lookup, .forget = pf_forget, .getattr = pf_getattr,
     .setattr = pf_setattr,
-    /*.readlink = pf_readlink,*/
+    .canonical_path = pf_canonical_path,
     .mknod = pf_mknod, .mkdir = pf_mkdir, .unlink = pf_unlink,
     .rmdir = pf_rmdir,
     /*.symlink = pf_symlink,*/
@@ -1469,11 +1477,11 @@
     });
 
 static void fuse_logger(enum fuse_log_level level, const char* fmt, va_list ap) {
-    __android_log_vprint(fuse_to_android_loglevel.at(level), LOG_TAG, fmt, ap);
+    __android_log_vprint(fuse_to_android_loglevel.at(level), LIBFUSE_LOG_TAG, fmt, ap);
 }
 
 bool FuseDaemon::ShouldOpenWithFuse(int fd, bool for_read, const std::string& path) {
-    TRACE << "Checking if file should be opened with FUSE " << path;
+    TRACE_VERBOSE << "Checking if file should be opened with FUSE " << path;
     bool use_fuse = false;
 
     if (active.load(std::memory_order_acquire)) {
diff --git a/tests/jni/RedactionInfoTest.cpp b/jni/RedactionInfoTest.cpp
similarity index 79%
rename from tests/jni/RedactionInfoTest.cpp
rename to jni/RedactionInfoTest.cpp
index fc6be40..9d98058 100644
--- a/tests/jni/RedactionInfoTest.cpp
+++ b/jni/RedactionInfoTest.cpp
@@ -43,8 +43,8 @@
     RedactionInfo info(0, nullptr);
     EXPECT_EQ(0, info.size());
     EXPECT_EQ(false, info.isRedactionNeeded());
-    
-    auto overlapping_rr =  info.getOverlappingRedactionRanges(/*size*/1000, /*off*/1000);
+
+    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 1000, /*off*/ 1000);
     EXPECT_EQ(0, overlapping_rr->size());
 }
 
@@ -52,31 +52,34 @@
  * Test the case where there is 1 redaction range.
  */
 TEST(RedactionInfoTest, testSingleRedactionRange) {
-    off64_t ranges[2] = { 1, 10, };
+    off64_t ranges[2] = {
+            1,
+            10,
+    };
     RedactionInfo info(1, ranges);
     EXPECT_EQ(1, info.size());
     EXPECT_EQ(true, info.isRedactionNeeded());
     // Overlapping ranges
-    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/1000, /*off*/0);
+    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 1000, /*off*/ 0);
     EXPECT_EQ(*(createRedactionRangeVector(1, ranges)), *overlapping_rr);
 
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/5, /*off*/0);
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 5, /*off*/ 0);
     EXPECT_EQ(*(createRedactionRangeVector(1, ranges)), *overlapping_rr);
 
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/5, /*off*/5);
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 5, /*off*/ 5);
     EXPECT_EQ(*(createRedactionRangeVector(1, ranges)), *overlapping_rr);
 
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/10, /*off*/1);
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 10, /*off*/ 1);
     EXPECT_EQ(*(createRedactionRangeVector(1, ranges)), *overlapping_rr);
 
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/1, /*off*/1);
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 1, /*off*/ 1);
     EXPECT_EQ(*(createRedactionRangeVector(1, ranges)), *overlapping_rr);
 
     // Non-overlapping range
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/100, /*off*/11);
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 100, /*off*/ 11);
     EXPECT_EQ(*(createRedactionRangeVector(0, nullptr)), *overlapping_rr);
 
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/1, /*off*/11);
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 1, /*off*/ 11);
     EXPECT_EQ(*(createRedactionRangeVector(0, nullptr)), *overlapping_rr);
 }
 
@@ -84,29 +87,33 @@
  * Test the case where the redaction ranges don't require sorting or merging
  */
 TEST(RedactionInfoTest, testSortedAndNonOverlappingRedactionRanges) {
-    off64_t ranges[6] = { 1, 10,
-                          15, 21,
-                          32, 40, };
+    off64_t ranges[6] = {
+            1, 10, 15, 21, 32, 40,
+    };
 
     RedactionInfo info = RedactionInfo(3, ranges);
     EXPECT_EQ(3, info.size());
     EXPECT_EQ(true, info.isRedactionNeeded());
 
     // Read request strictly contains all ranges: [0, 49]
-    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/50, /*off*/0);
-    off64_t expected1[] = { 1, 10,
-                            15, 21,
-                            32, 40, };
+    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 50, /*off*/ 0);
+    off64_t expected1[] = {
+            1, 10, 15, 21, 32, 40,
+    };
     EXPECT_EQ(*(createRedactionRangeVector(3, expected1)), *overlapping_rr);
 
     // Read request strictly contains a subset of the ranges: [15, 40]
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/26, /*off*/15);
-    off64_t expected2[] = { 15, 21,
-                            32, 40, };
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 26, /*off*/ 15);
+    off64_t expected2[] = {
+            15,
+            21,
+            32,
+            40,
+    };
     EXPECT_EQ(*(createRedactionRangeVector(2, expected2)), *overlapping_rr);
 
     // Read request intersects with a subset of the ranges" [16, 32]
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/17, /*off*/16);
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 17, /*off*/ 16);
     EXPECT_EQ(*(createRedactionRangeVector(2, expected2)), *overlapping_rr);
 }
 
@@ -114,29 +121,33 @@
  * Test the case where the redaction ranges require sorting
  */
 TEST(RedactionInfoTest, testSortRedactionRanges) {
-    off64_t ranges[6] = { 1, 10,
-                          32, 40,
-                          15, 21, };
+    off64_t ranges[6] = {
+            1, 10, 32, 40, 15, 21,
+    };
 
     RedactionInfo info = RedactionInfo(3, ranges);
     EXPECT_EQ(3, info.size());
     EXPECT_EQ(true, info.isRedactionNeeded());
 
     // Read request strictly contains all ranges: [0, 49]
-    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/50, /*off*/0);
-    off64_t expected1[] = { 1, 10,
-                            15, 21,
-                            32, 40, };
+    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 50, /*off*/ 0);
+    off64_t expected1[] = {
+            1, 10, 15, 21, 32, 40,
+    };
     EXPECT_EQ(*(createRedactionRangeVector(3, expected1)), *overlapping_rr);
 
     // Read request strictly contains a subset of the ranges: [15, 40]
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/26, /*off*/15);
-    off64_t expected2[] = { 15, 21,
-                            32, 40, };
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 26, /*off*/ 15);
+    off64_t expected2[] = {
+            15,
+            21,
+            32,
+            40,
+    };
     EXPECT_EQ(*(createRedactionRangeVector(2, expected2)), *overlapping_rr);
 
     // Read request intersects with a subset of the ranges" [16, 32]
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/17, /*off*/16);
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 17, /*off*/ 16);
     EXPECT_EQ(*(createRedactionRangeVector(2, expected2)), *overlapping_rr);
 }
 
@@ -144,30 +155,33 @@
  * Test the case where the redaction ranges require sorting or merging
  */
 TEST(RedactionInfoTest, testSortAndMergeRedactionRanges) {
-    off64_t ranges[8] = { 35, 40,
-                          1, 10,
-                          32, 35,
-                          15, 21, };
+    off64_t ranges[8] = {
+            35, 40, 1, 10, 32, 35, 15, 21,
+    };
 
     RedactionInfo info = RedactionInfo(4, ranges);
     EXPECT_EQ(3, info.size());
     EXPECT_EQ(true, info.isRedactionNeeded());
 
     // Read request strictly contains all ranges: [0, 49]
-    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/50, /*off*/0);
-    off64_t expected1[] = { 1, 10,
-                            15, 21,
-                            32, 40, };
+    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 50, /*off*/ 0);
+    off64_t expected1[] = {
+            1, 10, 15, 21, 32, 40,
+    };
     EXPECT_EQ(*(createRedactionRangeVector(3, expected1)), *overlapping_rr);
 
     // Read request strictly contains a subset of the ranges: [15, 40]
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/26, /*off*/15);
-    off64_t expected2[] = { 15, 21,
-                            32, 40, };
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 26, /*off*/ 15);
+    off64_t expected2[] = {
+            15,
+            21,
+            32,
+            40,
+    };
     EXPECT_EQ(*(createRedactionRangeVector(2, expected2)), *overlapping_rr);
 
     // Read request intersects with a subset of the ranges" [16, 32]
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/17, /*off*/16);
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 17, /*off*/ 16);
     EXPECT_EQ(*(createRedactionRangeVector(2, expected2)), *overlapping_rr);
 }
 
@@ -175,27 +189,25 @@
  * Test the case where the redaction ranges all merge into the first range
  */
 TEST(RedactionInfoTest, testMergeAllRangesIntoTheFirstRange) {
-    off64_t ranges[10] = { 1, 100,
-                           2, 99,
-                           3, 98,
-                           4, 97,
-                           3, 15, };
+    off64_t ranges[10] = {
+            1, 100, 2, 99, 3, 98, 4, 97, 3, 15,
+    };
 
     RedactionInfo info = RedactionInfo(5, ranges);
     EXPECT_EQ(1, info.size());
     EXPECT_EQ(true, info.isRedactionNeeded());
 
     // Read request equals the range: [1, 100]
-    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/100, /*off*/1);
+    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 100, /*off*/ 1);
     off64_t expected[] = {1, 100};
     EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
 
     // Read request is contained in the range: [15, 40]
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/26, /*off*/15);
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 26, /*off*/ 15);
     EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
 
     // Read request that strictly contains all of the redaction ranges
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/1000, /*off*/0);
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 1000, /*off*/ 0);
     EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
 }
 
@@ -203,27 +215,25 @@
  * Test the case where the redaction ranges all merge into the last range
  */
 TEST(RedactionInfoTest, testMergeAllRangesIntoTheLastRange) {
-    off64_t ranges[10] = { 4, 96,
-                           3, 97,
-                           2, 98,
-                           1, 99,
-                           0, 100, };
+    off64_t ranges[10] = {
+            4, 96, 3, 97, 2, 98, 1, 99, 0, 100,
+    };
 
     RedactionInfo info = RedactionInfo(5, ranges);
     EXPECT_EQ(1, info.size());
     EXPECT_EQ(true, info.isRedactionNeeded());
 
     // Read request equals the range: [0, 100]
-    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/100, /*off*/0);
+    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 100, /*off*/ 0);
     off64_t expected[] = {0, 100};
     EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
 
     // Read request is contained in the range: [15, 40]
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/26, /*off*/15);
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 26, /*off*/ 15);
     EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
 
     // Read request that strictly contains all of the redaction ranges
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/1000, /*off*/0);
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 1000, /*off*/ 0);
     EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
 }
 
@@ -231,40 +241,36 @@
  * Test the case where the redaction ranges progressively merge
  */
 TEST(RedactionInfoTest, testMergeAllRangesProgressively) {
-    off64_t ranges[10] = { 1, 11,
-                           2, 12,
-                           3, 13,
-                           4, 14,
-                           5, 15, };
+    off64_t ranges[10] = {
+            1, 11, 2, 12, 3, 13, 4, 14, 5, 15,
+    };
 
     RedactionInfo info = RedactionInfo(5, ranges);
     EXPECT_EQ(1, info.size());
     EXPECT_EQ(true, info.isRedactionNeeded());
 
     // Read request equals the range: [1, 15]
-    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/15, /*off*/1);
+    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 15, /*off*/ 1);
     off64_t expected[] = {1, 15};
     EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
 
     // Read request is contained in the range: [2, 12]
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/10, /*off*/2);
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 10, /*off*/ 2);
     EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
 
     // Read request that strictly contains all of the redaction ranges
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/100, /*off*/0);
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 100, /*off*/ 0);
     EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
 
-    off64_t reverse_rr[10] = { 5, 15,
-                               4, 14,
-                               3, 13,
-                               2, 12,
-                               1, 11, };
+    off64_t reverse_rr[10] = {
+            5, 15, 4, 14, 3, 13, 2, 12, 1, 11,
+    };
 
     RedactionInfo reverse_info = RedactionInfo(5, reverse_rr);
     EXPECT_EQ(1, info.size());
     EXPECT_EQ(true, info.isRedactionNeeded());
 
     // Read request equals the range: [1, 15]
-    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/15, /*off*/1);
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 15, /*off*/ 1);
     EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
 }
diff --git a/tests/jni/RedactionInfoTest.xml b/jni/RedactionInfoTest.xml
similarity index 100%
rename from tests/jni/RedactionInfoTest.xml
rename to jni/RedactionInfoTest.xml
diff --git a/jni/TEST_MAPPING b/jni/TEST_MAPPING
index be62f80..a871459 100644
--- a/jni/TEST_MAPPING
+++ b/jni/TEST_MAPPING
@@ -2,6 +2,9 @@
   "presubmit": [
     {
       "name": "RedactionInfoTest"
+    },
+    {
+      "name": "fuse_node_test"
     }
   ]
 }
diff --git a/legacy/src/com/android/providers/media/LegacyMediaProvider.java b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
index bc0bbc9..4085a3a 100644
--- a/legacy/src/com/android/providers/media/LegacyMediaProvider.java
+++ b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
@@ -101,7 +101,9 @@
     @Override
     public Uri insert(Uri uri, ContentValues values) {
         try {
-            new File(values.getAsString(MediaColumns.DATA)).createNewFile();
+            final File file = new File(values.getAsString(MediaColumns.DATA));
+            file.getParentFile().mkdirs();
+            file.createNewFile();
         } catch (IOException e) {
             throw new IllegalStateException(e);
         }
diff --git a/logging.sh b/logging.sh
index cf14af5..43f0d86 100755
--- a/logging.sh
+++ b/logging.sh
@@ -1,28 +1,29 @@
 #!/bin/bash
 
 level=$1
-uids=$(adb shell cat /data/system/packages.list |grep -Po "providers.media[a-z\.]* \K\d+")
 
 if [ $level == "on" ] || [ $level == "extreme" ]
 then
     adb shell setprop log.tag.MediaProvider VERBOSE
     adb shell setprop log.tag.ModernMediaScanner VERBOSE
+    adb shell setprop log.tag.FuseDaemon DEBUG
+    adb shell setprop log.tag.libfuse DEBUG
 else
     adb shell setprop log.tag.MediaProvider INFO
     adb shell setprop log.tag.ModernMediaScanner INFO
+    adb shell setprop log.tag.FuseDaemon INFO
+    adb shell setprop log.tag.libfuse INFO
 fi
 
 if [ $level == "extreme" ]
 then
-    for uid in $uids;
-        do adb shell setprop db.log.slow_query_threshold.$uid 0;
-    done
-    adb shell setprop db.log.bindargs 1
+    adb shell setprop log.tag.SQLiteQueryBuilder VERBOSE
+    adb shell setprop log.tag.FuseDaemon VERBOSE
+    adb shell setprop log.tag.libfuse VERBOSE
 else
-    for uid in $uids;
-        do adb shell setprop db.log.slow_query_threshold.$uid 10000;
-    done
-    adb shell setprop db.log.bindargs 0
+    adb shell setprop log.tag.SQLiteQueryBuilder INFO
+    adb shell setprop log.tag.FuseDaemon INFO
+    adb shell setprop log.tag.libfuse INFO
 fi
 
 # Kill process to kick new settings into place
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index ccf5627..c579e80 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -70,6 +70,7 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.UUID;
+import java.util.function.LongSupplier;
 import java.util.regex.Matcher;
 
 /**
@@ -78,16 +79,16 @@
  * on demand, create and upgrade the schema, etc.
  */
 public class DatabaseHelper extends SQLiteOpenHelper implements AutoCloseable {
-    // maximum number of cached external databases to keep
-    private static final int MAX_EXTERNAL_DATABASES = 3;
-
-    // Delete databases that have not been used in two months
-    // 60 days in milliseconds (1000 * 60 * 60 * 24 * 60)
-    private static final long OBSOLETE_DATABASE_DB = 5184000000L;
-
     static final String INTERNAL_DATABASE_NAME = "internal.db";
     static final String EXTERNAL_DATABASE_NAME = "external.db";
 
+    /**
+     * Raw SQL clause that can be used to obtain the current generation, which
+     * is designed to be populated into {@link MediaColumns#GENERATION_ADDED} or
+     * {@link MediaColumns#GENERATION_MODIFIED}.
+     */
+    public static final String CURRENT_GENERATION_CLAUSE = "SELECT generation FROM local_metadata";
+
     final Context mContext;
     final String mName;
     final int mVersion;
@@ -180,7 +181,7 @@
         if (Looper.myLooper() == Looper.getMainLooper()) {
             Log.wtf(TAG, "Database operations must not happen on main thread", new Throwable());
         }
-        return super.getReadableDatabase();
+        return super.getWritableDatabase();
     }
 
     @Override
@@ -201,89 +202,6 @@
         downgradeDatabase(db, oldV, newV);
     }
 
-    /**
-     * For devices that have removable storage, we support keeping multiple databases
-     * to allow users to switch between a number of cards.
-     * On such devices, touch this particular database and garbage collect old databases.
-     * An LRU cache system is used to clean up databases for old external
-     * storage volumes.
-     */
-    @Override
-    public void onOpen(SQLiteDatabase db) {
-        if (mEarlyUpgrade) return; // Doing early upgrade.
-        if (mInternal) return;  // The internal database is kept separately.
-
-        // the code below is only needed on devices with removable storage
-        if (!Environment.isExternalStorageRemovable()) return;
-
-        // touch the database file to show it is most recently used
-        File file = new File(db.getPath());
-        long now = System.currentTimeMillis();
-        file.setLastModified(now);
-
-        // delete least recently used databases if we are over the limit
-        String[] databases = mContext.databaseList();
-        // Don't delete wal auxiliary files(db-shm and db-wal) directly because db file may
-        // not be deleted, and it will cause Disk I/O error when accessing this database.
-        List<String> dbList = new ArrayList<String>();
-        for (String database : databases) {
-            if (database != null && database.endsWith(".db")) {
-                dbList.add(database);
-            }
-        }
-        databases = dbList.toArray(new String[0]);
-        int count = databases.length;
-        int limit = MAX_EXTERNAL_DATABASES;
-
-        // delete external databases that have not been used in the past two months
-        long twoMonthsAgo = now - OBSOLETE_DATABASE_DB;
-        for (int i = 0; i < databases.length; i++) {
-            File other = mContext.getDatabasePath(databases[i]);
-            if (INTERNAL_DATABASE_NAME.equals(databases[i]) || file.equals(other)) {
-                databases[i] = null;
-                count--;
-                if (file.equals(other)) {
-                    // reduce limit to account for the existence of the database we
-                    // are about to open, which we removed from the list.
-                    limit--;
-                }
-            } else {
-                long time = other.lastModified();
-                if (time < twoMonthsAgo) {
-                    if (LOGV) Log.v(TAG, "Deleting old database " + databases[i]);
-                    mContext.deleteDatabase(databases[i]);
-                    databases[i] = null;
-                    count--;
-                }
-            }
-        }
-
-        // delete least recently used databases until
-        // we are no longer over the limit
-        while (count > limit) {
-            int lruIndex = -1;
-            long lruTime = 0;
-
-            for (int i = 0; i < databases.length; i++) {
-                if (databases[i] != null) {
-                    long time = mContext.getDatabasePath(databases[i]).lastModified();
-                    if (lruTime == 0 || time < lruTime) {
-                        lruIndex = i;
-                        lruTime = time;
-                    }
-                }
-            }
-
-            // delete least recently used database
-            if (lruIndex != -1) {
-                if (LOGV) Log.v(TAG, "Deleting old database " + databases[lruIndex]);
-                mContext.deleteDatabase(databases[lruIndex]);
-                databases[lruIndex] = null;
-                count--;
-            }
-        }
-    }
-
     @GuardedBy("mProjectionMapCache")
     private final ArrayMap<Class<?>[], ArrayMap<String, String>>
             mProjectionMapCache = new ArrayMap<>();
@@ -327,11 +245,16 @@
 
     public void beginTransaction() {
         getWritableDatabase().beginTransaction();
+        getWritableDatabase().execSQL("UPDATE local_metadata SET generation=generation+1;");
         mNotifyChanges.set(new ArrayList<>());
     }
 
     public void setTransactionSuccessful() {
         getWritableDatabase().setTransactionSuccessful();
+    }
+
+    public void endTransaction() {
+        getWritableDatabase().endTransaction();
         final List<Uri> uris = mNotifyChanges.get();
         if (uris != null) {
             BackgroundThread.getExecutor().execute(() -> {
@@ -341,8 +264,26 @@
         mNotifyChanges.remove();
     }
 
-    public void endTransaction() {
-        getWritableDatabase().endTransaction();
+    /**
+     * Execute the given runnable inside a transaction. If the calling thread is
+     * not already in an active transaction, this method will wrap the given
+     * runnable inside a new transaction.
+     */
+    public long runWithTransaction(@NonNull LongSupplier s) {
+        if (mNotifyChanges.get() != null) {
+            // Already inside a transaction, so we can run directly
+            return s.getAsLong();
+        } else {
+            // Not inside a transaction, so we need to make one
+            beginTransaction();
+            try {
+                final long res = s.getAsLong();
+                setTransactionSuccessful();
+                return res;
+            } finally {
+                endTransaction();
+            }
+        }
     }
 
     /**
@@ -461,6 +402,9 @@
 
         makePristineSchema(db);
 
+        db.execSQL("CREATE TABLE local_metadata (generation INTEGER DEFAULT 0)");
+        db.execSQL("INSERT INTO local_metadata VALUES (0)");
+
         db.execSQL("CREATE TABLE android_metadata (locale TEXT)");
         db.execSQL("CREATE TABLE thumbnails (_id INTEGER PRIMARY KEY,_data TEXT,image_id INTEGER,"
                 + "kind INTEGER,width INTEGER,height INTEGER)");
@@ -498,7 +442,8 @@
                 + "is_favorite INTEGER DEFAULT 0, num_tracks INTEGER DEFAULT NULL,"
                 + "writer TEXT DEFAULT NULL, exposure_time TEXT DEFAULT NULL,"
                 + "f_number TEXT DEFAULT NULL, iso INTEGER DEFAULT NULL,"
-                + "scene_capture_type INTEGER DEFAULT NULL)");
+                + "scene_capture_type INTEGER DEFAULT NULL, generation_added INTEGER DEFAULT 0,"
+                + "generation_modified INTEGER DEFAULT 0)");
 
         db.execSQL("CREATE TABLE log (time DATETIME, message TEXT)");
         if (!mInternal) {
@@ -681,7 +626,7 @@
 
         db.execSQL("CREATE VIEW audio_artists AS SELECT "
                 + "  artist_id AS " + Audio.Artists._ID
-                + ", artist AS " + Audio.Artists.ARTIST
+                + ", MIN(artist) AS " + Audio.Artists.ARTIST
                 + ", artist_key AS " + Audio.Artists.ARTIST_KEY
                 + ", COUNT(DISTINCT album_id) AS " + Audio.Artists.NUMBER_OF_ALBUMS
                 + ", COUNT(DISTINCT _id) AS " + Audio.Artists.NUMBER_OF_TRACKS
@@ -692,7 +637,7 @@
         db.execSQL("CREATE VIEW audio_albums AS SELECT "
                 + "  album_id AS " + Audio.Albums._ID
                 + ", album_id AS " + Audio.Albums.ALBUM_ID
-                + ", album AS " + Audio.Albums.ALBUM
+                + ", MIN(album) AS " + Audio.Albums.ALBUM
                 + ", album_key AS " + Audio.Albums.ALBUM_KEY
                 + ", artist_id AS " + Audio.Albums.ARTIST_ID
                 + ", artist AS " + Audio.Albums.ARTIST
@@ -708,7 +653,7 @@
 
         db.execSQL("CREATE VIEW audio_genres AS SELECT "
                 + "  genre_id AS " + Audio.Genres._ID
-                + ", genre AS " + Audio.Genres.NAME
+                + ", MIN(genre) AS " + Audio.Genres.NAME
                 + " FROM audio"
                 + " WHERE volume_name IN " + filterVolumeNames
                 + " GROUP BY genre_id");
@@ -905,6 +850,16 @@
         db.execSQL("DELETE FROM log;");
     }
 
+    private static void updateAddLocalMetadata(SQLiteDatabase db, boolean internal) {
+        db.execSQL("CREATE TABLE local_metadata (generation INTEGER DEFAULT 0)");
+        db.execSQL("INSERT INTO local_metadata VALUES (0)");
+    }
+
+    private static void updateAddGeneration(SQLiteDatabase db, boolean internal) {
+        db.execSQL("ALTER TABLE files ADD COLUMN generation_added INTEGER DEFAULT 0;");
+        db.execSQL("ALTER TABLE files ADD COLUMN generation_modified INTEGER DEFAULT 0;");
+    }
+
     private static void recomputeDataValues(SQLiteDatabase db, boolean internal) {
         try (Cursor c = db.query("files", new String[] { FileColumns._ID, FileColumns.DATA },
                 null, null, null, null, null, null)) {
@@ -933,7 +888,7 @@
     static final int VERSION_O = 800;
     static final int VERSION_P = 900;
     static final int VERSION_Q = 1023;
-    static final int VERSION_R = 1107;
+    static final int VERSION_R = 1109;
     static final int VERSION_LATEST = VERSION_R;
 
     /**
@@ -1055,6 +1010,12 @@
             if (fromVersion < 1107) {
                 updateAddSceneCaptureType(db, internal);
             }
+            if (fromVersion < 1108) {
+                updateAddLocalMetadata(db, internal);
+            }
+            if (fromVersion < 1109) {
+                updateAddGeneration(db, internal);
+            }
 
             if (recomputeDataValues) {
                 recomputeDataValues(db, internal);
@@ -1114,6 +1075,16 @@
     }
 
     /**
+     * Return the current generation that will be populated into
+     * {@link MediaColumns#GENERATION_ADDED} or
+     * {@link MediaColumns#GENERATION_MODIFIED}.
+     */
+    public long getGeneration() {
+        return android.database.DatabaseUtils.longForQuery(getReadableDatabase(),
+                CURRENT_GENERATION_CLAUSE + ";", null);
+    }
+
+    /**
      * Return total number of items tracked inside this database. This includes
      * only real media items, and does not include directories.
      */
@@ -1126,12 +1097,8 @@
      * only real media items, and does not include directories.
      */
     private long getItemCount(SQLiteDatabase db) {
-        try (Cursor c = db.query(false, "files", new String[] { "COUNT(_id)" },
-                FileColumns.MIME_TYPE + " IS NOT NULL", null, null, null, null, null, null)) {
-            if (c.moveToFirst()) {
-                return c.getLong(0);
-            }
-        }
-        return 0;
+        return android.database.DatabaseUtils.longForQuery(db,
+                "SELECT COUNT(_id) FROM files WHERE " + FileColumns.MIME_TYPE + " IS NOT NULL",
+                null);
     }
 }
diff --git a/src/com/android/providers/media/LocalCallingIdentity.java b/src/com/android/providers/media/LocalCallingIdentity.java
index d4f32f3..3942057 100644
--- a/src/com/android/providers/media/LocalCallingIdentity.java
+++ b/src/com/android/providers/media/LocalCallingIdentity.java
@@ -23,6 +23,7 @@
 
 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.checkPermissionReadAudio;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionReadImages;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionReadStorage;
@@ -186,6 +187,7 @@
     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;
 
     private int hasPermission;
     private int hasPermissionResolved;
@@ -233,6 +235,8 @@
                 return checkPermissionWriteVideo(context, pid, uid, getPackageName());
             case PERMISSION_WRITE_IMAGES:
                 return checkPermissionWriteImages(context, pid, uid, getPackageName());
+            case PERMISSION_MANAGE_EXTERNAL_STORAGE:
+                return checkPermissionManageExternalStorage(context, pid, uid, packageName);
             default:
                 return false;
         }
diff --git a/src/com/android/providers/media/MediaDocumentsProvider.java b/src/com/android/providers/media/MediaDocumentsProvider.java
index 65150c8..8576f2e 100644
--- a/src/com/android/providers/media/MediaDocumentsProvider.java
+++ b/src/com/android/providers/media/MediaDocumentsProvider.java
@@ -86,7 +86,7 @@
 public class MediaDocumentsProvider extends DocumentsProvider {
     private static final String TAG = "MediaDocumentsProvider";
 
-    private static final String AUTHORITY = "com.android.providers.media.documents";
+    public static final String AUTHORITY = "com.android.providers.media.documents";
 
     private static final String SUPPORTED_QUERY_ARGS = joinNewline(
             DocumentsContract.QUERY_ARG_DISPLAY_NAME,
@@ -112,18 +112,18 @@
     private static final String AUDIO_MIME_TYPES = joinNewline(
             "audio/*", "application/ogg", "application/x-flac");
 
-    private static final String TYPE_IMAGES_ROOT = "images_root";
-    private static final String TYPE_IMAGES_BUCKET = "images_bucket";
-    private static final String TYPE_IMAGE = "image";
+    static final String TYPE_IMAGES_ROOT = "images_root";
+    static final String TYPE_IMAGES_BUCKET = "images_bucket";
+    static final String TYPE_IMAGE = "image";
 
-    private static final String TYPE_VIDEOS_ROOT = "videos_root";
-    private static final String TYPE_VIDEOS_BUCKET = "videos_bucket";
-    private static final String TYPE_VIDEO = "video";
+    static final String TYPE_VIDEOS_ROOT = "videos_root";
+    static final String TYPE_VIDEOS_BUCKET = "videos_bucket";
+    static final String TYPE_VIDEO = "video";
 
-    private static final String TYPE_AUDIO_ROOT = "audio_root";
-    private static final String TYPE_AUDIO = "audio";
-    private static final String TYPE_ARTIST = "artist";
-    private static final String TYPE_ALBUM = "album";
+    static final String TYPE_AUDIO_ROOT = "audio_root";
+    static final String TYPE_AUDIO = "audio";
+    static final String TYPE_ARTIST = "artist";
+    static final String TYPE_ALBUM = "album";
 
     private static boolean sReturnedImagesEmpty = false;
     private static boolean sReturnedVideosEmpty = false;
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 1f0c262..5e3fcf9 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -44,6 +44,7 @@
 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE;
 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_READ_AUDIO;
 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_IMAGES;
 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_VIDEO;
@@ -535,7 +536,7 @@
      * devices. We only do this once per volume so we don't annoy the user if
      * deleted manually.
      */
-    private void ensureDefaultFolders(String volumeName, DatabaseHelper helper, SQLiteDatabase db) {
+    private void ensureDefaultFolders(String volumeName, DatabaseHelper helper) {
         try {
             final File path = getVolumePath(volumeName);
             final StorageVolume vol = mStorageManager.getStorageVolume(path);
@@ -553,7 +554,7 @@
                     final File folder = new File(vol.getDirectory(), folderName);
                     if (!folder.exists()) {
                         folder.mkdirs();
-                        insertDirectory(db, folder.getAbsolutePath());
+                        insertDirectory(helper, folder.getAbsolutePath());
                     }
                 }
 
@@ -1071,8 +1072,8 @@
     /**
      * Updates database entry for given {@code path} with {@code values}
      */
-    private boolean updateDatabaseForFuseRename(SQLiteDatabase db, String oldPath, String newPath,
-            ContentValues values) {
+    private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper,
+            @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values) {
         final Uri uriOldPath = Files.getContentUriForPath(oldPath);
         boolean allowHidden = isCallingPackageAllowedHidden();
         final SQLiteQueryBuilder qbForUpdate = getQueryBuilder(TYPE_UPDATE,
@@ -1082,7 +1083,7 @@
         boolean retryUpdateWithReplace = false;
 
         try {
-            count = qbForUpdate.update(db, values, selection, new String[]{oldPath});
+            count = qbForUpdate.update(helper, values, selection, new String[]{oldPath});
         } catch (SQLiteConstraintException e) {
             Log.w(TAG, "Database update failed while renaming " + oldPath, e);
             retryUpdateWithReplace = true;
@@ -1094,9 +1095,9 @@
             final Uri uriNewPath = Files.getContentUriForPath(oldPath);
             final SQLiteQueryBuilder qbForDelete = getQueryBuilder(TYPE_DELETE,
                     matchUri(uriNewPath, allowHidden), uriNewPath, Bundle.EMPTY, null);
-            if (qbForDelete.delete(db, selection, new String[] {newPath}) == 1) {
+            if (qbForDelete.delete(helper, selection, new String[] {newPath}) == 1) {
                 Log.i(TAG, "Retrying database update after deleting conflicting entry");
-                count = qbForUpdate.update(db, values, selection, new String[]{oldPath});
+                count = qbForUpdate.update(helper, values, selection, new String[]{oldPath});
             } else {
                 return false;
             }
@@ -1219,8 +1220,8 @@
 
         ArrayList<String> fileList = new ArrayList<>();
         final String[] projection = {MediaColumns.DATA, MediaColumns.MIME_TYPE};
-        try (Cursor c = qb.query(helper.getReadableDatabase(), projection, selection, null,
-                null, null, null)) {
+        try (Cursor c = qb.query(helper, projection, selection, null,
+                null, null, null, null, null)) {
             // Check if the calling package has write permission to all files in the given
             // directory. If calling package has write permission to all files in the directory, the
             // query with update uri should return same number of files as previous query.
@@ -1283,21 +1284,20 @@
             return -OsConstants.EPERM;
         }
 
-        final SQLiteDatabase db;
+        final DatabaseHelper helper;
         try {
-            final DatabaseHelper helper = getDatabaseForUri(Files.getContentUriForPath(oldPath));
-            db = helper.getWritableDatabase();
+            helper = getDatabaseForUri(Files.getContentUriForPath(oldPath));
         } catch (VolumeNotFoundException e) {
             throw new IllegalStateException("Volume not found while trying to update database for "
                     + oldPath, e);
         }
 
-        db.beginTransaction();
+        helper.beginTransaction();
         try {
             for (String filePath : fileList) {
                 final String newFilePath = newPath + "/" + filePath;
                 final String mimeType = MimeUtils.resolveMimeType(new File(newFilePath));
-                if(!updateDatabaseForFuseRename(db, oldPath + "/" + filePath, newFilePath,
+                if(!updateDatabaseForFuseRename(helper, oldPath + "/" + filePath, newFilePath,
                         getContentValuesForFuseRename(newFilePath, mimeType, mimeType))) {
                     Log.e(TAG, "Calling package doesn't have write permission to rename file.");
                     return -OsConstants.EPERM;
@@ -1307,12 +1307,12 @@
             // Rename the directory in lower file system.
             int errno = renameInLowerFs(oldPath, newPath);
             if (errno == 0) {
-                db.setTransactionSuccessful();
+                helper.setTransactionSuccessful();
             } else {
                 return errno;
             }
         } finally {
-            db.endTransaction();
+            helper.endTransaction();
         }
         // Process metadata in background thread.
         postProcessMetadataForFuseRename(oldPath, newPath);
@@ -1343,19 +1343,18 @@
             return -OsConstants.EPERM;
         }
 
-        final SQLiteDatabase db;
+        final DatabaseHelper helper;
         try {
-            final DatabaseHelper helper = getDatabaseForUri(Files.getContentUriForPath(oldPath));
-            db = helper.getWritableDatabase();
+            helper = getDatabaseForUri(Files.getContentUriForPath(oldPath));
         } catch (VolumeNotFoundException e) {
             throw new IllegalStateException("Volume not found while trying to update database for"
                 + oldPath + ". Rename failed due to database update error", e);
         }
 
-        db.beginTransaction();
+        helper.beginTransaction();
         try {
             final String oldMimeType = MimeUtils.resolveMimeType(new File(oldPath));
-            if (!updateDatabaseForFuseRename(db, oldPath, newPath,
+            if (!updateDatabaseForFuseRename(helper, oldPath, newPath,
                     getContentValuesForFuseRename(newPath, oldMimeType, newMimeType))) {
                 Log.e(TAG, "Calling package doesn't have write permission to rename file.");
                 return -OsConstants.EPERM;
@@ -1364,12 +1363,12 @@
             // Try renaming oldPath to newPath in lower file system.
             int errno = renameInLowerFs(oldPath, newPath);
             if (errno == 0) {
-                db.setTransactionSuccessful();
+                helper.setTransactionSuccessful();
             } else {
                 return errno;
             }
         } finally {
-            db.endTransaction();
+            helper.endTransaction();
         }
         // Process metadata in background thread.
         postProcessMetadataForFuseRename(oldPath, newPath);
@@ -1488,10 +1487,8 @@
             final int table = matchUri(uri, allowHidden);
 
             final DatabaseHelper helper;
-            final SQLiteDatabase db;
             try {
                 helper = getDatabaseForUri(uri);
-                db = helper.getReadableDatabase();
             } catch (VolumeNotFoundException e) {
                 return PackageManager.PERMISSION_DENIED;
             }
@@ -1504,8 +1501,8 @@
             }
 
             final SQLiteQueryBuilder qb = getQueryBuilder(type, table, uri, Bundle.EMPTY, null);
-            try (Cursor c = qb.query(db,
-                    new String[] { BaseColumns._ID }, null, null, null, null, null)) {
+            try (Cursor c = qb.query(helper,
+                    new String[] { BaseColumns._ID }, null, null, null, null, null, null, null)) {
                 if (c.getCount() == 1) {
                     return PackageManager.PERMISSION_GRANTED;
                 }
@@ -1574,54 +1571,8 @@
         }
 
         final DatabaseHelper helper = getDatabaseForUri(uri);
-        final SQLiteDatabase db = helper.getReadableDatabase();
-
         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, queryArgs,
                 honoredArgs::add);
-        String filter = uri.getQueryParameter("filter");
-        String [] keywords = null;
-        if (filter != null) {
-            filter = Uri.decode(filter).trim();
-            if (!TextUtils.isEmpty(filter)) {
-                String [] searchWords = filter.split(" ");
-                keywords = new String[searchWords.length];
-                for (int i = 0; i < searchWords.length; i++) {
-                    String key = MediaStore.Audio.keyFor(searchWords[i]);
-                    key = key.replace("\\", "\\\\");
-                    key = key.replace("%", "\\%");
-                    key = key.replace("_", "\\_");
-                    keywords[i] = key;
-                }
-            }
-        }
-
-        String keywordColumn = null;
-        switch (table) {
-            case AUDIO_MEDIA:
-            case AUDIO_GENRES_ALL_MEMBERS:
-            case AUDIO_GENRES_ID_MEMBERS:
-            case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
-            case AUDIO_PLAYLISTS_ID_MEMBERS:
-                keywordColumn = MediaStore.Audio.Media.ARTIST_KEY +
-                        "||" + MediaStore.Audio.Media.ALBUM_KEY +
-                        "||" + MediaStore.Audio.Media.TITLE_KEY;
-                break;
-            case AUDIO_ARTISTS_ID_ALBUMS:
-            case AUDIO_ALBUMS:
-                keywordColumn = MediaStore.Audio.Media.ARTIST_KEY + "||"
-                        + MediaStore.Audio.Media.ALBUM_KEY;
-                break;
-            case AUDIO_ARTISTS:
-                keywordColumn = MediaStore.Audio.Media.ARTIST_KEY;
-                break;
-        }
-
-        if (keywordColumn != null) {
-            for (int i = 0; keywords != null && i < keywords.length; i++) {
-                appendWhereStandalone(qb, keywordColumn + " LIKE ? ESCAPE '\\'",
-                        "%" + keywords[i] + "%");
-            }
-        }
 
         if (targetSdkVersion < Build.VERSION_CODES.R) {
             // Some apps are abusing "ORDER BY" clauses to inject "LIMIT"
@@ -1687,8 +1638,8 @@
         final String sortOrder = queryArgs.getString(QUERY_ARG_SQL_SORT_ORDER);
         final String limit = queryArgs.getString(QUERY_ARG_SQL_LIMIT);
 
-        final Cursor c = qb.query(db, projection,
-                selection, selectionArgs, groupBy, having, sortOrder, limit, signal);
+        final Cursor c = qb.query(helper, projection, selection, selectionArgs, groupBy, having,
+                sortOrder, limit, signal);
 
         if (c != null) {
             // As a performance optimization, only configure notifications when
@@ -2124,10 +2075,8 @@
         }
 
         final DatabaseHelper helper;
-        final SQLiteDatabase db;
         try {
             helper = getDatabaseForUri(uri);
-            db = helper.getWritableDatabase();
         } catch (VolumeNotFoundException e) {
             return e.translateForUpdateDelete(targetSdkVersion);
         }
@@ -2142,12 +2091,12 @@
         }
     }
 
-    private long insertDirectory(SQLiteDatabase db, String path) {
+    private long insertDirectory(DatabaseHelper helper, String path) {
         if (LOGV) Log.v(TAG, "inserting directory " + path);
         ContentValues values = new ContentValues();
         values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION);
         values.put(FileColumns.DATA, path);
-        values.put(FileColumns.PARENT, getParent(db, path));
+        values.put(FileColumns.PARENT, getParent(helper, path));
         values.put(FileColumns.OWNER_PACKAGE_NAME, extractPathOwnerPackageName(path));
         values.put(FileColumns.VOLUME_NAME, extractVolumeName(path));
         values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path));
@@ -2157,11 +2106,12 @@
         if (file.exists()) {
             values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000);
         }
+        final SQLiteDatabase db = helper.getWritableDatabase();
         long rowId = db.insert("files", FileColumns.DATE_MODIFIED, values);
         return rowId;
     }
 
-    private long getParent(SQLiteDatabase db, String path) {
+    private long getParent(DatabaseHelper helper, String path) {
         final String parentPath = new File(path).getParent();
         if (Objects.equals("/", parentPath)) {
             return -1;
@@ -2174,12 +2124,13 @@
             }
 
             final long id;
+            final SQLiteDatabase db = helper.getReadableDatabase();
             try (Cursor c = db.query("files", new String[] { FileColumns._ID },
                     FileColumns.DATA + "=?", new String[] { parentPath }, null, null, null)) {
                 if (c.moveToFirst()) {
                     id = c.getLong(0);
                 } else {
-                    id = insertDirectory(db, parentPath);
+                    id = insertDirectory(helper, parentPath);
                 }
             }
 
@@ -2325,8 +2276,8 @@
         }
     }
 
-    private long insertFile(@NonNull SQLiteQueryBuilder qb, @NonNull SQLiteDatabase db, int match,
-            @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values,
+    private long insertFile(@NonNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper,
+            int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values,
             int mediaType, boolean notify) {
         boolean wasPathEmpty = !values.containsKey(MediaStore.MediaColumns.DATA)
                 || TextUtils.isEmpty(values.getAsString(MediaStore.MediaColumns.DATA));
@@ -2427,12 +2378,12 @@
             Long parent = values.getAsLong(FileColumns.PARENT);
             if (parent == null) {
                 if (path != null) {
-                    long parentId = getParent(db, path);
+                    long parentId = getParent(helper, path);
                     values.put(FileColumns.PARENT, parentId);
                 }
             }
 
-            rowId = db.insert("files", FileColumns.DATE_MODIFIED, values);
+            rowId = qb.insert(helper, values);
         }
         if (format == MtpConstants.FORMAT_ASSOCIATION) {
             synchronized (mDirectoryCache) {
@@ -2483,7 +2434,15 @@
             @Nullable Bundle extras) {
         Trace.beginSection("insert");
         try {
-            return insertInternal(uri, values, extras);
+            try {
+                return insertInternal(uri, values, extras);
+            } catch (SQLiteConstraintException e) {
+                if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) {
+                    throw e;
+                } else {
+                    return null;
+                }
+            }
         } catch (FallbackException e) {
             return e.translateForInsert(getCallingPackageTargetSdkVersion());
         } finally {
@@ -2577,15 +2536,13 @@
         Uri newUri = null;
 
         final DatabaseHelper helper = getDatabaseForUri(uri);
-        final SQLiteDatabase db = helper.getWritableDatabase();
-
         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_INSERT, match, uri, extras, null);
 
         switch (match) {
             case IMAGES_MEDIA: {
                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
                 final boolean isDownload = maybeMarkAsDownload(initialValues);
-                rowId = insertFile(qb, db, match, uri, extras, initialValues,
+                rowId = insertFile(qb, helper, match, uri, extras, initialValues,
                         FileColumns.MEDIA_TYPE_IMAGE, true);
                 if (rowId > 0) {
                     MediaDocumentsProvider.onMediaStoreInsert(
@@ -2610,7 +2567,7 @@
 
                 ensureUniqueFileColumns(match, uri, extras, initialValues);
 
-                rowId = qb.insert(db, initialValues);
+                rowId = qb.insert(helper, initialValues);
                 if (rowId > 0) {
                     newUri = ContentUris.withAppendedId(Images.Thumbnails.
                             getContentUri(originalVolumeName), rowId);
@@ -2632,7 +2589,7 @@
 
                 ensureUniqueFileColumns(match, uri, extras, initialValues);
 
-                rowId = qb.insert(db, initialValues);
+                rowId = qb.insert(helper, initialValues);
                 if (rowId > 0) {
                     newUri = ContentUris.withAppendedId(Video.Thumbnails.
                             getContentUri(originalVolumeName), rowId);
@@ -2643,7 +2600,7 @@
             case AUDIO_MEDIA: {
                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
                 final boolean isDownload = maybeMarkAsDownload(initialValues);
-                rowId = insertFile(qb, db, match, uri, extras, initialValues,
+                rowId = insertFile(qb, helper, match, uri, extras, initialValues,
                         FileColumns.MEDIA_TYPE_AUDIO, true);
                 if (rowId > 0) {
                     MediaDocumentsProvider.onMediaStoreInsert(
@@ -2663,7 +2620,7 @@
                 final long audioId = Long.parseLong(uri.getPathSegments().get(2));
                 enforceCallingPermission(ContentUris.withAppendedId(
                         MediaStore.Audio.Media.getContentUri(resolvedVolumeName), audioId),
-                        Bundle.EMPTY, true);
+                        Bundle.EMPTY, false);
                 final long playlistId = initialValues
                         .getAsLong(MediaStore.Audio.Playlists.Members.PLAYLIST_ID);
                 enforceCallingPermission(ContentUris.withAppendedId(
@@ -2672,10 +2629,10 @@
 
                 ContentValues values = new ContentValues(initialValues);
                 values.put(Audio.Playlists.Members.AUDIO_ID, audioId);
-                rowId = qb.insert(db, values);
+                rowId = qb.insert(helper, values);
                 if (rowId > 0) {
                     newUri = ContentUris.withAppendedId(uri, rowId);
-                    updatePlaylistDateModifiedToNow(db, playlistId);
+                    updatePlaylistDateModifiedToNow(helper, playlistId);
                 }
                 break;
             }
@@ -2693,7 +2650,7 @@
                 final boolean isDownload = maybeMarkAsDownload(initialValues);
                 ContentValues values = new ContentValues(initialValues);
                 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000);
-                rowId = insertFile(qb, db, match, uri, extras, values,
+                rowId = insertFile(qb, helper, match, uri, extras, values,
                         FileColumns.MEDIA_TYPE_PLAYLIST, true);
                 if (rowId > 0) {
                     newUri = ContentUris.withAppendedId(
@@ -2709,7 +2666,7 @@
                         .getAsLong(MediaStore.Audio.Playlists.Members.AUDIO_ID);
                 enforceCallingPermission(ContentUris.withAppendedId(
                         MediaStore.Audio.Media.getContentUri(resolvedVolumeName), audioId),
-                        Bundle.EMPTY, true);
+                        Bundle.EMPTY, false);
                 final long playlistId = Long.parseLong(uri.getPathSegments().get(3));
                 enforceCallingPermission(ContentUris.withAppendedId(
                         MediaStore.Audio.Playlists.getContentUri(resolvedVolumeName), playlistId),
@@ -2717,10 +2674,10 @@
 
                 ContentValues values = new ContentValues(initialValues);
                 values.put(Audio.Playlists.Members.PLAYLIST_ID, playlistId);
-                rowId = qb.insert(db, values);
+                rowId = qb.insert(helper, values);
                 if (rowId > 0) {
                     newUri = ContentUris.withAppendedId(uri, rowId);
-                    updatePlaylistDateModifiedToNow(db, playlistId);
+                    updatePlaylistDateModifiedToNow(helper, playlistId);
                 }
                 break;
             }
@@ -2728,7 +2685,7 @@
             case VIDEO_MEDIA: {
                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
                 final boolean isDownload = maybeMarkAsDownload(initialValues);
-                rowId = insertFile(qb, db, match, uri, extras, initialValues,
+                rowId = insertFile(qb, helper, match, uri, extras, initialValues,
                         FileColumns.MEDIA_TYPE_VIDEO, true);
                 if (rowId > 0) {
                     MediaDocumentsProvider.onMediaStoreInsert(
@@ -2746,7 +2703,7 @@
 
                 ensureUniqueFileColumns(match, uri, extras, initialValues);
 
-                rowId = qb.insert(db, initialValues);
+                rowId = qb.insert(helper, initialValues);
                 if (rowId > 0) {
                     newUri = ContentUris.withAppendedId(uri, rowId);
                 }
@@ -2756,7 +2713,7 @@
             case FILES: {
                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
                 final boolean isDownload = maybeMarkAsDownload(initialValues);
-                rowId = insertFile(qb, db, match, uri, extras, initialValues,
+                rowId = insertFile(qb, helper, match, uri, extras, initialValues,
                         FileColumns.MEDIA_TYPE_NONE, true);
                 if (rowId > 0) {
                     MediaDocumentsProvider.onMediaStoreInsert(
@@ -2769,7 +2726,7 @@
             case DOWNLOADS:
                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
                 initialValues.put(FileColumns.IS_DOWNLOAD, true);
-                rowId = insertFile(qb, db, match, uri, extras, initialValues,
+                rowId = insertFile(qb, helper, match, uri, extras, initialValues,
                         FileColumns.MEDIA_TYPE_NONE, false);
                 if (rowId > 0) {
                     final int mediaType = initialValues.getAsInteger(FileColumns.MEDIA_TYPE);
@@ -2851,6 +2808,15 @@
         qb.appendWhereStandalone(DatabaseUtils.bindSelection(selection, selectionArgs));
     }
 
+    private static void appendWhereStandaloneFilter(@NonNull SQLiteQueryBuilder qb,
+            @NonNull String[] columns, @Nullable String filter) {
+        if (TextUtils.isEmpty(filter)) return;
+        for (String filterWord : filter.split("\\s+")) {
+            appendWhereStandalone(qb, String.join("||", columns) + " LIKE ? ESCAPE '\\'",
+                    "%" + DatabaseUtils.escapeForLike(Audio.keyFor(filterWord)) + "%");
+        }
+    }
+
     private static boolean parseBoolean(String value) {
         if (value == null) return false;
         if ("1".equals(value)) return true;
@@ -2943,6 +2909,9 @@
         if (matchTrashed == MATCH_DEFAULT) matchTrashed = MATCH_EXCLUDE;
         if (matchFavorite == MATCH_DEFAULT) matchFavorite = MATCH_INCLUDE;
 
+        // Handle callers using legacy filtering
+        final String filter = uri.getQueryParameter("filter");
+
         boolean includeAllVolumes = false;
 
         switch (match) {
@@ -3025,6 +2994,9 @@
                                     + " IN " + sharedPackages
                                     + " OR is_ringtone=1 OR is_alarm=1 OR is_notification=1"));
                 }
+                appendWhereStandaloneFilter(qb, new String[] {
+                        AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
+                }, filter);
                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending);
                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed);
                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite);
@@ -3100,7 +3072,9 @@
                 } else {
                     throw new UnsupportedOperationException("Genres cannot be directly modified");
                 }
-
+                appendWhereStandaloneFilter(qb, new String[] {
+                        AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
+                }, filter);
                 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
                     // We don't have a great way to filter parsed metadata by
                     // owner, so callers need to hold READ_MEDIA_AUDIO
@@ -3162,6 +3136,9 @@
                     qb.setTables("audio_playlists_map");
                     qb.setProjectionMap(getProjectionMap(Audio.Playlists.Members.class));
                 }
+                appendWhereStandaloneFilter(qb, new String[] {
+                        AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
+                }, filter);
                 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
                     // We don't have a great way to filter parsed metadata by
                     // owner, so callers need to hold READ_MEDIA_AUDIO
@@ -3198,6 +3175,9 @@
                 } else {
                     throw new UnsupportedOperationException("Albums cannot be directly modified");
                 }
+                appendWhereStandaloneFilter(qb, new String[] {
+                        AudioColumns.ALBUM_KEY
+                }, filter);
                 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
                     // We don't have a great way to filter parsed metadata by
                     // owner, so callers need to hold READ_MEDIA_AUDIO
@@ -3215,6 +3195,9 @@
                 } else {
                     throw new UnsupportedOperationException("Artists cannot be directly modified");
                 }
+                appendWhereStandaloneFilter(qb, new String[] {
+                        AudioColumns.ARTIST_KEY
+                }, filter);
                 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
                     // We don't have a great way to filter parsed metadata by
                     // owner, so callers need to hold READ_MEDIA_AUDIO
@@ -3232,6 +3215,9 @@
                 } else {
                     throw new UnsupportedOperationException("Albums cannot be directly modified");
                 }
+                appendWhereStandaloneFilter(qb, new String[] {
+                        AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY
+                }, filter);
                 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
                     // We don't have a great way to filter parsed metadata by
                     // owner, so callers need to hold READ_MEDIA_AUDIO
@@ -3329,6 +3315,9 @@
                     appendWhereStandalone(qb, TextUtils.join(" OR ", options));
                 }
 
+                appendWhereStandaloneFilter(qb, new String[] {
+                        AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
+                }, filter);
                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending);
                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed);
                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite);
@@ -3492,11 +3481,9 @@
         }
 
         final DatabaseHelper helper = getDatabaseForUri(uri);
-        final SQLiteDatabase db = helper.getWritableDatabase();
+        final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, match, uri, extras, null);
 
         {
-            final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, match, uri, extras, null);
-
             // Give callers interacting with a specific media item a chance to
             // escalate access if they don't already have it
             switch (match) {
@@ -3518,8 +3505,8 @@
             if (isFilesTable) {
                 String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA);
                 if (deleteparam == null || ! deleteparam.equals("false")) {
-                    Cursor c = qb.query(db, projection, userWhere, userWhereArgs,
-                            null, null, null, null);
+                    Cursor c = qb.query(helper, projection, userWhere, userWhereArgs,
+                            null, null, null, null, null);
                     String [] idvalue = new String[] { "" };
                     String [] playlistvalues = new String[] { "", "" };
                     try {
@@ -3563,6 +3550,7 @@
                                     idvalue[0] = String.valueOf(id);
                                     // for each playlist that the item appears in, move
                                     // all the items behind it forward by one
+                                    final SQLiteDatabase db = helper.getWritableDatabase();
                                     Cursor cc = db.query("audio_playlists_map",
                                                 sPlaylistIdPlayOrder,
                                                 "audio_id=?", idvalue, null, null, null);
@@ -3575,7 +3563,7 @@
                                                     " SET play_order=play_order-1" +
                                                     " WHERE playlist_id=? AND play_order>?",
                                                     playlistvalues);
-                                            updatePlaylistDateModifiedToNow(db, playlistId);
+                                            updatePlaylistDateModifiedToNow(helper, playlistId);
                                         }
                                         db.delete("audio_playlists_map", "audio_id=?", idvalue);
                                     } finally {
@@ -3608,8 +3596,8 @@
                 case VIDEO_THUMBNAILS_ID:
                 case VIDEO_THUMBNAILS:
                     // Delete the referenced files first.
-                    Cursor c = qb.query(db, sDataOnlyColumn, userWhere, userWhereArgs, null, null,
-                            null, null);
+                    Cursor c = qb.query(helper, sDataOnlyColumn, userWhere, userWhereArgs, null,
+                            null, null, null, null);
                     if (c != null) {
                         try {
                             while (c.moveToNext()) {
@@ -3619,18 +3607,18 @@
                             FileUtils.closeQuietly(c);
                         }
                     }
-                    count = deleteRecursive(qb, db, userWhere, userWhereArgs);
+                    count = deleteRecursive(qb, helper, userWhere, userWhereArgs);
                     break;
 
                 case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
                     long playlistId = Long.parseLong(uri.getPathSegments().get(3));
-                    count = deleteRecursive(qb, db, userWhere, userWhereArgs);
+                    count = deleteRecursive(qb, helper, userWhere, userWhereArgs);
                     if (count > 0) {
-                        updatePlaylistDateModifiedToNow(db, playlistId);
+                        updatePlaylistDateModifiedToNow(helper, playlistId);
                     }
                     break;
                 default:
-                    count = deleteRecursive(qb, db, userWhere, userWhereArgs);
+                    count = deleteRecursive(qb, helper, userWhere, userWhereArgs);
                     break;
             }
 
@@ -3662,23 +3650,23 @@
      * can be used to recursively delete all matching entries, since it only
      * deletes parents when no references remaining.
      */
-    private int deleteRecursive(SQLiteQueryBuilder qb, SQLiteDatabase db, String userWhere,
+    private int deleteRecursive(SQLiteQueryBuilder qb, DatabaseHelper helper, String userWhere,
             String[] userWhereArgs) {
         synchronized (mDirectoryCache) {
             mDirectoryCache.clear();
 
-            db.beginTransaction();
+            helper.beginTransaction();
             try {
                 int n = 0;
                 int total = 0;
                 do {
-                    n = qb.delete(db, userWhere, userWhereArgs);
+                    n = qb.delete(helper, userWhere, userWhereArgs);
                     total += n;
                 } while (n > 0);
-                db.setTransactionSuccessful();
+                helper.setTransactionSuccessful();
                 return total;
             } finally {
-                db.endTransaction();
+                helper.endTransaction();
             }
         }
     }
@@ -3750,6 +3738,21 @@
                 res.putString(Intent.EXTRA_TEXT, version);
                 return res;
             }
+            case MediaStore.GET_GENERATION_CALL: {
+                final String volumeName = extras.getString(Intent.EXTRA_TEXT);
+
+                final long generation;
+                try {
+                    generation = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName))
+                            .getGeneration();
+                } catch (VolumeNotFoundException e) {
+                    throw e.rethrowAsIllegalArgumentException();
+                }
+
+                final Bundle res = new Bundle();
+                res.putLong(Intent.EXTRA_INDEX, generation);
+                return res;
+            }
             case MediaStore.GET_DOCUMENT_URI_CALL: {
                 final Uri mediaUri = extras.getParcelable(MediaStore.EXTRA_URI);
                 enforceCallingPermission(mediaUri, extras, false);
@@ -4134,8 +4137,6 @@
         final int match = matchUri(uri, allowHidden);
 
         final DatabaseHelper helper = getDatabaseForUri(uri);
-        final SQLiteDatabase db = helper.getWritableDatabase();
-
         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, match, uri, extras, null);
 
         // Give callers interacting with a specific media item a chance to
@@ -4354,8 +4355,8 @@
         if (triggerInvalidate || triggerScan) {
             Trace.beginSection("snapshot");
             final LocalCallingIdentity token = clearLocalCallingIdentity();
-            try (Cursor c = qb.query(db, new String[] { FileColumns._ID },
-                    userWhere, userWhereArgs, null, null, null)) {
+            try (Cursor c = qb.query(helper, new String[] { FileColumns._ID },
+                    userWhere, userWhereArgs, null, null, null, null, null)) {
                 while (c.moveToNext()) {
                     updatedIds.add(c.getLong(0));
                 }
@@ -4386,16 +4387,16 @@
             case AUDIO_MEDIA_ID_PLAYLISTS_ID:
             case AUDIO_PLAYLISTS_ID:
                 long playlistId = ContentUris.parseId(uri);
-                count = qb.update(db, values, userWhere, userWhereArgs);
+                count = qb.update(helper, values, userWhere, userWhereArgs);
                 if (count > 0) {
-                    updatePlaylistDateModifiedToNow(db, playlistId);
+                    updatePlaylistDateModifiedToNow(helper, playlistId);
                 }
                 break;
             case AUDIO_PLAYLISTS_ID_MEMBERS:
                 long playlistIdMembers = Long.parseLong(uri.getPathSegments().get(3));
-                count = qb.update(db, values, userWhere, userWhereArgs);
+                count = qb.update(helper, values, userWhere, userWhereArgs);
                 if (count > 0) {
-                    updatePlaylistDateModifiedToNow(db, playlistIdMembers);
+                    updatePlaylistDateModifiedToNow(helper, playlistIdMembers);
                 }
                 break;
             case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
@@ -4407,9 +4408,10 @@
                         List <String> segments = uri.getPathSegments();
                         long playlist = Long.parseLong(segments.get(3));
                         int oldpos = Integer.parseInt(segments.get(5));
-                        int rowsChanged = movePlaylistEntry(volumeName, helper, db, playlist, oldpos, newpos);
+                        int rowsChanged = movePlaylistEntry(volumeName, helper,
+                                playlist, oldpos, newpos);
                         if (rowsChanged > 0) {
-                            updatePlaylistDateModifiedToNow(db, playlist);
+                            updatePlaylistDateModifiedToNow(helper, playlist);
                         }
 
                         return rowsChanged;
@@ -4419,7 +4421,7 @@
                 }
                 // fall through
             default:
-                count = qb.update(db, values, userWhere, userWhereArgs);
+                count = qb.update(helper, values, userWhere, userWhereArgs);
                 break;
         }
 
@@ -4457,11 +4459,12 @@
         return count;
     }
 
-    private int movePlaylistEntry(String volumeName, DatabaseHelper helper, SQLiteDatabase db,
+    private int movePlaylistEntry(String volumeName, DatabaseHelper helper,
             long playlist, int from, int to) {
         if (from == to) {
             return 0;
         }
+        final SQLiteDatabase db = helper.getWritableDatabase();
         db.beginTransaction();
         int numlines = 0;
         Cursor c = null;
@@ -4515,19 +4518,18 @@
         return numlines;
     }
 
-    private void updatePlaylistDateModifiedToNow(SQLiteDatabase database, long playlistId) {
+    private void updatePlaylistDateModifiedToNow(DatabaseHelper helper, long playlistId) {
         ContentValues values = new ContentValues();
         values.put(
                 FileColumns.DATE_MODIFIED,
                 TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
         );
 
-        database.update(
-                MediaStore.Files.TABLE,
-                values,
-                MediaStore.Files.FileColumns._ID + "=?",
-                new String[]{String.valueOf(playlistId)}
-        );
+        final Uri uri = ContentUris.withAppendedId(
+                MediaStore.Audio.Playlists.getContentUri(helper.mVolumeName), playlistId);
+        final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, AUDIO_PLAYLISTS_ID,
+                uri, Bundle.EMPTY, null);
+        qb.update(helper, values, null, null);
     }
 
     @Override
@@ -4971,10 +4973,15 @@
     private boolean shouldBypassFuseRestrictions(boolean forWrite) {
         boolean isRequestingLegacyStorage = forWrite ? isCallingPackageLegacyWrite()
                 : isCallingPackageLegacyRead();
+        if (isRequestingLegacyStorage) {
+            return true;
+        }
 
-        // TODO(b/137755945): We should let file managers bypass FUSE restrictions as well.
-        //  Remember to change the documentation above when this is addressed.
-        return isRequestingLegacyStorage;
+        if (mCallingIdentity.get().hasPermission(PERMISSION_MANAGE_EXTERNAL_STORAGE)) {
+            return true;
+        }
+
+        return false;
     }
 
     /**
@@ -5033,6 +5040,16 @@
         }
     }
 
+    @Nullable
+    private String getAbsoluteSanitizedPath(String path) {
+        final String[] pathSegments = sanitizePath(path);
+        if (pathSegments.length == 0) {
+            return null;
+        }
+        return path = "/" + String.join("/",
+                Arrays.copyOfRange(pathSegments, 1, pathSegments.length));
+    }
+
     /**
      * Calculates the ranges that need to be redacted for the given file and user that wants to
      * access the file.
@@ -5066,6 +5083,12 @@
                 return res;
             }
 
+            // TODO(b/147741933): Quick fix. Add tests
+            path = getAbsoluteSanitizedPath(path);
+            if (path == null) {
+                throw new IOException("Invalid path " + path);
+            }
+
             final Uri contentUri = Files.getContentUri(MediaStore.getVolumeName(new File(path)));
             final String[] projection = new String[]{
                     MediaColumns.OWNER_PACKAGE_NAME, MediaColumns._ID };
@@ -5197,6 +5220,13 @@
                 return -OsConstants.EACCES;
             }
 
+            // TODO(b/147741933): Quick fix. Add tests
+            path = getAbsoluteSanitizedPath(path);
+            if (path == null) {
+                Log.e(TAG, "Invalid path " + path);
+                return -OsConstants.EPERM;
+            }
+
             final Uri contentUri = Files.getContentUri(MediaStore.getVolumeName(new File(path)));
             final String[] projection = new String[]{
                     MediaColumns._ID,
@@ -5677,10 +5707,8 @@
         }
 
         final DatabaseHelper helper;
-        final SQLiteDatabase db;
         try {
             helper = getDatabaseForUri(uri);
-            db = helper.getReadableDatabase();
         } catch (VolumeNotFoundException e) {
             throw e.rethrowAsIllegalArgumentException();
         }
@@ -5691,7 +5719,8 @@
         // First, check to see if caller has direct write access
         if (forWrite) {
             final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, table, uri, extras, null);
-            try (Cursor c = qb.query(db, new String[0], null, null, null, null, null)) {
+            try (Cursor c = qb.query(helper, new String[0],
+                    null, null, null, null, null, null, null)) {
                 if (c.moveToFirst()) {
                     // Direct write access granted, yay!
                     return;
@@ -5713,7 +5742,8 @@
 
         // Second, check to see if caller has direct read access
         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, extras, null);
-        try (Cursor c = qb.query(db, new String[0], null, null, null, null, null)) {
+        try (Cursor c = qb.query(helper, new String[0],
+                null, null, null, null, null, null, null)) {
             if (c.moveToFirst()) {
                 if (!forWrite) {
                     // Direct read access granted, yay!
@@ -5934,12 +5964,13 @@
         }
 
         final Uri uri = MediaStore.AUTHORITY_URI.buildUpon().appendPath(volume).build();
-        getContext().getContentResolver().notifyChange(uri, null);
+        final DatabaseHelper helper = MediaStore.VOLUME_INTERNAL.equals(volume)
+                ? mInternalDatabase : mExternalDatabase;
+        acceptWithExpansion(helper::notifyChange, uri);
         if (LOGV) Log.v(TAG, "Attached volume: " + volume);
         if (!MediaStore.VOLUME_INTERNAL.equals(volume)) {
             BackgroundThread.getExecutor().execute(() -> {
-                final DatabaseHelper helper = mExternalDatabase;
-                ensureDefaultFolders(volume, helper, helper.getWritableDatabase());
+                ensureDefaultFolders(volume, helper);
             });
         }
         return uri;
@@ -5971,7 +6002,9 @@
         }
 
         final Uri uri = MediaStore.AUTHORITY_URI.buildUpon().appendPath(volume).build();
-        getContext().getContentResolver().notifyChange(uri, null);
+        final DatabaseHelper helper = MediaStore.VOLUME_INTERNAL.equals(volume)
+                ? mInternalDatabase : mExternalDatabase;
+        acceptWithExpansion(helper::notifyChange, uri);
         if (LOGV) Log.v(TAG, "Detached volume: " + volume);
     }
 
diff --git a/src/com/android/providers/media/PrioritizedFutureTask.java b/src/com/android/providers/media/PrioritizedFutureTask.java
deleted file mode 100644
index 7c7fd33..0000000
--- a/src/com/android/providers/media/PrioritizedFutureTask.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2018 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;
-
-import android.os.SystemClock;
-
-import java.util.concurrent.Callable;
-import java.util.concurrent.FutureTask;
-
-public class PrioritizedFutureTask<T> extends FutureTask<T>
-        implements Comparable<PrioritizedFutureTask<T>> {
-    static final int PRIORITY_LOW = 20;
-    static final int PRIORITY_NORMAL = 10;
-    static final int PRIORITY_HIGH = 5;
-    static final int PRIORITY_CRITICAL = 0;
-
-    final long requestTime;
-    final int priority;
-
-    public PrioritizedFutureTask(Callable<T> callable, int priority) {
-        super(callable);
-        this.requestTime = SystemClock.elapsedRealtime();
-        this.priority = priority;
-    }
-
-    @Override
-    public final int compareTo(PrioritizedFutureTask<T> other) {
-        if (this.priority != other.priority) {
-            return this.priority < other.priority ? -1 : 1;
-        }
-        if (this.requestTime != other.requestTime) {
-            return this.requestTime < other.requestTime ? -1 : 1;
-        }
-        return 0;
-    }
-}
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index 58c453a..aef7376 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -86,6 +86,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.providers.media.util.DatabaseUtils;
 import com.android.providers.media.util.ExifUtils;
 import com.android.providers.media.util.FileUtils;
 import com.android.providers.media.util.IsoInterface;
@@ -263,7 +264,7 @@
         private final Uri mFilesUri;
         private final CancellationSignal mSignal;
 
-        private final long mStartCurrentTime;
+        private final long mStartGeneration;
         private final boolean mSingleFile;
         private final Set<Path> mAcquiredDirectoryLocks = new ArraySet<>();
         private final ArrayList<ContentProviderOperation> mPending = new ArrayList<>();
@@ -291,7 +292,7 @@
             mFilesUri = MediaStore.Files.getContentUri(mVolumeName);
             mSignal = getOrCreateSignal(mVolumeName);
 
-            mStartCurrentTime = System.currentTimeMillis();
+            mStartGeneration = MediaStore.getGeneration(mResolver, mVolumeName);
             mSingleFile = mRoot.isFile();
 
             Trace.endSection();
@@ -362,12 +363,12 @@
                     + MtpConstants.FORMAT_UNDEFINED + ") != "
                     + MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST;
             final String dataClause = FileColumns.DATA + " LIKE ? ESCAPE '\\'";
-            final String addedClause = "ifnull(" + FileColumns.DATE_ADDED + ",0) < "
-                    + mStartCurrentTime;
+            final String generationClause = FileColumns.GENERATION_ADDED + " <= "
+                    + mStartGeneration;
 
             final Bundle queryArgs = new Bundle();
             queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
-                    formatClause + " AND " + dataClause + " AND " + addedClause);
+                    formatClause + " AND " + dataClause + " AND " + generationClause);
             queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
                     new String[] { escapeForLike(mRoot.getAbsolutePath(), mSingleFile) });
             queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER,
@@ -1263,19 +1264,12 @@
      * Escape the given argument for use in a {@code LIKE} statement.
      */
     static String escapeForLike(String arg, boolean singleFile) {
-        final StringBuilder sb = new StringBuilder();
-        for (int i = 0; i < arg.length(); i++) {
-            final char c = arg.charAt(i);
-            switch (c) {
-                case '%': sb.append('\\');
-                case '_': sb.append('\\');
-            }
-            sb.append(c);
+        final String escaped = DatabaseUtils.escapeForLike(arg);
+        if (singleFile) {
+            return escaped;
+        } else {
+            return escaped + "/%";
         }
-        if (!singleFile) {
-            sb.append("/%");
-        }
-        return sb.toString();
     }
 
     static void logTroubleScanning(File file, Exception e) {
diff --git a/src/com/android/providers/media/util/DatabaseUtils.java b/src/com/android/providers/media/util/DatabaseUtils.java
index b5ad720..6fd35c7 100644
--- a/src/com/android/providers/media/util/DatabaseUtils.java
+++ b/src/com/android/providers/media/util/DatabaseUtils.java
@@ -445,43 +445,55 @@
         return res;
     }
 
-    public static int executeSql(@NonNull SQLiteDatabase db, @NonNull String sql,
+    public static long executeInsert(@NonNull SQLiteDatabase db, @NonNull String sql,
             @Nullable Object[] bindArgs) throws SQLException {
         try (SQLiteStatement st = db.compileStatement(sql)) {
-            if (bindArgs != null) {
-                for (int i = 0; i < bindArgs.length; i++) {
-                    final Object bindArg = bindArgs[i];
-                    switch (DatabaseUtils.getTypeOfObject(bindArg)) {
-                        case Cursor.FIELD_TYPE_NULL:
-                            st.bindNull(i + 1);
-                            break;
-                        case Cursor.FIELD_TYPE_INTEGER:
-                            st.bindLong(i + 1, ((Number) bindArg).longValue());
-                            break;
-                        case Cursor.FIELD_TYPE_FLOAT:
-                            st.bindDouble(i + 1, ((Number) bindArg).doubleValue());
-                            break;
-                        case Cursor.FIELD_TYPE_BLOB:
-                            st.bindBlob(i + 1, (byte[]) bindArg);
-                            break;
-                        case Cursor.FIELD_TYPE_STRING:
-                        default:
-                            if (bindArg instanceof Boolean) {
-                                // Provide compatibility with legacy
-                                // applications which may pass Boolean values in
-                                // bind args.
-                                st.bindLong(i + 1, ((Boolean) bindArg).booleanValue() ? 1 : 0);
-                            } else {
-                                st.bindString(i + 1, bindArg.toString());
-                            }
-                            break;
-                    }
-                }
-            }
+            bindArgs(st, bindArgs);
+            return st.executeInsert();
+        }
+    }
+
+    public static int executeUpdateDelete(@NonNull SQLiteDatabase db, @NonNull String sql,
+            @Nullable Object[] bindArgs) throws SQLException {
+        try (SQLiteStatement st = db.compileStatement(sql)) {
+            bindArgs(st, bindArgs);
             return st.executeUpdateDelete();
         }
     }
 
+    private static void bindArgs(@NonNull SQLiteStatement st, @Nullable Object[] bindArgs) {
+        if (bindArgs == null) return;
+
+        for (int i = 0; i < bindArgs.length; i++) {
+            final Object bindArg = bindArgs[i];
+            switch (getTypeOfObject(bindArg)) {
+                case Cursor.FIELD_TYPE_NULL:
+                    st.bindNull(i + 1);
+                    break;
+                case Cursor.FIELD_TYPE_INTEGER:
+                    st.bindLong(i + 1, ((Number) bindArg).longValue());
+                    break;
+                case Cursor.FIELD_TYPE_FLOAT:
+                    st.bindDouble(i + 1, ((Number) bindArg).doubleValue());
+                    break;
+                case Cursor.FIELD_TYPE_BLOB:
+                    st.bindBlob(i + 1, (byte[]) bindArg);
+                    break;
+                case Cursor.FIELD_TYPE_STRING:
+                default:
+                    if (bindArg instanceof Boolean) {
+                        // Provide compatibility with legacy
+                        // applications which may pass Boolean values in
+                        // bind args.
+                        st.bindLong(i + 1, ((Boolean) bindArg).booleanValue() ? 1 : 0);
+                    } else {
+                        st.bindString(i + 1, bindArg.toString());
+                    }
+                    break;
+            }
+        }
+    }
+
     public static @NonNull String bindList(@NonNull Object... args) {
         final StringBuilder sb = new StringBuilder();
         sb.append('(');
@@ -494,4 +506,20 @@
         sb.append(')');
         return DatabaseUtils.bindSelection(sb.toString(), args);
     }
+
+    /**
+     * Escape the given argument for use in a {@code LIKE} statement.
+     */
+    public static String escapeForLike(@NonNull String arg) {
+        final StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < arg.length(); i++) {
+            final char c = arg.charAt(i);
+            switch (c) {
+                case '%': sb.append('\\');
+                case '_': sb.append('\\');
+            }
+            sb.append(c);
+        }
+        return sb.toString();
+    }
 }
diff --git a/src/com/android/providers/media/util/PermissionUtils.java b/src/com/android/providers/media/util/PermissionUtils.java
index 732b0ce..0786c88 100644
--- a/src/com/android/providers/media/util/PermissionUtils.java
+++ b/src/com/android/providers/media/util/PermissionUtils.java
@@ -22,6 +22,7 @@
 import static android.Manifest.permission.WRITE_MEDIA_STORAGE;
 import static android.app.AppOpsManager.MODE_ALLOWED;
 import static android.app.AppOpsManager.OPSTR_LEGACY_STORAGE;
+import static android.app.AppOpsManager.OPSTR_MANAGE_EXTERNAL_STORAGE;
 import static android.app.AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE;
 import static android.app.AppOpsManager.OPSTR_READ_MEDIA_AUDIO;
 import static android.app.AppOpsManager.OPSTR_READ_MEDIA_IMAGES;
@@ -32,6 +33,7 @@
 import static android.app.AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 
+import android.annotation.NonNull;
 import android.app.AppOpsManager;
 import android.content.Context;
 
@@ -68,6 +70,11 @@
         return context.checkPermission(BACKUP, pid, uid) == PERMISSION_GRANTED;
     }
 
+    public static boolean checkPermissionManageExternalStorage(Context context, int pid, int uid,
+            String packageName) {
+        return hasAppOpPermission(context, pid, uid, packageName, OPSTR_MANAGE_EXTERNAL_STORAGE);
+    }
+
     public static boolean checkPermissionWriteStorage(Context context,
             int pid, int uid, String packageName) {
         return checkPermissionAndAppOp(context, pid,
@@ -160,6 +167,23 @@
         }
     }
 
+    /**
+     * Checks if calling app is allowed the app-op. If its app-op mode is
+     * {@link AppOpsManager#MODE_DEFAULT} then it falls back checking the appropriate permission for
+     * the app-op. The permissions is retrieved from {@link AppOpsManager#opToPermission(String)}.
+     */
+    private static boolean hasAppOpPermission(@NonNull Context context, int pid, int uid,
+            @NonNull String packageName, @NonNull String op) {
+        final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
+        final int mode = appOps.noteOpNoThrow(op, uid, packageName, null, null);
+        if (mode == AppOpsManager.MODE_DEFAULT) {
+            final String permission = AppOpsManager.opToPermission(op);
+            return permission != null
+                    && context.checkPermission(permission, pid, uid) == PERMISSION_GRANTED;
+        }
+        return mode == AppOpsManager.MODE_ALLOWED;
+    }
+
     private static boolean noteAppOpAllowingLegacy(Context context,
             int pid, int uid, String packageName, String op) {
         final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
diff --git a/src/com/android/providers/media/util/RedactingFileDescriptor.java b/src/com/android/providers/media/util/RedactingFileDescriptor.java
index 09aef01..459c629 100644
--- a/src/com/android/providers/media/util/RedactingFileDescriptor.java
+++ b/src/com/android/providers/media/util/RedactingFileDescriptor.java
@@ -73,85 +73,6 @@
     }
 
     /** {@hide} */
-    public static int translateModeStringToPosix(String mode) {
-        // Sanity check for invalid chars
-        for (int i = 0; i < mode.length(); i++) {
-            switch (mode.charAt(i)) {
-                case 'r':
-                case 'w':
-                case 't':
-                case 'a':
-                    break;
-                default:
-                    throw new IllegalArgumentException("Bad mode: " + mode);
-            }
-        }
-
-        int res = 0;
-        if (mode.startsWith("rw")) {
-            res = O_RDWR | O_CREAT;
-        } else if (mode.startsWith("w")) {
-            res = O_WRONLY | O_CREAT;
-        } else if (mode.startsWith("r")) {
-            res = O_RDONLY;
-        } else {
-            throw new IllegalArgumentException("Bad mode: " + mode);
-        }
-        if (mode.indexOf('t') != -1) {
-            res |= O_TRUNC;
-        }
-        if (mode.indexOf('a') != -1) {
-            res |= O_APPEND;
-        }
-        return res;
-    }
-
-    /** {@hide} */
-    public static String translateModePosixToString(int mode) {
-        String res = "";
-        if ((mode & O_ACCMODE) == O_RDWR) {
-            res = "rw";
-        } else if ((mode & O_ACCMODE) == O_WRONLY) {
-            res = "w";
-        } else if ((mode & O_ACCMODE) == O_RDONLY) {
-            res = "r";
-        } else {
-            throw new IllegalArgumentException("Bad mode: " + mode);
-        }
-        if ((mode & O_TRUNC) == O_TRUNC) {
-            res += "t";
-        }
-        if ((mode & O_APPEND) == O_APPEND) {
-            res += "a";
-        }
-        return res;
-    }
-
-    /** {@hide} */
-    public static int translateModePosixToPfd(int mode) {
-        int res = 0;
-        if ((mode & O_ACCMODE) == O_RDWR) {
-            res = MODE_READ_WRITE;
-        } else if ((mode & O_ACCMODE) == O_WRONLY) {
-            res = MODE_WRITE_ONLY;
-        } else if ((mode & O_ACCMODE) == O_RDONLY) {
-            res = MODE_READ_ONLY;
-        } else {
-            throw new IllegalArgumentException("Bad mode: " + mode);
-        }
-        if ((mode & O_CREAT) == O_CREAT) {
-            res |= MODE_CREATE;
-        }
-        if ((mode & O_TRUNC) == O_TRUNC) {
-            res |= MODE_TRUNCATE;
-        }
-        if ((mode & O_APPEND) == O_APPEND) {
-            res |= MODE_APPEND;
-        }
-        return res;
-    }
-
-    /** {@hide} */
     public static int translateModePfdToPosix(int mode) {
         int res = 0;
         if ((mode & MODE_READ_WRITE) == MODE_READ_WRITE) {
@@ -175,23 +96,6 @@
         return res;
     }
 
-    /** {@hide} */
-    public static int translateModeAccessToPosix(int mode) {
-        if (mode == F_OK) {
-            // There's not an exact mapping, so we attempt a read-only open to
-            // determine if a file exists
-            return O_RDONLY;
-        } else if ((mode & (R_OK | W_OK)) == (R_OK | W_OK)) {
-            return O_RDWR;
-        } else if ((mode & R_OK) == R_OK) {
-            return O_RDONLY;
-        } else if ((mode & W_OK) == W_OK) {
-            return O_WRONLY;
-        } else {
-            throw new IllegalArgumentException("Bad mode: " + mode);
-        }
-    }
-
     private RedactingFileDescriptor(
             Context context, File file, int mode, long[] redactRanges, long[] freeOffsets)
             throws IOException {
diff --git a/src/com/android/providers/media/util/SQLiteQueryBuilder.java b/src/com/android/providers/media/util/SQLiteQueryBuilder.java
index d972c40..fac5112 100644
--- a/src/com/android/providers/media/util/SQLiteQueryBuilder.java
+++ b/src/com/android/providers/media/util/SQLiteQueryBuilder.java
@@ -25,10 +25,13 @@
 import android.os.CancellationSignal;
 import android.os.OperationCanceledException;
 import android.provider.BaseColumns;
+import android.provider.MediaStore.MediaColumns;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
 
+import com.android.providers.media.DatabaseHelper;
+
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Iterator;
@@ -387,80 +390,11 @@
         s.append(' ');
     }
 
-    /**
-     * Perform a query by combining all current settings and the
-     * information passed into this method.
-     *
-     * @param db the database to query on
-     * @param projectionIn A list of which columns to return. Passing
-     *   null will return all columns, which is discouraged to prevent
-     *   reading data from storage that isn't going to be used.
-     * @param selection A filter declaring which rows to return,
-     *   formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
-     *   itself). Passing null will return all rows for the given URL.
-     * @param selectionArgs You may include ?s in selection, which
-     *   will be replaced by the values from selectionArgs, in order
-     *   that they appear in the selection. The values will be bound
-     *   as Strings.
-     * @param groupBy A filter declaring how to group rows, formatted
-     *   as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY}
-     *   itself). Passing null will cause the rows to not be grouped.
-     * @param having A filter declare which row groups to include in
-     *   the cursor, if row grouping is being used, formatted as an
-     *   SQL {@code HAVING} clause (excluding the {@code HAVING} itself).  Passing
-     *   null will cause all row groups to be included, and is
-     *   required when row grouping is not being used.
-     * @param sortOrder How to order the rows, formatted as an SQL
-     *   {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
-     *   will use the default sort order, which may be unordered.
-     * @return a cursor over the result set
-     * @see android.content.ContentResolver#query(android.net.Uri, String[],
-     *      String, String[], String)
-     */
-    public Cursor query(SQLiteDatabase db, String[] projectionIn,
+    public Cursor query(DatabaseHelper helper, String[] projectionIn,
             String selection, String[] selectionArgs, String groupBy,
-            String having, String sortOrder) {
-        return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder,
-                null /* limit */, null /* cancellationSignal */);
-    }
-
-    /**
-     * Perform a query by combining all current settings and the
-     * information passed into this method.
-     *
-     * @param db the database to query on
-     * @param projectionIn A list of which columns to return. Passing
-     *   null will return all columns, which is discouraged to prevent
-     *   reading data from storage that isn't going to be used.
-     * @param selection A filter declaring which rows to return,
-     *   formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
-     *   itself). Passing null will return all rows for the given URL.
-     * @param selectionArgs You may include ?s in selection, which
-     *   will be replaced by the values from selectionArgs, in order
-     *   that they appear in the selection. The values will be bound
-     *   as Strings.
-     * @param groupBy A filter declaring how to group rows, formatted
-     *   as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY}
-     *   itself). Passing null will cause the rows to not be grouped.
-     * @param having A filter declare which row groups to include in
-     *   the cursor, if row grouping is being used, formatted as an
-     *   SQL {@code HAVING} clause (excluding the {@code HAVING} itself).  Passing
-     *   null will cause all row groups to be included, and is
-     *   required when row grouping is not being used.
-     * @param sortOrder How to order the rows, formatted as an SQL
-     *   {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
-     *   will use the default sort order, which may be unordered.
-     * @param limit Limits the number of rows returned by the query,
-     *   formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
-     * @return a cursor over the result set
-     * @see android.content.ContentResolver#query(android.net.Uri, String[],
-     *      String, String[], String)
-     */
-    public Cursor query(SQLiteDatabase db, String[] projectionIn,
-            String selection, String[] selectionArgs, String groupBy,
-            String having, String sortOrder, String limit) {
-        return query(db, projectionIn, selection, selectionArgs,
-                groupBy, having, sortOrder, limit, null);
+            String having, String sortOrder, String limit, CancellationSignal cancellationSignal) {
+        return query(helper.getReadableDatabase(), projectionIn, selection, selectionArgs, groupBy,
+                having, sortOrder, limit, cancellationSignal);
     }
 
     /**
@@ -555,6 +489,14 @@
                 cancellationSignal); // will throw if query is invalid
     }
 
+    public long insert(@NonNull DatabaseHelper helper, @NonNull ContentValues values) {
+        // We force wrap in a transaction to ensure that all mutations increment
+        // the generation counter
+        return (int) helper.runWithTransaction(() -> {
+            return insert(helper.getWritableDatabase(), values);
+        });
+    }
+
     /**
      * Perform an insert by combining all current settings and the
      * information passed into this method.
@@ -587,7 +529,16 @@
                 Log.d(TAG, sql);
             }
         }
-        return com.android.providers.media.util.DatabaseUtils.executeSql(db, sql, sqlArgs);
+        return com.android.providers.media.util.DatabaseUtils.executeInsert(db, sql, sqlArgs);
+    }
+
+    public int update(@NonNull DatabaseHelper helper, @NonNull ContentValues values,
+            @Nullable String selection, @Nullable String[] selectionArgs) {
+        // We force wrap in a transaction to ensure that all mutations increment
+        // the generation counter
+        return (int) helper.runWithTransaction(() -> {
+            return update(helper.getWritableDatabase(), values, selection, selectionArgs);
+        });
     }
 
     /**
@@ -664,7 +615,16 @@
                 Log.d(TAG, sql);
             }
         }
-        return com.android.providers.media.util.DatabaseUtils.executeSql(db, sql, sqlArgs);
+        return com.android.providers.media.util.DatabaseUtils.executeUpdateDelete(db, sql, sqlArgs);
+    }
+
+    public int delete(@NonNull DatabaseHelper helper, @Nullable String selection,
+            @Nullable String[] selectionArgs) {
+        // We force wrap in a transaction to ensure that all mutations increment
+        // the generation counter
+        return (int) helper.runWithTransaction(() -> {
+            return delete(helper.getWritableDatabase(), selection, selectionArgs);
+        });
     }
 
     /**
@@ -724,7 +684,7 @@
                 Log.d(TAG, sql);
             }
         }
-        return com.android.providers.media.util.DatabaseUtils.executeSql(db, sql, sqlArgs);
+        return com.android.providers.media.util.DatabaseUtils.executeUpdateDelete(db, sql, sqlArgs);
     }
 
     private void enforceStrictColumns(@Nullable String[] projection) {
@@ -837,6 +797,12 @@
         sql.append(SQLiteDatabase.findEditTable(mTables));
         sql.append(" (");
 
+        final boolean hasGeneration = Objects.equals(mTables, "files");
+        if (hasGeneration) {
+            values.remove(MediaColumns.GENERATION_ADDED);
+            values.remove(MediaColumns.GENERATION_MODIFIED);
+        }
+
         final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils
                 .getValues(values);
         for (int i = 0; i < rawValues.size(); i++) {
@@ -845,6 +811,12 @@
             }
             sql.append(rawValues.keyAt(i));
         }
+        if (hasGeneration) {
+            sql.append(',');
+            sql.append(MediaColumns.GENERATION_ADDED);
+            sql.append(',');
+            sql.append(MediaColumns.GENERATION_MODIFIED);
+        }
         sql.append(") VALUES (");
         for (int i = 0; i < rawValues.size(); i++) {
             if (i > 0) {
@@ -852,6 +824,16 @@
             }
             sql.append('?');
         }
+        if (hasGeneration) {
+            sql.append(',');
+            sql.append('(');
+            sql.append(DatabaseHelper.CURRENT_GENERATION_CLAUSE);
+            sql.append(')');
+            sql.append(',');
+            sql.append('(');
+            sql.append(DatabaseHelper.CURRENT_GENERATION_CLAUSE);
+            sql.append(')');
+        }
         sql.append(")");
         return sql.toString();
     }
@@ -867,6 +849,12 @@
         sql.append(SQLiteDatabase.findEditTable(mTables));
         sql.append(" SET ");
 
+        final boolean hasGeneration = Objects.equals(mTables, "files");
+        if (hasGeneration) {
+            values.remove(MediaColumns.GENERATION_ADDED);
+            values.remove(MediaColumns.GENERATION_MODIFIED);
+        }
+
         final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils
                 .getValues(values);
         for (int i = 0; i < rawValues.size(); i++) {
@@ -876,6 +864,14 @@
             sql.append(rawValues.keyAt(i));
             sql.append("=?");
         }
+        if (hasGeneration) {
+            sql.append(',');
+            sql.append(MediaColumns.GENERATION_MODIFIED);
+            sql.append('=');
+            sql.append('(');
+            sql.append(DatabaseHelper.CURRENT_GENERATION_CLAUSE);
+            sql.append(')');
+        }
 
         final String where = computeWhere(selection);
         appendClause(sql, " WHERE ", where);
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 32e5fe3..04e2b5c 100644
--- a/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
+++ b/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
@@ -210,6 +210,7 @@
         executeShellCommand("pm clear " + modernProvider.applicationInfo.packageName, ui);
 
         // And force a scan to confirm upgraded data survives
+        MediaStore.waitForIdle(context.getContentResolver());
         MediaStore.scanVolume(context.getContentResolver(),
                 MediaStore.getVolumeName(collectionUri));
 
diff --git a/tests/jni/Android.bp b/tests/jni/Android.bp
deleted file mode 100644
index 54a6642..0000000
--- a/tests/jni/Android.bp
+++ /dev/null
@@ -1,10 +0,0 @@
-cc_test {
-    name: "RedactionInfoTest",
-    test_suites: ["device-tests"],
-    srcs: ["RedactionInfoTest.cpp"],
-    cflags: ["-Wall", "-Werror"],
-    shared_libs: [
-        "libfuse_jni",
-    ],
-    test_config: "RedactionInfoTest.xml",
-}
\ No newline at end of file
diff --git a/tests/jni/FuseDaemonTest/Android.bp b/tests/jni/FuseDaemonTest/Android.bp
index 70c4a78..2f0c78f 100644
--- a/tests/jni/FuseDaemonTest/Android.bp
+++ b/tests/jni/FuseDaemonTest/Android.bp
@@ -31,7 +31,7 @@
     manifest: "AndroidManifest.xml",
     srcs: ["src/**/*.java"],
     static_libs: ["androidx.test.rules", "truth-prebuilt", "tests-fusedaemon-lib"],
-    test_suites: ["general-tests"],
+    test_suites: ["general-tests", "mts"],
     sdk_version: "test_current",
     java_resources: [
         ":TestAppA",
@@ -44,7 +44,7 @@
     manifest: "legacy/AndroidManifest.xml",
     srcs: ["legacy/src/**/*.java"],
     static_libs: ["androidx.test.rules", "truth-prebuilt"],
-    test_suites: ["general-tests"],
+    test_suites: ["general-tests", "mts"],
     sdk_version: "29"
 }
 
@@ -53,6 +53,6 @@
     srcs: ["host/src/**/*.java"],
     libs: ["tradefed"],
     static_libs: ["testng"],
-    test_suites: ["general-tests"],
+    test_suites: ["general-tests", "mts"],
     test_config: "FuseDaemonHostTest.xml",
 }
diff --git a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
index 391ef1c..46203dc 100644
--- a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
+++ b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
@@ -183,4 +183,9 @@
     public void testRenameEmptyDirectory() throws Exception {
         runDeviceTest("testRenameEmptyDirectory");
     }
+
+    @Test
+    public void testManageExternalStorageBypassesMediaProviderRestrictions() throws Exception {
+        runDeviceTest("testManageExternalStorageBypassesMediaProviderRestrictions");
+    }
 }
diff --git a/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java b/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java
index b4202e5..f831dce 100644
--- a/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java
+++ b/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java
@@ -96,6 +96,14 @@
         }
     }
 
+    public static void adoptShellPermissionIdentity(String... permissions) {
+        sUiAutomation.adoptShellPermissionIdentity(permissions);
+    }
+
+    public static void dropShellPermissionIdentity() {
+        sUiAutomation.dropShellPermissionIdentity();
+    }
+
     public static String executeShellCommand(String cmd) throws Exception {
         try (FileInputStream output = new FileInputStream (sUiAutomation.executeShellCommand(cmd)
                 .getFileDescriptor())) {
diff --git a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
index 793aaf7..7cc1b20 100644
--- a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
+++ b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
@@ -25,9 +25,11 @@
 import static com.android.tests.fused.lib.RedactionTestHelper.assertExifMetadataMismatch;
 import static com.android.tests.fused.lib.RedactionTestHelper.getExifMetadata;
 import static com.android.tests.fused.lib.RedactionTestHelper.getExifMetadataFromRawResource;
+import static com.android.tests.fused.lib.TestUtils.adoptShellPermissionIdentity;
 import static com.android.tests.fused.lib.TestUtils.assertThrows;
 import static com.android.tests.fused.lib.TestUtils.createFileAs;
 import static com.android.tests.fused.lib.TestUtils.deleteFileAs;
+import static com.android.tests.fused.lib.TestUtils.dropShellPermissionIdentity;
 import static com.android.tests.fused.lib.TestUtils.executeShellCommand;
 import static com.android.tests.fused.lib.TestUtils.getFileMimeTypeFromDatabase;
 import static com.android.tests.fused.lib.TestUtils.getFileRowIdFromDatabase;
@@ -40,6 +42,7 @@
 
 import static org.junit.Assume.assumeTrue;
 
+import android.Manifest;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
@@ -1006,6 +1009,16 @@
         }
     }
 
+    @Test
+    public void testManageExternalStorageBypassesMediaProviderRestrictions() throws Exception {
+        adoptShellPermissionIdentity(Manifest.permission.MANAGE_EXTERNAL_STORAGE);
+        try {
+            assertCanCreateFile(new File(EXTERNAL_STORAGE_DIR, NONMEDIA_FILE_NAME));
+        } finally {
+            dropShellPermissionIdentity();
+        }
+    }
+
     private void deleteWithMediaProvider(String relativePath, String displayName) throws Exception {
         String selection = MediaColumns.RELATIVE_PATH + " = ? AND "
                 + MediaColumns.DISPLAY_NAME + " = ?";
diff --git a/tests/src/com/android/providers/media/DatabaseHelperTest.java b/tests/src/com/android/providers/media/DatabaseHelperTest.java
index eb1e133..a3936e5 100644
--- a/tests/src/com/android/providers/media/DatabaseHelperTest.java
+++ b/tests/src/com/android/providers/media/DatabaseHelperTest.java
@@ -154,6 +154,22 @@
     }
 
     @Test
+    public void testTransactions() throws Exception {
+        try (DatabaseHelper helper = new DatabaseHelperR(getContext(), TEST_CLEAN_DB)) {
+            helper.beginTransaction();
+            try {
+                helper.setTransactionSuccessful();
+            } finally {
+                helper.endTransaction();
+            }
+
+            helper.runWithTransaction(() -> {
+                return 0;
+            });
+        }
+    }
+
+    @Test
     public void testRtoO() throws Exception {
         assertDowngrade(DatabaseHelperR.class, DatabaseHelperO.class);
     }
diff --git a/tests/src/com/android/providers/media/MediaDocumentsProviderTest.java b/tests/src/com/android/providers/media/MediaDocumentsProviderTest.java
new file mode 100644
index 0000000..dc1767f
--- /dev/null
+++ b/tests/src/com/android/providers/media/MediaDocumentsProviderTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+public class MediaDocumentsProviderTest {
+    @Test
+    public void testSimple() {
+        final Context context = InstrumentationRegistry.getTargetContext();
+        final Context isolatedContext = new IsolatedContext(context, "modern");
+        final ContentResolver resolver = isolatedContext.getContentResolver();
+
+        assertProbe(resolver, "root");
+
+        for (String root : new String[] {
+                MediaDocumentsProvider.TYPE_AUDIO_ROOT,
+                MediaDocumentsProvider.TYPE_VIDEOS_ROOT,
+                MediaDocumentsProvider.TYPE_IMAGES_ROOT,
+        }) {
+            assertProbe(resolver, "root", root, "search");
+
+            assertProbe(resolver, "document", root);
+            assertProbe(resolver, "document", root, "children");
+        }
+
+        for (String recent : new String[] {
+                MediaDocumentsProvider.TYPE_VIDEOS_ROOT,
+                MediaDocumentsProvider.TYPE_IMAGES_ROOT,
+        }) {
+            assertProbe(resolver, "root", recent, "recent");
+        }
+
+        for (String dir : new String[] {
+                MediaDocumentsProvider.TYPE_VIDEOS_BUCKET,
+                MediaDocumentsProvider.TYPE_IMAGES_BUCKET,
+        }) {
+            assertProbe(resolver, "document", dir, "children");
+        }
+
+        for (String item : new String[] {
+                MediaDocumentsProvider.TYPE_ARTIST,
+                MediaDocumentsProvider.TYPE_ALBUM,
+                MediaDocumentsProvider.TYPE_VIDEOS_BUCKET,
+                MediaDocumentsProvider.TYPE_IMAGES_BUCKET,
+
+                MediaDocumentsProvider.TYPE_AUDIO,
+                MediaDocumentsProvider.TYPE_VIDEO,
+                MediaDocumentsProvider.TYPE_IMAGE,
+        }) {
+                assertProbe(resolver, "document", item);
+        }
+    }
+
+    private static void assertProbe(ContentResolver resolver, String... paths) {
+        final Uri.Builder probe = Uri.parse("content://" + MediaDocumentsProvider.AUTHORITY)
+                .buildUpon();
+        for (String path : paths) {
+            probe.appendPath(path);
+        }
+        try (Cursor c = resolver.query(probe.build(), null, Bundle.EMPTY, null)) {
+            assertNotNull(Arrays.toString(paths), c);
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index 842bc22..7473603 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -27,11 +27,13 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.CancellationSignal;
 import android.os.Environment;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Images.ImageColumns;
@@ -110,6 +112,23 @@
             try (Cursor c = isolatedResolver.query(probe, null, null, null)) {
                 assertNotNull("probe", c);
             }
+            try {
+                isolatedResolver.getType(probe);
+            } catch (IllegalStateException tolerated) {
+            }
+        }
+    }
+
+    @Test
+    public void testLocale() {
+        final Context context = InstrumentationRegistry.getTargetContext();
+        final Context isolatedContext = new IsolatedContext(context, "modern");
+        final ContentResolver isolatedResolver = isolatedContext.getContentResolver();
+
+        try (ContentProviderClient cpc = isolatedResolver
+                .acquireContentProviderClient(MediaStore.AUTHORITY)) {
+            ((MediaProvider) cpc.getLocalContentProvider())
+                    .onLocaleChanged();
         }
     }
 
@@ -269,6 +288,14 @@
     }
 
     @Test
+    public void testBuildData_Downloads() throws Exception {
+        final Uri uri = MediaStore.Downloads
+                .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+        assertEndsWith("/Download/linux.iso",
+                buildFile(uri, null, "linux.iso", "application/x-iso9660-image"));
+    }
+
+    @Test
     public void testGreylist() throws Exception {
         assertFalse(isGreylistMatch(
                 "SELECT secret FROM other_table"));
diff --git a/tests/src/com/android/providers/media/scan/LegacyMediaScannerTest.java b/tests/src/com/android/providers/media/scan/LegacyMediaScannerTest.java
new file mode 100644
index 0000000..1c24b2e
--- /dev/null
+++ b/tests/src/com/android/providers/media/scan/LegacyMediaScannerTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.scan;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.provider.MediaStore;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+@RunWith(AndroidJUnit4.class)
+public class LegacyMediaScannerTest {
+    @Test
+    public void testSimple() throws Exception {
+        final LegacyMediaScanner scanner = new LegacyMediaScanner(
+                InstrumentationRegistry.getTargetContext());
+        assertNotNull(scanner.getContext());
+
+        try {
+            scanner.scanDirectory(new File("/dev/null"), MediaScanner.REASON_UNKNOWN);
+        } catch (UnsupportedOperationException expected) {
+        }
+        try {
+            scanner.scanFile(new File("/dev/null"), MediaScanner.REASON_UNKNOWN);
+        } catch (UnsupportedOperationException expected) {
+        }
+        try {
+            scanner.onDetachVolume(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+        } catch (UnsupportedOperationException expected) {
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/media/scan/MediaScannerTest.java b/tests/src/com/android/providers/media/scan/MediaScannerTest.java
index 4c72ca6..44d5cfb 100644
--- a/tests/src/com/android/providers/media/scan/MediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/MediaScannerTest.java
@@ -43,6 +43,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.providers.media.MediaDocumentsProvider;
 import com.android.providers.media.MediaProvider;
 import com.android.providers.media.R;
 import com.android.providers.media.util.FileUtils;
@@ -67,6 +68,7 @@
         private final File mDir;
         private final MockContentResolver mResolver;
         private final MediaProvider mProvider;
+        private final MediaDocumentsProvider mDocumentsProvider;
 
         public IsolatedContext(Context base, String tag) {
             super(base);
@@ -80,8 +82,14 @@
                     .resolveContentProvider(MediaStore.AUTHORITY, 0);
             mProvider = new MediaProvider();
             mProvider.attachInfo(this, info);
-
             mResolver.addProvider(MediaStore.AUTHORITY, mProvider);
+
+            final ProviderInfo documentsInfo = base.getPackageManager()
+                    .resolveContentProvider(MediaDocumentsProvider.AUTHORITY, 0);
+            mDocumentsProvider = new MediaDocumentsProvider();
+            mDocumentsProvider.attachInfo(this, documentsInfo);
+            mResolver.addProvider(MediaDocumentsProvider.AUTHORITY, mDocumentsProvider);
+
             mResolver.addProvider(Settings.AUTHORITY, new MockContentProvider() {
                 @Override
                 public Bundle call(String method, String request, Bundle args) {
diff --git a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
index f1ece3d..781f22a 100644
--- a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
@@ -35,7 +35,6 @@
 import android.graphics.Bitmap;
 import android.media.ExifInterface;
 import android.net.Uri;
-import android.os.Environment;
 import android.os.ParcelFileDescriptor;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Files.FileColumns;
@@ -44,8 +43,8 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
 import com.android.providers.media.R;
+import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
 import com.android.providers.media.util.FileUtils;
 
 import org.junit.After;
@@ -87,6 +86,11 @@
     }
 
     @Test
+    public void testSimple() throws Exception {
+        assertNotNull(mModern.getContext());
+    }
+
+    @Test
     public void testOverrideMimeType() throws Exception {
         assertFalse(parseOptionalMimeType("image/png", null).isPresent());
         assertFalse(parseOptionalMimeType("image/png", "image").isPresent());
@@ -290,6 +294,36 @@
     }
 
     @Test
+    public void testFilter() throws Exception {
+        final File music = new File(mDir, "Music");
+        music.mkdirs();
+        stage(R.raw.test_audio, new File(music, "example.mp3"));
+        mModern.scanDirectory(mDir, REASON_UNKNOWN);
+
+        // Exact matches
+        assertQueryCount(1, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+                .buildUpon().appendQueryParameter("filter", "artist").build());
+        assertQueryCount(1, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+                .buildUpon().appendQueryParameter("filter", "album").build());
+        assertQueryCount(1, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+                .buildUpon().appendQueryParameter("filter", "title").build());
+
+        // Partial matches mid-string
+        assertQueryCount(1, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+                .buildUpon().appendQueryParameter("filter", "ArT").build());
+
+        // Filter should only apply to narrow collection type
+        assertQueryCount(0, MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI
+                .buildUpon().appendQueryParameter("filter", "title").build());
+
+        // Other unrelated search terms
+        assertQueryCount(0, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+                .buildUpon().appendQueryParameter("filter", "example").build());
+        assertQueryCount(0, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+                .buildUpon().appendQueryParameter("filter", "チ").build());
+    }
+
+    @Test
     public void testScan_Common() throws Exception {
         final File file = new File(mDir, "red.jpg");
         stage(R.raw.test_image, file);
diff --git a/tests/src/com/android/providers/media/scan/NullMediaScannerTest.java b/tests/src/com/android/providers/media/scan/NullMediaScannerTest.java
new file mode 100644
index 0000000..06f5ce8
--- /dev/null
+++ b/tests/src/com/android/providers/media/scan/NullMediaScannerTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.scan;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.provider.MediaStore;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+@RunWith(AndroidJUnit4.class)
+public class NullMediaScannerTest {
+    @Test
+    public void testSimple() throws Exception {
+        final NullMediaScanner scanner = new NullMediaScanner(
+                InstrumentationRegistry.getTargetContext());
+        assertNotNull(scanner.getContext());
+
+        scanner.scanDirectory(new File("/dev/null"), MediaScanner.REASON_UNKNOWN);
+        scanner.scanFile(new File("/dev/null"), MediaScanner.REASON_UNKNOWN);
+
+        scanner.onDetachVolume(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+    }
+}
diff --git a/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java b/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java
index ed84dcc..635b042 100644
--- a/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java
@@ -28,6 +28,7 @@
 import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION;
 import static android.content.ContentResolver.QUERY_ARG_SQL_SORT_ORDER;
 import static android.content.ContentResolver.QUERY_SORT_DIRECTION_ASCENDING;
+import static android.database.DatabaseUtils.bindSelection;
 
 import static com.android.providers.media.util.DatabaseUtils.maybeBalance;
 import static com.android.providers.media.util.DatabaseUtils.recoverAbusiveLimit;
@@ -58,6 +59,8 @@
     private final Bundle args = new Bundle();
     private final ArraySet<String> honored = new ArraySet<>();
 
+    private static final Object[] ARGS = { "baz", 4, null };
+
     @Before
     public void setUp() {
         args.clear();
@@ -65,6 +68,41 @@
     }
 
     @Test
+    public void testBindSelection_none() throws Exception {
+        assertEquals(null,
+                bindSelection(null, ARGS));
+        assertEquals("",
+                bindSelection("", ARGS));
+        assertEquals("foo=bar",
+                bindSelection("foo=bar", ARGS));
+    }
+
+    @Test
+    public void testBindSelection_normal() throws Exception {
+        assertEquals("foo='baz'",
+                bindSelection("foo=?", ARGS));
+        assertEquals("foo='baz' AND bar=4",
+                bindSelection("foo=? AND bar=?", ARGS));
+        assertEquals("foo='baz' AND bar=4 AND meow=NULL",
+                bindSelection("foo=? AND bar=? AND meow=?", ARGS));
+    }
+
+    @Test
+    public void testBindSelection_whitespace() throws Exception {
+        assertEquals("BETWEEN 5 AND 10",
+                bindSelection("BETWEEN? AND ?", 5, 10));
+        assertEquals("IN 'foo'",
+                bindSelection("IN?", "foo"));
+    }
+
+    @Test
+    public void testBindSelection_indexed() throws Exception {
+        assertEquals("foo=10 AND bar=11 AND meow=1",
+                bindSelection("foo=?10 AND bar=? AND meow=?1",
+                        1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12));
+    }
+
+    @Test
     public void testResolveQueryArgs_GroupBy() throws Exception {
         args.putStringArray(QUERY_ARG_GROUP_COLUMNS, new String[] { "foo", "bar" });
         args.putString(QUERY_ARG_SQL_GROUP_BY, "raw");
diff --git a/tests/src/com/android/providers/media/util/LongArrayTest.java b/tests/src/com/android/providers/media/util/LongArrayTest.java
new file mode 100644
index 0000000..f2e82f3
--- /dev/null
+++ b/tests/src/com/android/providers/media/util/LongArrayTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2017 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.util;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class LongArrayTest {
+
+    @Test
+    public void testLongArray() {
+        LongArray a = new LongArray();
+        a.add(1);
+        a.add(2);
+        a.add(3);
+        verify(new long[]{1, 2, 3}, a);
+
+        LongArray b = LongArray.fromArray(new long[]{4, 5, 6, 7, 8}, 3);
+        a.addAll(b);
+        verify(new long[]{1, 2, 3, 4, 5, 6}, a);
+
+        a.resize(2);
+        verify(new long[]{1, 2}, a);
+
+        a.resize(8);
+        verify(new long[]{1, 2, 0, 0, 0, 0, 0, 0}, a);
+
+        a.set(5, 10);
+        verify(new long[]{1, 2, 0, 0, 0, 10, 0, 0}, a);
+
+        a.add(5, 20);
+        assertEquals(20, a.get(5));
+        assertEquals(5, a.indexOf(20));
+        verify(new long[]{1, 2, 0, 0, 0, 20, 10, 0, 0}, a);
+
+        assertEquals(-1, a.indexOf(99));
+
+        a.resize(15);
+        a.set(14, 30);
+        verify(new long[]{1, 2, 0, 0, 0, 20, 10, 0, 0, 0, 0, 0, 0, 0, 30}, a);
+
+        long[] backingArray = new long[]{1, 2, 3, 4};
+        a = LongArray.wrap(backingArray);
+        a.set(0, 10);
+        assertEquals(10, backingArray[0]);
+        backingArray[1] = 20;
+        backingArray[2] = 30;
+        verify(backingArray, a);
+        assertEquals(2, a.indexOf(30));
+
+        a.resize(2);
+        assertEquals(0, backingArray[2]);
+        assertEquals(0, backingArray[3]);
+
+        a.add(50);
+        verify(new long[]{10, 20, 50}, a);
+    }
+
+    public void verify(long[] expected, LongArray longArrays) {
+        assertEquals(expected.length, longArrays.size());
+        assertArrayEquals(expected, longArrays.toArray());
+    }
+}
diff --git a/tests/src/com/android/providers/media/util/MemoryTest.java b/tests/src/com/android/providers/media/util/MemoryTest.java
new file mode 100644
index 0000000..aa9511b
--- /dev/null
+++ b/tests/src/com/android/providers/media/util/MemoryTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.util;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+public class MemoryTest {
+    private final byte[] buf = new byte[4];
+
+    @Test
+    public void testBigEndian() {
+        final int expected = 42;
+        Memory.pokeInt(buf, 0, expected, ByteOrder.BIG_ENDIAN);
+        final int actual = Memory.peekInt(buf, 0, ByteOrder.BIG_ENDIAN);
+        assertEquals(expected, actual);
+    }
+
+    @Test
+    public void testLittleEndian() {
+        final int expected = 42;
+        Memory.pokeInt(buf, 0, expected, ByteOrder.LITTLE_ENDIAN);
+        final int actual = Memory.peekInt(buf, 0, ByteOrder.LITTLE_ENDIAN);
+        assertEquals(expected, actual);
+    }
+}
diff --git a/tests/src/com/android/providers/media/util/MetricsTest.java b/tests/src/com/android/providers/media/util/MetricsTest.java
new file mode 100644
index 0000000..6041734
--- /dev/null
+++ b/tests/src/com/android/providers/media/util/MetricsTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.util;
+
+import android.provider.MediaStore;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.providers.media.scan.MediaScanner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class MetricsTest {
+
+    /**
+     * The best we can do for coverage is make sure we don't explode?
+     */
+    @Test
+    public void testSimple() throws Exception {
+        final String volumeName = MediaStore.VOLUME_EXTERNAL_PRIMARY;
+        final String packageName = "com.example";
+
+        Metrics.logScan(volumeName, MediaScanner.REASON_UNKNOWN, 42, 42, 42, 42, 42);
+        Metrics.logDeletion(volumeName, 42, packageName, 42);
+        Metrics.logPermissionGranted(volumeName, 42, packageName, 42);
+        Metrics.logPermissionDenied(volumeName, 42, packageName, 42);
+        Metrics.logSchemaChange(volumeName, 42, 42, 42, 42);
+        Metrics.logIdleMaintenance(volumeName, 42, 42, 42, 42);
+    }
+}
diff --git a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
new file mode 100644
index 0000000..af7535f
--- /dev/null
+++ b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.util;
+
+import static com.android.providers.media.util.PermissionUtils.checkPermissionBackup;
+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.checkPermissionWriteAudio;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteImages;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteStorage;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteVideo;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class PermissionUtilsTest {
+    /**
+     * The best we can do here is assert that we're granted the permissions that
+     * we expect to be holding.
+     */
+    @Test
+    public void testSelfPermissions() throws Exception {
+        final Context context = InstrumentationRegistry.getContext();
+        final int pid = android.os.Process.myPid();
+        final int uid = android.os.Process.myUid();
+        final String packageName = context.getPackageName();
+
+        assertTrue(checkPermissionSystem(context, pid, uid, packageName));
+        assertFalse(checkPermissionBackup(context, pid, uid));
+
+        assertTrue(checkPermissionReadStorage(context, pid, uid, packageName));
+        assertTrue(checkPermissionWriteStorage(context, pid, uid, packageName));
+
+        assertTrue(checkPermissionReadAudio(context, pid, uid, packageName));
+        assertFalse(checkPermissionWriteAudio(context, pid, uid, packageName));
+        assertTrue(checkPermissionReadVideo(context, pid, uid, packageName));
+        assertFalse(checkPermissionWriteVideo(context, pid, uid, packageName));
+        assertTrue(checkPermissionReadImages(context, pid, uid, packageName));
+        assertFalse(checkPermissionWriteImages(context, pid, uid, packageName));
+    }
+}
diff --git a/tests/src/com/android/providers/media/util/SQLiteQueryBuilderTest.java b/tests/src/com/android/providers/media/util/SQLiteQueryBuilderTest.java
index ea689e8..dce5063 100644
--- a/tests/src/com/android/providers/media/util/SQLiteQueryBuilderTest.java
+++ b/tests/src/com/android/providers/media/util/SQLiteQueryBuilderTest.java
@@ -218,7 +218,7 @@
         sqliteQueryBuilder.setTables("Employee");
         Cursor cursor = sqliteQueryBuilder.query(mDatabase,
                 new String[] { "name", "sum(salary)" }, null, null,
-                "name", "sum(salary)>1000", "name");
+                "name", "sum(salary)>1000", "name", null, null);
         assertNotNull(cursor);
         assertEquals(3, cursor.getCount());
 
@@ -239,7 +239,7 @@
         cursor = sqliteQueryBuilder.query(mDatabase,
                 new String[] { "name", "sum(salary)" }, null, null,
                 "name", "sum(salary)>1000", "name", "2" // limit is 2
-                );
+                , null);
         assertNotNull(cursor);
         assertEquals(2, cursor.getCount());
         cursor.moveToFirst();
@@ -473,25 +473,25 @@
         final SQLiteQueryBuilder qb = mStrictBuilder;
 
         // Should normally only be able to see one row
-        try (Cursor c = qb.query(mDatabase, null, null, null, null, null, null)) {
+        try (Cursor c = qb.query(mDatabase, null, null, null, null, null, null, null, null)) {
             assertEquals(1, c.getCount());
         }
 
         // Trying sneaky queries should fail; even if they somehow succeed, we
         // shouldn't get to see any other data.
-        try (Cursor c = qb.query(mDatabase, null, "1=1", null, null, null, null)) {
+        try (Cursor c = qb.query(mDatabase, null, "1=1", null, null, null, null, null, null)) {
             assertEquals(1, c.getCount());
         } catch (Exception tolerated) {
         }
-        try (Cursor c = qb.query(mDatabase, null, "1=1 --", null, null, null, null)) {
+        try (Cursor c = qb.query(mDatabase, null, "1=1 --", null, null, null, null, null, null)) {
             assertEquals(1, c.getCount());
         } catch (Exception tolerated) {
         }
-        try (Cursor c = qb.query(mDatabase, null, "1=1) OR (1=1", null, null, null, null)) {
+        try (Cursor c = qb.query(mDatabase, null, "1=1) OR (1=1", null, null, null, null, null, null)) {
             assertEquals(1, c.getCount());
         } catch (Exception tolerated) {
         }
-        try (Cursor c = qb.query(mDatabase, null, "1=1)) OR ((1=1", null, null, null, null)) {
+        try (Cursor c = qb.query(mDatabase, null, "1=1)) OR ((1=1", null, null, null, null, null, null)) {
             assertEquals(1, c.getCount());
         } catch (Exception tolerated) {
         }