Merge "Support Windows style file path for a playlist"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index dfb2139..ecd5943 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -18,12 +18,14 @@
     <uses-permission android:name="android.permission.WRITE_MEDIA_STORAGE" />
     <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.USE_RESERVED_DISK" />
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 
     <application
             android:name="com.android.providers.media.MediaApplication"
             android:label="@string/app_label"
             android:allowBackup="false"
             android:supportsRtl="true"
+            android:forceQueryable="true"
             android:usesCleartextTraffic="true">
         <provider
                 android:name="com.android.providers.media.MediaProvider"
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 952649c..5a16a9c 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -1247,6 +1247,24 @@
         @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
         public static final String GENERATION_MODIFIED = "generation_modified";
 
+        /**
+         * Indexed XMP metadata extracted from this media item.
+         * <p>
+         * The structure of this metadata is defined by the <a href=
+         * "https://en.wikipedia.org/wiki/Extensible_Metadata_Platform"><em>XMP
+         * Media Management</em> standard</a>, published as ISO 16684-1:2012.
+         * <p>
+         * This metadata is typically extracted from a
+         * {@link ExifInterface#TAG_XMP} contained inside an image file or from
+         * a {@code XMP_} box contained inside an ISO/IEC base media file format
+         * (MPEG-4 Part 12).
+         * <p>
+         * Note that any location details are redacted from this metadata for
+         * privacy reasons.
+         */
+        @Column(value = Cursor.FIELD_TYPE_BLOB, readOnly = true)
+        public static final String XMP = "xmp";
+
         // =======================================
         // ==== MediaMetadataRetriever values ====
         // =======================================
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index 7c78666..1c10335 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -417,10 +417,8 @@
         return NULL;
     }
 
-    node = parent->LookupChildByName(name);
-    if (node) {
-        node->Acquire();
-    } else {
+    node = parent->LookupChildByName(name, true /* acquire */);
+    if (!node) {
         node = ::node::Create(parent, name, &fuse->lock);
         fuse->NodeCreated(node);
     }
@@ -726,7 +724,7 @@
         return;
     }
 
-    node* child_node = parent_node->LookupChildByName(name);
+    node* child_node = parent_node->LookupChildByName(name, false /* acquire */);
     if (child_node) {
         child_node->SetDeleted();
     }
@@ -756,7 +754,7 @@
         return;
     }
 
-    node* child_node = parent_node->LookupChildByName(name);
+    node* child_node = parent_node->LookupChildByName(name, false /* acquire */);
     if (child_node) {
         child_node->SetDeleted();
     }
@@ -797,8 +795,7 @@
                      << safe_name(old_parent_node) << ") -> " << new_parent << " ("
                      << safe_name(new_parent_node) << ")";
 
-    node* child_node = old_parent_node->LookupChildByName(name);
-    child_node->Acquire();
+    node* child_node = old_parent_node->LookupChildByName(name, true /* acquire */);
 
     const string old_child_path = child_node->BuildPath();
     const string new_child_path = new_parent_path + "/" + new_name;
@@ -1481,7 +1478,7 @@
 };
 
 static struct fuse_loop_config config = {
-        .clone_fd = 0,
+        .clone_fd = 1,
         .max_idle_threads = 10,
 };
 
