Merge "Don't overwrite is_download column" into rvc-dev
diff --git a/apex/framework/Android.bp b/apex/framework/Android.bp
index 7cb77f9..bf35418 100644
--- a/apex/framework/Android.bp
+++ b/apex/framework/Android.bp
@@ -51,10 +51,7 @@
 
 stubs_defaults {
     name: "framework-mediaprovider-stubs-srcs-defaults",
-    srcs: [
-        ":framework-mediaprovider-sources",
-    ],
-    sdk_version: "system_current",
+    srcs: [":framework-mediaprovider-sources"],
 }
 
 droidstubs {
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 27e61ec..16a0892 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -896,11 +896,8 @@
         final ContentValues values = new ContentValues();
         if (value) {
             values.put(MediaColumns.IS_TRASHED, 1);
-            values.put(MediaColumns.DATE_EXPIRES,
-                    (System.currentTimeMillis() + DateUtils.WEEK_IN_MILLIS) / 1000);
         } else {
             values.put(MediaColumns.IS_TRASHED, 0);
-            values.putNull(MediaColumns.DATE_EXPIRES);
         }
         return createRequest(resolver, CREATE_TRASH_REQUEST_CALL, uris, values);
     }
@@ -1103,9 +1100,17 @@
          * The time the media item should be considered expired. Typically only
          * meaningful in the context of {@link #IS_PENDING} or
          * {@link #IS_TRASHED}.
+         * <p>
+         * The value stored in this column is automatically calculated when
+         * {@link #IS_PENDING} or {@link #IS_TRASHED} is changed. The default
+         * pending expiration is typically 7 days, and the default trashed
+         * expiration is typically 30 days.
+         * <p>
+         * Expired media items are automatically deleted once their expiration
+         * time has passed, typically during during the next device idle period.
          */
         @CurrentTimeSecondsLong
-        @Column(Cursor.FIELD_TYPE_INTEGER)
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
         public static final String DATE_EXPIRES = "date_expires";
 
         /**
@@ -2058,7 +2063,6 @@
                 values.put(MediaColumns.MIME_TYPE, "image/jpeg");
                 values.put(MediaColumns.DATE_ADDED, now / 1000);
                 values.put(MediaColumns.DATE_MODIFIED, now / 1000);
-                values.put(MediaColumns.DATE_EXPIRES, (now + DateUtils.DAY_IN_MILLIS) / 1000);
                 values.put(MediaColumns.IS_PENDING, 1);
 
                 final Uri uri = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
@@ -2070,7 +2074,6 @@
                     // Everything went well above, publish it!
                     values.clear();
                     values.put(MediaColumns.IS_PENDING, 0);
-                    values.putNull(MediaColumns.DATE_EXPIRES);
                     cr.update(uri, values, null, null);
                     return uri.toString();
                 } catch (Exception e) {
diff --git a/jni/Android.bp b/jni/Android.bp
index 0ff937b..265f7fb 100644
--- a/jni/Android.bp
+++ b/jni/Android.bp
@@ -65,7 +65,14 @@
 
 cc_test {
     name: "fuse_node_test",
-    test_suites: ["device-tests"],
+    test_suites: ["device-tests", "mts"],
+
+    compile_multilib: "both",
+    multilib: {
+        lib32: { suffix: "32", },
+        lib64: { suffix: "64", },
+    },
+
     srcs: [
         "node_test.cpp",
         "node.cpp",
@@ -91,7 +98,14 @@
 
 cc_test {
     name: "RedactionInfoTest",
-    test_suites: ["device-tests"],
+    test_suites: ["device-tests", "mts"],
+
+    compile_multilib: "both",
+    multilib: {
+        lib32: { suffix: "32", },
+        lib64: { suffix: "64", },
+    },
+
     srcs: [
         "RedactionInfoTest.cpp",
         "RedactionInfo.cpp",
@@ -111,4 +125,4 @@
 
     sdk_version: "current",
     stl: "c++_static",
-}
\ No newline at end of file
+}
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index b9c3214..f1bc4e4 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -99,14 +99,6 @@
 constexpr size_t MAX_READ_SIZE = 128 * 1024;
 // Stolen from: UserHandle#getUserId
 constexpr int PER_USER_RANGE = 100000;
-// Cache inode attributes forever to improve performance
-// Whenver attributes could have changed on the lower filesystem outside the FUSE driver, we call
-// fuse_invalidate_entry_cache
-constexpr double DEFAULT_ATTR_TIMEOUT_SECONDS = std::numeric_limits<double>::max();
-// Cache dentries forever to improve performance
-// Whenver attributes could have changed on the lower filesystem outside the FUSE driver, we call
-// fuse_invalidate_entry_cache
-constexpr double DEFAULT_ENTRY_TIMEOUT_SECONDS = std::numeric_limits<double>::max();
 
 /*
  * In order to avoid double caching with fuse, call fadvise on the file handles
@@ -354,30 +346,6 @@
     return reinterpret_cast<struct fuse*>(fuse_req_userdata(req));
 }
 
-static bool is_android_path(const string& path, const string& fuse_path, uid_t uid) {
-    int user_id = uid / PER_USER_RANGE;
-    const std::string android_path = fuse_path + "/" + std::to_string(user_id) + "/Android";
-    return path.rfind(android_path, 0) == 0;
-}
-
-static double get_attr_timeout(const string& path, uid_t uid, struct fuse* fuse, node* parent) {
-    if (fuse->IsRoot(parent) || is_android_path(path, fuse->path, uid)) {
-        // The /0 and /0/Android attrs can be always cached, as they never change
-        return std::numeric_limits<double>::max();
-    } else {
-        return DEFAULT_ATTR_TIMEOUT_SECONDS;
-    }
-}
-
-static double get_entry_timeout(const string& path, uid_t uid, struct fuse* fuse, node* parent) {
-    if (fuse->IsRoot(parent) || is_android_path(path, fuse->path, uid)) {
-        // The /0 and /0/Android dentries can be always cached, as they are visible to all apps
-        return std::numeric_limits<double>::max();
-    } else {
-        return DEFAULT_ENTRY_TIMEOUT_SECONDS;
-    }
-}
-
 static node* make_node_entry(fuse_req_t req, node* parent, const string& name, const string& path,
                              struct fuse_entry_param* e, int* error_code) {
     struct fuse* fuse = get_fuse(req);
@@ -402,8 +370,8 @@
     // reuse inode numbers.
     e->generation = 0;
     e->ino = fuse->ToInode(node);
-    e->entry_timeout = get_entry_timeout(path, ctx->uid, fuse, parent);
-    e->attr_timeout = get_attr_timeout(path, ctx->uid, fuse, parent);
+    e->entry_timeout = std::numeric_limits<double>::max();
+    e->attr_timeout = std::numeric_limits<double>::max();
 
     return node;
 }
@@ -525,7 +493,7 @@
     if (lstat(path.c_str(), &s) < 0) {
         fuse_reply_err(req, errno);
     } else {
-        fuse_reply_attr(req, &s, get_attr_timeout(path, ctx->uid, fuse, nullptr));
+        fuse_reply_attr(req, &s, std::numeric_limits<double>::max());
     }
 }
 
@@ -591,7 +559,7 @@
     }
 
     lstat(path.c_str(), attr);
-    fuse_reply_attr(req, attr, get_attr_timeout(path, ctx->uid, fuse, nullptr));
+    fuse_reply_attr(req, attr, std::numeric_limits<double>::max());
 }
 
 static void pf_canonical_path(fuse_req_t req, fuse_ino_t ino)
@@ -1498,22 +1466,16 @@
 void FuseDaemon::InvalidateFuseDentryCache(const std::string& path) {
     LOG(VERBOSE) << "Invalidating FUSE dentry cache";
     if (active.load(std::memory_order_acquire)) {
-        string name;
-        fuse_ino_t parent;
+        std::lock_guard<std::recursive_mutex> guard(fuse->lock);
 
-        {
-            std::lock_guard<std::recursive_mutex> guard(fuse->lock);
-            const node* node = node::LookupAbsolutePath(fuse->root, path);
-            if (node) {
-                name = node->GetName();
-                parent = fuse->ToInode(node->GetParent());
+        const node* node = node::LookupAbsolutePath(fuse->root, path);
+        if (node) {
+            string name = node->GetName();
+            fuse_ino_t parent = fuse->ToInode(node->GetParent());
+            if (fuse_lowlevel_notify_inval_entry(fuse->se, parent, name.c_str(), name.size())) {
+                LOG(WARNING) << "Failed to invalidate dentry for path";
             }
         }
-
-        if (!name.empty() &&
-            fuse_lowlevel_notify_inval_entry(fuse->se, parent, name.c_str(), name.size())) {
-            LOG(WARNING) << "Failed to invalidate dentry for path";
-        }
     } else {
         LOG(WARNING) << "FUSE daemon is inactive. Cannot invalidate dentry";
     }
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 181ea3e..7d9b6d0 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -19,7 +19,7 @@
     <string name="uid_label" msgid="8421971615411294156">"Medier"</string>
     <string name="storage_description" msgid="4081716890357580107">"Lokalt lager"</string>
     <string name="app_label" msgid="9035307001052716210">"Medielagring"</string>
-    <string name="artist_label" msgid="8105600993099120273">"Musiker"</string>
+    <string name="artist_label" msgid="8105600993099120273">"Kunstner"</string>
     <string name="unknown" msgid="2059049215682829375">"Ukendt"</string>
     <string name="root_images" msgid="5861633549189045666">"Billeder"</string>
     <string name="root_videos" msgid="8792703517064649453">"Videoer"</string>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 00c1c13..b9352eb 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -42,67 +42,67 @@
     <string name="allow" msgid="8885707816848569619">"Permitir"</string>
     <string name="deny" msgid="6040983710442068936">"Recusar"</string>
     <plurals name="permission_write_audio" formatted="false" msgid="8914759422381305478">
-      <item quantity="other">Pretende permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> ficheiros de áudio?</item>
-      <item quantity="one">Pretende permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> modifique este ficheiro de áudio?</item>
+      <item quantity="other">Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> ficheiros de áudio?</item>
+      <item quantity="one">Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> modifique este ficheiro de áudio?</item>
     </plurals>
     <plurals name="permission_write_video" formatted="false" msgid="1098082003326873084">
-      <item quantity="other">Pretende permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> vídeos?</item>
-      <item quantity="one">Pretende permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> modifique este vídeo?</item>
+      <item quantity="other">Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> vídeos?</item>
+      <item quantity="one">Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> modifique este vídeo?</item>
     </plurals>
     <plurals name="permission_write_image" formatted="false" msgid="748745548893845892">
-      <item quantity="other">Pretende permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> fotos?</item>
-      <item quantity="one">Pretende permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> modifique esta foto?</item>
+      <item quantity="other">Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> fotos?</item>
+      <item quantity="one">Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> modifique esta foto?</item>
     </plurals>
     <plurals name="permission_write_generic" formatted="false" msgid="3270172714743671779">
-      <item quantity="other">Pretende permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> itens?</item>
-      <item quantity="one">Pretende permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> modifique este item?</item>
+      <item quantity="other">Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> itens?</item>
+      <item quantity="one">Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> modifique este item?</item>
     </plurals>
     <plurals name="permission_trash_audio" formatted="false" msgid="8907813869381755423">
-      <item quantity="other">Pretende permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> mova <xliff:g id="COUNT">^2</xliff:g> ficheiros de áudio para o lixo?</item>
-      <item quantity="one">Pretende permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> mova este ficheiro de áudio para o lixo?</item>
+      <item quantity="other">Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> mova <xliff:g id="COUNT">^2</xliff:g> ficheiros de áudio para o lixo?</item>
+      <item quantity="one">Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> mova este ficheiro de áudio para o lixo?</item>
     </plurals>
     <plurals name="permission_trash_video" formatted="false" msgid="4672871911555787438">
-      <item quantity="other">Pretende permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> mova <xliff:g id="COUNT">^2</xliff:g> vídeos para o lixo?</item>
-      <item quantity="one">Pretende permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> mova este vídeo para o lixo?</item>
+      <item quantity="other">Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> mova <xliff:g id="COUNT">^2</xliff:g> vídeos para o lixo?</item>
+      <item quantity="one">Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> mova este vídeo para o lixo?</item>
     </plurals>
     <plurals name="permission_trash_image" formatted="false" msgid="6400475304599873227">
-      <item quantity="other">Pretende permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> mova <xliff:g id="COUNT">^2</xliff:g> fotos para o lixo?</item>
-      <item quantity="one">Pretende permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> mova esta foto para o lixo?</item>
+      <item quantity="other">Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> mova <xliff:g id="COUNT">^2</xliff:g> fotos para o lixo?</item>
+      <item quantity="one">Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> mova esta foto para o lixo?</item>
     </plurals>
     <plurals name="permission_trash_generic" formatted="false" msgid="3814167365075039711">
-      <item quantity="other">Pretende permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> mova <xliff:g id="COUNT">^2</xliff:g> itens para o lixo?</item>
-      <item quantity="one">Pretende permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> mova este item para o lixo?</item>
+      <item quantity="other">Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> mova <xliff:g id="COUNT">^2</xliff:g> itens para o lixo?</item>
+      <item quantity="one">Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> mova este item para o lixo?</item>
     </plurals>
     <plurals name="permission_untrash_audio" formatted="false" msgid="7795265980168966321">
-      <item quantity="other">Pretende permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> retire <xliff:g id="COUNT">^2</xliff:g> ficheiros de áudio do lixo?</item>
-      <item quantity="one">Pretende permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> retire este ficheiro de áudio do lixo?</item>
+      <item quantity="other">Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> retire <xliff:g id="COUNT">^2</xliff:g> ficheiros de áudio do lixo?</item>
+      <item quantity="one">Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> retire este ficheiro de áudio do lixo?</item>
     </plurals>
     <plurals name="permission_untrash_video" formatted="false" msgid="332894888445508879">
-      <item quantity="other">Pretende permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> retire <xliff:g id="COUNT">^2</xliff:g> vídeos do lixo?</item>
-      <item quantity="one">Pretende permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> retire este vídeo do lixo?</item>
+      <item quantity="other">Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> retire <xliff:g id="COUNT">^2</xliff:g> vídeos do lixo?</item>
+      <item quantity="one">Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> retire este vídeo do lixo?</item>
     </plurals>
     <plurals name="permission_untrash_image" formatted="false" msgid="7024071378733595056">
-      <item quantity="other">Pretende permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> retire <xliff:g id="COUNT">^2</xliff:g> fotos do lixo?</item>
-      <item quantity="one">Pretende permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> retire esta foto do lixo?</item>
+      <item quantity="other">Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> retire <xliff:g id="COUNT">^2</xliff:g> fotos do lixo?</item>
+      <item quantity="one">Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> retire esta foto do lixo?</item>
     </plurals>
     <plurals name="permission_untrash_generic" formatted="false" msgid="6872817093731198374">
-      <item quantity="other">Pretende permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> retire <xliff:g id="COUNT">^2</xliff:g> itens do lixo?</item>
-      <item quantity="one">Pretende permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> retire este item do lixo?</item>
+      <item quantity="other">Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> retire <xliff:g id="COUNT">^2</xliff:g> itens do lixo?</item>
+      <item quantity="one">Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> retire este item do lixo?</item>
     </plurals>
     <plurals name="permission_delete_audio" formatted="false" msgid="6848547621165184719">
-      <item quantity="other">Pretende permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> elimine <xliff:g id="COUNT">^2</xliff:g> ficheiros de áudio?</item>
-      <item quantity="one">Pretende permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> elimine este ficheiro de áudio?</item>
+      <item quantity="other">Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> elimine <xliff:g id="COUNT">^2</xliff:g> ficheiros de áudio?</item>
+      <item quantity="one">Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> elimine este ficheiro de áudio?</item>
     </plurals>
     <plurals name="permission_delete_video" formatted="false" msgid="1251942606336748563">
-      <item quantity="other">Pretende permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> elimine <xliff:g id="COUNT">^2</xliff:g> vídeos?</item>
-      <item quantity="one">Pretende permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> elimine este vídeo?</item>
+      <item quantity="other">Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> elimine <xliff:g id="COUNT">^2</xliff:g> vídeos?</item>
+      <item quantity="one">Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> elimine este vídeo?</item>
     </plurals>
     <plurals name="permission_delete_image" formatted="false" msgid="2303409455224710111">
-      <item quantity="other">Pretende permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> elimine <xliff:g id="COUNT">^2</xliff:g> fotos?</item>
-      <item quantity="one">Pretende permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> elimine esta foto?</item>
+      <item quantity="other">Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> elimine <xliff:g id="COUNT">^2</xliff:g> fotos?</item>
+      <item quantity="one">Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> elimine esta foto?</item>
     </plurals>
     <plurals name="permission_delete_generic" formatted="false" msgid="1412218850351841181">
-      <item quantity="other">Pretende permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> elimine <xliff:g id="COUNT">^2</xliff:g> itens?</item>
-      <item quantity="one">Pretende permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> elimine este item?</item>
+      <item quantity="other">Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> elimine <xliff:g id="COUNT">^2</xliff:g> itens?</item>
+      <item quantity="one">Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> elimine este item?</item>
     </plurals>
 </resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 51a5d6c..77cc361 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -22,7 +22,7 @@
     <string name="artist_label" msgid="8105600993099120273">"Artist"</string>
     <string name="unknown" msgid="2059049215682829375">"Okänd"</string>
     <string name="root_images" msgid="5861633549189045666">"Bilder"</string>
-    <string name="root_videos" msgid="8792703517064649453">"Videoklipp"</string>
+    <string name="root_videos" msgid="8792703517064649453">"Videor"</string>
     <string name="root_audio" msgid="3505830755201326018">"Ljud"</string>
     <string name="root_documents" msgid="3829103301363849237">"Dokument"</string>
     <string name="permission_required" msgid="1460820436132943754">"Behörighet krävs för att ändra eller radera objektet."</string>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index 9fa73bf..5958e2c 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -58,36 +58,36 @@
       <item quantity="one">இந்த ஃபைலில் மாற்றங்களைச் செய்ய <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
     </plurals>
     <plurals name="permission_trash_audio" formatted="false" msgid="8907813869381755423">
-      <item quantity="other"><xliff:g id="COUNT">^2</xliff:g> ஆடியோ ஃபைல்களைக் குப்பைக்கு நகர்த்த <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
-      <item quantity="one">இந்த ஆடியோ ஃபைலைக் குப்பைக்கு நகர்த்த <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
+      <item quantity="other"><xliff:g id="COUNT">^2</xliff:g> ஆடியோ ஃபைல்களை நீக்கியவற்றிற்கு நகர்த்த <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
+      <item quantity="one">இந்த ஆடியோ ஃபைலை நீக்கியவற்றிற்கு நகர்த்த <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
     </plurals>
     <plurals name="permission_trash_video" formatted="false" msgid="4672871911555787438">
-      <item quantity="other"><xliff:g id="COUNT">^2</xliff:g> வீடியோக்களைக் குப்பைக்கு நகர்த்த <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
-      <item quantity="one">இந்த வீடியோவைக் குப்பைக்கு நகர்த்த <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
+      <item quantity="other"><xliff:g id="COUNT">^2</xliff:g> வீடியோக்களை நீக்கியவற்றிற்கு நகர்த்த <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
+      <item quantity="one">இந்த வீடியோவை நீக்கியவற்றிற்கு நகர்த்த <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
     </plurals>
     <plurals name="permission_trash_image" formatted="false" msgid="6400475304599873227">
-      <item quantity="other"><xliff:g id="COUNT">^2</xliff:g> படங்களைக் குப்பைக்கு நகர்த்த <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
-      <item quantity="one">இந்தப் படத்தைக் குப்பைக்கு நகர்த்த <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
+      <item quantity="other"><xliff:g id="COUNT">^2</xliff:g> படங்களை நீக்கியவற்றிற்கு நகர்த்த <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
+      <item quantity="one">இந்தப் படத்தை நீக்கியவற்றிற்கு நகர்த்த <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
     </plurals>
     <plurals name="permission_trash_generic" formatted="false" msgid="3814167365075039711">
-      <item quantity="other"><xliff:g id="COUNT">^2</xliff:g> ஃபைல்களைக் குப்பைக்கு நகர்த்த <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
-      <item quantity="one">இந்த ஃபைலைக் குப்பைக்கு நகர்த்த <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
+      <item quantity="other"><xliff:g id="COUNT">^2</xliff:g> ஃபைல்களை நீக்கியவற்றிற்கு நகர்த்த <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
+      <item quantity="one">இந்த ஃபைலை நீக்கியவற்றிற்கு நகர்த்த <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
     </plurals>
     <plurals name="permission_untrash_audio" formatted="false" msgid="7795265980168966321">
-      <item quantity="other"><xliff:g id="COUNT">^2</xliff:g> ஆடியோ ஃபைல்களைக் குப்பையிலிருந்து நகர்த்த <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
-      <item quantity="one">இந்த ஆடியோ ஃபைலைக் குப்பையிலிருந்து நகர்த்த <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
+      <item quantity="other"><xliff:g id="COUNT">^2</xliff:g> ஆடியோ ஃபைல்களை நீக்கியவற்றில் இருந்து நகர்த்த <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
+      <item quantity="one">இந்த ஆடியோ ஃபைலை நீக்கியவற்றில் இருந்து நகர்த்த <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
     </plurals>
     <plurals name="permission_untrash_video" formatted="false" msgid="332894888445508879">
-      <item quantity="other"><xliff:g id="COUNT">^2</xliff:g> வீடியோக்களைக் குப்பையிலிருந்து நகர்த்த <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
-      <item quantity="one">இந்த வீடியோவைக் குப்பையிலிருந்து நகர்த்த <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
+      <item quantity="other"><xliff:g id="COUNT">^2</xliff:g> வீடியோக்களை நீக்கியவற்றில் இருந்து நகர்த்த <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
+      <item quantity="one">இந்த வீடியோவை நீக்கியவற்றில் இருந்து நகர்த்த <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
     </plurals>
     <plurals name="permission_untrash_image" formatted="false" msgid="7024071378733595056">
-      <item quantity="other"><xliff:g id="COUNT">^2</xliff:g> படங்களைக் குப்பையிலிருந்து நகர்த்த <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
-      <item quantity="one">இந்தப் படத்தைக் குப்பையிலிருந்து நகர்த்த <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
+      <item quantity="other"><xliff:g id="COUNT">^2</xliff:g> படங்களை நீக்கியவற்றில் இருந்து நகர்த்த <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
+      <item quantity="one">இந்தப் படத்தை நீக்கியவற்றில் இருந்து நகர்த்த <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
     </plurals>
     <plurals name="permission_untrash_generic" formatted="false" msgid="6872817093731198374">
-      <item quantity="other"><xliff:g id="COUNT">^2</xliff:g> ஃபைல்களைக் குப்பையிலிருந்து நகர்த்த <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
-      <item quantity="one">இந்த ஃபைலைக் குப்பையிலிருந்து நகர்த்த <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
+      <item quantity="other"><xliff:g id="COUNT">^2</xliff:g> ஃபைல்களை நீக்கியவற்றில் இருந்து நகர்த்த <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
+      <item quantity="one">இந்த ஃபைலை நீக்கியவற்றில் இருந்து நகர்த்த <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
     </plurals>
     <plurals name="permission_delete_audio" formatted="false" msgid="6848547621165184719">
       <item quantity="other"><xliff:g id="COUNT">^2</xliff:g> ஆடியோ ஃபைல்களை நீக்க <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?</item>
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index 55ec897..afa3d63 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -135,7 +135,7 @@
         public void onUpdate(@NonNull DatabaseHelper helper, @NonNull String volumeName,
                 long oldId, int oldMediaType, boolean oldIsDownload,
                 long newId, int newMediaType, boolean newIsDownload,
-                String ownerPackage, String oldPath);
+                String oldOwnerPackage, String newOwnerPackage, String oldPath);
         public void onDelete(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id,
                 int mediaType, boolean isDownload, String ownerPackage, String path);
     }
@@ -269,15 +269,16 @@
                 final long newId = Long.parseLong(split[4]);
                 final int newMediaType = Integer.parseInt(split[5]);
                 final boolean newIsDownload = Integer.parseInt(split[6]) != 0;
-                final String ownerPackage = split[7];
-                // Path can include ':',  assume rest of split[8..length] is path.
-                final String oldPath = String.join(":", Arrays.copyOfRange(split, 8, split.length));
+                final String oldOwnerPackage = split[7];
+                final String newOwnerPackage = split[8];
+                // Path can include ':',  assume rest of split[9..length] is path.
+                final String oldPath = String.join(":", Arrays.copyOfRange(split, 9, split.length));
 
                 Trace.beginSection("_UPDATE");
                 try {
                     mFilesListener.onUpdate(DatabaseHelper.this, volumeName, oldId,
                             oldMediaType, oldIsDownload, newId, newMediaType, newIsDownload,
-                            ownerPackage, oldPath);
+                            oldOwnerPackage, newOwnerPackage, oldPath);
                 } finally {
                     Trace.endSection();
                 }
@@ -989,7 +990,8 @@
         final String updateArg =
                 "old.volume_name||':'||old._id||':'||old.media_type||':'||old.is_download"
                         + "||':'||new._id||':'||new.media_type||':'||new.is_download"
-                        + "||':'||ifnull(old.owner_package_name,'null')||':'||old._data";
+                        + "||':'||ifnull(old.owner_package_name,'null')"
+                        + "||':'||ifnull(new.owner_package_name,'null')||':'||old._data";
         final String deleteArg =
                 "old.volume_name||':'||old._id||':'||old.media_type||':'||old.is_download"
                         + "||':'||ifnull(old.owner_package_name,'null')||':'||old._data";
@@ -1247,7 +1249,7 @@
     static final int VERSION_O = 800;
     static final int VERSION_P = 900;
     static final int VERSION_Q = 1023;
-    static final int VERSION_R = 1113;
+    static final int VERSION_R = 1114;
     static final int VERSION_LATEST = VERSION_R;
 
     /**
@@ -1387,6 +1389,9 @@
             if (fromVersion < 1113) {
                 // Empty version bump to ensure triggers are recreated
             }
+            if (fromVersion < 1114) {
+                // Empty version bump to ensure triggers are recreated
+            }
 
             if (recomputeDataValues) {
                 recomputeDataValues(db, internal);
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 627b752..37d1e17 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -60,6 +60,7 @@
 import static com.android.providers.media.util.FileUtils.extractTopLevelDir;
 import static com.android.providers.media.util.FileUtils.extractVolumeName;
 import static com.android.providers.media.util.FileUtils.getAbsoluteSanitizedPath;
+import static com.android.providers.media.util.FileUtils.isDataOrObbPath;
 import static com.android.providers.media.util.FileUtils.isDownload;
 import static com.android.providers.media.util.FileUtils.sanitizePath;
 import static com.android.providers.media.util.Logging.LOGV;
@@ -564,9 +565,10 @@
         public void onUpdate(@NonNull DatabaseHelper helper, @NonNull String volumeName,
                 long oldId, int oldMediaType, boolean oldIsDownload,
                 long newId, int newMediaType, boolean newIsDownload,
-                String ownerPackage, String oldPath) {
+                String oldOwnerPackage, String newOwnerPackage, String oldPath) {
             final boolean isDownload = oldIsDownload || newIsDownload;
-            handleUpdatedRowForFuse(oldPath, ownerPackage, oldId, newId);
+            handleUpdatedRowForFuse(oldPath, oldOwnerPackage, oldId, newId);
+            handleOwnerPackageNameChange(oldPath, oldOwnerPackage, newOwnerPackage);
             acceptWithExpansion(helper::notifyUpdate, volumeName, oldId, oldMediaType, isDownload);
 
             if (newMediaType != oldMediaType) {
@@ -1276,6 +1278,13 @@
                 return new String[] {""};
             }
 
+            // Do not allow apps to list Android/data or Android/obb dirs. Installer and
+            // MOUNT_EXTERNAL_ANDROID_WRITABLE apps won't be blocked by this, as their OBB dirs
+            // are mounted to lowerfs directly.
+            if (isDataOrObbPath(path)) {
+                return new String[] {""};
+            }
+
             if (shouldBypassFuseRestrictions(/*forWrite*/ false, path)) {
                 return new String[] {"/"};
             }
@@ -1342,6 +1351,55 @@
                 mimeType.startsWith(supportedPrimaryMimeType));
     }
 