diff --git a/jni/MediaProviderWrapper.cpp b/jni/MediaProviderWrapper.cpp
index 6cfcb57..821dc8a 100644
--- a/jni/MediaProviderWrapper.cpp
+++ b/jni/MediaProviderWrapper.cpp
@@ -358,7 +358,7 @@
 void MediaProviderWrapper::ScanFile(const string& path) {
     // Don't send in path by reference, since the memory might be deleted before we get the chances
     // to perfrom the task.
-    PostAsyncTask([this, path](JNIEnv* env) {
+    PostAndWaitForTask([this, path](JNIEnv* env) {
         scanFileInternal(env, media_provider_object_, mid_scan_file_, path);
     });
 }
diff --git a/jni/node-inl.h b/jni/node-inl.h
index 487583c..4b688f1 100644
--- a/jni/node-inl.h
+++ b/jni/node-inl.h
@@ -89,14 +89,6 @@
         return static_cast<__u64>(reinterpret_cast<uintptr_t>(node));
     }
 
-    // Acquires a reference to a node. This maps to the "lookup count" specified
-    // by the FUSE documentation and must only happen under the circumstances
-    // documented in libfuse/include/fuse_lowlevel.h.
-    inline void Acquire() {
-        std::lock_guard<std::recursive_mutex> guard(*lock_);
-        refcount_++;
-    }
-
     // Releases a reference to a node. Returns true iff the refcount dropped to
     // zero as a result of this call to Release, meaning that it's no longer
     // safe to perform any operations on references to this node.
@@ -120,8 +112,9 @@
     // associated with its descendants.
     std::string BuildPath() const;
 
-    // Looks up a direct descendant of this node by name.
-    node* LookupChildByName(const std::string& name) const {
+    // Looks up a direct descendant of this node by name. If |acquire| is true,
+    // also Acquire the node before returning a reference to it.
+    node* LookupChildByName(const std::string& name, bool acquire) const {
         std::lock_guard<std::recursive_mutex> guard(*lock_);
 
         for (node* child : children_) {
@@ -131,6 +124,10 @@
             // will not work because the source and target nodes are the same.
 
             if ((name == child->name_) && !child->deleted_) {
+                if (acquire) {
+                    child->Acquire();
+                }
+
                 return child;
             }
         }
@@ -219,6 +216,14 @@
         }
     }
 
+    // Acquires a reference to a node. This maps to the "lookup count" specified
+    // by the FUSE documentation and must only happen under the circumstances
+    // documented in libfuse/include/fuse_lowlevel.h.
+    inline void Acquire() {
+        std::lock_guard<std::recursive_mutex> guard(*lock_);
+        refcount_++;
+    }
+
     // Adds this node to a specified parent.
     void AddToParent(node* parent) {
         std::lock_guard<std::recursive_mutex> guard(*lock_);
diff --git a/jni/node.cpp b/jni/node.cpp
index 1974a91..8898b7b 100644
--- a/jni/node.cpp
+++ b/jni/node.cpp
@@ -68,7 +68,7 @@
 
     const node* node = root;
     for (const std::string& segment : segments) {
-        node = node->LookupChildByName(segment);
+        node = node->LookupChildByName(segment, false /* acquire */);
         if (!node) {
             return nullptr;
         }
diff --git a/jni/node_test.cpp b/jni/node_test.cpp
index c72c8ee..ba876a0 100644
--- a/jni/node_test.cpp
+++ b/jni/node_test.cpp
@@ -22,6 +22,8 @@
     // Forward destruction here, as NodeTest is a friend class.
     static void destroy(node* node) { delete node; }
 
+    static void acquire(node* node) { node->Acquire(); }
+
     typedef std::unique_ptr<node, decltype(&NodeTest::destroy)> unique_node_ptr;
 
     unique_node_ptr CreateNode(node* parent, const std::string& path) {
@@ -46,21 +48,14 @@
     ASSERT_EQ(2, GetRefCount(parent.get()));
 
     // Make sure the node has been added to the parents list of children.
-    ASSERT_EQ(child.get(), parent->LookupChildByName("subdir"));
-}
-
-TEST_F(NodeTest, TestAcquire) {
-    unique_node_ptr node = CreateNode(nullptr, "/path");
-
-    ASSERT_EQ(1, GetRefCount(node.get()));
-    node->Acquire();
-    ASSERT_EQ(2, GetRefCount(node.get()));
+    ASSERT_EQ(child.get(), parent->LookupChildByName("subdir", false /* acquire */));
+    ASSERT_EQ(1, GetRefCount(child.get()));
 }
 
 TEST_F(NodeTest, TestRelease) {
     node* node = node::Create(nullptr, "/path", &lock_);
-    node->Acquire();
-    node->Acquire();
+    acquire(node);
+    acquire(node);
     ASSERT_EQ(3, GetRefCount(node));
 
     ASSERT_FALSE(node->Release(1));
@@ -79,15 +74,16 @@
 
     unique_node_ptr child = CreateNode(parent.get(), "subdir");
     ASSERT_EQ(2, GetRefCount(parent.get()));
-    ASSERT_EQ(child.get(), parent->LookupChildByName("subdir"));
+    ASSERT_EQ(child.get(), parent->LookupChildByName("subdir", false /* acquire */));
 
     child->Rename("subdir_new", parent.get());
 
     ASSERT_EQ(2, GetRefCount(parent.get()));
-    ASSERT_EQ(nullptr, parent->LookupChildByName("subdir"));
-    ASSERT_EQ(child.get(), parent->LookupChildByName("subdir_new"));
+    ASSERT_EQ(nullptr, parent->LookupChildByName("subdir", false /* acquire */));
+    ASSERT_EQ(child.get(), parent->LookupChildByName("subdir_new", false /* acquire */));
 
     ASSERT_EQ("/path/subdir_new", child->BuildPath());
+    ASSERT_EQ(1, GetRefCount(child.get()));
 }
 
 TEST_F(NodeTest, TestRenameWithParent) {
@@ -96,16 +92,17 @@
 
     unique_node_ptr child = CreateNode(parent1.get(), "subdir");
     ASSERT_EQ(2, GetRefCount(parent1.get()));
-    ASSERT_EQ(child.get(), parent1->LookupChildByName("subdir"));
+    ASSERT_EQ(child.get(), parent1->LookupChildByName("subdir", false /* acquire */));
 
     child->Rename("subdir", parent2.get());
     ASSERT_EQ(1, GetRefCount(parent1.get()));
-    ASSERT_EQ(nullptr, parent1->LookupChildByName("subdir"));
+    ASSERT_EQ(nullptr, parent1->LookupChildByName("subdir", false /* acquire */));
 
     ASSERT_EQ(2, GetRefCount(parent2.get()));
-    ASSERT_EQ(child.get(), parent2->LookupChildByName("subdir"));
+    ASSERT_EQ(child.get(), parent2->LookupChildByName("subdir", false /* acquire */));
 
     ASSERT_EQ("/path2/subdir", child->BuildPath());
+    ASSERT_EQ(1, GetRefCount(child.get()));
 }
 
 TEST_F(NodeTest, TestRenameWithNameAndParent) {
@@ -114,17 +111,18 @@
 
     unique_node_ptr child = CreateNode(parent1.get(), "subdir");
     ASSERT_EQ(2, GetRefCount(parent1.get()));
-    ASSERT_EQ(child.get(), parent1->LookupChildByName("subdir"));
+    ASSERT_EQ(child.get(), parent1->LookupChildByName("subdir", false /* acquire */));
 
     child->Rename("subdir_new", parent2.get());
     ASSERT_EQ(1, GetRefCount(parent1.get()));
-    ASSERT_EQ(nullptr, parent1->LookupChildByName("subdir"));
-    ASSERT_EQ(nullptr, parent1->LookupChildByName("subdir_new"));
+    ASSERT_EQ(nullptr, parent1->LookupChildByName("subdir", false /* acquire */));
+    ASSERT_EQ(nullptr, parent1->LookupChildByName("subdir_new", false /* acquire */));
 
     ASSERT_EQ(2, GetRefCount(parent2.get()));
-    ASSERT_EQ(child.get(), parent2->LookupChildByName("subdir_new"));
+    ASSERT_EQ(child.get(), parent2->LookupChildByName("subdir_new", false /* acquire */));
 
     ASSERT_EQ("/path2/subdir_new", child->BuildPath());
+    ASSERT_EQ(1, GetRefCount(child.get()));
 }
 
 TEST_F(NodeTest, TestBuildPath) {
@@ -145,9 +143,9 @@
     unique_node_ptr parent = CreateNode(nullptr, "/path");
     unique_node_ptr child = CreateNode(parent.get(), "subdir");
 
-    ASSERT_EQ(child.get(), parent->LookupChildByName("subdir"));
+    ASSERT_EQ(child.get(), parent->LookupChildByName("subdir", false /* acquire */));
     child->SetDeleted();
-    ASSERT_EQ(nullptr, parent->LookupChildByName("subdir"));
+    ASSERT_EQ(nullptr, parent->LookupChildByName("subdir", false /* acquire */));
 }
 
 TEST_F(NodeTest, DeleteTree) {
@@ -159,17 +157,28 @@
     node* subchild2 = node::Create(child, "s2", &lock_);
     node::Create(subchild2, "sc2", &lock_);
 
-    ASSERT_EQ(child, parent->LookupChildByName("subdir"));
+    ASSERT_EQ(child, parent->LookupChildByName("subdir", false /* acquire */));
     node::DeleteTree(child);
-    ASSERT_EQ(nullptr, parent->LookupChildByName("subdir"));
+    ASSERT_EQ(nullptr, parent->LookupChildByName("subdir", false /* acquire */));
 }
 
 TEST_F(NodeTest, LookupChildByName_empty) {
     unique_node_ptr parent = CreateNode(nullptr, "/path");
     unique_node_ptr child = CreateNode(parent.get(), "subdir");
 
-    ASSERT_EQ(child.get(), parent->LookupChildByName("subdir"));
-    ASSERT_EQ(nullptr, parent->LookupChildByName(""));
+    ASSERT_EQ(child.get(), parent->LookupChildByName("subdir", false /* acquire */));
+    ASSERT_EQ(nullptr, parent->LookupChildByName("", false /* acquire */));
+}
+
+TEST_F(NodeTest, LookupChildByName_refcounts) {
+    unique_node_ptr parent = CreateNode(nullptr, "/path");
+    unique_node_ptr child = CreateNode(parent.get(), "subdir");
+
+    ASSERT_EQ(child.get(), parent->LookupChildByName("subdir", false /* acquire */));
+    ASSERT_EQ(1, GetRefCount(child.get()));
+
+    ASSERT_EQ(child.get(), parent->LookupChildByName("subdir", true /* acquire */));
+    ASSERT_EQ(2, GetRefCount(child.get()));
 }
 
 TEST_F(NodeTest, LookupAbsolutePath) {
diff --git a/legacy/AndroidManifest.xml b/legacy/AndroidManifest.xml
index 15349b1..960e5ba 100644
--- a/legacy/AndroidManifest.xml
+++ b/legacy/AndroidManifest.xml
@@ -11,6 +11,7 @@
             android:process="android.process.media"
             android:allowBackup="false"
             android:supportsRtl="true"
+            android:forceQueryable="true"
             android:usesCleartextTraffic="true">
         <provider
                 android:name="com.android.providers.media.LegacyMediaProvider"
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index b96358a..4943d6b 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -335,7 +335,7 @@
          * {@link ContentResolver#notifyChange}, but are instead being collected
          * due to this ongoing transaction.
          */
-        public final List<Uri> notifyChanges = new ArrayList<>();
+        public final Set<Uri> notifyChanges = new ArraySet<>();
     }
 
     public void beginTransaction() {
@@ -556,7 +556,7 @@
                 + "writer TEXT DEFAULT NULL, exposure_time TEXT DEFAULT NULL,"
                 + "f_number TEXT DEFAULT NULL, iso INTEGER DEFAULT NULL,"
                 + "scene_capture_type INTEGER DEFAULT NULL, generation_added INTEGER DEFAULT 0,"
-                + "generation_modified INTEGER DEFAULT 0)");
+                + "generation_modified INTEGER DEFAULT 0, xmp BLOB DEFAULT NULL)");
 
         db.execSQL("CREATE TABLE log (time DATETIME, message TEXT)");
         if (!mInternal) {
@@ -892,7 +892,7 @@
         db.execSQL("ALTER TABLE files ADD COLUMN secondary_directory TEXT DEFAULT NULL;");
     }
 
-    private static void updateAddXmp(SQLiteDatabase db, boolean internal) {
+    private static void updateAddXmpMm(SQLiteDatabase db, boolean internal) {
         db.execSQL("ALTER TABLE files ADD COLUMN document_id TEXT DEFAULT NULL;");
         db.execSQL("ALTER TABLE files ADD COLUMN instance_id TEXT DEFAULT NULL;");
         db.execSQL("ALTER TABLE files ADD COLUMN original_document_id TEXT DEFAULT NULL;");
@@ -988,6 +988,10 @@
         db.execSQL("ALTER TABLE files ADD COLUMN generation_modified INTEGER DEFAULT 0;");
     }
 
+    private static void updateAddXmp(SQLiteDatabase db, boolean internal) {
+        db.execSQL("ALTER TABLE files ADD COLUMN xmp BLOB DEFAULT NULL;");
+    }
+
     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)) {
@@ -1043,7 +1047,7 @@
     static final int VERSION_O = 800;
     static final int VERSION_P = 900;
     static final int VERSION_Q = 1023;
-    static final int VERSION_R = 1111;
+    static final int VERSION_R = 1112;
     static final int VERSION_LATEST = VERSION_R;
 
     /**
@@ -1106,7 +1110,7 @@
                 recomputeDataValues = true;
             }
             if (fromVersion < 1014) {
-                updateAddXmp(db, internal);
+                updateAddXmpMm(db, internal);
             }
             if (fromVersion < 1015) {
                 // Empty version bump to ensure views are recreated
@@ -1177,6 +1181,9 @@
             if (fromVersion < 1111) {
                 recomputeMediaTypeValues(db);
             }
+            if (fromVersion < 1112) {
+                updateAddXmp(db, internal);
+            }
 
             if (recomputeDataValues) {
                 recomputeDataValues(db, internal);
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index bc74663..ad8f840 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -186,7 +186,6 @@
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
@@ -3907,27 +3906,62 @@
         public abstract Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal)
                 throws IOException;
 
-        public File ensureThumbnail(Uri uri, CancellationSignal signal) throws IOException {
+        public ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal)
+                throws IOException {
+            // First attempt to fast-path by opening the thumbnail; if it
+            // doesn't exist we fall through to create it below
             final File thumbFile = getThumbnailFile(uri);
+            try {
+                return ParcelFileDescriptor.open(thumbFile,
+                        ParcelFileDescriptor.MODE_READ_ONLY);
+            } catch (FileNotFoundException ignored) {
+            }
+
             final File thumbDir = thumbFile.getParentFile();
             thumbDir.mkdirs();
-            if (!thumbFile.exists()) {
-                // When multiple threads race for the same thumbnail, the second
-                // thread could return a file with a thumbnail still in
-                // progress. We could add heavy per-ID locking to mitigate this
-                // rare race condition, but it's simpler to have both threads
-                // generate the same thumbnail using temporary files and rename
-                // them into place once finished.
-                final File thumbTempFile = File.createTempFile("thumb", null, thumbDir);
+
+            // When multiple threads race for the same thumbnail, the second
+            // thread could return a file with a thumbnail still in
+            // progress. We could add heavy per-ID locking to mitigate this
+            // rare race condition, but it's simpler to have both threads
+            // generate the same thumbnail using temporary files and rename
+            // them into place once finished.
+            final File thumbTempFile = File.createTempFile("thumb", null, thumbDir);
+
+            ParcelFileDescriptor thumbWrite = null;
+            ParcelFileDescriptor thumbRead = null;
+            try {
+                // Open our temporary file twice: once for local writing, and
+                // once for remote reading. Both FDs point at the same
+                // underlying inode on disk, so they're stable across renames
+                // to avoid race conditions between threads.
+                thumbWrite = ParcelFileDescriptor.open(thumbTempFile,
+                        ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_CREATE);
+                thumbRead = ParcelFileDescriptor.open(thumbTempFile,
+                        ParcelFileDescriptor.MODE_READ_ONLY);
+
                 final Bitmap thumbnail = getThumbnailBitmap(uri, signal);
-                try (OutputStream out = new FileOutputStream(thumbTempFile)) {
-                    thumbnail.compress(Bitmap.CompressFormat.JPEG, 90, out);
+                thumbnail.compress(Bitmap.CompressFormat.JPEG, 90,
+                        new FileOutputStream(thumbWrite.getFileDescriptor()));
+
+                try {
+                    // Use direct syscall for better failure logs
+                    Os.rename(thumbTempFile.getAbsolutePath(), thumbFile.getAbsolutePath());
+                } catch (ErrnoException e) {
+                    e.rethrowAsIOException();
                 }
-                if (!thumbTempFile.renameTo(thumbFile)) {
-                    thumbTempFile.delete();
-                }
+
+                // Everything above went peachy, so return a duplicate of our
+                // already-opened read FD to keep our finally logic below simple
+                return thumbRead.dup();
+
+            } finally {
+                // Regardless of success or failure, try cleaning up any
+                // remaining temporary file and close all our local FDs
+                FileUtils.closeQuietly(thumbWrite);
+                FileUtils.closeQuietly(thumbRead);
+                thumbTempFile.delete();
             }
-            return thumbFile;
         }
 
         public void invalidateThumbnail(Uri uri) throws IOException {
@@ -4468,30 +4502,25 @@
                 final long albumId = Long.parseLong(uri.getPathSegments().get(3));
                 final Uri targetUri = ContentUris
                         .withAppendedId(Audio.Albums.getContentUri(volumeName), albumId);
-                return ParcelFileDescriptor.open(ensureThumbnail(targetUri, signal),
-                        ParcelFileDescriptor.MODE_READ_ONLY);
-
+                return ensureThumbnail(targetUri, signal);
             }
             case AUDIO_ALBUMART_FILE_ID: {
                 final long audioId = Long.parseLong(uri.getPathSegments().get(3));
                 final Uri targetUri = ContentUris
                         .withAppendedId(Audio.Media.getContentUri(volumeName), audioId);
-                return ParcelFileDescriptor.open(ensureThumbnail(targetUri, signal),
-                        ParcelFileDescriptor.MODE_READ_ONLY);
+                return ensureThumbnail(targetUri, signal);
             }
             case VIDEO_MEDIA_ID_THUMBNAIL: {
                 final long videoId = Long.parseLong(uri.getPathSegments().get(3));
                 final Uri targetUri = ContentUris
                         .withAppendedId(Video.Media.getContentUri(volumeName), videoId);
-                return ParcelFileDescriptor.open(ensureThumbnail(targetUri, signal),
-                        ParcelFileDescriptor.MODE_READ_ONLY);
+                return ensureThumbnail(targetUri, signal);
             }
             case IMAGES_MEDIA_ID_THUMBNAIL: {
                 final long imageId = Long.parseLong(uri.getPathSegments().get(3));
                 final Uri targetUri = ContentUris
                         .withAppendedId(Images.Media.getContentUri(volumeName), imageId);
-                return ParcelFileDescriptor.open(ensureThumbnail(targetUri, signal),
-                        ParcelFileDescriptor.MODE_READ_ONLY);
+                return ensureThumbnail(targetUri, signal);
             }
         }
 
@@ -4520,10 +4549,8 @@
         final boolean wantsThumb = (opts != null) && opts.containsKey(ContentResolver.EXTRA_SIZE)
                 && (mimeTypeFilter != null) && mimeTypeFilter.startsWith("image/");
         if (wantsThumb) {
-            final File thumbFile = ensureThumbnail(uri, signal);
-            return new AssetFileDescriptor(
-                    ParcelFileDescriptor.open(thumbFile, ParcelFileDescriptor.MODE_READ_ONLY),
-                    0, AssetFileDescriptor.UNKNOWN_LENGTH);
+            final ParcelFileDescriptor pfd = ensureThumbnail(uri, signal);
+            return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
         }
 
         // Worst case, return the underlying file
@@ -4531,14 +4558,14 @@
                 AssetFileDescriptor.UNKNOWN_LENGTH);
     }
 
-    private File ensureThumbnail(Uri uri, CancellationSignal signal) throws FileNotFoundException {
+    private ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal)
+            throws FileNotFoundException {
         final boolean allowHidden = isCallingPackageAllowedHidden();
         final int match = matchUri(uri, allowHidden);
 
         Trace.beginSection("ensureThumbnail");
         final LocalCallingIdentity token = clearLocalCallingIdentity();
         try {
-            final File thumbFile;
             switch (match) {
                 case AUDIO_ALBUMS_ID: {
                     final String volumeName = MediaStore.getVolumeName(uri);
@@ -4970,6 +4997,9 @@
             IsoInterface.BOX_GPS0,
     };
 
+    public static final Set<String> sRedactedExifTags = new ArraySet<>(
+            Arrays.asList(REDACTED_EXIF_TAGS));
+
     private static final class RedactionInfo {
         public final long[] redactionRanges;
         public final long[] freeOffsets;
@@ -5078,7 +5108,6 @@
         final LongArray res = new LongArray();
         final LongArray freeOffsets = new LongArray();
         try (FileInputStream is = new FileInputStream(file)) {
-            final Set<String> redactedXmpTags = new ArraySet<>(Arrays.asList(REDACTED_EXIF_TAGS));
             final String mimeType = MimeUtils.resolveMimeType(file);
             if (ExifInterface.isSupportedMimeType(mimeType)) {
                 final ExifInterface exif = new ExifInterface(is.getFD());
@@ -5090,7 +5119,7 @@
                     }
                 }
                 // Redact xmp where present
-                final XmpInterface exifXmp = XmpInterface.fromContainer(exif, redactedXmpTags);
+                final XmpInterface exifXmp = XmpInterface.fromContainer(exif);
                 res.addAll(exifXmp.getRedactionRanges());
             }
 
@@ -5106,7 +5135,7 @@
                     }
                 }
                 // Redact xmp where present
-                final XmpInterface isoXmp = XmpInterface.fromContainer(iso, redactedXmpTags);
+                final XmpInterface isoXmp = XmpInterface.fromContainer(iso);
                 res.addAll(isoXmp.getRedactionRanges());
             }
         } catch (FileNotFoundException ignored) {
@@ -5853,6 +5882,10 @@
         final String volumeName = resolveVolumeName(uri);
         synchronized (mAttachedVolumeNames) {
             if (!mAttachedVolumeNames.contains(volumeName)) {
+                // Maybe we are racing onVolumeStateChanged, update our cache and try again
+                updateVolumes();
+            }
+            if (!mAttachedVolumeNames.contains(volumeName)) {
                 throw new VolumeNotFoundException(volumeName);
             }
         }
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index 4a3ab82..4e8318f 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -337,6 +337,7 @@
                 }
                 try {
                     Files.walkFileTree(mRoot.toPath(), this);
+                    applyPending();
                 } catch (IOException e) {
                     // This should never happen, so yell loudly
                     throw new IllegalStateException(e);
@@ -346,7 +347,6 @@
                     }
                     Trace.endSection();
                 }
-                applyPending();
             }
         }
 
@@ -801,6 +801,7 @@
         op.withValue(MediaColumns.DOCUMENT_ID, xmp.getDocumentId());
         op.withValue(MediaColumns.INSTANCE_ID, xmp.getInstanceId());
         op.withValue(MediaColumns.ORIGINAL_DOCUMENT_ID, xmp.getOriginalDocumentId());
+        op.withValue(MediaColumns.XMP, xmp.getRedactedXmp());
     }
 
     /**
@@ -1051,7 +1052,7 @@
     }
 
     private static @NonNull <T> Optional<T> parseOptionalOrZero(@Nullable T value) {
-        if (value instanceof String && ((String) value).equals("0")) {
+        if (value instanceof String && isZero((String) value)) {
             return Optional.empty();
         } else if (value instanceof Number && ((Number) value).intValue() == 0) {
             return Optional.empty();
@@ -1275,6 +1276,18 @@
         return (path.size() == 4) && path.get(1).equals("audio") && path.get(2).equals("playlists");
     }
 
+    static boolean isZero(@NonNull String value) {
+        if (value.length() == 0) {
+            return false;
+        }
+        for (int i = 0; i < value.length(); i++) {
+            if (value.charAt(i) != '0') {
+                return false;
+            }
+        }
+        return true;
+    }
+
     static void logTroubleScanning(File file, Exception e) {
         if (LOGW) Log.w(TAG, "Trouble scanning " + file + ": " + e);
     }
diff --git a/src/com/android/providers/media/util/XmpInterface.java b/src/com/android/providers/media/util/XmpInterface.java
index 63f736f..1a316e7 100644
--- a/src/com/android/providers/media/util/XmpInterface.java
+++ b/src/com/android/providers/media/util/XmpInterface.java
@@ -28,16 +28,18 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.providers.media.MediaProvider;
+
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.ByteArrayInputStream;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
-import java.util.Collections;
+import java.nio.file.Files;
+import java.util.Arrays;
 import java.util.Set;
 import java.util.UUID;
 
@@ -62,35 +64,29 @@
     private static final String NAME_ORIGINAL_DOCUMENT_ID = "OriginalDocumentID";
     private static final String NAME_INSTANCE_ID = "InstanceID";
 
-    private final ByteCountingInputStream mIn;
-    private final Set<String> mRedactedExifTags;
-    private final long mXmpOffset;
-    private final LongArray mRedactedRanges;
+    private final LongArray mRedactedRanges = new LongArray();
+    private byte[] mRedactedXmp;
     private String mFormat;
     private String mDocumentId;
     private String mInstanceId;
     private String mOriginalDocumentId;
 
-    private XmpInterface(@NonNull InputStream in) throws IOException {
-        this(in, Collections.emptySet(), new long[0]);
-    }
+    private XmpInterface(@NonNull byte[] rawXmp, @NonNull Set<String> redactedExifTags,
+            @NonNull long[] xmpOffsets) throws IOException {
+        mRedactedXmp = rawXmp;
 
-    private XmpInterface(
-            @NonNull InputStream in, @NonNull Set<String> redactedExifTags, long[] xmpOffsets)
-            throws IOException {
-        mIn = new ByteCountingInputStream(in);
-        mRedactedExifTags = redactedExifTags;
-        mXmpOffset = xmpOffsets.length == 0 ? 0 : xmpOffsets[0];
-        mRedactedRanges = new LongArray();
+        final ByteCountingInputStream in = new ByteCountingInputStream(
+                new ByteArrayInputStream(rawXmp));
+        final long xmpOffset = xmpOffsets.length == 0 ? 0 : xmpOffsets[0];
         try {
             final XmlPullParser parser = Xml.newPullParser();
-            parser.setInput(mIn, StandardCharsets.UTF_8.name());
+            parser.setInput(in, StandardCharsets.UTF_8.name());
 
             long offset = 0;
             int type;
             while ((type = parser.next()) != END_DOCUMENT) {
                 if (type != START_TAG) {
-                    offset = mIn.getOffset(parser);
+                    offset = in.getOffset(parser);
                     continue;
                 }
 
@@ -117,14 +113,19 @@
                     mInstanceId = maybeOverride(mInstanceId, parser.nextText());
                 } else if (NS_XMPMM.equals(ns) && NAME_ORIGINAL_DOCUMENT_ID.equals(name)) {
                     mOriginalDocumentId = maybeOverride(mOriginalDocumentId, parser.nextText());
-                } else if (NS_EXIF.equals(ns) && mRedactedExifTags.contains(name)) {
+                } else if (NS_EXIF.equals(ns) && redactedExifTags.contains(name)) {
                     long start = offset;
                     do {
                         type = parser.next();
                     } while (type != END_TAG || !parser.getName().equals(name));
-                    offset = mIn.getOffset(parser);
-                    mRedactedRanges.add(mXmpOffset + start);
-                    mRedactedRanges.add(mXmpOffset + offset);
+                    offset = in.getOffset(parser);
+
+                    // Redact range within entire file
+                    mRedactedRanges.add(xmpOffset + start);
+                    mRedactedRanges.add(xmpOffset + offset);
+
+                    // Redact range within local copy
+                    Arrays.fill(mRedactedXmp, (int) start, (int) offset, (byte) ' ');
                 }
             }
         } catch (XmlPullParserException e) {
@@ -144,7 +145,7 @@
 
     public static @NonNull XmpInterface fromContainer(@NonNull ExifInterface exif)
             throws IOException {
-        return fromContainer(exif, Collections.emptySet());
+        return fromContainer(exif, MediaProvider.sRedactedExifTags);
     }
 
     public static @NonNull XmpInterface fromContainer(@NonNull ExifInterface exif,
@@ -158,12 +159,12 @@
             buf = new byte[0];
             xmpOffsets = new long[0];
         }
-        return new XmpInterface(new ByteArrayInputStream(buf), redactedExifTags, xmpOffsets);
+        return new XmpInterface(buf, redactedExifTags, xmpOffsets);
     }
 
     public static @NonNull XmpInterface fromContainer(@NonNull IsoInterface iso)
             throws IOException {
-        return fromContainer(iso, Collections.emptySet());
+        return fromContainer(iso, MediaProvider.sRedactedExifTags);
     }
 
     public static @NonNull XmpInterface fromContainer(@NonNull IsoInterface iso,
@@ -183,12 +184,13 @@
             buf = new byte[0];
             xmpOffsets = new long[0];
         }
-        return new XmpInterface(new ByteArrayInputStream(buf), redactedExifTags, xmpOffsets);
+        return new XmpInterface(buf, redactedExifTags, xmpOffsets);
     }
 
     public static @NonNull XmpInterface fromSidecar(@NonNull File file)
             throws IOException {
-        return new XmpInterface(new FileInputStream(file));
+        return new XmpInterface(Files.readAllBytes(file.toPath()),
+                MediaProvider.sRedactedExifTags, new long[0]);
     }
 
     private static @Nullable String maybeOverride(@Nullable String existing,
@@ -221,6 +223,10 @@
         return mOriginalDocumentId;
     }
 
+    public @Nullable byte[] getRedactedXmp() {
+        return mRedactedXmp;
+    }
+
     /** The [start, end] offsets in the original file where to-be redacted info is stored */
     public LongArray getRedactionRanges() {
         return mRedactedRanges;
diff --git a/tests/jni/FuseDaemonTest/Android.bp b/tests/jni/FuseDaemonTest/Android.bp
index 2f0c78f..1c4afe6 100644
--- a/tests/jni/FuseDaemonTest/Android.bp
+++ b/tests/jni/FuseDaemonTest/Android.bp
@@ -43,9 +43,10 @@
     name: "FuseDaemonLegacyTest",
     manifest: "legacy/AndroidManifest.xml",
     srcs: ["legacy/src/**/*.java"],
-    static_libs: ["androidx.test.rules", "truth-prebuilt"],
+    static_libs: ["androidx.test.rules", "truth-prebuilt",  "tests-fusedaemon-lib"],
     test_suites: ["general-tests", "mts"],
-    sdk_version: "29"
+    sdk_version: "test_current",
+    target_sdk_version: "29"
 }
 
 java_test_host {
diff --git a/tests/jni/FuseDaemonTest/legacy/src/com/android/tests/fused/legacy/LegacyFileAccessTest.java b/tests/jni/FuseDaemonTest/legacy/src/com/android/tests/fused/legacy/LegacyFileAccessTest.java
index 151ec50..1b09f5e 100644
--- a/tests/jni/FuseDaemonTest/legacy/src/com/android/tests/fused/legacy/LegacyFileAccessTest.java
+++ b/tests/jni/FuseDaemonTest/legacy/src/com/android/tests/fused/legacy/LegacyFileAccessTest.java
@@ -16,6 +16,8 @@
 
 package com.android.tests.fused.legacy;
 
+import static com.android.tests.fused.lib.TestUtils.pollForExternalStorageState;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.fail;
@@ -31,6 +33,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -56,6 +59,11 @@
     private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
     private static final long POLLING_SLEEP_MILLIS = 100;
 
+    @Before
+    public void setUp() throws Exception {
+        pollForExternalStorageState();
+    }
+
     /**
      * Tests that legacy apps bypass the type-path conformity restrictions imposed by MediaProvider.
      * <p> Assumes we have WRITE_EXTERNAL_STORAGE.
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 25ccee2..e564915 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
@@ -38,6 +38,7 @@
 import android.content.IntentFilter;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Environment;
 import android.os.ParcelFileDescriptor;
 import android.provider.MediaStore;
 import android.util.Log;
@@ -58,6 +59,7 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /**
  * General helper functions for FuseDaemon tests.
@@ -71,6 +73,9 @@
     public static final String CREATE_FILE_QUERY = "com.android.tests.fused.createfile";
     public static final String DELETE_FILE_QUERY = "com.android.tests.fused.deletefile";
 
+    private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
+    private static final long POLLING_SLEEP_MILLIS = 100;
+
 
     private static final UiAutomation sUiAutomation = InstrumentationRegistry.getInstrumentation()
             .getUiAutomation();
@@ -380,6 +385,17 @@
         return path.delete();
     }
 
+    public static void pollForExternalStorageState() throws Exception {
+        for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) {
+            if(Environment.getExternalStorageState(Environment.getExternalStorageDirectory())
+                    .equals(Environment.MEDIA_MOUNTED)) {
+                return;
+            }
+            Thread.sleep(POLLING_SLEEP_MILLIS);
+        }
+        fail("Timed out while waiting for ExternalStorageState to be MEDIA_MOUNTED");
+    }
+
     /**
      * <p>This method drops shell permission identity.
      */
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 a9b0274..00e698e 100644
--- a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
+++ b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
@@ -47,6 +47,7 @@
 import static com.android.tests.fused.lib.TestUtils.uninstallApp;
 import static com.android.tests.fused.lib.TestUtils.uninstallAppNoThrow;
 import static com.android.tests.fused.lib.TestUtils.updateDisplayNameWithMediaProvider;
+import static com.android.tests.fused.lib.TestUtils.pollForExternalStorageState;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -131,10 +132,13 @@
 
     private static final String[] SYSTEM_GALERY_APPOPS = { AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES,
             AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO };
-    // skips all test cases if FUSE is not active.
+
     @Before
-    public void assumeFuseIsOn() {
+    public void setUp() throws Exception {
+        // skips all test cases if FUSE is not active.
         assumeTrue(getBoolean("persist.sys.fuse", false));
+
+        pollForExternalStorageState();
         EXTERNAL_FILES_DIR.mkdirs();
     }
 
diff --git a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
index fbefb44..1bbcb45 100644
--- a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
@@ -229,6 +229,19 @@
     }
 
     @Test
+    public void testIsZero() throws Exception {
+        assertFalse(ModernMediaScanner.isZero(""));
+        assertFalse(ModernMediaScanner.isZero("meow"));
+        assertFalse(ModernMediaScanner.isZero("1"));
+        assertFalse(ModernMediaScanner.isZero("01"));
+        assertFalse(ModernMediaScanner.isZero("010"));
+
+        assertTrue(ModernMediaScanner.isZero("0"));
+        assertTrue(ModernMediaScanner.isZero("00"));
+        assertTrue(ModernMediaScanner.isZero("000"));
+    }
+
+    @Test
     public void testPlaylistM3u() throws Exception {
         doPlaylist(R.raw.test_m3u, "test.m3u");
     }
diff --git a/tests/src/com/android/providers/media/util/XmpInterfaceTest.java b/tests/src/com/android/providers/media/util/XmpInterfaceTest.java
index e9876a3..6995219 100644
--- a/tests/src/com/android/providers/media/util/XmpInterfaceTest.java
+++ b/tests/src/com/android/providers/media/util/XmpInterfaceTest.java
@@ -18,7 +18,9 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import android.content.Context;
@@ -30,9 +32,6 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.providers.media.R;
-import com.android.providers.media.util.FileUtils;
-import com.android.providers.media.util.IsoInterface;
-import com.android.providers.media.util.XmpInterface;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -93,14 +92,22 @@
         redactionTags.add(ExifInterface.TAG_GPS_TIMESTAMP);
         redactionTags.add(ExifInterface.TAG_GPS_VERSION_ID);
 
-        // The XMP contents start at byte 1809. These are the file offsets.
-        final long[] expectedRanges = new long[]{2625,2675,2678,2730,2733,2792,2795,2841};
         final Context context = InstrumentationRegistry.getContext();
         try (InputStream in = context.getResources().openRawResource(R.raw.lg_g4_iso_800_jpg)) {
             ExifInterface exif = new ExifInterface(in);
             assertEquals(1809, exif.getAttributeRange(ExifInterface.TAG_XMP)[0]);
             final XmpInterface xmp = XmpInterface.fromContainer(exif, redactionTags);
+
+            // Confirm redact range within entire file
+            // The XMP contents start at byte 1809. These are the file offsets.
+            final long[] expectedRanges = new long[]{2625,2675,2678,2730,2733,2792,2795,2841};
             assertArrayEquals(expectedRanges, xmp.getRedactionRanges().toArray());
+
+            // Confirm redact range within local copy
+            final String redactedXmp = new String(xmp.getRedactedXmp());
+            assertFalse(redactedXmp.contains("exif:GPSLatitude"));
+            assertFalse(redactedXmp.contains("exif:GPSLongitude"));
+            assertTrue(redactedXmp.contains("exif:ShutterSpeedValue"));
         }
     }
 
@@ -116,9 +123,16 @@
         final IsoInterface mp4 = IsoInterface.fromFile(file);
         final XmpInterface xmp = XmpInterface.fromContainer(mp4, redactionTags);
 
+        // Confirm redact range within entire file
         // The XMP contents start at byte 30286. These are the file offsets.
         final long[] expectedRanges = new long[]{37299,37349,37352,37404,37407,37466,37469,37515};
         assertArrayEquals(expectedRanges, xmp.getRedactionRanges().toArray());
+
+        // Confirm redact range within local copy
+        final String redactedXmp = new String(xmp.getRedactedXmp());
+        assertFalse(redactedXmp.contains("exif:GPSLatitude"));
+        assertFalse(redactedXmp.contains("exif:GPSLongitude"));
+        assertTrue(redactedXmp.contains("exif:ShutterSpeedValue"));
     }
 
     @Test