+    /**
+     * Removes owner package for the renamed path if the calling package doesn't own the db row
+     *
+     * When oldPath is renamed to newPath, if newPath exists in the database, and caller is not the
+     * owner of the file, owner package is set to 'null'. This prevents previous owner of newPath
+     * from accessing renamed file.
+     * @return {@code true} if
+     * <ul>
+     * <li> there is no corresponding database row for given {@code path}
+     * <li> shared calling package is the owner of the database row
+     * <li> owner package name is already set to 'null'
+     * <li> updating owner package name to 'null' was successful.
+     * </ul>
+     * Returns {@code false} otherwise.
+     */
+    private boolean maybeRemoveOwnerPackageForFuseRename(@NonNull DatabaseHelper helper,
+            @NonNull String path) {
+
+        final Uri uri = Files.getContentUriForPath(path);
+        final int match = matchUri(uri, isCallingPackageAllowedHidden());
+        final String ownerPackageName;
+        final String selection = MediaColumns.DATA + " =? AND "
+                + MediaColumns.OWNER_PACKAGE_NAME + " != 'null'";
+        final String[] selectionArgs = new String[] {path};
+
+        final SQLiteQueryBuilder qbForQuery =
+                getQueryBuilder(TYPE_QUERY, match, uri, Bundle.EMPTY, null);
+        try (Cursor c = qbForQuery.query(helper, new String[] {FileColumns.OWNER_PACKAGE_NAME},
+                selection, selectionArgs, null, null, null, null, null)) {
+            if (!c.moveToFirst()) {
+                // We don't need to remove owner_package from db row if path doesn't exist in
+                // database or owner_package is already set to 'null'
+                return true;
+            }
+            ownerPackageName = c.getString(0);
+            if (isCallingIdentitySharedPackageName(ownerPackageName)) {
+                // We don't need to remove owner_package from db row if calling package is the owner
+                // of the database row
+                return true;
+            }
+        }
+
+        final SQLiteQueryBuilder qbForUpdate =
+                getQueryBuilder(TYPE_UPDATE, match, uri, Bundle.EMPTY, null);
+        ContentValues values = new ContentValues();
+        values.put(FileColumns.OWNER_PACKAGE_NAME, "null");
+        return qbForUpdate.update(helper, values, selection, selectionArgs) == 1;
+    }
+
     private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper,
             @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values) {
         return updateDatabaseForFuseRename(helper, oldPath, newPath, values, Bundle.EMPTY);
@@ -1623,26 +1681,34 @@
         if (!isMimeTypeSupportedInPath(newPath, newMimeType)) {
             return OsConstants.EPERM;
         }
-        return renameFileUncheckedForFuse(oldPath, newPath);
+        return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ false) ;
     }
 
     private int renameFileUncheckedForFuse(String oldPath, String newPath) {
-        final String newMimeType = MimeUtils.resolveMimeType(new File(newPath));
+        return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ true) ;
+    }
+
+    private int renameFileForFuse(String oldPath, String newPath, boolean bypassRestrictions) {
         final DatabaseHelper helper;
         try {
             helper = getDatabaseForUri(Files.getContentUriForPath(oldPath));
         } catch (VolumeNotFoundException e) {
-            throw new IllegalStateException("Volume not found while trying to update database for"
-                + oldPath + ". Rename failed due to database update error", e);
+            throw new IllegalStateException("Failed to update database row with " + oldPath, e);
         }
 
         helper.beginTransaction();
         try {
+            final String newMimeType = MimeUtils.resolveMimeType(new File(newPath));
             final String oldMimeType = MimeUtils.resolveMimeType(new File(oldPath));
             if (!updateDatabaseForFuseRename(helper, oldPath, newPath,
                     getContentValuesForFuseRename(newPath, oldMimeType, newMimeType))) {
-                Log.e(TAG, "Calling package doesn't have write permission to rename file.");
-                return OsConstants.EPERM;
+                if (!bypassRestrictions) {
+                    Log.e(TAG, "Calling package doesn't have write permission to rename file.");
+                    return OsConstants.EPERM;
+                } else if (!maybeRemoveOwnerPackageForFuseRename(helper, newPath)) {
+                    Log.wtf(TAG, "Couldn't clear owner package name for " + newPath);
+                    return OsConstants.EPERM;
+                }
             }
 
             // Try renaming oldPath to newPath in lower file system.
@@ -2679,8 +2745,7 @@
             try {
                 return qb.insert(helper, values);
             } catch (SQLiteConstraintException e) {
-                final long rowId = getIdIfPathExistsForPackage(qb, helper, path,
-                        getCallingPackageOrSelf());
+                final long rowId = getIdIfPathExistsForCallingPackage(qb, helper, path);
                 // Apps sometimes create a file via direct path and then insert it into
                 // MediaStore via ContentResolver. The former should create a database entry,
                 // so we have to treat the latter as an upsert.
@@ -2696,21 +2761,21 @@
     }
 
     /**
-     * @return row id of the entry with path {@code path} and owner {@code packageName}, if it
+     * @return row id of the entry with path {@code path} and owner as shared calling package, if it
      * exists.
      */
-    private long getIdIfPathExistsForPackage(@NonNull SQLiteQueryBuilder qb,
-            @NonNull DatabaseHelper helper, String path, String packageName) {
-        final String[] projection = new String[] {FileColumns._ID};
-        final String selection = FileColumns.DATA + " LIKE ? AND " +
-                FileColumns.OWNER_PACKAGE_NAME + " LIKE ? ";
+    private long getIdIfPathExistsForCallingPackage(@NonNull SQLiteQueryBuilder qb,
+            @NonNull DatabaseHelper helper, String path) {
+        final String[] projection = new String[] {FileColumns._ID, FileColumns.OWNER_PACKAGE_NAME};
+        final String selection = FileColumns.DATA + " =? ";
 
-        // TODO(b:149842708) Handle sharedUid. Package name check will fail if FUSE didn't
-        // use the right package name for sharedUid.
-        try (Cursor c = qb.query(helper, projection, selection, new String[] {path, packageName},
-                null, null, null, null, null)) {
+        try (Cursor c = qb.query(helper, projection, selection, new String[] {path}, null, null,
+                null, null, null)) {
             if (c.moveToFirst()) {
-                return c.getLong(0);
+                final String ownerPackage = c.getString(1);
+                if (ownerPackage != null && isCallingIdentitySharedPackageName(ownerPackage)) {
+                    return c.getLong(0);
+                }
             }
         }
         return -1;
@@ -2836,6 +2901,9 @@
             // IDs are forever; nobody should be editing them
             initialValues.remove(MediaColumns._ID);
 
+            // Expiration times are hard-coded; let's derive them
+            FileUtils.computeDateExpires(initialValues);
+
             // Ignore or augment incoming raw filesystem paths
             for (String column : sDataColumns.keySet()) {
                 if (!initialValues.containsKey(column)) continue;
@@ -3187,11 +3255,8 @@
             // When caller is system, such as the media scanner, we're willing
             // to let them access any columns they want
         } else {
-            // TODO: re-enable regardless of target SDK in b/146518586
-            if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) {
-                qb.setStrictColumns(true);
-                qb.setStrictGrammar(true);
-            }
+            qb.setStrictColumns(true);
+            qb.setStrictGrammar(true);
         }
 
         final String callingPackage = getCallingPackageOrSelf();
@@ -3910,7 +3975,7 @@
             }
 
             if (isFilesTable && !isCallingPackageSystem()) {
-                Metrics.logDeletion(volumeName, System.currentTimeMillis(),
+                Metrics.logDeletion(volumeName, mCallingIdentity.get().uid,
                         getCallingPackageOrSelf(), count);
             }
         }
@@ -4159,8 +4224,7 @@
                 break;
             case MediaStore.CREATE_TRASH_REQUEST_CALL:
                 allowedColumns = Arrays.asList(
-                        MediaColumns.IS_TRASHED,
-                        MediaColumns.DATE_EXPIRES);
+                        MediaColumns.IS_TRASHED);
                 break;
             default:
                 allowedColumns = Arrays.asList();
@@ -4544,6 +4608,9 @@
             // IDs are forever; nobody should be editing them
             initialValues.remove(MediaColumns._ID);
 
+            // Expiration times are hard-coded; let's derive them
+            FileUtils.computeDateExpires(initialValues);
+
             // Ignore or augment incoming raw filesystem paths
             for (String column : sDataColumns.keySet()) {
                 if (!initialValues.containsKey(column)) continue;
@@ -5209,6 +5276,16 @@
         mCallingIdentity.get().addDeletedRowId(path, rowId);
     }
 
+    private void handleOwnerPackageNameChange(@NonNull String oldPath,
+            @NonNull String oldOwnerPackage, @NonNull String newOwnerPackage) {
+        if (Objects.equals(oldOwnerPackage, newOwnerPackage)) {
+            return;
+        }
+        // Invalidate saved owned ID's of the previous owner of the renamed path, this prevents old
+        // owner from gaining access to replaced file.
+        invalidateLocalCallingIdentityCache(oldOwnerPackage, "owner_package_changed:" + oldPath);
+    }
+
     /**
      * Return the {@link MediaColumns#DATA} field for the given {@code Uri}.
      */
@@ -5454,6 +5531,16 @@
                     }
                 } else {
                     Log.i(TAG, "Using lower FS for " + filePath);
+                    if (forWrite) {
+                        // When opening for write on the lower filesystem, invalidate the VFS dentry
+                        // so subsequent open/getattr calls will return correctly.
+                        //
+                        // A 'dirty' dentry with write back cache enabled can cause the kernel to
+                        // ignore file attributes or even see stale page cache data when the lower
+                        // filesystem has been modified outside of the FUSE driver
+                        invalidateFuseDentry(file);
+                    }
+
                     pfd = lowerFsFd;
                 }
             }
@@ -6813,7 +6900,6 @@
         sMutableColumns.add(MediaStore.MediaColumns.IS_PENDING);
         sMutableColumns.add(MediaStore.MediaColumns.IS_TRASHED);
         sMutableColumns.add(MediaStore.MediaColumns.IS_FAVORITE);
-        sMutableColumns.add(MediaStore.MediaColumns.DATE_EXPIRES);
 
         sMutableColumns.add(MediaStore.Audio.AudioColumns.BOOKMARK);
 
diff --git a/src/com/android/providers/media/PermissionActivity.java b/src/com/android/providers/media/PermissionActivity.java
index 85d679c..0c398dd 100644
--- a/src/com/android/providers/media/PermissionActivity.java
+++ b/src/com/android/providers/media/PermissionActivity.java
@@ -30,6 +30,7 @@
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
@@ -50,7 +51,9 @@
 import android.util.Size;
 import android.view.KeyEvent;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
 import android.widget.ImageView;
 import android.widget.TextView;
 
@@ -64,6 +67,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 /**
@@ -88,6 +92,9 @@
     private String verb;
     private String data;
     private String volumeName;
+    private ApplicationInfo appInfo;
+
+    private TextView titleView;
 
     private static final String VERB_WRITE = "write";
     private static final String VERB_TRASH = "trash";
@@ -116,7 +123,8 @@
             uris = collectUris(getIntent().getExtras().getParcelable(MediaStore.EXTRA_CLIP_DATA));
             values = getIntent().getExtras().getParcelable(MediaStore.EXTRA_CONTENT_VALUES);
 
-            label = resolveCallingLabel();
+            appInfo = resolveCallingAppInfo();
+            label = resolveAppLabel(appInfo);
             verb = resolveVerb();
             data = resolveData();
             volumeName = MediaStore.getVolumeName(uris.get(0));
@@ -159,6 +167,12 @@
         final WindowManager.LayoutParams params = dialog.getWindow().getAttributes();
         params.width = getResources().getDimensionPixelSize(R.dimen.permission_dialog_width);
         dialog.getWindow().setAttributes(params);
+
+        // Hunt around to find the title of our newly created dialog so we can
+        // adjust accessibility focus once descriptions have been loaded
+        titleView = (TextView) findViewByPredicate(dialog.getWindow().getDecorView(), (view) -> {
+            return (view instanceof TextView) && view.isImportantForAccessibility();
+        });
     }
 
     private void onPositiveAction(DialogInterface dialog, int which) {
@@ -166,8 +180,8 @@
             @Override
             protected Void doInBackground(Void... params) {
                 Log.d(TAG, "User allowed grant for " + uris);
-                Metrics.logPermissionGranted(volumeName,
-                        System.currentTimeMillis(), getCallingPackage(), 1);
+                Metrics.logPermissionGranted(volumeName, appInfo.uid,
+                        getCallingPackage(), 1);
                 try {
                     switch (getIntent().getAction()) {
                         case MediaStore.CREATE_WRITE_REQUEST_CALL: {
@@ -220,8 +234,8 @@
             @Override
             protected Void doInBackground(Void... params) {
                 Log.d(TAG, "User declined request for " + uris);
-                Metrics.logPermissionDenied(volumeName,
-                        System.currentTimeMillis(), getCallingPackage(), 1);
+                Metrics.logPermissionDenied(volumeName, appInfo.uid, getCallingPackage(),
+                        1);
                 return null;
             }
 
@@ -246,18 +260,12 @@
     }
 
     /**
-     * Resolve a label that represents the remote calling app, typically the
-     * name of that app.
+     * Resolve a label that represents the app denoted by given {@link ApplicationInfo}.
      */
-    private @NonNull CharSequence resolveCallingLabel() throws NameNotFoundException {
-        final String callingPackage = getCallingPackage();
-        if (TextUtils.isEmpty(callingPackage)) {
-            throw new NameNotFoundException("Missing calling package");
-        }
-
+    private @NonNull CharSequence resolveAppLabel(final ApplicationInfo ai)
+            throws NameNotFoundException {
         final PackageManager pm = getPackageManager();
-        final CharSequence callingLabel = pm
-                .getApplicationLabel(pm.getApplicationInfo(callingPackage, 0));
+        final CharSequence callingLabel = pm.getApplicationLabel(ai);
         if (TextUtils.isEmpty(callingLabel)) {
             throw new NameNotFoundException("Missing calling package");
         }
@@ -265,6 +273,18 @@
         return callingLabel;
     }
 
+    /**
+     * Resolve the application info of the calling app.
+     */
+    private @NonNull ApplicationInfo resolveCallingAppInfo() throws NameNotFoundException {
+        final String callingPackage = getCallingPackage();
+        if (TextUtils.isEmpty(callingPackage)) {
+            throw new NameNotFoundException("Missing calling package");
+        }
+
+        return getPackageManager().getApplicationInfo(callingPackage, 0);
+    }
+
     private @NonNull String resolveVerb() {
         switch (getIntent().getAction()) {
             case MediaStore.CREATE_WRITE_REQUEST_CALL:
@@ -361,6 +381,27 @@
     }
 
     /**
+     * Recursively walk the given view hierarchy looking for the first
+     * {@link View} which matches the given predicate.
+     */
+    private static @Nullable View findViewByPredicate(@NonNull View root,
+            @NonNull Predicate<View> predicate) {
+        if (predicate.test(root)) {
+            return root;
+        }
+        if (root instanceof ViewGroup) {
+            final ViewGroup group = (ViewGroup) root;
+            for (int i = 0; i < group.getChildCount(); i++) {
+                final View res = findViewByPredicate(group.getChildAt(i), predicate);
+                if (res != null) {
+                    return res;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
      * Task that will load a set of {@link Description} to be eventually
      * displayed in the body of the dialog.
      */
@@ -423,6 +464,11 @@
             } else {
                 bindAsText(results);
             }
+
+            // This is pretty hacky, but somehow our dynamic loading of content
+            // can confuse accessibility focus, so refocus on the actual dialog
+            // title to announce ourselves properly
+            titleView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
         }
 
         /**
diff --git a/src/com/android/providers/media/scan/MediaScanner.java b/src/com/android/providers/media/scan/MediaScanner.java
index 6034ca2..b217da1 100644
--- a/src/com/android/providers/media/scan/MediaScanner.java
+++ b/src/com/android/providers/media/scan/MediaScanner.java
@@ -16,10 +16,10 @@
 
 package com.android.providers.media.scan;
 
-import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCAN_EVENT__REASON__DEMAND;
-import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCAN_EVENT__REASON__IDLE;
-import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCAN_EVENT__REASON__MOUNTED;
-import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCAN_EVENT__REASON__UNKNOWN;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCAN_OCCURRED__REASON__DEMAND;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCAN_OCCURRED__REASON__IDLE;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCAN_OCCURRED__REASON__MOUNTED;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCAN_OCCURRED__REASON__UNKNOWN;
 
 import android.content.Context;
 import android.net.Uri;
@@ -29,10 +29,10 @@
 import java.io.File;
 
 public interface MediaScanner {
-    public static final int REASON_UNKNOWN = MEDIA_PROVIDER_SCAN_EVENT__REASON__UNKNOWN;
-    public static final int REASON_MOUNTED = MEDIA_PROVIDER_SCAN_EVENT__REASON__MOUNTED;
-    public static final int REASON_DEMAND = MEDIA_PROVIDER_SCAN_EVENT__REASON__DEMAND;
-    public static final int REASON_IDLE = MEDIA_PROVIDER_SCAN_EVENT__REASON__IDLE;
+    public static final int REASON_UNKNOWN = MEDIA_PROVIDER_SCAN_OCCURRED__REASON__UNKNOWN;
+    public static final int REASON_MOUNTED = MEDIA_PROVIDER_SCAN_OCCURRED__REASON__MOUNTED;
+    public static final int REASON_DEMAND = MEDIA_PROVIDER_SCAN_OCCURRED__REASON__DEMAND;
+    public static final int REASON_IDLE = MEDIA_PROVIDER_SCAN_OCCURRED__REASON__IDLE;
 
     public Context getContext();
     public void scanDirectory(File file, int reason);
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index 7f12f9f..6ef88c5 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -758,13 +758,12 @@
      */
     private static void withGenericValues(ContentProviderOperation.Builder op,
             File file, BasicFileAttributes attrs, String mimeType) {
-        op.withValue(FileColumns.MEDIA_TYPE, MimeUtils.resolveMediaType(mimeType));
+        withOptionalMimeType(op, Optional.ofNullable(mimeType));
 
         op.withValue(MediaColumns.DATA, file.getAbsolutePath());
         op.withValue(MediaColumns.SIZE, attrs.size());
         op.withValue(MediaColumns.DATE_MODIFIED, lastModifiedTime(file, attrs));
         op.withValue(MediaColumns.DATE_TAKEN, null);
-        op.withValue(MediaColumns.MIME_TYPE, mimeType);
         op.withValue(MediaColumns.IS_DRM, 0);
         op.withValue(MediaColumns.WIDTH, null);
         op.withValue(MediaColumns.HEIGHT, null);
@@ -799,11 +798,11 @@
      */
     private static void withRetrieverValues(ContentProviderOperation.Builder op,
             MediaMetadataRetriever mmr, String mimeType) {
-        withOptionalValue(op, MediaColumns.DATE_TAKEN,
-                parseOptionalDate(mmr.extractMetadata(METADATA_KEY_DATE)));
-        withOptionalValue(op, MediaColumns.MIME_TYPE,
+        withOptionalMimeType(op,
                 parseOptionalMimeType(mimeType, mmr.extractMetadata(METADATA_KEY_MIMETYPE)));
 
+        withOptionalValue(op, MediaColumns.DATE_TAKEN,
+                parseOptionalDate(mmr.extractMetadata(METADATA_KEY_DATE)));
         withOptionalValue(op, MediaColumns.CD_TRACK_NUMBER,
                 parseOptional(mmr.extractMetadata(METADATA_KEY_CD_TRACK_NUMBER)));
         withOptionalValue(op, MediaColumns.ALBUM,
@@ -845,8 +844,9 @@
      */
     private static void withXmpValues(ContentProviderOperation.Builder op,
             XmpInterface xmp, String mimeType) {
-        withOptionalValue(op, MediaColumns.MIME_TYPE,
+        withOptionalMimeType(op,
                 parseOptionalMimeType(mimeType, xmp.getFormat()));
+
         op.withValue(MediaColumns.DOCUMENT_ID, xmp.getDocumentId());
         op.withValue(MediaColumns.INSTANCE_ID, xmp.getInstanceId());
         op.withValue(MediaColumns.ORIGINAL_DOCUMENT_ID, xmp.getOriginalDocumentId());
@@ -857,22 +857,39 @@
      * Overwrite a value in the given {@link ContentProviderOperation}, but only
      * when the given {@link Optional} value is present.
      */
-    private static void withOptionalValue(ContentProviderOperation.Builder op,
-            String key, Optional<?> value) {
+    private static void withOptionalValue(@NonNull ContentProviderOperation.Builder op,
+            @NonNull String key, @NonNull Optional<?> value) {
         if (value.isPresent()) {
             op.withValue(key, value.get());
         }
     }
 
+    /**
+     * Overwrite the {@link MediaColumns#MIME_TYPE} and
+     * {@link FileColumns#MEDIA_TYPE} values in the given
+     * {@link ContentProviderOperation}, but only when the given
+     * {@link Optional} value is present.
+     *
+     * @param value An optional MIME type to apply to this operation.
+     */
+    private static void withOptionalMimeType(@NonNull ContentProviderOperation.Builder op,
+            @NonNull Optional<String> value) {
+        if (value.isPresent()) {
+            final String mimeType = value.get();
+            final int mediaType = MimeUtils.resolveMediaType(mimeType);
+
+            op.withValue(MediaColumns.MIME_TYPE, mimeType);
+            op.withValue(FileColumns.MEDIA_TYPE, mediaType);
+        }
+    }
+
     private static @NonNull ContentProviderOperation.Builder scanItemDirectory(long existingId,
             File file, BasicFileAttributes attrs, String mimeType, String volumeName) {
         final ContentProviderOperation.Builder op = newUpsert(volumeName, existingId);
         withGenericValues(op, file, attrs, mimeType);
 
         try {
-            op.withValue(FileColumns.MEDIA_TYPE, 0);
             op.withValue(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION);
-            op.withValue(FileColumns.MIME_TYPE, null);
         } catch (Exception e) {
             logTroubleScanning(file, e);
         }
diff --git a/src/com/android/providers/media/util/FileUtils.java b/src/com/android/providers/media/util/FileUtils.java
index 8d64d74..78f2c6f 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -804,13 +804,13 @@
      * Default duration that {@link MediaColumns#IS_PENDING} items should be
      * preserved for until automatically cleaned by {@link #runIdleMaintenance}.
      */
-    public static final long DEFAULT_DURATION_PENDING = DateUtils.WEEK_IN_MILLIS;
+    public static final long DEFAULT_DURATION_PENDING = 7 * DateUtils.DAY_IN_MILLIS;
 
     /**
      * Default duration that {@link MediaColumns#IS_TRASHED} items should be
      * preserved for until automatically cleaned by {@link #runIdleMaintenance}.
      */
-    public static final long DEFAULT_DURATION_TRASHED = DateUtils.WEEK_IN_MILLIS;
+    public static final long DEFAULT_DURATION_TRASHED = 30 * DateUtils.DAY_IN_MILLIS;
 
     public static boolean isDownload(@NonNull String path) {
         return PATTERN_DOWNLOADS_FILE.matcher(path).matches();
@@ -834,6 +834,12 @@
             "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|media|obb|sandbox)/([^/]+)(/.*)?");
 
     /**
+     * Regex that matches Android/obb or Android/data path.
+     */
+    public static final Pattern PATTERN_DATA_OR_OBB_PATH = Pattern.compile(
+            "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb)/?$");
+
+    /**
      * Regex that matches paths for {@link MediaColumns#RELATIVE_PATH}; it
      * captures both top-level paths and sandboxed paths.
      */
@@ -920,6 +926,15 @@
     }
 
     /**
+     * Returns true if relative path is Android/data or Android/obb path.
+     */
+    public static boolean isDataOrObbPath(String path) {
+        if (path == null) return false;
+        final Matcher m = PATTERN_DATA_OR_OBB_PATH.matcher(path);
+        return m.matches();
+    }
+
+    /**
      * Returns the name of the top level directory, or null if the path doesn't go through the
      * external storage directory.
      */
@@ -933,6 +948,36 @@
     }
 
     /**
+     * Compute the value of {@link MediaColumns#DATE_EXPIRES} based on other
+     * columns being modified by this operation.
+     */
+    public static void computeDateExpires(@NonNull ContentValues values) {
+        // External apps have no ability to change this field
+        values.remove(MediaColumns.DATE_EXPIRES);
+
+        // Only define the field when this modification is actually adjusting
+        // one of the flags that should influence the expiration
+        final Integer pending = values.getAsInteger(MediaColumns.IS_PENDING);
+        if (pending != null) {
+            if (pending != 0) {
+                values.put(MediaColumns.DATE_EXPIRES,
+                        (System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000);
+            } else {
+                values.putNull(MediaColumns.DATE_EXPIRES);
+            }
+        }
+        final Integer trashed = values.getAsInteger(MediaColumns.IS_TRASHED);
+        if (trashed != null) {
+            if (trashed != 0) {
+                values.put(MediaColumns.DATE_EXPIRES,
+                        (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000);
+            } else {
+                values.putNull(MediaColumns.DATE_EXPIRES);
+            }
+        }
+    }
+
+    /**
      * Compute several scattered {@link MediaColumns} values from
      * {@link MediaColumns#DATA}. This method performs no enforcement of
      * argument validity.
diff --git a/src/com/android/providers/media/util/Metrics.java b/src/com/android/providers/media/util/Metrics.java
index 2b40301..86a0302 100644
--- a/src/com/android/providers/media/util/Metrics.java
+++ b/src/com/android/providers/media/util/Metrics.java
@@ -16,17 +16,17 @@
 
 package com.android.providers.media.util;
 
-import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_DELETION_EVENT;
-import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_IDLE_MAINTENANCE;
-import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_PERMISSION_EVENT;
-import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_PERMISSION_EVENT__RESULT__USER_DENIED;
-import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_PERMISSION_EVENT__RESULT__USER_GRANTED;
-import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCAN_EVENT;
-import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCAN_EVENT__VOLUME_TYPE__EXTERNAL_OTHER;
-import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCAN_EVENT__VOLUME_TYPE__EXTERNAL_PRIMARY;
-import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCAN_EVENT__VOLUME_TYPE__INTERNAL;
-import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCAN_EVENT__VOLUME_TYPE__UNKNOWN;
-import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCHEMA_CHANGE;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_CONTENT_DELETED;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_IDLE_MAINTENANCE_FINISHED;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_PERMISSION_REQUESTED;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_PERMISSION_REQUESTED__RESULT__USER_DENIED;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_PERMISSION_REQUESTED__RESULT__USER_GRANTED;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCAN_OCCURRED;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCAN_OCCURRED__VOLUME_TYPE__EXTERNAL_OTHER;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCAN_OCCURRED__VOLUME_TYPE__EXTERNAL_PRIMARY;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCAN_OCCURRED__VOLUME_TYPE__INTERNAL;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCAN_OCCURRED__VOLUME_TYPE__UNKNOWN;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_SCHEMA_CHANGED;
 import static com.android.providers.media.scan.MediaScanner.REASON_DEMAND;
 import static com.android.providers.media.scan.MediaScanner.REASON_IDLE;
 import static com.android.providers.media.scan.MediaScanner.REASON_MOUNTED;
@@ -55,41 +55,41 @@
         final float normalizedUpdateCount = ((float) updateCount) / itemCount;
         final float normalizedDeleteCount = ((float) deleteCount) / itemCount;
 
-        MediaProviderStatsLog.write(MEDIA_PROVIDER_SCAN_EVENT,
+        MediaProviderStatsLog.write(MEDIA_PROVIDER_SCAN_OCCURRED,
                 translateVolumeName(volumeName), reason, itemCount, normalizedDurationMillis,
                 normalizedInsertCount, normalizedUpdateCount, normalizedDeleteCount);
     }
 
-    public static void logDeletion(@NonNull String volumeName, long timestampMillis,
-            String packageName, int itemCount) {
+    public static void logDeletion(@NonNull String volumeName, int uid, String packageName,
+            int itemCount) {
         Logging.logPersistent(String.format(
-                "Deleted %4$d items on %1$s due to %3$s",
-                volumeName, timestampMillis, packageName, itemCount));
+                "Deleted %3$d items on %1$s due to %2$s",
+                volumeName, packageName, itemCount));
 
-        MediaProviderStatsLog.write(MEDIA_PROVIDER_DELETION_EVENT,
-                translateVolumeName(volumeName), timestampMillis, packageName, itemCount);
+        MediaProviderStatsLog.write(MEDIA_CONTENT_DELETED,
+                translateVolumeName(volumeName), uid, itemCount);
     }
 
-    public static void logPermissionGranted(@NonNull String volumeName, long timestampMillis,
-            String packageName, int itemCount) {
+    public static void logPermissionGranted(@NonNull String volumeName, int uid, String packageName,
+            int itemCount) {
         Logging.logPersistent(String.format(
-                "Granted permission to %4$d items on %1$s to %3$s",
-                volumeName, timestampMillis, packageName, itemCount));
+                "Granted permission to %3$d items on %1$s to %2$s",
+                volumeName, packageName, itemCount));
 
-        MediaProviderStatsLog.write(MEDIA_PROVIDER_PERMISSION_EVENT,
-                translateVolumeName(volumeName), timestampMillis, packageName, itemCount,
-                MEDIA_PROVIDER_PERMISSION_EVENT__RESULT__USER_GRANTED);
+        MediaProviderStatsLog.write(MEDIA_PROVIDER_PERMISSION_REQUESTED,
+                translateVolumeName(volumeName), uid, itemCount,
+                MEDIA_PROVIDER_PERMISSION_REQUESTED__RESULT__USER_GRANTED);
     }
 
-    public static void logPermissionDenied(@NonNull String volumeName, long timestampMillis,
-            String packageName, int itemCount) {
+    public static void logPermissionDenied(@NonNull String volumeName, int uid, String packageName,
+            int itemCount) {
         Logging.logPersistent(String.format(
-                "Denied permission to %4$d items on %1$s to %3$s",
-                volumeName, timestampMillis, packageName, itemCount));
+                "Denied permission to %3$d items on %1$s to %2$s",
+                volumeName, packageName, itemCount));
 
-        MediaProviderStatsLog.write(MEDIA_PROVIDER_PERMISSION_EVENT,
-                translateVolumeName(volumeName), timestampMillis, packageName, itemCount,
-                MEDIA_PROVIDER_PERMISSION_EVENT__RESULT__USER_DENIED);
+        MediaProviderStatsLog.write(MEDIA_PROVIDER_PERMISSION_REQUESTED,
+                translateVolumeName(volumeName), uid, itemCount,
+                MEDIA_PROVIDER_PERMISSION_REQUESTED__RESULT__USER_DENIED);
     }
 
     public static void logSchemaChange(@NonNull String volumeName, int versionFrom, int versionTo,
@@ -100,7 +100,7 @@
 
         final float normalizedDurationMillis = ((float) durationMillis) / itemCount;
 
-        MediaProviderStatsLog.write(MEDIA_PROVIDER_SCHEMA_CHANGE,
+        MediaProviderStatsLog.write(MEDIA_PROVIDER_SCHEMA_CHANGED,
                 translateVolumeName(volumeName), versionFrom, versionTo, itemCount,
                 normalizedDurationMillis);
     }
@@ -115,7 +115,7 @@
         final float normalizedStaleThumbnails = ((float) staleThumbnails) / itemCount;
         final float normalizedExpiredMedia = ((float) expiredMedia) / itemCount;
 
-        MediaProviderStatsLog.write(MEDIA_PROVIDER_IDLE_MAINTENANCE,
+        MediaProviderStatsLog.write(MEDIA_PROVIDER_IDLE_MAINTENANCE_FINISHED,
                 translateVolumeName(volumeName), itemCount, normalizedDurationMillis,
                 normalizedStaleThumbnails, normalizedExpiredMedia);
     }
@@ -133,16 +133,16 @@
     private static int translateVolumeName(@NonNull String volumeName) {
         switch (volumeName) {
             case MediaStore.VOLUME_INTERNAL:
-                return MEDIA_PROVIDER_SCAN_EVENT__VOLUME_TYPE__INTERNAL;
+                return MEDIA_PROVIDER_SCAN_OCCURRED__VOLUME_TYPE__INTERNAL;
             case MediaStore.VOLUME_EXTERNAL:
                 // Callers using generic "external" volume name end up applying
                 // to all external volumes, so we can't tell which volumes were
                 // actually changed
-                return MEDIA_PROVIDER_SCAN_EVENT__VOLUME_TYPE__UNKNOWN;
+                return MEDIA_PROVIDER_SCAN_OCCURRED__VOLUME_TYPE__UNKNOWN;
             case MediaStore.VOLUME_EXTERNAL_PRIMARY:
-                return MEDIA_PROVIDER_SCAN_EVENT__VOLUME_TYPE__EXTERNAL_PRIMARY;
+                return MEDIA_PROVIDER_SCAN_OCCURRED__VOLUME_TYPE__EXTERNAL_PRIMARY;
             default:
-                return MEDIA_PROVIDER_SCAN_EVENT__VOLUME_TYPE__EXTERNAL_OTHER;
+                return MEDIA_PROVIDER_SCAN_OCCURRED__VOLUME_TYPE__EXTERNAL_OTHER;
         }
     }
 }
diff --git a/src/com/android/providers/media/util/PermissionUtils.java b/src/com/android/providers/media/util/PermissionUtils.java
index c076914..387ced0 100644
--- a/src/com/android/providers/media/util/PermissionUtils.java
+++ b/src/com/android/providers/media/util/PermissionUtils.java
@@ -35,16 +35,22 @@
 import android.annotation.NonNull;
 import android.app.AppOpsManager;
 import android.content.Context;
+import android.provider.MediaStore;
 
 public class PermissionUtils {
     // Callers must hold both the old and new permissions, so that we can
     // handle obscure cases like when an app targets Q but was installed on
     // a device that was originally running on P before being upgraded to Q.
 
+    private static volatile int sLegacyMediaProviderUid = -1;
+
     public static boolean checkPermissionSystem(Context context,
             int pid, int uid, String packageName) {
+        // Apps sharing legacy MediaProvider's uid like DownloadProvider and MTP are treated as
+        // system.
         return uid == android.os.Process.SYSTEM_UID || uid == android.os.Process.myUid()
-                || uid == android.os.Process.SHELL_UID || uid == android.os.Process.ROOT_UID;
+                || uid == android.os.Process.SHELL_UID || uid == android.os.Process.ROOT_UID
+                || isLegacyMediaProvider(context, uid);
     }
 
     public static boolean checkPermissionBackup(Context context, int pid, int uid) {
@@ -209,4 +215,15 @@
                 throw new IllegalStateException(op + " has unknown mode " + mode);
         }
     }
+
+    private static boolean isLegacyMediaProvider(Context context, int uid) {
+        if (sLegacyMediaProviderUid == -1) {
+            // Uid stays constant while legacy Media Provider stays installed. Cache legacy
+            // MediaProvider's uid for the first time.
+            sLegacyMediaProviderUid = context.getPackageManager()
+                    .resolveContentProvider(MediaStore.AUTHORITY_LEGACY, 0)
+                    .applicationInfo.uid;
+        }
+        return (uid == sLegacyMediaProviderUid);
+    }
 }
diff --git a/tests/Android.bp b/tests/Android.bp
index e79f576..ee5143d 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -9,6 +9,7 @@
         "device-tests",
         "mts",
     ],
+    compile_multilib: "both",
 
     manifest: "AndroidManifest.xml",
 
diff --git a/tests/client/Android.bp b/tests/client/Android.bp
index 65b45cd..9d5be41 100644
--- a/tests/client/Android.bp
+++ b/tests/client/Android.bp
@@ -4,6 +4,7 @@
         "device-tests",
         "mts",
     ],
+    compile_multilib: "both",
 
     manifest: "AndroidManifest.xml",
 
diff --git a/tests/jni/FuseDaemonTest/Android.bp b/tests/jni/FuseDaemonTest/Android.bp
index 1208e1d..e3e92b0 100644
--- a/tests/jni/FuseDaemonTest/Android.bp
+++ b/tests/jni/FuseDaemonTest/Android.bp
@@ -47,6 +47,7 @@
     manifest: "AndroidManifest.xml",
     srcs: ["src/**/*.java"],
     static_libs: ["androidx.test.rules", "truth-prebuilt", "tests-fusedaemon-lib"],
+    compile_multilib: "both",
     test_suites: ["general-tests", "mts"],
     sdk_version: "test_current",
     java_resources: [
@@ -61,6 +62,7 @@
     manifest: "legacy/AndroidManifest.xml",
     srcs: ["legacy/src/**/*.java"],
     static_libs: ["androidx.test.rules", "truth-prebuilt",  "tests-fusedaemon-lib"],
+    compile_multilib: "both",
     test_suites: ["general-tests", "mts"],
     sdk_version: "test_current",
     target_sdk_version: "29",
diff --git a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
index c8690c5..259266b 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
@@ -159,6 +159,7 @@
         runDeviceTest("testOpenContentResolverDup");
         runDeviceTest("testContentResolverDelete");
         runDeviceTest("testContentResolverUpdate");
+        runDeviceTest("testOpenContentResolverClose");
     }
 
     @Test
diff --git a/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/ReaddirTestHelper.java b/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/ReaddirTestHelper.java
index 074231b..29986bc 100644
--- a/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/ReaddirTestHelper.java
+++ b/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/ReaddirTestHelper.java
@@ -18,6 +18,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.DirectoryIteratorException;
 import java.nio.file.DirectoryStream;
 import java.nio.file.DirectoryStream.Filter;
 import java.nio.file.Files;
@@ -69,14 +70,15 @@
     public static ArrayList<String> readDirectory(String directoryPath, Filter<Path> filter) {
         ArrayList<String> directoryEntries = new ArrayList<String>();
         File dir = new File(directoryPath);
+
         try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dir.toPath(),
                 filter)) {
             for (Path de: directoryStream) {
                 directoryEntries.add(de.getFileName().toString());
             }
-        } catch (IOException x) {
+        } catch (IOException | DirectoryIteratorException x) {
             Log.e(TAG, "IOException occurred while readding directory entries from " +
-                  directoryPath);
+                  directoryPath, x);
         }
         return directoryEntries;
     }
diff --git a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
index 124aa3b..bcb50a4 100644
--- a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
+++ b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
@@ -777,7 +777,7 @@
                     ParcelFileDescriptor.MODE_READ_WRITE);
             ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw");
 
-            assertRWR(readPfd.getFileDescriptor(), writePfd.getFileDescriptor());
+            assertRWR(readPfd, writePfd);
             assertUpperFsFd(writePfd); // With cache
         } finally {
             file.delete();
@@ -796,7 +796,7 @@
             ParcelFileDescriptor readPfd = ParcelFileDescriptor.open(file,
                     ParcelFileDescriptor.MODE_READ_WRITE);
 
-            assertRWR(readPfd.getFileDescriptor(), writePfd.getFileDescriptor());
+            assertRWR(readPfd, writePfd);
             assertLowerFsFd(writePfd);
         } finally {
             file.delete();
@@ -815,7 +815,7 @@
                     ParcelFileDescriptor.MODE_READ_WRITE);
             ParcelFileDescriptor readPfd = openWithMediaProvider(file, "rw");
 
-            assertRWR(readPfd.getFileDescriptor(), writePfd.getFileDescriptor());
+            assertRWR(readPfd, writePfd);
             assertUpperFsFd(readPfd); // With cache
         } finally {
             file.delete();
@@ -834,7 +834,7 @@
             ParcelFileDescriptor writePfd = ParcelFileDescriptor.open(file,
                     ParcelFileDescriptor.MODE_READ_WRITE);
 
-            assertRWR(readPfd.getFileDescriptor(), writePfd.getFileDescriptor());
+            assertRWR(readPfd, writePfd);
             assertLowerFsFd(readPfd);
         } finally {
             file.delete();
@@ -854,7 +854,7 @@
             ParcelFileDescriptor writePfd = openWithMediaProvider(file, "w");
             ParcelFileDescriptor readPfd = openWithMediaProvider(file, "rw");
 
-            assertRWR(readPfd.getFileDescriptor(), writePfd.getFileDescriptor());
+            assertRWR(readPfd, writePfd);
             assertLowerFsFd(writePfd);
             assertUpperFsFd(readPfd); // Without cache
         } finally {
@@ -878,7 +878,7 @@
             ParcelFileDescriptor readPfd = ParcelFileDescriptor.open(file,
                     ParcelFileDescriptor.MODE_READ_WRITE);
 
-            assertRWR(readPfd.getFileDescriptor(), writePfdDup.getFileDescriptor());
+            assertRWR(readPfd, writePfdDup);
             assertLowerFsFd(writePfdDup);
         } finally {
             file.delete();
@@ -886,6 +886,38 @@
     }
 
     @Test
+    public void testOpenContentResolverClose() throws Exception {
+        String displayName = "open_content_resolver_close.jpg";
+        File file = new File(DCIM_DIR, displayName);
+
+        try {
+            byte[] readBuffer = new byte[10];
+            byte[] writeBuffer = new byte[10];
+            Arrays.fill(writeBuffer, (byte) 1);
+
+            assertThat(file.createNewFile()).isTrue();
+
+            // Lower fs open and write
+            ParcelFileDescriptor writePfd = openWithMediaProvider(file, "rw");
+            Os.pwrite(writePfd.getFileDescriptor(), writeBuffer, 0, 10, 0);
+
+            // Close so upper fs open will not use direct_io
+            writePfd.close();
+
+            // Upper fs open and read without direct_io
+            ParcelFileDescriptor readPfd = ParcelFileDescriptor.open(file,
+                    ParcelFileDescriptor.MODE_READ_WRITE);
+            Os.pread(readPfd.getFileDescriptor(), readBuffer, 0, 10, 0);
+
+            // Last write on lower fs is visible via upper fs
+            assertThat(readBuffer).isEqualTo(writeBuffer);
+            assertThat(readPfd.getStatSize()).isEqualTo(writeBuffer.length);
+        } finally {
+            file.delete();
+        }
+    }
+
+    @Test
     public void testContentResolverDelete() throws Exception {
         String displayName = "content_resolver_delete.jpg";
         File file = new File(DCIM_DIR, displayName);
@@ -1819,7 +1851,11 @@
      * underlying file on disk but may be derived from different mount points and in that case
      * have separate VFS caches.
      */
-    private void assertRWR(FileDescriptor readFd, FileDescriptor writeFd) throws Exception {
+    private void assertRWR(ParcelFileDescriptor readPfd, ParcelFileDescriptor writePfd)
+            throws Exception {
+        FileDescriptor readFd = readPfd.getFileDescriptor();
+        FileDescriptor writeFd = writePfd.getFileDescriptor();
+
         byte[] readBuffer = new byte[10];
         byte[] writeBuffer = new byte[10];
         Arrays.fill(writeBuffer, (byte) 1);
@@ -1840,6 +1876,7 @@
 
         // Assert that the last write is indeed visible via readFd
         assertThat(readBuffer).isEqualTo(writeBuffer);
+        assertThat(readPfd.getStatSize()).isEqualTo(writePfd.getStatSize());
     }
 
     private void assertLowerFsFd(ParcelFileDescriptor pfd) throws Exception {
diff --git a/tests/res/raw/test_m4a.m4a b/tests/res/raw/test_m4a.m4a
new file mode 100644
index 0000000..977e479
--- /dev/null
+++ b/tests/res/raw/test_m4a.m4a
Binary files differ
diff --git a/tests/src/com/android/providers/media/PermissionActivityTest.java b/tests/src/com/android/providers/media/PermissionActivityTest.java
index e751a90..7bbce62 100644
--- a/tests/src/com/android/providers/media/PermissionActivityTest.java
+++ b/tests/src/com/android/providers/media/PermissionActivityTest.java
@@ -19,15 +19,22 @@
 import android.app.Instrumentation;
 import android.content.ClipData;
 import android.content.ContentValues;
+import android.content.Context;
 import android.content.Intent;
+import android.net.Uri;
+import android.os.Environment;
 import android.provider.MediaStore;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.providers.media.scan.MediaScannerTest;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.File;
+
 /**
  * We already have solid coverage of this logic in {@code CtsProviderTestCases},
  * but the coverage system currently doesn't measure that, so we add the bare
@@ -45,11 +52,18 @@
         activity.startActivityForResult(createIntent(), 42);
     }
 
-    private static Intent createIntent() {
+    private static Intent createIntent() throws Exception {
+        final Context context = InstrumentationRegistry.getContext();
+
+        final File dir = Environment
+                .getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
+        final File file = MediaScannerTest.stage(R.raw.test_image,
+                new File(dir, "test" + System.nanoTime() + ".jpg"));
+        final Uri uri = MediaStore.scanFile(context.getContentResolver(), file);
+
         final Intent intent = new Intent(MediaStore.CREATE_WRITE_REQUEST_CALL, null,
-                InstrumentationRegistry.getContext(), PermissionActivity.class);
-        intent.putExtra(MediaStore.EXTRA_CLIP_DATA, ClipData.newRawUri("",
-                MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY, 42)));
+                context, PermissionActivity.class);
+        intent.putExtra(MediaStore.EXTRA_CLIP_DATA, ClipData.newRawUri("", uri));
         intent.putExtra(MediaStore.EXTRA_CONTENT_VALUES, new ContentValues());
         return intent;
     }
diff --git a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
index 336741c..e94d599 100644
--- a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
@@ -719,6 +719,25 @@
         }
     }
 
+    /**
+     * Verify a narrow exception where we allow an {@code mp4} video file on
+     * disk to be indexed as an {@code m4a} audio file.
+     */
+    @Test
+    public void testScan_148316354() throws Exception {
+        final File file = new File(mDir, "148316354.mp4");
+        stage(R.raw.test_m4a, file);
+
+        final Uri uri = mModern.scanFile(file, REASON_UNKNOWN);
+        try (Cursor cursor = mIsolatedResolver
+                .query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) {
+            assertEquals(1, cursor.getCount());
+            cursor.moveToFirst();
+            assertEquals("audio/mp4",
+                    cursor.getString(cursor.getColumnIndex(MediaColumns.MIME_TYPE)));
+        }
+    }
+
     @Test
     public void testAlbumArtPattern() throws Exception {
         for (String path : new String[] {
diff --git a/tests/src/com/android/providers/media/util/FileUtilsTest.java b/tests/src/com/android/providers/media/util/FileUtilsTest.java
index 35f789e..da8ddce 100644
--- a/tests/src/com/android/providers/media/util/FileUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/FileUtilsTest.java
@@ -51,6 +51,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -61,6 +62,9 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.google.common.collect.Range;
+import com.google.common.truth.Truth;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -548,6 +552,63 @@
         assertEquals("/", values.get(MediaColumns.RELATIVE_PATH));
     }
 
+    @Test
+    public void testComputeDateExpires_None() throws Exception {
+        final ContentValues values = new ContentValues();
+        values.put(MediaColumns.DATE_EXPIRES, 1577836800L);
+
+        FileUtils.computeDateExpires(values);
+        assertFalse(values.containsKey(MediaColumns.DATE_EXPIRES));
+    }
+
+    @Test
+    public void testComputeDateExpires_Pending_Set() throws Exception {
+        final ContentValues values = new ContentValues();
+        values.put(MediaColumns.IS_PENDING, 1);
+        values.put(MediaColumns.DATE_EXPIRES, 1577836800L);
+
+        FileUtils.computeDateExpires(values);
+        final long target = (System.currentTimeMillis()
+                + FileUtils.DEFAULT_DURATION_PENDING) / 1_000;
+        Truth.assertThat(values.getAsLong(MediaColumns.DATE_EXPIRES))
+                .isIn(Range.closed(target - 5, target + 5));
+    }
+
+    @Test
+    public void testComputeDateExpires_Pending_Clear() throws Exception {
+        final ContentValues values = new ContentValues();
+        values.put(MediaColumns.IS_PENDING, 0);
+        values.put(MediaColumns.DATE_EXPIRES, 1577836800L);
+
+        FileUtils.computeDateExpires(values);
+        assertTrue(values.containsKey(MediaColumns.DATE_EXPIRES));
+        assertNull(values.get(MediaColumns.DATE_EXPIRES));
+    }
+
+    @Test
+    public void testComputeDateExpires_Trashed_Set() throws Exception {
+        final ContentValues values = new ContentValues();
+        values.put(MediaColumns.IS_TRASHED, 1);
+        values.put(MediaColumns.DATE_EXPIRES, 1577836800L);
+
+        FileUtils.computeDateExpires(values);
+        final long target = (System.currentTimeMillis()
+                + FileUtils.DEFAULT_DURATION_TRASHED) / 1_000;
+        Truth.assertThat(values.getAsLong(MediaColumns.DATE_EXPIRES))
+                .isIn(Range.closed(target - 5, target + 5));
+    }
+
+    @Test
+    public void testComputeDateExpires_Trashed_Clear() throws Exception {
+        final ContentValues values = new ContentValues();
+        values.put(MediaColumns.IS_TRASHED, 0);
+        values.put(MediaColumns.DATE_EXPIRES, 1577836800L);
+
+        FileUtils.computeDateExpires(values);
+        assertTrue(values.containsKey(MediaColumns.DATE_EXPIRES));
+        assertNull(values.get(MediaColumns.DATE_EXPIRES));
+    }
+
     private static File touch(File dir, String name) throws IOException {
         final File res = new File(dir, name);
         res.createNewFile();