[automerger skipped] Handle 64-bit and end-of-file box lengths in IsoInterface am: e0eec79247 am: 7f98f6c695 -s ours

am skip reason: Change-Id Idad81e161bb998c933ac8a81a3ebf12f59391a16 with SHA-1 f44d9bc12a is in history

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/providers/MediaProvider/+/13143318

Change-Id: I22737914f048e9c97a5a3be587057ac8d40cdab6
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..9127163
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,12 @@
+BasedOnStyle: Google
+AccessModifierOffset: -2
+AllowShortFunctionsOnASingleLine: Inline
+ColumnLimit: 100
+CommentPragmas: NOLINT:.*
+DerivePointerAlignment: false
+IndentWidth: 4
+ContinuationIndentWidth: 8
+PointerAlignment: Left
+TabWidth: 4
+UseTab: Never
+PenaltyExcessCharacter: 32
diff --git a/Android.bp b/Android.bp
index c71dc5e..e1ac31d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,24 +1,99 @@
+
 android_app {
     name: "MediaProvider",
-
     manifest: "AndroidManifest.xml",
 
+    static_libs: [
+        "androidx.appcompat_appcompat",
+        "androidx.core_core",
+        "guava",
+    ],
+
+    libs: [
+        "unsupportedappusage",
+        "app-compat-annotations",
+        "framework-mediaprovider.impl",
+        "framework_mediaprovider_annotation",
+        "framework-statsd",
+    ],
+
+    jni_libs: [
+        "libfuse_jni",
+        "libfuse"
+    ],
+
     resource_dirs: [
         "res",
     ],
-
     srcs: [
-        "src/**/*.aidl",
-        "src/**/*.java",
+        ":mediaprovider-sources",
     ],
 
     optimize: {
         proguard_flags_files: ["proguard.flags"],
     },
 
-    platform_apis: true,
+    plugins: [
+        "java_api_finder",
+        "error_prone_mediaprovider",
+    ],
+
+    sdk_version: "module_current",
 
     certificate: "media",
-
     privileged: true,
+
+    aaptflags: ["--custom-package com.android.providers.media"],
+
+    errorprone: {
+        javacflags: [
+            "-Xep:FallThrough:ERROR",
+            "-Xep:GuardedBy:ERROR",
+            "-Xep:StreamResourceLeak:ERROR",
+
+            // Local checkers specific to this module
+            "-Xep:MediaProviderLocaleRoot:ERROR",
+            "-Xep:MediaProviderMimeType:ERROR",
+        ],
+    },
+}
+
+// Used by MediaProvider and MediaProviderTests
+filegroup {
+    name: "mediaprovider-sources",
+    srcs: [
+        "src/**/*.aidl",
+        "src/**/*.java",
+        ":mediaprovider-database-sources",
+        ":statslog-mediaprovider-java-gen",
+    ],
+}
+
+// This is defined to give LegacyMediaProvider the bare minimum it needs
+// to keep the legacy database schema working while also building
+// against "system_current"
+filegroup {
+    name: "mediaprovider-database-sources",
+    srcs: [
+        "src/com/android/providers/media/DatabaseHelper.java",
+        "src/com/android/providers/media/util/BackgroundThread.java",
+        "src/com/android/providers/media/util/DatabaseUtils.java",
+        "src/com/android/providers/media/util/FileUtils.java",
+        "src/com/android/providers/media/util/ForegroundThread.java",
+        "src/com/android/providers/media/util/HandlerExecutor.java",
+        "src/com/android/providers/media/util/Logging.java",
+        "src/com/android/providers/media/util/MimeUtils.java",
+    ],
+}
+
+platform_compat_config {
+    name: "media-provider-platform-compat-config",
+    src: ":MediaProvider",
+}
+
+genrule {
+    name: "statslog-mediaprovider-java-gen",
+    tools: ["stats-log-api-gen"],
+    cmd: "$(location stats-log-api-gen) --java $(out) --module mediaprovider --javaPackage com.android.providers.media --javaClass MediaProviderStatsLog",
+    out: ["com/android/providers/media/MediaProviderStatsLog.java"],
 }
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b9bf106..305027b 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1,39 +1,46 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.providers.media"
-        android:sharedUserId="android.media"
-        android:sharedUserLabel="@string/uid_label"
-        android:versionCode="1023">
+        package="com.android.providers.media.module">
 
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
-    <uses-permission android:name="android.permission.RECEIVE_DEVICE_CUSTOMIZATION_READY" />
     <uses-permission android:name="android.permission.WRITE_SETTINGS" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.WRITE_MEDIA_STORAGE" />
-    <uses-permission android:name="android.permission.ACCESS_MTP" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+
     <uses-permission android:name="android.permission.MANAGE_USERS" />
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
-    <uses-permission android:name="android.permission.USE_RESERVED_DISK" />
+
     <uses-permission android:name="android.permission.WATCH_APPOPS" />
+    <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
     <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" />
 
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <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" />
+    <uses-permission android:name="android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS" />
+
+    <!-- Permissions required for reading and logging compat changes -->
+    <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>
+    <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG"/>
 
     <application
-            android:process="android.process.media"
+            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=".MediaProvider"
+                android:name="com.android.providers.media.MediaProvider"
                 android:authorities="media"
                 android:grantUriPermissions="true"
                 android:forceUriPermissions="true"
                 android:exported="true" />
 
         <provider
-                android:name=".MediaDocumentsProvider"
+                android:name="com.android.providers.media.MediaDocumentsProvider"
                 android:label="@string/storage_description"
                 android:authorities="com.android.providers.media.documents"
                 android:grantUriPermissions="true"
@@ -45,7 +52,7 @@
         </provider>
 
         <!-- Handles database upgrades after OTAs, then disables itself -->
-        <receiver android:name="MediaUpgradeReceiver">
+        <receiver android:name="com.android.providers.media.MediaUpgradeReceiver">
             <!-- This broadcast is sent after the core system has finished
                  booting, before the home app is launched or BOOT_COMPLETED
                  is sent. -->
@@ -54,10 +61,11 @@
             </intent-filter>
         </receiver>
 
-        <receiver android:name=".MediaReceiver">
+        <receiver android:name="com.android.providers.media.MediaReceiver">
             <intent-filter>
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
-                <action android:name="android.intent.action.DEVICE_CUSTOMIZATION_READY"/>
+            </intent-filter>
+            <intent-filter>
                 <action android:name="android.intent.action.LOCALE_CHANGED" />
             </intent-filter>
             <intent-filter>
@@ -76,49 +84,43 @@
         </receiver>
 
         <service
-            android:name=".IdleService"
+            android:name="com.android.providers.media.IdleService"
             android:exported="true"
             android:permission="android.permission.BIND_JOB_SERVICE" />
 
         <service
-            android:name=".MediaService"
-            android:exported="false" />
+            android:name="com.android.providers.media.MediaService"
+            android:exported="true"
+            android:permission="android.permission.BIND_JOB_SERVICE" />
 
-        <service
-            android:name=".MediaScannerService"
-            android:exported="true">
+        <service android:name="com.android.providers.media.fuse.ExternalStorageServiceImpl"
+                 android:permission="android.permission.BIND_EXTERNAL_STORAGE_SERVICE">
             <intent-filter>
-                <action android:name="android.media.IMediaScannerService" />
+                <action android:name="android.service.storage.ExternalStorageService" />
             </intent-filter>
         </service>
 
-        <receiver android:name=".MtpReceiver">
-            <intent-filter>
-                <action android:name="android.intent.action.BOOT_COMPLETED" />
-            </intent-filter>
-            <intent-filter>
-                <action android:name="android.hardware.usb.action.USB_STATE" />
-            </intent-filter>
-        </receiver>
-
-        <service android:name="MtpService" />
-
-        <service android:name="RingtoneOverlayService" />
-
-        <activity android:name="RingtonePickerActivity"
+        <activity
+                android:name="com.android.providers.media.PermissionActivity"
+                android:configChanges="orientation|keyboardHidden|screenSize"
                 android:theme="@style/PickerDialogTheme"
-                android:enabled="@*android:bool/config_defaultRingtonePickerEnabled"
-                android:excludeFromRecents="true">
+                android:exported="false"
+                android:excludeFromRecents="true" />
+
+        <activity
+                android:name="com.android.providers.media.CacheClearingActivity"
+                android:configChanges="orientation|keyboardHidden|screenSize"
+                android:exported="true"
+                android:theme="@style/CacheClearingAlertDialogTheme"
+                android:finishOnCloseSystemDialogs="true"
+                android:launchMode="singleTop"
+                android:excludeFromRecents="true"
+                android:visibleToInstantApps="true"
+                android:priority="100" >
             <intent-filter>
-                <action android:name="android.intent.action.RINGTONE_PICKER" />
+                <action android:name="android.os.storage.action.CLEAR_APP_CACHE" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-
-        <activity
-            android:name=".PermissionActivity"
-            android:theme="@style/PickerDialogTheme"
-            android:permission="android.permission.WRITE_MEDIA_STORAGE"
-            android:excludeFromRecents="true" />
     </application>
 </manifest>
diff --git a/OWNERS b/OWNERS
index f6fbe68..9156e6b 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,2 +1,6 @@
-marcone@google.com
 jsharkey@android.com
+maco@google.com
+marcone@google.com
+nandana@google.com
+shafik@google.com
+zezeozue@google.com
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
new file mode 100644
index 0000000..c8dbf77
--- /dev/null
+++ b/PREUPLOAD.cfg
@@ -0,0 +1,5 @@
+[Builtin Hooks]
+clang_format = true
+
+[Builtin Hooks Options]
+clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp
diff --git a/TEST_MAPPING b/TEST_MAPPING
index f900811..9d973e9 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -4,12 +4,26 @@
             "name": "MediaProviderTests"
         },
         {
+            "name": "MediaProviderClientTests"
+        },
+        {
             "name": "CtsProviderTestCases",
             "options": [
                 {
-                    "include-annotation": "android.platform.test.annotations.Presubmit"
+                    "include-filter": "android.provider.cts.media."
                 }
             ]
+        },
+        {
+            // This is a typo and is tracked in b/155715039 but flaky on CF.
+            // Will fix this once the root cause of flake is fixed.
+            "name": "AdoptableHostTest"
+        },
+        {
+            "name": "CtsScopedStorageHostTest"
+        },
+        {
+            "name": "fuse_node_test"
         }
     ]
 }
diff --git a/apex/Android.bp b/apex/Android.bp
new file mode 100644
index 0000000..c792c72
--- /dev/null
+++ b/apex/Android.bp
@@ -0,0 +1,26 @@
+apex {
+    name: "com.android.mediaprovider",
+    defaults: ["com.android.mediaprovider-defaults"],
+    manifest: "apex_manifest.json",
+    apps: ["MediaProvider"],
+    prebuilts: ["media-provider-platform-compat-config"],
+}
+
+apex_defaults {
+    name: "com.android.mediaprovider-defaults",
+    java_libs: ["framework-mediaprovider"],
+    key: "com.android.mediaprovider.key",
+    certificate: ":com.android.mediaprovider.certificate",
+    file_contexts: ":com.android.mediaprovider-file_contexts",
+}
+
+apex_key {
+    name: "com.android.mediaprovider.key",
+    public_key: "com.android.mediaprovider.avbpubkey",
+    private_key: "com.android.mediaprovider.pem",
+}
+
+android_app_certificate {
+    name: "com.android.mediaprovider.certificate",
+    certificate: "com.android.mediaprovider",
+}
diff --git a/apex/apex_manifest.json b/apex/apex_manifest.json
new file mode 100644
index 0000000..ffef8fb
--- /dev/null
+++ b/apex/apex_manifest.json
@@ -0,0 +1,4 @@
+{
+  "name": "com.android.mediaprovider",
+  "version": 300000000
+}
diff --git a/apex/com.android.mediaprovider.avbpubkey b/apex/com.android.mediaprovider.avbpubkey
new file mode 100644
index 0000000..c1b8dda
--- /dev/null
+++ b/apex/com.android.mediaprovider.avbpubkey
Binary files differ
diff --git a/apex/com.android.mediaprovider.pem b/apex/com.android.mediaprovider.pem
new file mode 100644
index 0000000..ef296f1
--- /dev/null
+++ b/apex/com.android.mediaprovider.pem
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIJKAIBAAKCAgEAw4cjN3bDtVP2BNyjL8BcU4IesvKBIy/bSvmweZVOOj6ztuxX
+cCXlbP8tUGJFSXYgvXJZPxCNUQR5epjbR5ql+YN266L+v1HtUudUZjSHFrl9PNwe
+BmAEcJwaVuLowaYoJuuNjKPWU66G3mheftOMcbbOqYETxoNDM6wlQ9m4o+LRdxYi
+I5wjKAMUUg/PADwK4vuLFtuxb7i0mRi3Rj7m+u+tar9leNX3Hkx109zcmfnwSUdm
+Ja1PGfgehYAKko42/L7lq7rUbNqNNrIOqgWKUdBWorE3Hhoj1cZjRx7Fosn7Efcz
+cM1sM4OmCqA0wG6Roa8S/dsk3Au0x+ITHnL+GVbQ/KoNdnZOmTPnXHAzwcikh/o7
+xtB+Qi4l7U1XXDPXWmSXgVHATraiWLFjbIqsWblEg1gQXjs8Tu3fZlMCkavAwbYQ
+6BToHegZ+XTBWKw6n2HZ0FMKiTz5yDwXbwGArfrvOAosjaONti0hLJ9pmRLWgOkC
+sUclzjvxAOQmzGBtg22M4O+AMLuJVqhUuwa7IeyTPzHbxc/qrB6PC1sIlx2dhNJw
+vY4SxjwepA2JFZJ3ll+Mxi/BksbnaB8iyfZ2Zs1aBjXhIzjdmbMSinszkn6r2ItK
+xpEvt5dGM4+JmFKO/EaZIVbkPQjgzo1O6N9vXJjfsw5aqw0Zkv519h4sNTsCAwEA
+AQKCAgBncirGoFYArFAf2F+Z1urB37jAMh7yb9JUg2/Dk6OUmzHXmIjWM6aDGSpv
+Od6wdAf0Uvg2rlolv/lJTG/PHVmsfco/9rdSn1SxJOR5QlcqRa6lsUtNUiVnMp+Y
+0moUcgUSOlNkUhYFpfOC3bMI5IGMdAw4j6rXvwCh6PS6Rgky48CwgYxETMoo1CA/
+HlAFLr8Pwcefq1Hf6tfZm8LzLjpkDsQwEtzrpkAczpUUJuirqhlfm5/GVT/hXG1B
+cjGlwgoDhDfcuHbEtIg7pM6vU0kP6eq8Hogrq+bDpjdGHFixvKzRn3ieuIADbSY+
+vu3J1XXFFew1VGjW5naZvdkhpfMapceiyH9cVIXNt1THE25y498OG6L3R9zCW0im
+QdHiI6H4oY1haXpUB/qk4PJKLHuRz9wESGDcJAM8/1NMxK01ril9L7VphdMUakVo
+bmzzHzyIKqb8IS+ifWzi6inUx1xt+4wFjVdYAOeVuo5HLrt0mGRe1vZTJHGMimg5
+jMzZhLd3bttAfqH5nRJrBJdgoaKU7HueM/Szxe6LrPUiNMLf+MiGjS7Y8qEzeY+C
+hd3dIAx2m31gkH8yTtZV5oOAMMyNQy8JHxNKMwrHzQhgji+BKqRukOCOzACqYBEW
+PmRgJI0b7aYUdOckiCxSYQWIZBSC5pDzXpix3+SvmnBqBdjtyQKCAQEA7CkglxVV
+sJBYxUcz1YdFIlSg19JMyUUWwTuvtodl5KE9Zf8ps0rqSbH5lMgxn/p0xCwgI3Bm
+FN/JZK466sFCqv5VXXeyt+DT98laQsemD7VuGgyVk1K0Ih+SPBWLCuGDOdZNBNvE
+HTqzjyjgGPo5+y1HvO0VJ2+0gzVlUHHnJU2YOmsEDCw7FZWA4vrG2ulxnbdZomGf
+meZgyAt1mOl6pQ7bVxl0/hTS1N2L2HAZR86jNXe4kKHBX3I1ezvg3BPopb/okbwf
+csP9Tu6GycKIbqvXyhMn1tsgZnIYyDl80McKMcpLz1wpJFaSOYXbSP8P0mMhLMr6
+y12UeNjj5m3GJwKCAQEA0/QpXHQu1L63Dt5O1UzofL6m6sOH7qMB0+s9n2A7QggM
+D5GylPVNuiQKafeMs/FHDF5bkX94xG1NwExvbfC9YUtfdBT61aEy2UIfkb2s0lE7
+xU8HZ4bW5+1eNXx7kAPT8wuL1UBTzdd0NwhJHxJtsLWB4zXaEFWSXzSAa8/XjY8Y
+SXYrdim0DC5TGg6CiugXwSfDGThWOiJgadhFFQUu4yc4UhSPVb6K4zgsyget3+al
+oCuZ7yGRuwgY43YpsnHsYaWlbQ/4bF0tJDjxD51bw7+kN85u26XPpSnK4XW6hz4n
+02vzAOh0peqL2ZQEGl74tXoF6ZgDauRVLavdIzQ4zQKCAQEA0NXlrfT9268p8Ppw
+Ceke56byvSeJI4rm/mWrkrvVDQLQ6rtjwtsCp7ysujb2O0QdscRbfztL6jWjQFca
+XrIcJF3YW1kYP6afi0URfnBSlx2XiY2LT8YY6PwGMv/R/wNBKgop/Zvhpv9b/cLl
+uPC+CTIXeCgnRsJvXIABYcv33XLqpO4xnBoY/ZNBcHbXForNH+0dZIzzuAKTnxMT
+IO+GtyOWL0WhwXXkorxviCxJDsjaZtPB8JQ6Dg9O3/UpeTRZzQkRvp4fKTvLattf
+cleGkmd/4YBjAIqx8iX/aD/98hmxGCqOM4aKE0pC8H9AVSLIeQGUOqoccW4Fd4BE
+uGZmjQKCAQAqh01dTbqv2WylwRmUVxA3J5IafQ1jZrXRLdx5hDefsjmpThTFKstj
+1CN6y78hxAH7i1tYQHmdAEmfkjaxmqKFSThn9GW4Q9YOHeRzu9lXghE6L/dOYJHy
+i4Bhvw6UARCgLerq6TNnlBmviSwS3gHAeUsMqhuNN35dOA2klFmt/85hvyJNXWTR
+JOJr0gmDkbpieXX+M32pwIgT9N83roiP76ivX4y7KbZ4jSb/irbqvD7zeowynUu+
+eYt06JrJQW6q0S/2SEQSikeqgvprpalqB6QNxpAb5pNZkp8wygyZYnFfTMO/pOTy
+/bL9/2MrX28MCuXwzx2wbwxgU6HVlH7pAoIBAHKhmczDSU91GxI1fRlpu13B0u6K
+cs3eON4xNkAmjv3N/uhyU2PPh6rpT+UXm0+S3PyV7y1kEWbWjEaLJKncEZ+RuxnI
+GwVg49Y5atrLn4cJ0kQB4Aylf/U/Oy9pt2189ihLMNt3uX3PtxRKXteG6tmzHJIn
+It7Tjp7gVmFW2UxhgOx9BzOhmNs+V+FcgAApRW/36yhRT0o10Zjotol9NgVbv0qz
+Flo8jUxKcePRrBMxHIce4bTVt43ODobr3fmOTtKrAUpv7Z5sqpUPSkErrN79FX3z
+INlgNFX7TUSFj49P3IQc15svKKezthTKr0bQPmV711msIYg/hsgjLw3JfWQ=
+-----END RSA PRIVATE KEY-----
diff --git a/apex/com.android.mediaprovider.pk8 b/apex/com.android.mediaprovider.pk8
new file mode 100644
index 0000000..31b63ca
--- /dev/null
+++ b/apex/com.android.mediaprovider.pk8
Binary files differ
diff --git a/apex/com.android.mediaprovider.x509.pem b/apex/com.android.mediaprovider.x509.pem
new file mode 100644
index 0000000..0c1c17a
--- /dev/null
+++ b/apex/com.android.mediaprovider.x509.pem
@@ -0,0 +1,36 @@
+-----BEGIN CERTIFICATE-----
+MIIGVTCCBD2gAwIBAgIUN6PqRpp7SGwboC3Cmk73a1Cg6+YwDQYJKoZIhvcNAQEL
+BQAwgbgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH
+DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMSIwIAYDVQQLDBljb20u
+YW5kcm9pZC5tZWRpYXByb3ZpZGVyMSIwIAYDVQQDDBljb20uYW5kcm9pZC5tZWRp
+YXByb3ZpZGVyMSIwIAYJKoZIhvcNAQkBFhNhbmRyb2lkQGFuZHJvaWQuY29tMCAX
+DTE5MTIwNDIzNDMyOFoYDzQ3NTcxMDMwMjM0MzI4WjCBuDELMAkGA1UEBhMCVVMx
+EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxEDAO
+BgNVBAoMB0FuZHJvaWQxIjAgBgNVBAsMGWNvbS5hbmRyb2lkLm1lZGlhcHJvdmlk
+ZXIxIjAgBgNVBAMMGWNvbS5hbmRyb2lkLm1lZGlhcHJvdmlkZXIxIjAgBgkqhkiG
+9w0BCQEWE2FuZHJvaWRAYW5kcm9pZC5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4IC
+DwAwggIKAoICAQCbHpgeFlM3gdMnook63srvzvpRyW4erkdjfvkstIj4lhlNGD1k
+jntIeeZ4soShLzxyhsQhBl/xD/KVDtfLvPmcfDF9UvHtan/CPHhehq2xiCGr0AsR
+M7mJmw+zOzb+hZWgSD8iFvFCgL6OJIUL/wu5A1DtXEJ3eaKgtIGSyOLOBgIKi4mN
+Ed4XDBLbIBg50EYVsNFI8UIycTC6unbPHHLSAASuHvWovnrh7R6vZSeEOQTiRloa
+0kg1h4BTiSGgUsIA2ByxDQMIfiuQ2ZWxVRaMQdwBUQDa1gSEKzlidFbU5YrOI5GS
+T5HrCIjPSTT0kRp9K7hy6nqImK42IQWhEdNm/e51r77d/hJYMRrV4UMTCtphH8Fq
+1ECcEvW7DhJ+IecrrWxfuRH4CUqFqi8Q9c++zNTqLS/62aBrKNVFYn9H87Ap8mq+
+Iw/kHyV1IP3dozvPaZXDSR9T7/oFQH6cvJwDnlwUk074XUk3h/9K7CGajii5Iz+I
+DcUdp9s21RIK9/aEd8VtTYMio+JXvLT6DBkNhb7lRI3KJG+QR3YudSm9arvZYv5H
+/ozMe9nsqyTXWuGYWsc2pskXvZP6tn6XaM+rrNx4pAQcHx3j9KB97u69cb6gGgN9
+wY3UrmgNnklHd5FreXqyhkgH4yGf2bm8f2NohnTv9yr9nKP8EliK/XxIzQIDAQAB
+o1MwUTAdBgNVHQ4EFgQUyS7tYST9r6bOZeZM81uoNlzd0ZIwHwYDVR0jBBgwFoAU
+yS7tYST9r6bOZeZM81uoNlzd0ZIwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B
+AQsFAAOCAgEAZXZQuYyiNAsAMRFGYXfr4C2cpAIKrpdenfqgFWRzxW9CvtvB/Coo
+7uNwrOnfDjOKPhaEbSjnnuM0NXlClUY8IWdvm8ypxeY0wv9RB6uBaC50qUl8592L
+2NNgNdAbmRxGubFaNHQ3wYTSXCREf+0hAjCMIsyQL5az1piTOhK+xb3mLwp1l6Ry
+vrdqR1b7feWcEKsS0hCskKv+GC4aqr9boIcBA0Vm+Le/goVl9gJPtp1qRWifdENF
+va8rZRRqKllGS0hUmP4MdiPD5A/gFKTW4LeGD4VOrIxOUwla8SNr9B1dT/PKRcIL
+suo+pX+vnWlEWhmKFJPwMyPTpw7EUfJO10RDinrh8X2mmlWspfzkj4QS03lZvyeB
+OemGCycJcVdoYvrFwoYqhelrcmnEDJFEWP5YEvqhW/Ej567eUbD40knIHZLbm9SK
+Qmqk6GZjtShS3f6BqljVCGl3uCUVPXX1oSxDCnM4Iob69N+UjDrvCzEhTfTwkogc
+NVXwXHSXIOclOfGmtVAIZQAYIn+IO+vAJVFygYEhsAqD3sh4omV6o+jWAFwcjjJH
+K1w6X0vytIx0kpoR+mJK/Vlalo3hHS7Qnolc7MKwv1x24M2u6bQXieTebtzYFTcE
+ADSNi5tm1miSYIo5W3B7JP9EeP7sP1OvLHEqycjwUpaW8zU24efEQu0=
+-----END CERTIFICATE-----
diff --git a/apex/framework/Android.bp b/apex/framework/Android.bp
new file mode 100644
index 0000000..c6be2ad
--- /dev/null
+++ b/apex/framework/Android.bp
@@ -0,0 +1,59 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_sdk_library {
+    name: "framework-mediaprovider",
+    defaults: ["framework-module-defaults"],
+
+    srcs: [
+        ":framework-mediaprovider-sources",
+    ],
+
+    permitted_packages: [
+        "android.provider",
+    ],
+
+    installable: true,
+
+    libs: [
+        "framework_mediaprovider_annotation",
+        "unsupportedappusage",
+    ],
+
+    hostdex: true, // for hiddenapi check
+    visibility: ["//visibility:public"],
+    impl_library_visibility: [
+        "//visibility:override",
+        "//packages/providers/MediaProvider:__subpackages__",
+    ],
+    apex_available: [
+        "com.android.mediaprovider",
+        "test_com.android.mediaprovider",
+    ],
+}
+
+filegroup {
+    name: "framework-mediaprovider-sources",
+    srcs: [
+        "java/**/*.java",
+    ],
+    path: "java",
+}
+
+java_library {
+    name: "framework_mediaprovider_annotation",
+    srcs: [":framework-mediaprovider-annotation-sources"],
+    installable: false,
+    sdk_version: "core_current",
+}
diff --git a/apex/framework/api/current.txt b/apex/framework/api/current.txt
new file mode 100644
index 0000000..52439fb
--- /dev/null
+++ b/apex/framework/api/current.txt
@@ -0,0 +1,404 @@
+// Signature format: 2.0
+package android.provider {
+
+  public final class MediaStore {
+    ctor public MediaStore();
+    method @NonNull public static android.app.PendingIntent createDeleteRequest(@NonNull android.content.ContentResolver, @NonNull java.util.Collection<android.net.Uri>);
+    method @NonNull public static android.app.PendingIntent createFavoriteRequest(@NonNull android.content.ContentResolver, @NonNull java.util.Collection<android.net.Uri>, boolean);
+    method @NonNull public static android.app.PendingIntent createTrashRequest(@NonNull android.content.ContentResolver, @NonNull java.util.Collection<android.net.Uri>, boolean);
+    method @NonNull public static android.app.PendingIntent createWriteRequest(@NonNull android.content.ContentResolver, @NonNull java.util.Collection<android.net.Uri>);
+    method @Nullable public static android.net.Uri getDocumentUri(@NonNull android.content.Context, @NonNull android.net.Uri);
+    method @NonNull public static java.util.Set<java.lang.String> getExternalVolumeNames(@NonNull android.content.Context);
+    method public static long getGeneration(@NonNull android.content.Context, @NonNull String);
+    method public static android.net.Uri getMediaScannerUri();
+    method @Nullable public static android.net.Uri getMediaUri(@NonNull android.content.Context, @NonNull android.net.Uri);
+    method @NonNull public static java.util.Set<java.lang.String> getRecentExternalVolumeNames(@NonNull android.content.Context);
+    method public static boolean getRequireOriginal(@NonNull android.net.Uri);
+    method @NonNull public static String getVersion(@NonNull android.content.Context);
+    method @NonNull public static String getVersion(@NonNull android.content.Context, @NonNull String);
+    method @NonNull public static String getVolumeName(@NonNull android.net.Uri);
+    method @Deprecated @NonNull public static android.net.Uri setIncludePending(@NonNull android.net.Uri);
+    method @NonNull public static android.net.Uri setRequireOriginal(@NonNull android.net.Uri);
+    field public static final String ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE";
+    field public static final String ACTION_IMAGE_CAPTURE_SECURE = "android.media.action.IMAGE_CAPTURE_SECURE";
+    field public static final String ACTION_REVIEW = "android.provider.action.REVIEW";
+    field public static final String ACTION_REVIEW_SECURE = "android.provider.action.REVIEW_SECURE";
+    field public static final String ACTION_VIDEO_CAPTURE = "android.media.action.VIDEO_CAPTURE";
+    field public static final String AUTHORITY = "media";
+    field @NonNull public static final android.net.Uri AUTHORITY_URI;
+    field public static final String EXTRA_BRIGHTNESS = "android.provider.extra.BRIGHTNESS";
+    field public static final String EXTRA_DURATION_LIMIT = "android.intent.extra.durationLimit";
+    field public static final String EXTRA_FINISH_ON_COMPLETION = "android.intent.extra.finishOnCompletion";
+    field public static final String EXTRA_FULL_SCREEN = "android.intent.extra.fullScreen";
+    field public static final String EXTRA_MEDIA_ALBUM = "android.intent.extra.album";
+    field public static final String EXTRA_MEDIA_ARTIST = "android.intent.extra.artist";
+    field public static final String EXTRA_MEDIA_FOCUS = "android.intent.extra.focus";
+    field public static final String EXTRA_MEDIA_GENRE = "android.intent.extra.genre";
+    field public static final String EXTRA_MEDIA_PLAYLIST = "android.intent.extra.playlist";
+    field public static final String EXTRA_MEDIA_RADIO_CHANNEL = "android.intent.extra.radio_channel";
+    field public static final String EXTRA_MEDIA_TITLE = "android.intent.extra.title";
+    field public static final String EXTRA_OUTPUT = "output";
+    field public static final String EXTRA_SCREEN_ORIENTATION = "android.intent.extra.screenOrientation";
+    field public static final String EXTRA_SHOW_ACTION_ICONS = "android.intent.extra.showActionIcons";
+    field public static final String EXTRA_SIZE_LIMIT = "android.intent.extra.sizeLimit";
+    field public static final String EXTRA_VIDEO_QUALITY = "android.intent.extra.videoQuality";
+    field public static final String INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH = "android.media.action.MEDIA_PLAY_FROM_SEARCH";
+    field public static final String INTENT_ACTION_MEDIA_SEARCH = "android.intent.action.MEDIA_SEARCH";
+    field @Deprecated public static final String INTENT_ACTION_MUSIC_PLAYER = "android.intent.action.MUSIC_PLAYER";
+    field public static final String INTENT_ACTION_STILL_IMAGE_CAMERA = "android.media.action.STILL_IMAGE_CAMERA";
+    field public static final String INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE = "android.media.action.STILL_IMAGE_CAMERA_SECURE";
+    field public static final String INTENT_ACTION_TEXT_OPEN_FROM_SEARCH = "android.media.action.TEXT_OPEN_FROM_SEARCH";
+    field public static final String INTENT_ACTION_VIDEO_CAMERA = "android.media.action.VIDEO_CAMERA";
+    field public static final String INTENT_ACTION_VIDEO_PLAY_FROM_SEARCH = "android.media.action.VIDEO_PLAY_FROM_SEARCH";
+    field public static final int MATCH_DEFAULT = 0; // 0x0
+    field public static final int MATCH_EXCLUDE = 2; // 0x2
+    field public static final int MATCH_INCLUDE = 1; // 0x1
+    field public static final int MATCH_ONLY = 3; // 0x3
+    field public static final String MEDIA_IGNORE_FILENAME = ".nomedia";
+    field public static final String MEDIA_SCANNER_VOLUME = "volume";
+    field public static final String META_DATA_REVIEW_GALLERY_PREWARM_SERVICE = "android.media.review_gallery_prewarm_service";
+    field public static final String META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE = "android.media.still_image_camera_preview_service";
+    field public static final String QUERY_ARG_MATCH_FAVORITE = "android:query-arg-match-favorite";
+    field public static final String QUERY_ARG_MATCH_PENDING = "android:query-arg-match-pending";
+    field public static final String QUERY_ARG_MATCH_TRASHED = "android:query-arg-match-trashed";
+    field public static final String QUERY_ARG_RELATED_URI = "android:query-arg-related-uri";
+    field public static final String UNKNOWN_STRING = "<unknown>";
+    field public static final String VOLUME_EXTERNAL = "external";
+    field public static final String VOLUME_EXTERNAL_PRIMARY = "external_primary";
+    field public static final String VOLUME_INTERNAL = "internal";
+  }
+
+  public static final class MediaStore.Audio {
+    ctor public MediaStore.Audio();
+    method @Deprecated @Nullable public static String keyFor(@Nullable String);
+  }
+
+  public static interface MediaStore.Audio.AlbumColumns {
+    field public static final String ALBUM = "album";
+    field @Deprecated public static final String ALBUM_ART = "album_art";
+    field public static final String ALBUM_ID = "album_id";
+    field @Deprecated public static final String ALBUM_KEY = "album_key";
+    field public static final String ARTIST = "artist";
+    field public static final String ARTIST_ID = "artist_id";
+    field @Deprecated public static final String ARTIST_KEY = "artist_key";
+    field public static final String FIRST_YEAR = "minyear";
+    field public static final String LAST_YEAR = "maxyear";
+    field public static final String NUMBER_OF_SONGS = "numsongs";
+    field public static final String NUMBER_OF_SONGS_FOR_ARTIST = "numsongs_by_artist";
+  }
+
+  public static final class MediaStore.Audio.Albums implements android.provider.BaseColumns android.provider.MediaStore.Audio.AlbumColumns {
+    ctor public MediaStore.Audio.Albums();
+    method public static android.net.Uri getContentUri(String);
+    field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/albums";
+    field public static final String DEFAULT_SORT_ORDER = "album_key";
+    field public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/album";
+    field public static final android.net.Uri EXTERNAL_CONTENT_URI;
+    field public static final android.net.Uri INTERNAL_CONTENT_URI;
+  }
+
+  public static interface MediaStore.Audio.ArtistColumns {
+    field public static final String ARTIST = "artist";
+    field @Deprecated public static final String ARTIST_KEY = "artist_key";
+    field public static final String NUMBER_OF_ALBUMS = "number_of_albums";
+    field public static final String NUMBER_OF_TRACKS = "number_of_tracks";
+  }
+
+  public static final class MediaStore.Audio.Artists implements android.provider.BaseColumns android.provider.MediaStore.Audio.ArtistColumns {
+    ctor public MediaStore.Audio.Artists();
+    method public static android.net.Uri getContentUri(String);
+    field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/artists";
+    field public static final String DEFAULT_SORT_ORDER = "artist_key";
+    field public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/artist";
+    field public static final android.net.Uri EXTERNAL_CONTENT_URI;
+    field public static final android.net.Uri INTERNAL_CONTENT_URI;
+  }
+
+  public static final class MediaStore.Audio.Artists.Albums implements android.provider.MediaStore.Audio.AlbumColumns {
+    ctor public MediaStore.Audio.Artists.Albums();
+    method public static android.net.Uri getContentUri(String, long);
+  }
+
+  public static interface MediaStore.Audio.AudioColumns extends android.provider.MediaStore.MediaColumns {
+    field public static final String ALBUM_ID = "album_id";
+    field @Deprecated public static final String ALBUM_KEY = "album_key";
+    field public static final String ARTIST_ID = "artist_id";
+    field @Deprecated public static final String ARTIST_KEY = "artist_key";
+    field public static final String BOOKMARK = "bookmark";
+    field public static final String GENRE = "genre";
+    field public static final String GENRE_ID = "genre_id";
+    field @Deprecated public static final String GENRE_KEY = "genre_key";
+    field public static final String IS_ALARM = "is_alarm";
+    field public static final String IS_AUDIOBOOK = "is_audiobook";
+    field public static final String IS_MUSIC = "is_music";
+    field public static final String IS_NOTIFICATION = "is_notification";
+    field public static final String IS_PODCAST = "is_podcast";
+    field public static final String IS_RINGTONE = "is_ringtone";
+    field @Deprecated public static final String TITLE_KEY = "title_key";
+    field public static final String TITLE_RESOURCE_URI = "title_resource_uri";
+    field public static final String TRACK = "track";
+    field public static final String YEAR = "year";
+  }
+
+  public static final class MediaStore.Audio.Genres implements android.provider.BaseColumns android.provider.MediaStore.Audio.GenresColumns {
+    ctor public MediaStore.Audio.Genres();
+    method public static android.net.Uri getContentUri(String);
+    method public static android.net.Uri getContentUriForAudioId(String, int);
+    field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/genre";
+    field public static final String DEFAULT_SORT_ORDER = "name";
+    field public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/genre";
+    field public static final android.net.Uri EXTERNAL_CONTENT_URI;
+    field public static final android.net.Uri INTERNAL_CONTENT_URI;
+  }
+
+  public static final class MediaStore.Audio.Genres.Members implements android.provider.MediaStore.Audio.AudioColumns {
+    ctor public MediaStore.Audio.Genres.Members();
+    method public static android.net.Uri getContentUri(String, long);
+    field public static final String AUDIO_ID = "audio_id";
+    field public static final String CONTENT_DIRECTORY = "members";
+    field public static final String DEFAULT_SORT_ORDER = "title_key";
+    field public static final String GENRE_ID = "genre_id";
+  }
+
+  public static interface MediaStore.Audio.GenresColumns {
+    field public static final String NAME = "name";
+  }
+
+  public static final class MediaStore.Audio.Media implements android.provider.MediaStore.Audio.AudioColumns {
+    ctor public MediaStore.Audio.Media();
+    method public static android.net.Uri getContentUri(String);
+    method @NonNull public static android.net.Uri getContentUri(@NonNull String, long);
+    method @Deprecated @Nullable public static android.net.Uri getContentUriForPath(@NonNull String);
+    field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/audio";
+    field public static final String DEFAULT_SORT_ORDER = "title_key";
+    field public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/audio";
+    field public static final android.net.Uri EXTERNAL_CONTENT_URI;
+    field public static final String EXTRA_MAX_BYTES = "android.provider.MediaStore.extra.MAX_BYTES";
+    field public static final android.net.Uri INTERNAL_CONTENT_URI;
+    field public static final String RECORD_SOUND_ACTION = "android.provider.MediaStore.RECORD_SOUND";
+  }
+
+  public static final class MediaStore.Audio.Playlists implements android.provider.BaseColumns android.provider.MediaStore.Audio.PlaylistsColumns {
+    ctor public MediaStore.Audio.Playlists();
+    method public static android.net.Uri getContentUri(String);
+    field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/playlist";
+    field public static final String DEFAULT_SORT_ORDER = "name";
+    field public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/playlist";
+    field public static final android.net.Uri EXTERNAL_CONTENT_URI;
+    field public static final android.net.Uri INTERNAL_CONTENT_URI;
+  }
+
+  public static final class MediaStore.Audio.Playlists.Members implements android.provider.MediaStore.Audio.AudioColumns {
+    ctor public MediaStore.Audio.Playlists.Members();
+    method public static android.net.Uri getContentUri(String, long);
+    method public static boolean moveItem(android.content.ContentResolver, long, int, int);
+    field public static final String AUDIO_ID = "audio_id";
+    field public static final String CONTENT_DIRECTORY = "members";
+    field public static final String DEFAULT_SORT_ORDER = "play_order";
+    field public static final String PLAYLIST_ID = "playlist_id";
+    field public static final String PLAY_ORDER = "play_order";
+    field public static final String _ID = "_id";
+  }
+
+  public static interface MediaStore.Audio.PlaylistsColumns extends android.provider.MediaStore.MediaColumns {
+    field @Deprecated public static final String DATA = "_data";
+    field public static final String DATE_ADDED = "date_added";
+    field public static final String DATE_MODIFIED = "date_modified";
+    field public static final String NAME = "name";
+  }
+
+  public static final class MediaStore.Audio.Radio {
+    field public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/radio";
+  }
+
+  public static interface MediaStore.DownloadColumns extends android.provider.MediaStore.MediaColumns {
+    field public static final String DOWNLOAD_URI = "download_uri";
+    field public static final String REFERER_URI = "referer_uri";
+  }
+
+  public static final class MediaStore.Downloads implements android.provider.MediaStore.DownloadColumns {
+    method @NonNull public static android.net.Uri getContentUri(@NonNull String);
+    method @NonNull public static android.net.Uri getContentUri(@NonNull String, long);
+    field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/download";
+    field @NonNull public static final android.net.Uri EXTERNAL_CONTENT_URI;
+    field @NonNull public static final android.net.Uri INTERNAL_CONTENT_URI;
+  }
+
+  public static final class MediaStore.Files {
+    ctor public MediaStore.Files();
+    method public static android.net.Uri getContentUri(String);
+    method public static android.net.Uri getContentUri(String, long);
+  }
+
+  public static interface MediaStore.Files.FileColumns extends android.provider.MediaStore.MediaColumns {
+    field public static final String MEDIA_TYPE = "media_type";
+    field public static final int MEDIA_TYPE_AUDIO = 2; // 0x2
+    field public static final int MEDIA_TYPE_DOCUMENT = 6; // 0x6
+    field public static final int MEDIA_TYPE_IMAGE = 1; // 0x1
+    field public static final int MEDIA_TYPE_NONE = 0; // 0x0
+    field public static final int MEDIA_TYPE_PLAYLIST = 4; // 0x4
+    field public static final int MEDIA_TYPE_SUBTITLE = 5; // 0x5
+    field public static final int MEDIA_TYPE_VIDEO = 3; // 0x3
+    field public static final String MIME_TYPE = "mime_type";
+    field public static final String PARENT = "parent";
+  }
+
+  public static final class MediaStore.Images {
+    ctor public MediaStore.Images();
+  }
+
+  public static interface MediaStore.Images.ImageColumns extends android.provider.MediaStore.MediaColumns {
+    field public static final String DESCRIPTION = "description";
+    field public static final String EXPOSURE_TIME = "exposure_time";
+    field public static final String F_NUMBER = "f_number";
+    field public static final String ISO = "iso";
+    field public static final String IS_PRIVATE = "isprivate";
+    field @Deprecated public static final String LATITUDE = "latitude";
+    field @Deprecated public static final String LONGITUDE = "longitude";
+    field @Deprecated public static final String MINI_THUMB_MAGIC = "mini_thumb_magic";
+    field @Deprecated public static final String PICASA_ID = "picasa_id";
+    field public static final String SCENE_CAPTURE_TYPE = "scene_capture_type";
+  }
+
+  public static final class MediaStore.Images.Media implements android.provider.MediaStore.Images.ImageColumns {
+    ctor public MediaStore.Images.Media();
+    method @Deprecated public static android.graphics.Bitmap getBitmap(android.content.ContentResolver, android.net.Uri) throws java.io.FileNotFoundException, java.io.IOException;
+    method public static android.net.Uri getContentUri(String);
+    method @NonNull public static android.net.Uri getContentUri(@NonNull String, long);
+    method @Deprecated public static String insertImage(android.content.ContentResolver, String, String, String) throws java.io.FileNotFoundException;
+    method @Deprecated public static String insertImage(android.content.ContentResolver, android.graphics.Bitmap, String, String);
+    method @Deprecated public static android.database.Cursor query(android.content.ContentResolver, android.net.Uri, String[]);
+    method @Deprecated public static android.database.Cursor query(android.content.ContentResolver, android.net.Uri, String[], String, String);
+    method @Deprecated public static android.database.Cursor query(android.content.ContentResolver, android.net.Uri, String[], String, String[], String);
+    field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/image";
+    field public static final String DEFAULT_SORT_ORDER = "bucket_display_name";
+    field public static final android.net.Uri EXTERNAL_CONTENT_URI;
+    field public static final android.net.Uri INTERNAL_CONTENT_URI;
+  }
+
+  @Deprecated public static class MediaStore.Images.Thumbnails implements android.provider.BaseColumns {
+    ctor @Deprecated public MediaStore.Images.Thumbnails();
+    method @Deprecated public static void cancelThumbnailRequest(android.content.ContentResolver, long);
+    method @Deprecated public static void cancelThumbnailRequest(android.content.ContentResolver, long, long);
+    method @Deprecated public static android.net.Uri getContentUri(String);
+    method @Deprecated @NonNull public static android.util.Size getKindSize(int);
+    method @Deprecated public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, int, android.graphics.BitmapFactory.Options);
+    method @Deprecated public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, long, int, android.graphics.BitmapFactory.Options);
+    method @Deprecated public static final android.database.Cursor query(android.content.ContentResolver, android.net.Uri, String[]);
+    method @Deprecated public static final android.database.Cursor queryMiniThumbnail(android.content.ContentResolver, long, int, String[]);
+    method @Deprecated public static final android.database.Cursor queryMiniThumbnails(android.content.ContentResolver, android.net.Uri, int, String[]);
+    field @Deprecated public static final String DATA = "_data";
+    field @Deprecated public static final String DEFAULT_SORT_ORDER = "image_id ASC";
+    field @Deprecated public static final android.net.Uri EXTERNAL_CONTENT_URI;
+    field @Deprecated public static final int FULL_SCREEN_KIND = 2; // 0x2
+    field @Deprecated public static final String HEIGHT = "height";
+    field @Deprecated public static final String IMAGE_ID = "image_id";
+    field @Deprecated public static final android.net.Uri INTERNAL_CONTENT_URI;
+    field @Deprecated public static final String KIND = "kind";
+    field @Deprecated public static final int MICRO_KIND = 3; // 0x3
+    field @Deprecated public static final int MINI_KIND = 1; // 0x1
+    field @Deprecated public static final String THUMB_DATA = "thumb_data";
+    field @Deprecated public static final String WIDTH = "width";
+  }
+
+  public static interface MediaStore.MediaColumns extends android.provider.BaseColumns {
+    field public static final String ALBUM = "album";
+    field public static final String ALBUM_ARTIST = "album_artist";
+    field public static final String ARTIST = "artist";
+    field public static final String AUTHOR = "author";
+    field public static final String BITRATE = "bitrate";
+    field public static final String BUCKET_DISPLAY_NAME = "bucket_display_name";
+    field public static final String BUCKET_ID = "bucket_id";
+    field public static final String CAPTURE_FRAMERATE = "capture_framerate";
+    field public static final String CD_TRACK_NUMBER = "cd_track_number";
+    field public static final String COMPILATION = "compilation";
+    field public static final String COMPOSER = "composer";
+    field @Deprecated public static final String DATA = "_data";
+    field public static final String DATE_ADDED = "date_added";
+    field public static final String DATE_EXPIRES = "date_expires";
+    field public static final String DATE_MODIFIED = "date_modified";
+    field public static final String DATE_TAKEN = "datetaken";
+    field public static final String DISC_NUMBER = "disc_number";
+    field public static final String DISPLAY_NAME = "_display_name";
+    field public static final String DOCUMENT_ID = "document_id";
+    field public static final String DURATION = "duration";
+    field public static final String GENERATION_ADDED = "generation_added";
+    field public static final String GENERATION_MODIFIED = "generation_modified";
+    field public static final String GENRE = "genre";
+    field public static final String HEIGHT = "height";
+    field public static final String INSTANCE_ID = "instance_id";
+    field public static final String IS_DOWNLOAD = "is_download";
+    field public static final String IS_DRM = "is_drm";
+    field public static final String IS_FAVORITE = "is_favorite";
+    field public static final String IS_PENDING = "is_pending";
+    field public static final String IS_TRASHED = "is_trashed";
+    field public static final String MIME_TYPE = "mime_type";
+    field public static final String NUM_TRACKS = "num_tracks";
+    field public static final String ORIENTATION = "orientation";
+    field public static final String ORIGINAL_DOCUMENT_ID = "original_document_id";
+    field public static final String OWNER_PACKAGE_NAME = "owner_package_name";
+    field public static final String RELATIVE_PATH = "relative_path";
+    field public static final String RESOLUTION = "resolution";
+    field public static final String SIZE = "_size";
+    field public static final String TITLE = "title";
+    field public static final String VOLUME_NAME = "volume_name";
+    field public static final String WIDTH = "width";
+    field public static final String WRITER = "writer";
+    field public static final String XMP = "xmp";
+    field public static final String YEAR = "year";
+  }
+
+  public static final class MediaStore.Video {
+    ctor public MediaStore.Video();
+    method @Deprecated public static android.database.Cursor query(android.content.ContentResolver, android.net.Uri, String[]);
+    field public static final String DEFAULT_SORT_ORDER = "_display_name";
+  }
+
+  public static final class MediaStore.Video.Media implements android.provider.MediaStore.Video.VideoColumns {
+    ctor public MediaStore.Video.Media();
+    method public static android.net.Uri getContentUri(String);
+    method @NonNull public static android.net.Uri getContentUri(@NonNull String, long);
+    field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/video";
+    field public static final String DEFAULT_SORT_ORDER = "title";
+    field public static final android.net.Uri EXTERNAL_CONTENT_URI;
+    field public static final android.net.Uri INTERNAL_CONTENT_URI;
+  }
+
+  @Deprecated public static class MediaStore.Video.Thumbnails implements android.provider.BaseColumns {
+    ctor @Deprecated public MediaStore.Video.Thumbnails();
+    method @Deprecated public static void cancelThumbnailRequest(android.content.ContentResolver, long);
+    method @Deprecated public static void cancelThumbnailRequest(android.content.ContentResolver, long, long);
+    method @Deprecated public static android.net.Uri getContentUri(String);
+    method @Deprecated @NonNull public static android.util.Size getKindSize(int);
+    method @Deprecated public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, int, android.graphics.BitmapFactory.Options);
+    method @Deprecated public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, long, int, android.graphics.BitmapFactory.Options);
+    field @Deprecated public static final String DATA = "_data";
+    field @Deprecated public static final String DEFAULT_SORT_ORDER = "video_id ASC";
+    field @Deprecated public static final android.net.Uri EXTERNAL_CONTENT_URI;
+    field @Deprecated public static final int FULL_SCREEN_KIND = 2; // 0x2
+    field @Deprecated public static final String HEIGHT = "height";
+    field @Deprecated public static final android.net.Uri INTERNAL_CONTENT_URI;
+    field @Deprecated public static final String KIND = "kind";
+    field @Deprecated public static final int MICRO_KIND = 3; // 0x3
+    field @Deprecated public static final int MINI_KIND = 1; // 0x1
+    field @Deprecated public static final String VIDEO_ID = "video_id";
+    field @Deprecated public static final String WIDTH = "width";
+  }
+
+  public static interface MediaStore.Video.VideoColumns extends android.provider.MediaStore.MediaColumns {
+    field public static final String BOOKMARK = "bookmark";
+    field public static final String CATEGORY = "category";
+    field public static final String COLOR_RANGE = "color_range";
+    field public static final String COLOR_STANDARD = "color_standard";
+    field public static final String COLOR_TRANSFER = "color_transfer";
+    field public static final String DESCRIPTION = "description";
+    field public static final String IS_PRIVATE = "isprivate";
+    field public static final String LANGUAGE = "language";
+    field @Deprecated public static final String LATITUDE = "latitude";
+    field @Deprecated public static final String LONGITUDE = "longitude";
+    field @Deprecated public static final String MINI_THUMB_MAGIC = "mini_thumb_magic";
+    field public static final String TAGS = "tags";
+  }
+
+}
+
diff --git a/apex/framework/api/module-lib-current.txt b/apex/framework/api/module-lib-current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/apex/framework/api/module-lib-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/framework/api/module-lib-removed.txt b/apex/framework/api/module-lib-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/apex/framework/api/module-lib-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/framework/api/removed.txt b/apex/framework/api/removed.txt
new file mode 100644
index 0000000..183a7c9
--- /dev/null
+++ b/apex/framework/api/removed.txt
@@ -0,0 +1,43 @@
+// Signature format: 2.0
+package android.provider {
+
+  public static interface MediaStore.Audio.AudioColumns extends android.provider.MediaStore.MediaColumns {
+    field public static final String ALBUM = "album";
+    field public static final String ARTIST = "artist";
+    field public static final String COMPOSER = "composer";
+    field public static final String DURATION = "duration";
+  }
+
+  public static interface MediaStore.DownloadColumns extends android.provider.MediaStore.MediaColumns {
+    field @Deprecated public static final String DESCRIPTION = "description";
+  }
+
+  public static interface MediaStore.Files.FileColumns extends android.provider.MediaStore.MediaColumns {
+    field public static final String TITLE = "title";
+  }
+
+  public static interface MediaStore.Images.ImageColumns extends android.provider.MediaStore.MediaColumns {
+    field public static final String BUCKET_DISPLAY_NAME = "bucket_display_name";
+    field public static final String BUCKET_ID = "bucket_id";
+    field public static final String DATE_TAKEN = "datetaken";
+    field public static final String GROUP_ID = "group_id";
+    field public static final String ORIENTATION = "orientation";
+  }
+
+  public static interface MediaStore.MediaColumns extends android.provider.BaseColumns {
+    field @Deprecated public static final String GROUP_ID = "group_id";
+  }
+
+  public static interface MediaStore.Video.VideoColumns extends android.provider.MediaStore.MediaColumns {
+    field public static final String ALBUM = "album";
+    field public static final String ARTIST = "artist";
+    field public static final String BUCKET_DISPLAY_NAME = "bucket_display_name";
+    field public static final String BUCKET_ID = "bucket_id";
+    field public static final String DATE_TAKEN = "datetaken";
+    field public static final String DURATION = "duration";
+    field public static final String GROUP_ID = "group_id";
+    field public static final String RESOLUTION = "resolution";
+  }
+
+}
+
diff --git a/apex/framework/api/system-current.txt b/apex/framework/api/system-current.txt
new file mode 100644
index 0000000..5ce4218
--- /dev/null
+++ b/apex/framework/api/system-current.txt
@@ -0,0 +1,14 @@
+// Signature format: 2.0
+package android.provider {
+
+  public final class MediaStore {
+    method @NonNull public static android.net.Uri rewriteToLegacy(@NonNull android.net.Uri);
+    method @NonNull @WorkerThread public static android.net.Uri scanFile(@NonNull android.content.ContentResolver, @NonNull java.io.File);
+    method @WorkerThread public static void scanVolume(@NonNull android.content.ContentResolver, @NonNull String);
+    method @WorkerThread public static void waitForIdle(@NonNull android.content.ContentResolver);
+    field public static final String AUTHORITY_LEGACY = "media_legacy";
+    field @NonNull public static final android.net.Uri AUTHORITY_LEGACY_URI;
+  }
+
+}
+
diff --git a/apex/framework/api/system-removed.txt b/apex/framework/api/system-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/apex/framework/api/system-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/apex/framework/java/android/provider/Column.java b/apex/framework/java/android/provider/Column.java
new file mode 100644
index 0000000..1364fb8
--- /dev/null
+++ b/apex/framework/java/android/provider/Column.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Denotes that a field is a {@link ContentProvider} column. It can be used as a
+ * key for {@link ContentValues} when inserting or updating data, or as a
+ * projection when querying.
+ *
+ * @hide
+ */
+@Documented
+@Retention(RUNTIME)
+@Target({FIELD})
+public @interface Column {
+    /**
+     * The {@link Cursor#getType(int)} of the data stored in this column.
+     */
+    int value();
+
+    /**
+     * This column is read-only and cannot be defined during insert or updates.
+     */
+    boolean readOnly() default false;
+}
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
new file mode 100644
index 0000000..d518c18
--- /dev/null
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -0,0 +1,3976 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider;
+
+import android.annotation.BytesLong;
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.CurrentTimeSecondsLong;
+import android.annotation.DurationMillisLong;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.annotation.WorkerThread;
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ClipData;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.UriPermission;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.ImageDecoder;
+import android.graphics.PostProcessor;
+import android.media.ExifInterface;
+import android.media.MediaFormat;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Environment;
+import android.os.OperationCanceledException;
+import android.os.RemoteException;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageVolume;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Size;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * The contract between the media provider and applications. Contains
+ * definitions for the supported URIs and columns.
+ * <p>
+ * The media provider provides an indexed collection of common media types, such
+ * as {@link Audio}, {@link Video}, and {@link Images}, from any attached
+ * storage devices. Each collection is organized based on the primary MIME type
+ * of the underlying content; for example, {@code image/*} content is indexed
+ * under {@link Images}. The {@link Files} collection provides a broad view
+ * across all collections, and does not filter by MIME type.
+ */
+public final class MediaStore {
+    private final static String TAG = "MediaStore";
+
+    /** The authority for the media provider */
+    public static final String AUTHORITY = "media";
+    /** A content:// style uri to the authority for the media provider */
+    public static final @NonNull Uri AUTHORITY_URI =
+            Uri.parse("content://" + AUTHORITY);
+
+    /**
+     * The authority for a legacy instance of the media provider, before it was
+     * converted into a Mainline module. When initializing for the first time,
+     * the Mainline module will connect to this legacy instance to migrate
+     * important user settings, such as {@link BaseColumns#_ID},
+     * {@link MediaColumns#IS_FAVORITE}, and more.
+     * <p>
+     * The legacy instance is expected to meet the exact same API contract
+     * expressed here in {@link MediaStore}, to facilitate smooth data
+     * migrations. Interactions that would normally interact with
+     * {@link #AUTHORITY} can be redirected to work with the legacy instance
+     * using {@link #rewriteToLegacy(Uri)}.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final String AUTHORITY_LEGACY = "media_legacy";
+    /**
+     * @see #AUTHORITY_LEGACY
+     * @hide
+     */
+    @SystemApi
+    public static final @NonNull Uri AUTHORITY_LEGACY_URI =
+            Uri.parse("content://" + AUTHORITY_LEGACY);
+
+    /**
+     * Synthetic volume name that provides a view of all content across the
+     * "internal" storage of the device.
+     * <p>
+     * This synthetic volume provides a merged view of all media distributed
+     * with the device, such as built-in ringtones and wallpapers.
+     * <p>
+     * Because this is a synthetic volume, you can't insert new content into
+     * this volume.
+     */
+    public static final String VOLUME_INTERNAL = "internal";
+
+    /**
+     * Synthetic volume name that provides a view of all content across the
+     * "external" storage of the device.
+     * <p>
+     * This synthetic volume provides a merged view of all media across all
+     * currently attached external storage devices.
+     * <p>
+     * Because this is a synthetic volume, you can't insert new content into
+     * this volume. Instead, you can insert content into a specific storage
+     * volume obtained from {@link #getExternalVolumeNames(Context)}.
+     */
+    public static final String VOLUME_EXTERNAL = "external";
+
+    /**
+     * Specific volume name that represents the primary external storage device
+     * at {@link Environment#getExternalStorageDirectory()}.
+     * <p>
+     * This volume may not always be available, such as when the user has
+     * ejected the device. You can find a list of all specific volume names
+     * using {@link #getExternalVolumeNames(Context)}.
+     */
+    public static final String VOLUME_EXTERNAL_PRIMARY = "external_primary";
+
+    /** {@hide} */
+    public static final String VOLUME_DEMO = "demo";
+
+    /** {@hide} */
+    public static final String RESOLVE_PLAYLIST_MEMBERS_CALL = "resolve_playlist_members";
+    /** {@hide} */
+    public static final String RUN_IDLE_MAINTENANCE_CALL = "run_idle_maintenance";
+    /** {@hide} */
+    public static final String WAIT_FOR_IDLE_CALL = "wait_for_idle";
+    /** {@hide} */
+    public static final String SCAN_FILE_CALL = "scan_file";
+    /** {@hide} */
+    public static final String SCAN_VOLUME_CALL = "scan_volume";
+    /** {@hide} */
+    public static final String CREATE_WRITE_REQUEST_CALL = "create_write_request";
+    /** {@hide} */
+    public static final String CREATE_TRASH_REQUEST_CALL = "create_trash_request";
+    /** {@hide} */
+    public static final String CREATE_FAVORITE_REQUEST_CALL = "create_favorite_request";
+    /** {@hide} */
+    public static final String CREATE_DELETE_REQUEST_CALL = "create_delete_request";
+
+    /** {@hide} */
+    public static final String GET_VERSION_CALL = "get_version";
+    /** {@hide} */
+    public static final String GET_GENERATION_CALL = "get_generation";
+
+    /** {@hide} */
+    public static final String START_LEGACY_MIGRATION_CALL = "start_legacy_migration";
+    /** {@hide} */
+    public static final String FINISH_LEGACY_MIGRATION_CALL = "finish_legacy_migration";
+
+    /** {@hide} */
+    @Deprecated
+    public static final String EXTERNAL_STORAGE_PROVIDER_AUTHORITY =
+            "com.android.externalstorage.documents";
+
+    /** {@hide} */
+    public static final String GET_DOCUMENT_URI_CALL = "get_document_uri";
+    /** {@hide} */
+    public static final String GET_MEDIA_URI_CALL = "get_media_uri";
+
+    /** {@hide} */
+    public static final String EXTRA_URI = "uri";
+    /** {@hide} */
+    public static final String EXTRA_URI_PERMISSIONS = "uriPermissions";
+
+    /** {@hide} */
+    public static final String EXTRA_CLIP_DATA = "clip_data";
+    /** {@hide} */
+    public static final String EXTRA_CONTENT_VALUES = "content_values";
+    /** {@hide} */
+    public static final String EXTRA_RESULT = "result";
+
+    /**
+     * This is for internal use by the media scanner only.
+     * Name of the (optional) Uri parameter that determines whether to skip deleting
+     * the file pointed to by the _data column, when deleting the database entry.
+     * The only appropriate value for this parameter is "false", in which case the
+     * delete will be skipped. Note especially that setting this to true, or omitting
+     * the parameter altogether, will perform the default action, which is different
+     * for different types of media.
+     * @hide
+     */
+    public static final String PARAM_DELETE_DATA = "deletedata";
+
+    /** {@hide} */
+    public static final String PARAM_INCLUDE_PENDING = "includePending";
+    /** {@hide} */
+    public static final String PARAM_PROGRESS = "progress";
+    /** {@hide} */
+    public static final String PARAM_REQUIRE_ORIGINAL = "requireOriginal";
+    /** {@hide} */
+    public static final String PARAM_LIMIT = "limit";
+
+    /**
+     * Activity Action: Launch a music player.
+     * The activity should be able to play, browse, or manipulate music files stored on the device.
+     *
+     * @deprecated Use {@link android.content.Intent#CATEGORY_APP_MUSIC} instead.
+     */
+    @Deprecated
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String INTENT_ACTION_MUSIC_PLAYER = "android.intent.action.MUSIC_PLAYER";
+
+    /**
+     * Activity Action: Perform a search for media.
+     * Contains at least the {@link android.app.SearchManager#QUERY} extra.
+     * May also contain any combination of the following extras:
+     * EXTRA_MEDIA_ARTIST, EXTRA_MEDIA_ALBUM, EXTRA_MEDIA_TITLE, EXTRA_MEDIA_FOCUS
+     *
+     * @see android.provider.MediaStore#EXTRA_MEDIA_ARTIST
+     * @see android.provider.MediaStore#EXTRA_MEDIA_ALBUM
+     * @see android.provider.MediaStore#EXTRA_MEDIA_TITLE
+     * @see android.provider.MediaStore#EXTRA_MEDIA_FOCUS
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String INTENT_ACTION_MEDIA_SEARCH = "android.intent.action.MEDIA_SEARCH";
+
+    /**
+     * An intent to perform a search for music media and automatically play content from the
+     * result when possible. This can be fired, for example, by the result of a voice recognition
+     * command to listen to music.
+     * <p>This intent always includes the {@link android.provider.MediaStore#EXTRA_MEDIA_FOCUS}
+     * and {@link android.app.SearchManager#QUERY} extras. The
+     * {@link android.provider.MediaStore#EXTRA_MEDIA_FOCUS} extra determines the search mode, and
+     * the value of the {@link android.app.SearchManager#QUERY} extra depends on the search mode.
+     * For more information about the search modes for this intent, see
+     * <a href="{@docRoot}guide/components/intents-common.html#PlaySearch">Play music based
+     * on a search query</a> in <a href="{@docRoot}guide/components/intents-common.html">Common
+     * Intents</a>.</p>
+     *
+     * <p>This intent makes the most sense for apps that can support large-scale search of music,
+     * such as services connected to an online database of music which can be streamed and played
+     * on the device.</p>
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH =
+            "android.media.action.MEDIA_PLAY_FROM_SEARCH";
+
+    /**
+     * An intent to perform a search for readable media and automatically play content from the
+     * result when possible. This can be fired, for example, by the result of a voice recognition
+     * command to read a book or magazine.
+     * <p>
+     * Contains the {@link android.app.SearchManager#QUERY} extra, which is a string that can
+     * contain any type of unstructured text search, like the name of a book or magazine, an author
+     * a genre, a publisher, or any combination of these.
+     * <p>
+     * Because this intent includes an open-ended unstructured search string, it makes the most
+     * sense for apps that can support large-scale search of text media, such as services connected
+     * to an online database of books and/or magazines which can be read on the device.
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String INTENT_ACTION_TEXT_OPEN_FROM_SEARCH =
+            "android.media.action.TEXT_OPEN_FROM_SEARCH";
+
+    /**
+     * An intent to perform a search for video media and automatically play content from the
+     * result when possible. This can be fired, for example, by the result of a voice recognition
+     * command to play movies.
+     * <p>
+     * Contains the {@link android.app.SearchManager#QUERY} extra, which is a string that can
+     * contain any type of unstructured video search, like the name of a movie, one or more actors,
+     * a genre, or any combination of these.
+     * <p>
+     * Because this intent includes an open-ended unstructured search string, it makes the most
+     * sense for apps that can support large-scale search of video, such as services connected to an
+     * online database of videos which can be streamed and played on the device.
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String INTENT_ACTION_VIDEO_PLAY_FROM_SEARCH =
+            "android.media.action.VIDEO_PLAY_FROM_SEARCH";
+
+    /**
+     * The name of the Intent-extra used to define the artist
+     */
+    public static final String EXTRA_MEDIA_ARTIST = "android.intent.extra.artist";
+    /**
+     * The name of the Intent-extra used to define the album
+     */
+    public static final String EXTRA_MEDIA_ALBUM = "android.intent.extra.album";
+    /**
+     * The name of the Intent-extra used to define the song title
+     */
+    public static final String EXTRA_MEDIA_TITLE = "android.intent.extra.title";
+    /**
+     * The name of the Intent-extra used to define the genre.
+     */
+    public static final String EXTRA_MEDIA_GENRE = "android.intent.extra.genre";
+    /**
+     * The name of the Intent-extra used to define the playlist.
+     */
+    public static final String EXTRA_MEDIA_PLAYLIST = "android.intent.extra.playlist";
+    /**
+     * The name of the Intent-extra used to define the radio channel.
+     */
+    public static final String EXTRA_MEDIA_RADIO_CHANNEL = "android.intent.extra.radio_channel";
+    /**
+     * The name of the Intent-extra used to define the search focus. The search focus
+     * indicates whether the search should be for things related to the artist, album
+     * or song that is identified by the other extras.
+     */
+    public static final String EXTRA_MEDIA_FOCUS = "android.intent.extra.focus";
+
+    /**
+     * The name of the Intent-extra used to control the orientation of a ViewImage or a MovieView.
+     * This is an int property that overrides the activity's requestedOrientation.
+     * @see android.content.pm.ActivityInfo#SCREEN_ORIENTATION_UNSPECIFIED
+     */
+    public static final String EXTRA_SCREEN_ORIENTATION = "android.intent.extra.screenOrientation";
+
+    /**
+     * The name of an Intent-extra used to control the UI of a ViewImage.
+     * This is a boolean property that overrides the activity's default fullscreen state.
+     */
+    public static final String EXTRA_FULL_SCREEN = "android.intent.extra.fullScreen";
+
+    /**
+     * The name of an Intent-extra used to control the UI of a ViewImage.
+     * This is a boolean property that specifies whether or not to show action icons.
+     */
+    public static final String EXTRA_SHOW_ACTION_ICONS = "android.intent.extra.showActionIcons";
+
+    /**
+     * The name of the Intent-extra used to control the onCompletion behavior of a MovieView.
+     * This is a boolean property that specifies whether or not to finish the MovieView activity
+     * when the movie completes playing. The default value is true, which means to automatically
+     * exit the movie player activity when the movie completes playing.
+     */
+    public static final String EXTRA_FINISH_ON_COMPLETION = "android.intent.extra.finishOnCompletion";
+
+    /**
+     * The name of the Intent action used to launch a camera in still image mode.
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String INTENT_ACTION_STILL_IMAGE_CAMERA = "android.media.action.STILL_IMAGE_CAMERA";
+
+    /**
+     * Name under which an activity handling {@link #INTENT_ACTION_STILL_IMAGE_CAMERA} or
+     * {@link #INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE} publishes the service name for its prewarm
+     * service.
+     * <p>
+     * This meta-data should reference the fully qualified class name of the prewarm service
+     * extending {@code CameraPrewarmService}.
+     * <p>
+     * The prewarm service will get bound and receive a prewarm signal
+     * {@code CameraPrewarmService#onPrewarm()} when a camera launch intent fire might be imminent.
+     * An application implementing a prewarm service should do the absolute minimum amount of work
+     * to initialize the camera in order to reduce startup time in likely case that shortly after a
+     * camera launch intent would be sent.
+     */
+    public static final String META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE =
+            "android.media.still_image_camera_preview_service";
+
+    /**
+     * Name under which an activity handling {@link #ACTION_REVIEW} or
+     * {@link #ACTION_REVIEW_SECURE} publishes the service name for its prewarm
+     * service.
+     * <p>
+     * This meta-data should reference the fully qualified class name of the prewarm service
+     * <p>
+     * The prewarm service can be bound before starting {@link #ACTION_REVIEW} or
+     * {@link #ACTION_REVIEW_SECURE}.
+     * An application implementing this prewarm service should do the absolute minimum amount of
+     * work to initialize its resources to efficiently handle an {@link #ACTION_REVIEW} or
+     * {@link #ACTION_REVIEW_SECURE} in the near future.
+     */
+    public static final java.lang.String META_DATA_REVIEW_GALLERY_PREWARM_SERVICE =
+            "android.media.review_gallery_prewarm_service";
+
+    /**
+     * The name of the Intent action used to launch a camera in still image mode
+     * for use when the device is secured (e.g. with a pin, password, pattern,
+     * or face unlock). Applications responding to this intent must not expose
+     * any personal content like existing photos or videos on the device. The
+     * applications should be careful not to share any photo or video with other
+     * applications or internet. The activity should use {@link
+     * Activity#setShowWhenLocked} to display
+     * on top of the lock screen while secured. There is no activity stack when
+     * this flag is used, so launching more than one activity is strongly
+     * discouraged.
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE =
+            "android.media.action.STILL_IMAGE_CAMERA_SECURE";
+
+    /**
+     * The name of the Intent action used to launch a camera in video mode.
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String INTENT_ACTION_VIDEO_CAMERA = "android.media.action.VIDEO_CAMERA";
+
+    /**
+     * Standard Intent action that can be sent to have the camera application
+     * capture an image and return it.
+     * <p>
+     * The caller may pass an extra EXTRA_OUTPUT to control where this image will be written.
+     * If the EXTRA_OUTPUT is not present, then a small sized image is returned as a Bitmap
+     * object in the extra field. This is useful for applications that only need a small image.
+     * If the EXTRA_OUTPUT is present, then the full-sized image will be written to the Uri
+     * value of EXTRA_OUTPUT.
+     * As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this uri can also be supplied through
+     * {@link android.content.Intent#setClipData(ClipData)}. If using this approach, you still must
+     * supply the uri through the EXTRA_OUTPUT field for compatibility with old applications.
+     * If you don't set a ClipData, it will be copied there for you when calling
+     * {@link Context#startActivity(Intent)}.
+     *
+     * <p>Note: if you app targets {@link android.os.Build.VERSION_CODES#M M} and above
+     * and declares as using the {@link android.Manifest.permission#CAMERA} permission which
+     * is not granted, then attempting to use this action will result in a {@link
+     * java.lang.SecurityException}.
+     *
+     *  @see #EXTRA_OUTPUT
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public final static String ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE";
+
+    /**
+     * Intent action that can be sent to have the camera application capture an image and return
+     * it when the device is secured (e.g. with a pin, password, pattern, or face unlock).
+     * Applications responding to this intent must not expose any personal content like existing
+     * photos or videos on the device. The applications should be careful not to share any photo
+     * or video with other applications or Internet. The activity should use {@link
+     * Activity#setShowWhenLocked} to display on top of the
+     * lock screen while secured. There is no activity stack when this flag is used, so
+     * launching more than one activity is strongly discouraged.
+     * <p>
+     * The caller may pass an extra EXTRA_OUTPUT to control where this image will be written.
+     * If the EXTRA_OUTPUT is not present, then a small sized image is returned as a Bitmap
+     * object in the extra field. This is useful for applications that only need a small image.
+     * If the EXTRA_OUTPUT is present, then the full-sized image will be written to the Uri
+     * value of EXTRA_OUTPUT.
+     * As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this uri can also be supplied through
+     * {@link android.content.Intent#setClipData(ClipData)}. If using this approach, you still must
+     * supply the uri through the EXTRA_OUTPUT field for compatibility with old applications.
+     * If you don't set a ClipData, it will be copied there for you when calling
+     * {@link Context#startActivity(Intent)}.
+     *
+     * @see #ACTION_IMAGE_CAPTURE
+     * @see #EXTRA_OUTPUT
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_IMAGE_CAPTURE_SECURE =
+            "android.media.action.IMAGE_CAPTURE_SECURE";
+
+    /**
+     * Standard Intent action that can be sent to have the camera application
+     * capture a video and return it.
+     * <p>
+     * The caller may pass in an extra EXTRA_VIDEO_QUALITY to control the video quality.
+     * <p>
+     * The caller may pass in an extra EXTRA_OUTPUT to control
+     * where the video is written. If EXTRA_OUTPUT is not present the video will be
+     * written to the standard location for videos, and the Uri of that location will be
+     * returned in the data field of the Uri.
+     * As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this uri can also be supplied through
+     * {@link android.content.Intent#setClipData(ClipData)}. If using this approach, you still must
+     * supply the uri through the EXTRA_OUTPUT field for compatibility with old applications.
+     * If you don't set a ClipData, it will be copied there for you when calling
+     * {@link Context#startActivity(Intent)}.
+     *
+     * <p>Note: if you app targets {@link android.os.Build.VERSION_CODES#M M} and above
+     * and declares as using the {@link android.Manifest.permission#CAMERA} permission which
+     * is not granted, then atempting to use this action will result in a {@link
+     * java.lang.SecurityException}.
+     *
+     * @see #EXTRA_OUTPUT
+     * @see #EXTRA_VIDEO_QUALITY
+     * @see #EXTRA_SIZE_LIMIT
+     * @see #EXTRA_DURATION_LIMIT
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public final static String ACTION_VIDEO_CAPTURE = "android.media.action.VIDEO_CAPTURE";
+
+    /**
+     * Standard action that can be sent to review the given media file.
+     * <p>
+     * The launched application is expected to provide a large-scale view of the
+     * given media file, while allowing the user to quickly access other
+     * recently captured media files.
+     * <p>
+     * Input: {@link Intent#getData} is URI of the primary media item to
+     * initially display.
+     *
+     * @see #ACTION_REVIEW_SECURE
+     * @see #EXTRA_BRIGHTNESS
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public final static String ACTION_REVIEW = "android.provider.action.REVIEW";
+
+    /**
+     * Standard action that can be sent to review the given media file when the
+     * device is secured (e.g. with a pin, password, pattern, or face unlock).
+     * The applications should be careful not to share any media with other
+     * applications or Internet. The activity should use
+     * {@link Activity#setShowWhenLocked} to display on top of the lock screen
+     * while secured. There is no activity stack when this flag is used, so
+     * launching more than one activity is strongly discouraged.
+     * <p>
+     * The launched application is expected to provide a large-scale view of the
+     * given primary media file, while only allowing the user to quickly access
+     * other media from an explicit secondary list.
+     * <p>
+     * Input: {@link Intent#getData} is URI of the primary media item to
+     * initially display. {@link Intent#getClipData} is the limited list of
+     * secondary media items that the user is allowed to review. If
+     * {@link Intent#getClipData} is undefined, then no other media access
+     * should be allowed.
+     *
+     * @see #EXTRA_BRIGHTNESS
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public final static String ACTION_REVIEW_SECURE = "android.provider.action.REVIEW_SECURE";
+
+    /**
+     * When defined, the launched application is requested to set the given
+     * brightness value via
+     * {@link android.view.WindowManager.LayoutParams#screenBrightness} to help
+     * ensure a smooth transition when launching {@link #ACTION_REVIEW} or
+     * {@link #ACTION_REVIEW_SECURE} intents.
+     */
+    public final static String EXTRA_BRIGHTNESS = "android.provider.extra.BRIGHTNESS";
+
+    /**
+     * The name of the Intent-extra used to control the quality of a recorded video. This is an
+     * integer property. Currently value 0 means low quality, suitable for MMS messages, and
+     * value 1 means high quality. In the future other quality levels may be added.
+     */
+    public final static String EXTRA_VIDEO_QUALITY = "android.intent.extra.videoQuality";
+
+    /**
+     * Specify the maximum allowed size.
+     */
+    public final static String EXTRA_SIZE_LIMIT = "android.intent.extra.sizeLimit";
+
+    /**
+     * Specify the maximum allowed recording duration in seconds.
+     */
+    public final static String EXTRA_DURATION_LIMIT = "android.intent.extra.durationLimit";
+
+    /**
+     * The name of the Intent-extra used to indicate a content resolver Uri to be used to
+     * store the requested image or video.
+     */
+    public final static String EXTRA_OUTPUT = "output";
+
+    /**
+      * The string that is used when a media attribute is not known. For example,
+      * if an audio file does not have any meta data, the artist and album columns
+      * will be set to this value.
+      */
+    public static final String UNKNOWN_STRING = "<unknown>";
+
+    /**
+     * Specify a {@link Uri} that is "related" to the current operation being
+     * performed.
+     * <p>
+     * This is typically used to allow an operation that may normally be
+     * rejected, such as making a copy of a pre-existing image located under a
+     * {@link MediaColumns#RELATIVE_PATH} where new images are not allowed.
+     * <p>
+     * It's strongly recommended that when making a copy of pre-existing content
+     * that you define the "original document ID" GUID as defined by the <em>XMP
+     * Media Management</em> standard.
+     * <p>
+     * This key can be placed in a {@link Bundle} of extras and passed to
+     * {@link ContentResolver#insert}.
+     */
+    public static final String QUERY_ARG_RELATED_URI = "android:query-arg-related-uri";
+
+    /**
+     * Flag that can be used to enable movement of media items on disk through
+     * {@link ContentResolver#update} calls. This is typically true for
+     * third-party apps, but false for system components.
+     *
+     * @hide
+     */
+    public static final String QUERY_ARG_ALLOW_MOVEMENT = "android:query-arg-allow-movement";
+
+    /**
+     * Specify how {@link MediaColumns#IS_PENDING} items should be filtered when
+     * performing a {@link MediaStore} operation.
+     * <p>
+     * This key can be placed in a {@link Bundle} of extras and passed to
+     * {@link ContentResolver#query}, {@link ContentResolver#update}, or
+     * {@link ContentResolver#delete}.
+     * <p>
+     * By default, pending items are filtered away from operations.
+     */
+    @Match
+    public static final String QUERY_ARG_MATCH_PENDING = "android:query-arg-match-pending";
+
+    /**
+     * Specify how {@link MediaColumns#IS_TRASHED} items should be filtered when
+     * performing a {@link MediaStore} operation.
+     * <p>
+     * This key can be placed in a {@link Bundle} of extras and passed to
+     * {@link ContentResolver#query}, {@link ContentResolver#update}, or
+     * {@link ContentResolver#delete}.
+     * <p>
+     * By default, trashed items are filtered away from operations.
+     *
+     * @see MediaColumns#IS_TRASHED
+     * @see MediaStore#QUERY_ARG_MATCH_TRASHED
+     * @see MediaStore#createTrashRequest
+     */
+    @Match
+    public static final String QUERY_ARG_MATCH_TRASHED = "android:query-arg-match-trashed";
+
+    /**
+     * Specify how {@link MediaColumns#IS_FAVORITE} items should be filtered
+     * when performing a {@link MediaStore} operation.
+     * <p>
+     * This key can be placed in a {@link Bundle} of extras and passed to
+     * {@link ContentResolver#query}, {@link ContentResolver#update}, or
+     * {@link ContentResolver#delete}.
+     * <p>
+     * By default, favorite items are <em>not</em> filtered away from
+     * operations.
+     *
+     * @see MediaColumns#IS_FAVORITE
+     * @see MediaStore#QUERY_ARG_MATCH_FAVORITE
+     * @see MediaStore#createFavoriteRequest
+     */
+    @Match
+    public static final String QUERY_ARG_MATCH_FAVORITE = "android:query-arg-match-favorite";
+
+    /** @hide */
+    @IntDef(flag = true, prefix = { "MATCH_" }, value = {
+            MATCH_DEFAULT,
+            MATCH_INCLUDE,
+            MATCH_EXCLUDE,
+            MATCH_ONLY,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Match {}
+
+    /**
+     * Value indicating that the default matching behavior should be used, as
+     * defined by the key documentation.
+     */
+    public static final int MATCH_DEFAULT = 0;
+
+    /**
+     * Value indicating that operations should include items matching the
+     * criteria defined by this key.
+     * <p>
+     * Note that items <em>not</em> matching the criteria <em>may</em> also be
+     * included depending on the default behavior documented by the key. If you
+     * want to operate exclusively on matching items, use {@link #MATCH_ONLY}.
+     */
+    public static final int MATCH_INCLUDE = 1;
+
+    /**
+     * Value indicating that operations should exclude items matching the
+     * criteria defined by this key.
+     */
+    public static final int MATCH_EXCLUDE = 2;
+
+    /**
+     * Value indicating that operations should only operate on items explicitly
+     * matching the criteria defined by this key.
+     */
+    public static final int MATCH_ONLY = 3;
+
+    /**
+     * Update the given {@link Uri} to also include any pending media items from
+     * calls such as
+     * {@link ContentResolver#query(Uri, String[], Bundle, CancellationSignal)}.
+     * By default no pending items are returned.
+     *
+     * @see MediaColumns#IS_PENDING
+     * @deprecated consider migrating to {@link #QUERY_ARG_MATCH_PENDING} which
+     *             is more expressive.
+     */
+    @Deprecated
+    public static @NonNull Uri setIncludePending(@NonNull Uri uri) {
+        return setIncludePending(uri.buildUpon()).build();
+    }
+
+    /** @hide */
+    @Deprecated
+    public static @NonNull Uri.Builder setIncludePending(@NonNull Uri.Builder uriBuilder) {
+        return uriBuilder.appendQueryParameter(PARAM_INCLUDE_PENDING, "1");
+    }
+
+    /** @hide */
+    @Deprecated
+    public static boolean getIncludePending(@NonNull Uri uri) {
+        return uri.getBooleanQueryParameter(MediaStore.PARAM_INCLUDE_PENDING, false);
+    }
+
+    /**
+     * Update the given {@link Uri} to indicate that the caller requires the
+     * original file contents when calling
+     * {@link ContentResolver#openFileDescriptor(Uri, String)}.
+     * <p>
+     * This can be useful when the caller wants to ensure they're backing up the
+     * exact bytes of the underlying media, without any Exif redaction being
+     * performed.
+     * <p>
+     * If the original file contents cannot be provided, a
+     * {@link UnsupportedOperationException} will be thrown when the returned
+     * {@link Uri} is used, such as when the caller doesn't hold
+     * {@link android.Manifest.permission#ACCESS_MEDIA_LOCATION}.
+     *
+     * @see MediaStore#getRequireOriginal(Uri)
+     */
+    public static @NonNull Uri setRequireOriginal(@NonNull Uri uri) {
+        return uri.buildUpon().appendQueryParameter(PARAM_REQUIRE_ORIGINAL, "1").build();
+    }
+
+    /**
+     * Return if the caller requires the original file contents when calling
+     * {@link ContentResolver#openFileDescriptor(Uri, String)}.
+     *
+     * @see MediaStore#setRequireOriginal(Uri)
+     */
+    public static boolean getRequireOriginal(@NonNull Uri uri) {
+        return uri.getBooleanQueryParameter(MediaStore.PARAM_REQUIRE_ORIGINAL, false);
+    }
+
+    /**
+     * Rewrite the given {@link Uri} to point at
+     * {@link MediaStore#AUTHORITY_LEGACY}.
+     *
+     * @see #AUTHORITY_LEGACY
+     * @hide
+     */
+    @SystemApi
+    public static @NonNull Uri rewriteToLegacy(@NonNull Uri uri) {
+        return uri.buildUpon().authority(MediaStore.AUTHORITY_LEGACY).build();
+    }
+
+    /**
+     * Called by the Mainline module to signal to {@link #AUTHORITY_LEGACY} that
+     * data migration is starting.
+     *
+     * @hide
+     */
+    public static void startLegacyMigration(@NonNull ContentResolver resolver,
+            @NonNull String volumeName) {
+        try {
+            resolver.call(AUTHORITY_LEGACY, START_LEGACY_MIGRATION_CALL, volumeName, null);
+        } catch (Exception e) {
+            Log.wtf(TAG, "Failed to deliver legacy migration event", e);
+        }
+    }
+
+    /**
+     * Called by the Mainline module to signal to {@link #AUTHORITY_LEGACY} that
+     * data migration is finished. The legacy provider may choose to perform
+     * clean-up operations at this point, such as deleting databases.
+     *
+     * @hide
+     */
+    public static void finishLegacyMigration(@NonNull ContentResolver resolver,
+            @NonNull String volumeName) {
+        try {
+            resolver.call(AUTHORITY_LEGACY, FINISH_LEGACY_MIGRATION_CALL, volumeName, null);
+        } catch (Exception e) {
+            Log.wtf(TAG, "Failed to deliver legacy migration event", e);
+        }
+    }
+
+    private static @NonNull PendingIntent createRequest(@NonNull ContentResolver resolver,
+            @NonNull String method, @NonNull Collection<Uri> uris, @Nullable ContentValues values) {
+        Objects.requireNonNull(resolver);
+        Objects.requireNonNull(uris);
+
+        final Iterator<Uri> it = uris.iterator();
+        final ClipData clipData = ClipData.newRawUri(null, it.next());
+        while (it.hasNext()) {
+            clipData.addItem(new ClipData.Item(it.next()));
+        }
+
+        final Bundle extras = new Bundle();
+        extras.putParcelable(EXTRA_CLIP_DATA, clipData);
+        extras.putParcelable(EXTRA_CONTENT_VALUES, values);
+        return resolver.call(AUTHORITY, method, null, extras).getParcelable(EXTRA_RESULT);
+    }
+
+    /**
+     * Create a {@link PendingIntent} that will prompt the user to grant your
+     * app write access for the requested media items.
+     * <p>
+     * This call only generates the request for a prompt; to display the prompt,
+     * call {@link Activity#startIntentSenderForResult} with
+     * {@link PendingIntent#getIntentSender()}. You can then determine if the
+     * user granted your request by testing for {@link Activity#RESULT_OK} in
+     * {@link Activity#onActivityResult}. The requested operation will have
+     * completely finished before this activity result is delivered.
+     * <p>
+     * Permissions granted through this mechanism are tied to the lifecycle of
+     * the {@link Activity} that requests them. If you need to retain
+     * longer-term access for background actions, you can place items into a
+     * {@link ClipData} or {@link Intent} which can then be passed to
+     * {@link Context#startService} or
+     * {@link android.app.job.JobInfo.Builder#setClipData}. Be sure to include
+     * any relevant access modes you want to retain, such as
+     * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}.
+     * <p>
+     * The displayed prompt will reflect all the media items you're requesting,
+     * including those for which you already hold write access. If you want to
+     * determine if you already hold write access before requesting access, use
+     * {@code ContentResolver#checkUriPermission(Uri, int, int)} with
+     * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}.
+     * <p>
+     * For security and performance reasons this method does not support
+     * {@link Intent#FLAG_GRANT_PERSISTABLE_URI_PERMISSION} or
+     * {@link Intent#FLAG_GRANT_PREFIX_URI_PERMISSION}.
+     * <p>
+     * The write access granted through this request is general-purpose, and
+     * once obtained you can directly {@link ContentResolver#update} columns
+     * like {@link MediaColumns#IS_FAVORITE}, {@link MediaColumns#IS_TRASHED},
+     * or {@link ContentResolver#delete}.
+     *
+     * @param resolver Used to connect with {@link MediaStore#AUTHORITY}.
+     *            Typically this value is {@link Context#getContentResolver()},
+     *            but if you need more explicit lifecycle controls, you can
+     *            obtain a {@link ContentProviderClient} and wrap it using
+     *            {@link ContentResolver#wrap(ContentProviderClient)}.
+     * @param uris The set of media items to include in this request. Each item
+     *            must be hosted by {@link MediaStore#AUTHORITY} and must
+     *            reference a specific media item by {@link BaseColumns#_ID}.
+     */
+    public static @NonNull PendingIntent createWriteRequest(@NonNull ContentResolver resolver,
+            @NonNull Collection<Uri> uris) {
+        return createRequest(resolver, CREATE_WRITE_REQUEST_CALL, uris, null);
+    }
+
+    /**
+     * Create a {@link PendingIntent} that will prompt the user to trash the
+     * requested media items. When the user approves this request,
+     * {@link MediaColumns#IS_TRASHED} is set on these items.
+     * <p>
+     * This call only generates the request for a prompt; to display the prompt,
+     * call {@link Activity#startIntentSenderForResult} with
+     * {@link PendingIntent#getIntentSender()}. You can then determine if the
+     * user granted your request by testing for {@link Activity#RESULT_OK} in
+     * {@link Activity#onActivityResult}. The requested operation will have
+     * completely finished before this activity result is delivered.
+     * <p>
+     * The displayed prompt will reflect all the media items you're requesting,
+     * including those for which you already hold write access. If you want to
+     * determine if you already hold write access before requesting access, use
+     * {@code ContentResolver#checkUriPermission(Uri, int, int)} with
+     * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}.
+     *
+     * @param resolver Used to connect with {@link MediaStore#AUTHORITY}.
+     *            Typically this value is {@link Context#getContentResolver()},
+     *            but if you need more explicit lifecycle controls, you can
+     *            obtain a {@link ContentProviderClient} and wrap it using
+     *            {@link ContentResolver#wrap(ContentProviderClient)}.
+     * @param uris The set of media items to include in this request. Each item
+     *            must be hosted by {@link MediaStore#AUTHORITY} and must
+     *            reference a specific media item by {@link BaseColumns#_ID}.
+     * @param value The {@link MediaColumns#IS_TRASHED} value to apply.
+     * @see MediaColumns#IS_TRASHED
+     * @see MediaStore#QUERY_ARG_MATCH_TRASHED
+     */
+    public static @NonNull PendingIntent createTrashRequest(@NonNull ContentResolver resolver,
+            @NonNull Collection<Uri> uris, boolean value) {
+        final ContentValues values = new ContentValues();
+        if (value) {
+            values.put(MediaColumns.IS_TRASHED, 1);
+        } else {
+            values.put(MediaColumns.IS_TRASHED, 0);
+        }
+        return createRequest(resolver, CREATE_TRASH_REQUEST_CALL, uris, values);
+    }
+
+    /**
+     * Create a {@link PendingIntent} that will prompt the user to favorite the
+     * requested media items. When the user approves this request,
+     * {@link MediaColumns#IS_FAVORITE} is set on these items.
+     * <p>
+     * This call only generates the request for a prompt; to display the prompt,
+     * call {@link Activity#startIntentSenderForResult} with
+     * {@link PendingIntent#getIntentSender()}. You can then determine if the
+     * user granted your request by testing for {@link Activity#RESULT_OK} in
+     * {@link Activity#onActivityResult}. The requested operation will have
+     * completely finished before this activity result is delivered.
+     * <p>
+     * The displayed prompt will reflect all the media items you're requesting,
+     * including those for which you already hold write access. If you want to
+     * determine if you already hold write access before requesting access, use
+     * {@code ContentResolver#checkUriPermission(Uri, int, int)} with
+     * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}.
+     *
+     * @param resolver Used to connect with {@link MediaStore#AUTHORITY}.
+     *            Typically this value is {@link Context#getContentResolver()},
+     *            but if you need more explicit lifecycle controls, you can
+     *            obtain a {@link ContentProviderClient} and wrap it using
+     *            {@link ContentResolver#wrap(ContentProviderClient)}.
+     * @param uris The set of media items to include in this request. Each item
+     *            must be hosted by {@link MediaStore#AUTHORITY} and must
+     *            reference a specific media item by {@link BaseColumns#_ID}.
+     * @param value The {@link MediaColumns#IS_FAVORITE} value to apply.
+     * @see MediaColumns#IS_FAVORITE
+     * @see MediaStore#QUERY_ARG_MATCH_FAVORITE
+     */
+    public static @NonNull PendingIntent createFavoriteRequest(@NonNull ContentResolver resolver,
+            @NonNull Collection<Uri> uris, boolean value) {
+        final ContentValues values = new ContentValues();
+        if (value) {
+            values.put(MediaColumns.IS_FAVORITE, 1);
+        } else {
+            values.put(MediaColumns.IS_FAVORITE, 0);
+        }
+        return createRequest(resolver, CREATE_FAVORITE_REQUEST_CALL, uris, values);
+    }
+
+    /**
+     * Create a {@link PendingIntent} that will prompt the user to permanently
+     * delete the requested media items. When the user approves this request,
+     * {@link ContentResolver#delete} will be called on these items.
+     * <p>
+     * This call only generates the request for a prompt; to display the prompt,
+     * call {@link Activity#startIntentSenderForResult} with
+     * {@link PendingIntent#getIntentSender()}. You can then determine if the
+     * user granted your request by testing for {@link Activity#RESULT_OK} in
+     * {@link Activity#onActivityResult}. The requested operation will have
+     * completely finished before this activity result is delivered.
+     * <p>
+     * The displayed prompt will reflect all the media items you're requesting,
+     * including those for which you already hold write access. If you want to
+     * determine if you already hold write access before requesting access, use
+     * {@code ContentResolver#checkUriPermission(Uri, int, int)} with
+     * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}.
+     *
+     * @param resolver Used to connect with {@link MediaStore#AUTHORITY}.
+     *            Typically this value is {@link Context#getContentResolver()},
+     *            but if you need more explicit lifecycle controls, you can
+     *            obtain a {@link ContentProviderClient} and wrap it using
+     *            {@link ContentResolver#wrap(ContentProviderClient)}.
+     * @param uris The set of media items to include in this request. Each item
+     *            must be hosted by {@link MediaStore#AUTHORITY} and must
+     *            reference a specific media item by {@link BaseColumns#_ID}.
+     */
+    public static @NonNull PendingIntent createDeleteRequest(@NonNull ContentResolver resolver,
+            @NonNull Collection<Uri> uris) {
+        return createRequest(resolver, CREATE_DELETE_REQUEST_CALL, uris, null);
+    }
+
+    /**
+     * Common media metadata columns.
+     */
+    public interface MediaColumns extends BaseColumns {
+        /**
+         * Absolute filesystem path to the media item on disk.
+         * <p>
+         * On Android 11, you can use this value when you access an existing
+         * file using direct file paths. That's because this value has a valid
+         * file path. However, don't assume that the file is always available.
+         * Be prepared to handle any file-based I/O errors that could occur.
+         * <p>
+         * Don't use this value when you create or update a media file, even
+         * if you're on Android 11 and are using direct file paths. Instead,
+         * use the values of the {@link #DISPLAY_NAME} and
+         * {@link #RELATIVE_PATH} columns.
+         * <p>
+         * Note that apps may not have filesystem permissions to directly access
+         * this path. Instead of trying to open this path directly, apps should
+         * use {@link ContentResolver#openFileDescriptor(Uri, String)} to gain
+         * access.
+         *
+         * @deprecated Apps may not have filesystem permissions to directly
+         *             access this path. Instead of trying to open this path
+         *             directly, apps should use
+         *             {@link ContentResolver#openFileDescriptor(Uri, String)}
+         *             to gain access.
+         */
+        @Deprecated
+        @Column(Cursor.FIELD_TYPE_STRING)
+        public static final String DATA = "_data";
+
+        /**
+         * Indexed value of {@link File#length()} extracted from this media
+         * item.
+         */
+        @BytesLong
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        public static final String SIZE = "_size";
+
+        /**
+         * The display name of the media item.
+         * <p>
+         * For example, an item stored at
+         * {@code /storage/0000-0000/DCIM/Vacation/IMG1024.JPG} would have a
+         * display name of {@code IMG1024.JPG}.
+         */
+        @Column(Cursor.FIELD_TYPE_STRING)
+        public static final String DISPLAY_NAME = "_display_name";
+
+        /**
+         * The time the media item was first added.
+         */
+        @CurrentTimeSecondsLong
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        public static final String DATE_ADDED = "date_added";
+
+        /**
+         * Indexed value of {@link File#lastModified()} extracted from this
+         * media item.
+         */
+        @CurrentTimeSecondsLong
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        public static final String DATE_MODIFIED = "date_modified";
+
+        /**
+         * Indexed value of {@link MediaMetadataRetriever#METADATA_KEY_DATE} or
+         * {@link ExifInterface#TAG_DATETIME_ORIGINAL} extracted from this media
+         * item.
+         * <p>
+         * Note that images must define both
+         * {@link ExifInterface#TAG_DATETIME_ORIGINAL} and
+         * {@code ExifInterface#TAG_OFFSET_TIME_ORIGINAL} to reliably determine
+         * this value in relation to the epoch.
+         */
+        @CurrentTimeMillisLong
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        public static final String DATE_TAKEN = "datetaken";
+
+        /**
+         * The MIME type of the media item.
+         * <p>
+         * This is typically defined based on the file extension of the media
+         * item. However, it may be the value of the {@code format} attribute
+         * defined by the <em>Dublin Core Media Initiative</em> standard,
+         * extracted from any XMP metadata contained within this media item.
+         * <p class="note">
+         * Note: the {@code format} attribute may be ignored if the top-level
+         * MIME type disagrees with the file extension. For example, it's
+         * reasonable for an {@code image/jpeg} file to declare a {@code format}
+         * of {@code image/vnd.google.panorama360+jpg}, but declaring a
+         * {@code format} of {@code audio/ogg} would be ignored.
+         * <p>
+         * This is a read-only column that is automatically computed.
+         */
+        @Column(Cursor.FIELD_TYPE_STRING)
+        public static final String MIME_TYPE = "mime_type";
+
+        /**
+         * Flag indicating if a media item is DRM protected.
+         */
+        @Column(Cursor.FIELD_TYPE_INTEGER)
+        public static final String IS_DRM = "is_drm";
+
+        /**
+         * Flag indicating if a media item is pending, and still being inserted
+         * by its owner. While this flag is set, only the owner of the item can
+         * open the underlying file; requests from other apps will be rejected.
+         * <p>
+         * Pending items are retained either until they are published by setting
+         * the field to {@code 0}, or until they expire as defined by
+         * {@link #DATE_EXPIRES}.
+         *
+         * @see MediaStore#QUERY_ARG_MATCH_PENDING
+         */
+        @Column(Cursor.FIELD_TYPE_INTEGER)
+        public static final String IS_PENDING = "is_pending";
+
+        /**
+         * Flag indicating if a media item is trashed.
+         * <p>
+         * Trashed items are retained until they expire as defined by
+         * {@link #DATE_EXPIRES}.
+         *
+         * @see MediaColumns#IS_TRASHED
+         * @see MediaStore#QUERY_ARG_MATCH_TRASHED
+         * @see MediaStore#createTrashRequest
+         */
+        @Column(Cursor.FIELD_TYPE_INTEGER)
+        public static final String IS_TRASHED = "is_trashed";
+
+        /**
+         * 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(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        public static final String DATE_EXPIRES = "date_expires";
+
+        /**
+         * Indexed value of
+         * {@link MediaMetadataRetriever#METADATA_KEY_VIDEO_WIDTH},
+         * {@link MediaMetadataRetriever#METADATA_KEY_IMAGE_WIDTH} or
+         * {@link ExifInterface#TAG_IMAGE_WIDTH} extracted from this media item.
+         */
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        public static final String WIDTH = "width";
+
+        /**
+         * Indexed value of
+         * {@link MediaMetadataRetriever#METADATA_KEY_VIDEO_HEIGHT},
+         * {@link MediaMetadataRetriever#METADATA_KEY_IMAGE_HEIGHT} or
+         * {@link ExifInterface#TAG_IMAGE_LENGTH} extracted from this media
+         * item.
+         */
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        public static final String HEIGHT = "height";
+
+        /**
+         * Calculated value that combines {@link #WIDTH} and {@link #HEIGHT}
+         * into a user-presentable string.
+         */
+        @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+        public static final String RESOLUTION = "resolution";
+
+        /**
+         * Package name that contributed this media. The value may be
+         * {@code NULL} if ownership cannot be reliably determined.
+         */
+        @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+        public static final String OWNER_PACKAGE_NAME = "owner_package_name";
+
+        /**
+         * Volume name of the specific storage device where this media item is
+         * persisted. The value is typically one of the volume names returned
+         * from {@link MediaStore#getExternalVolumeNames(Context)}.
+         * <p>
+         * This is a read-only column that is automatically computed.
+         */
+        @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+        public static final String VOLUME_NAME = "volume_name";
+
+        /**
+         * Relative path of this media item within the storage device where it
+         * is persisted. For example, an item stored at
+         * {@code /storage/0000-0000/DCIM/Vacation/IMG1024.JPG} would have a
+         * path of {@code DCIM/Vacation/}.
+         * <p>
+         * This value should only be used for organizational purposes, and you
+         * should not attempt to construct or access a raw filesystem path using
+         * this value. If you need to open a media item, use an API like
+         * {@link ContentResolver#openFileDescriptor(Uri, String)}.
+         * <p>
+         * When this value is set to {@code NULL} during an
+         * {@link ContentResolver#insert} operation, the newly created item will
+         * be placed in a relevant default location based on the type of media
+         * being inserted. For example, a {@code image/jpeg} item will be placed
+         * under {@link Environment#DIRECTORY_PICTURES}.
+         * <p>
+         * You can modify this column during an {@link ContentResolver#update}
+         * call, which will move the underlying file on disk.
+         * <p>
+         * In both cases above, content must be placed under a top-level
+         * directory that is relevant to the media type. For example, attempting
+         * to place a {@code audio/mpeg} file under
+         * {@link Environment#DIRECTORY_PICTURES} will be rejected.
+         */
+        @Column(Cursor.FIELD_TYPE_STRING)
+        public static final String RELATIVE_PATH = "relative_path";
+
+        /**
+         * The primary bucket ID of this media item. This can be useful to
+         * present the user a first-level clustering of related media items.
+         * This is a read-only column that is automatically computed.
+         */
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        public static final String BUCKET_ID = "bucket_id";
+
+        /**
+         * The primary bucket display name of this media item. This can be
+         * useful to present the user a first-level clustering of related
+         * media items. This is a read-only column that is automatically
+         * computed.
+         */
+        @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+        public static final String BUCKET_DISPLAY_NAME = "bucket_display_name";
+
+        /**
+         * The group ID of this media item. This can be useful to present
+         * the user a grouping of related media items, such a burst of
+         * images, or a {@code JPG} and {@code DNG} version of the same
+         * image.
+         * <p>
+         * This is a read-only column that is automatically computed based
+         * on the first portion of the filename. For example,
+         * {@code IMG1024.BURST001.JPG} and {@code IMG1024.BURST002.JPG}
+         * will have the same {@link #GROUP_ID} because the first portion of
+         * their filenames is identical.
+         *
+         * @removed
+         */
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        @Deprecated
+        public static final String GROUP_ID = "group_id";
+
+        /**
+         * The "document ID" GUID as defined by the <em>XMP Media
+         * Management</em> standard, extracted from any XMP metadata contained
+         * within this media item. The value is {@code null} when no metadata
+         * was found.
+         * <p>
+         * Each "document ID" is created once for each new resource. Different
+         * renditions of that resource are expected to have different IDs.
+         */
+        @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+        public static final String DOCUMENT_ID = "document_id";
+
+        /**
+         * The "instance ID" GUID as defined by the <em>XMP Media
+         * Management</em> standard, extracted from any XMP metadata contained
+         * within this media item. The value is {@code null} when no metadata
+         * was found.
+         * <p>
+         * This "instance ID" changes with each save operation of a specific
+         * "document ID".
+         */
+        @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+        public static final String INSTANCE_ID = "instance_id";
+
+        /**
+         * The "original document ID" GUID as defined by the <em>XMP Media
+         * Management</em> standard, extracted from any XMP metadata contained
+         * within this media item.
+         * <p>
+         * This "original document ID" links a resource to its original source.
+         * For example, when you save a PSD document as a JPEG, then convert the
+         * JPEG to GIF format, the "original document ID" of both the JPEG and
+         * GIF files is the "document ID" of the original PSD file.
+         */
+        @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+        public static final String ORIGINAL_DOCUMENT_ID = "original_document_id";
+
+        /**
+         * Indexed value of
+         * {@link MediaMetadataRetriever#METADATA_KEY_VIDEO_ROTATION},
+         * {@link MediaMetadataRetriever#METADATA_KEY_IMAGE_ROTATION}, or
+         * {@link ExifInterface#TAG_ORIENTATION} extracted from this media item.
+         * <p>
+         * For consistency the indexed value is expressed in degrees, such as 0,
+         * 90, 180, or 270.
+         */
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        public static final String ORIENTATION = "orientation";
+
+        /**
+         * Flag indicating if the media item has been marked as being a
+         * "favorite" by the user.
+         *
+         * @see MediaColumns#IS_FAVORITE
+         * @see MediaStore#QUERY_ARG_MATCH_FAVORITE
+         * @see MediaStore#createFavoriteRequest
+         */
+        @Column(Cursor.FIELD_TYPE_INTEGER)
+        public static final String IS_FAVORITE = "is_favorite";
+
+        /**
+         * Flag indicating if the media item has been marked as being part of
+         * the {@link Downloads} collection.
+         */
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        public static final String IS_DOWNLOAD = "is_download";
+
+        /**
+         * Generation number at which metadata for this media item was first
+         * inserted. This is useful for apps that are attempting to quickly
+         * identify exactly which media items have been added since a previous
+         * point in time. Generation numbers are monotonically increasing over
+         * time, and can be safely arithmetically compared.
+         * <p>
+         * Detecting media additions using generation numbers is more robust
+         * than using {@link #DATE_ADDED}, since those values may change in
+         * unexpected ways when apps use {@link File#setLastModified(long)} or
+         * when the system clock is set incorrectly.
+         * <p>
+         * Note that before comparing these detailed generation values, you
+         * should first confirm that the overall version hasn't changed by
+         * checking {@link MediaStore#getVersion(Context, String)}, since that
+         * indicates when a more radical change has occurred. If the overall
+         * version changes, you should assume that generation numbers have been
+         * reset and perform a full synchronization pass.
+         *
+         * @see MediaStore#getGeneration(Context, String)
+         */
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        public static final String GENERATION_ADDED = "generation_added";
+
+        /**
+         * Generation number at which metadata for this media item was last
+         * changed. This is useful for apps that are attempting to quickly
+         * identify exactly which media items have changed since a previous
+         * point in time. Generation numbers are monotonically increasing over
+         * time, and can be safely arithmetically compared.
+         * <p>
+         * Detecting media changes using generation numbers is more robust than
+         * using {@link #DATE_MODIFIED}, since those values may change in
+         * unexpected ways when apps use {@link File#setLastModified(long)} or
+         * when the system clock is set incorrectly.
+         * <p>
+         * Note that before comparing these detailed generation values, you
+         * should first confirm that the overall version hasn't changed by
+         * checking {@link MediaStore#getVersion(Context, String)}, since that
+         * indicates when a more radical change has occurred. If the overall
+         * version changes, you should assume that generation numbers have been
+         * reset and perform a full synchronization pass.
+         *
+         * @see MediaStore#getGeneration(Context, String)
+         */
+        @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 ====
+        // =======================================
+
+        /**
+         * Indexed value of
+         * {@link MediaMetadataRetriever#METADATA_KEY_CD_TRACK_NUMBER} extracted
+         * from this media item.
+         */
+        @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+        public static final String CD_TRACK_NUMBER = "cd_track_number";
+
+        /**
+         * Indexed value of {@link MediaMetadataRetriever#METADATA_KEY_ALBUM}
+         * extracted from this media item.
+         */
+        @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+        public static final String ALBUM = "album";
+
+        /**
+         * Indexed value of {@link MediaMetadataRetriever#METADATA_KEY_ARTIST}
+         * or {@link ExifInterface#TAG_ARTIST} extracted from this media item.
+         */
+        @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+        public static final String ARTIST = "artist";
+
+        /**
+         * Indexed value of {@link MediaMetadataRetriever#METADATA_KEY_AUTHOR}
+         * extracted from this media item.
+         */
+        @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+        public static final String AUTHOR = "author";
+
+        /**
+         * Indexed value of {@link MediaMetadataRetriever#METADATA_KEY_COMPOSER}
+         * extracted from this media item.
+         */
+        @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+        public static final String COMPOSER = "composer";
+
+        // METADATA_KEY_DATE is DATE_TAKEN
+
+        /**
+         * Indexed value of {@link MediaMetadataRetriever#METADATA_KEY_GENRE}
+         * extracted from this media item.
+         */
+        @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+        public static final String GENRE = "genre";
+
+        /**
+         * Indexed value of {@link MediaMetadataRetriever#METADATA_KEY_TITLE}
+         * extracted from this media item.
+         */
+        @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+        public static final String TITLE = "title";
+
+        /**
+         * Indexed value of {@link MediaMetadataRetriever#METADATA_KEY_YEAR}
+         * extracted from this media item.
+         */
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        public static final String YEAR = "year";
+
+        /**
+         * Indexed value of {@link MediaMetadataRetriever#METADATA_KEY_DURATION}
+         * extracted from this media item.
+         */
+        @DurationMillisLong
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        public static final String DURATION = "duration";
+
+        /**
+         * Indexed value of {@link MediaMetadataRetriever#METADATA_KEY_NUM_TRACKS}
+         * extracted from this media item.
+         */
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        public static final String NUM_TRACKS = "num_tracks";
+
+        /**
+         * Indexed value of {@link MediaMetadataRetriever#METADATA_KEY_WRITER}
+         * extracted from this media item.
+         */
+        @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+        public static final String WRITER = "writer";
+
+        // METADATA_KEY_MIMETYPE is MIME_TYPE
+
+        /**
+         * Indexed value of {@link MediaMetadataRetriever#METADATA_KEY_ALBUMARTIST}
+         * extracted from this media item.
+         */
+        @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+        public static final String ALBUM_ARTIST = "album_artist";
+
+        /**
+         * Indexed value of {@link MediaMetadataRetriever#METADATA_KEY_DISC_NUMBER}
+         * extracted from this media item.
+         */
+        @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+        public static final String DISC_NUMBER = "disc_number";
+
+        /**
+         * Indexed value of {@link MediaMetadataRetriever#METADATA_KEY_COMPILATION}
+         * extracted from this media item.
+         */
+        @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+        public static final String COMPILATION = "compilation";
+
+        // HAS_AUDIO is ignored
+        // HAS_VIDEO is ignored
+        // VIDEO_WIDTH is WIDTH
+        // VIDEO_HEIGHT is HEIGHT
+
+        /**
+         * Indexed value of {@link MediaMetadataRetriever#METADATA_KEY_BITRATE}
+         * extracted from this media item.
+         */
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        public static final String BITRATE = "bitrate";
+
+        // TIMED_TEXT_LANGUAGES is ignored
+        // IS_DRM is ignored
+        // LOCATION is LATITUDE and LONGITUDE
+        // VIDEO_ROTATION is ORIENTATION
+
+        /**
+         * Indexed value of
+         * {@link MediaMetadataRetriever#METADATA_KEY_CAPTURE_FRAMERATE}
+         * extracted from this media item.
+         */
+        @Column(value = Cursor.FIELD_TYPE_FLOAT, readOnly = true)
+        public static final String CAPTURE_FRAMERATE = "capture_framerate";
+
+        // HAS_IMAGE is ignored
+        // IMAGE_COUNT is ignored
+        // IMAGE_PRIMARY is ignored
+        // IMAGE_WIDTH is WIDTH
+        // IMAGE_HEIGHT is HEIGHT
+        // IMAGE_ROTATION is ORIENTATION
+        // VIDEO_FRAME_COUNT is ignored
+        // EXIF_OFFSET is ignored
+        // EXIF_LENGTH is ignored
+        // COLOR_STANDARD is ignored
+        // COLOR_TRANSFER is ignored
+        // COLOR_RANGE is ignored
+        // SAMPLERATE is ignored
+        // BITS_PER_SAMPLE is ignored
+    }
+
+    /**
+     * Media provider table containing an index of all files in the media storage,
+     * including non-media files.  This should be used by applications that work with
+     * non-media file types (text, HTML, PDF, etc) as well as applications that need to
+     * work with multiple media file types in a single query.
+     */
+    public static final class Files {
+        /** @hide */
+        public static final String TABLE = "files";
+
+        /** @hide */
+        public static final Uri EXTERNAL_CONTENT_URI = getContentUri(VOLUME_EXTERNAL);
+
+        /**
+         * Get the content:// style URI for the files table on the
+         * given volume.
+         *
+         * @param volumeName the name of the volume to get the URI for
+         * @return the URI to the files table on the given volume
+         */
+        public static Uri getContentUri(String volumeName) {
+            return AUTHORITY_URI.buildUpon().appendPath(volumeName).appendPath("file").build();
+        }
+
+        /**
+         * Get the content:// style URI for a single row in the files table on the
+         * given volume.
+         *
+         * @param volumeName the name of the volume to get the URI for
+         * @param rowId the file to get the URI for
+         * @return the URI to the files table on the given volume
+         */
+        public static final Uri getContentUri(String volumeName,
+                long rowId) {
+            return ContentUris.withAppendedId(getContentUri(volumeName), rowId);
+        }
+
+        /** {@hide} */
+        @UnsupportedAppUsage
+        public static Uri getMtpObjectsUri(@NonNull String volumeName) {
+            return MediaStore.Files.getContentUri(volumeName);
+        }
+
+        /** {@hide} */
+        @UnsupportedAppUsage
+        public static final Uri getMtpObjectsUri(@NonNull String volumeName, long fileId) {
+            return MediaStore.Files.getContentUri(volumeName, fileId);
+        }
+
+        /** {@hide} */
+        @UnsupportedAppUsage
+        public static final Uri getMtpReferencesUri(@NonNull String volumeName, long fileId) {
+            return MediaStore.Files.getContentUri(volumeName, fileId);
+        }
+
+        /**
+         * Used to trigger special logic for directories.
+         * @hide
+         */
+        public static final Uri getDirectoryUri(String volumeName) {
+            return AUTHORITY_URI.buildUpon().appendPath(volumeName).appendPath("dir").build();
+        }
+
+        /** @hide */
+        public static final Uri getContentUriForPath(String path) {
+            return getContentUri(getVolumeName(new File(path)));
+        }
+
+        /**
+         * File metadata columns.
+         */
+        public interface FileColumns extends MediaColumns {
+            /**
+             * The MTP storage ID of the file
+             * @hide
+             */
+            @UnsupportedAppUsage
+            @Deprecated
+            // @Column(Cursor.FIELD_TYPE_INTEGER)
+            public static final String STORAGE_ID = "storage_id";
+
+            /**
+             * The MTP format code of the file
+             * @hide
+             */
+            @UnsupportedAppUsage
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String FORMAT = "format";
+
+            /**
+             * The index of the parent directory of the file
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String PARENT = "parent";
+
+            /**
+             * The MIME type of the media item.
+             * <p>
+             * This is typically defined based on the file extension of the media
+             * item. However, it may be the value of the {@code format} attribute
+             * defined by the <em>Dublin Core Media Initiative</em> standard,
+             * extracted from any XMP metadata contained within this media item.
+             * <p class="note">
+             * Note: the {@code format} attribute may be ignored if the top-level
+             * MIME type disagrees with the file extension. For example, it's
+             * reasonable for an {@code image/jpeg} file to declare a {@code format}
+             * of {@code image/vnd.google.panorama360+jpg}, but declaring a
+             * {@code format} of {@code audio/ogg} would be ignored.
+             * <p>
+             * This is a read-only column that is automatically computed.
+             */
+            @Column(Cursor.FIELD_TYPE_STRING)
+            public static final String MIME_TYPE = "mime_type";
+
+            /** @removed promoted to parent interface */
+            public static final String TITLE = "title";
+
+            /**
+             * The media type (audio, video, image, document, playlist or subtitle)
+             * of the file, or 0 for not a media file
+             */
+            @Column(Cursor.FIELD_TYPE_INTEGER)
+            public static final String MEDIA_TYPE = "media_type";
+
+            /**
+             * Constant for the {@link #MEDIA_TYPE} column indicating that file
+             * is not an audio, image, video, document, playlist, or subtitles file.
+             */
+            public static final int MEDIA_TYPE_NONE = 0;
+
+            /**
+             * Constant for the {@link #MEDIA_TYPE} column indicating that file
+             * is an image file.
+             */
+            public static final int MEDIA_TYPE_IMAGE = 1;
+
+            /**
+             * Constant for the {@link #MEDIA_TYPE} column indicating that file
+             * is an audio file.
+             */
+            public static final int MEDIA_TYPE_AUDIO = 2;
+
+            /**
+             * Constant for the {@link #MEDIA_TYPE} column indicating that file
+             * is a video file.
+             */
+            public static final int MEDIA_TYPE_VIDEO = 3;
+
+            /**
+             * Constant for the {@link #MEDIA_TYPE} column indicating that file
+             * is a playlist file.
+             */
+            public static final int MEDIA_TYPE_PLAYLIST = 4;
+
+            /**
+             * Constant for the {@link #MEDIA_TYPE} column indicating that file
+             * is a subtitles or lyrics file.
+             */
+            public static final int MEDIA_TYPE_SUBTITLE = 5;
+
+            /**
+             * Constant for the {@link #MEDIA_TYPE} column indicating that file is a document file.
+             */
+            public static final int MEDIA_TYPE_DOCUMENT = 6;
+        }
+    }
+
+    /** @hide */
+    public static class ThumbnailConstants {
+        public static final int MINI_KIND = 1;
+        public static final int FULL_SCREEN_KIND = 2;
+        public static final int MICRO_KIND = 3;
+
+        public static final Size MINI_SIZE = new Size(512, 384);
+        public static final Size FULL_SCREEN_SIZE = new Size(1024, 786);
+        public static final Size MICRO_SIZE = new Size(96, 96);
+
+        public static @NonNull Size getKindSize(int kind) {
+            if (kind == ThumbnailConstants.MICRO_KIND) {
+                return ThumbnailConstants.MICRO_SIZE;
+            } else if (kind == ThumbnailConstants.FULL_SCREEN_KIND) {
+                return ThumbnailConstants.FULL_SCREEN_SIZE;
+            } else if (kind == ThumbnailConstants.MINI_KIND) {
+                return ThumbnailConstants.MINI_SIZE;
+            } else {
+                throw new IllegalArgumentException("Unsupported kind: " + kind);
+            }
+        }
+    }
+
+    /**
+     * Download metadata columns.
+     */
+    public interface DownloadColumns extends MediaColumns {
+        /**
+         * Uri indicating where the item has been downloaded from.
+         */
+        @Column(Cursor.FIELD_TYPE_STRING)
+        String DOWNLOAD_URI = "download_uri";
+
+        /**
+         * Uri indicating HTTP referer of {@link #DOWNLOAD_URI}.
+         */
+        @Column(Cursor.FIELD_TYPE_STRING)
+        String REFERER_URI = "referer_uri";
+
+        /**
+         * The description of the download.
+         *
+         * @removed
+         */
+        @Deprecated
+        @Column(Cursor.FIELD_TYPE_STRING)
+        String DESCRIPTION = "description";
+    }
+
+    /**
+     * Collection of downloaded items.
+     */
+    public static final class Downloads implements DownloadColumns {
+        private Downloads() {}
+
+        /**
+         * The content:// style URI for the internal storage.
+         */
+        @NonNull
+        public static final Uri INTERNAL_CONTENT_URI =
+                getContentUri("internal");
+
+        /**
+         * The content:// style URI for the "primary" external storage
+         * volume.
+         */
+        @NonNull
+        public static final Uri EXTERNAL_CONTENT_URI =
+                getContentUri("external");
+
+        /**
+         * The MIME type for this table.
+         */
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/download";
+
+        /**
+         * Get the content:// style URI for the downloads table on the
+         * given volume.
+         *
+         * @param volumeName the name of the volume to get the URI for
+         * @return the URI to the image media table on the given volume
+         */
+        public static @NonNull Uri getContentUri(@NonNull String volumeName) {
+            return AUTHORITY_URI.buildUpon().appendPath(volumeName)
+                    .appendPath("downloads").build();
+        }
+
+        /**
+         * Get the content:// style URI for a single row in the downloads table
+         * on the given volume.
+         *
+         * @param volumeName the name of the volume to get the URI for
+         * @param id the download to get the URI for
+         * @return the URI to the downloads table on the given volume
+         */
+        public static @NonNull Uri getContentUri(@NonNull String volumeName, long id) {
+            return ContentUris.withAppendedId(getContentUri(volumeName), id);
+        }
+
+        /** @hide */
+        public static @NonNull Uri getContentUriForPath(@NonNull String path) {
+            return getContentUri(getVolumeName(new File(path)));
+        }
+    }
+
+    /**
+     * @deprecated since this method doesn't have a {@link Context}, we can't
+     *             find the actual {@link StorageVolume} for the given path, so
+     *             only a vague guess is returned. Callers should use
+     *             {@link StorageManager#getStorageVolume(File)} instead.
+     * @hide
+     */
+    @Deprecated
+    public static @NonNull String getVolumeName(@NonNull File path) {
+        // Ideally we'd find the relevant StorageVolume, but we don't have a
+        // Context to obtain it from, so the best we can do is assume
+        if (path.getAbsolutePath()
+                .startsWith(Environment.getStorageDirectory().getAbsolutePath())) {
+            return MediaStore.VOLUME_EXTERNAL;
+        } else {
+            return MediaStore.VOLUME_INTERNAL;
+        }
+    }
+
+    /**
+     * This class is used internally by Images.Thumbnails and Video.Thumbnails, it's not intended
+     * to be accessed elsewhere.
+     */
+    @Deprecated
+    private static class InternalThumbnails implements BaseColumns {
+        /**
+         * Currently outstanding thumbnail requests that can be cancelled.
+         */
+        // @GuardedBy("sPending")
+        private static ArrayMap<Uri, CancellationSignal> sPending = new ArrayMap<>();
+
+        /**
+         * Make a blocking request to obtain the given thumbnail, generating it
+         * if needed.
+         *
+         * @see #cancelThumbnail(ContentResolver, Uri)
+         */
+        @Deprecated
+        static @Nullable Bitmap getThumbnail(@NonNull ContentResolver cr, @NonNull Uri uri,
+                int kind, @Nullable BitmapFactory.Options opts) {
+            final Size size = ThumbnailConstants.getKindSize(kind);
+
+            CancellationSignal signal = null;
+            synchronized (sPending) {
+                signal = sPending.get(uri);
+                if (signal == null) {
+                    signal = new CancellationSignal();
+                    sPending.put(uri, signal);
+                }
+            }
+
+            try {
+                return cr.loadThumbnail(uri, size, signal);
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to obtain thumbnail for " + uri, e);
+                return null;
+            } finally {
+                synchronized (sPending) {
+                    sPending.remove(uri);
+                }
+            }
+        }
+
+        /**
+         * This method cancels the thumbnail request so clients waiting for
+         * {@link #getThumbnail} will be interrupted and return immediately.
+         * Only the original process which made the request can cancel their own
+         * requests.
+         */
+        @Deprecated
+        static void cancelThumbnail(@NonNull ContentResolver cr, @NonNull Uri uri) {
+            synchronized (sPending) {
+                final CancellationSignal signal = sPending.get(uri);
+                if (signal != null) {
+                    signal.cancel();
+                }
+            }
+        }
+    }
+
+    /**
+     * Collection of all media with MIME type of {@code image/*}.
+     */
+    public static final class Images {
+        /**
+         * Image metadata columns.
+         */
+        public interface ImageColumns extends MediaColumns {
+            /**
+             * The picasa id of the image
+             *
+             * @deprecated this value was only relevant for images hosted on
+             *             Picasa, which are no longer supported.
+             */
+            @Deprecated
+            @Column(Cursor.FIELD_TYPE_STRING)
+            public static final String PICASA_ID = "picasa_id";
+
+            /**
+             * Whether the video should be published as public or private
+             */
+            @Column(Cursor.FIELD_TYPE_INTEGER)
+            public static final String IS_PRIVATE = "isprivate";
+
+            /**
+             * The latitude where the image was captured.
+             *
+             * @deprecated location details are no longer indexed for privacy
+             *             reasons, and this value is now always {@code null}.
+             *             You can still manually obtain location metadata using
+             *             {@link ExifInterface#getLatLong(float[])}.
+             */
+            @Deprecated
+            @Column(value = Cursor.FIELD_TYPE_FLOAT, readOnly = true)
+            public static final String LATITUDE = "latitude";
+
+            /**
+             * The longitude where the image was captured.
+             *
+             * @deprecated location details are no longer indexed for privacy
+             *             reasons, and this value is now always {@code null}.
+             *             You can still manually obtain location metadata using
+             *             {@link ExifInterface#getLatLong(float[])}.
+             */
+            @Deprecated
+            @Column(value = Cursor.FIELD_TYPE_FLOAT, readOnly = true)
+            public static final String LONGITUDE = "longitude";
+
+            /** @removed promoted to parent interface */
+            public static final String DATE_TAKEN = "datetaken";
+            /** @removed promoted to parent interface */
+            public static final String ORIENTATION = "orientation";
+
+            /**
+             * The mini thumb id.
+             *
+             * @deprecated all thumbnails should be obtained via
+             *             {@link MediaStore.Images.Thumbnails#getThumbnail}, as this
+             *             value is no longer supported.
+             */
+            @Deprecated
+            @Column(Cursor.FIELD_TYPE_INTEGER)
+            public static final String MINI_THUMB_MAGIC = "mini_thumb_magic";
+
+            /** @removed promoted to parent interface */
+            public static final String BUCKET_ID = "bucket_id";
+            /** @removed promoted to parent interface */
+            public static final String BUCKET_DISPLAY_NAME = "bucket_display_name";
+            /** @removed promoted to parent interface */
+            public static final String GROUP_ID = "group_id";
+
+            /**
+             * Indexed value of {@link ExifInterface#TAG_IMAGE_DESCRIPTION}
+             * extracted from this media item.
+             */
+            @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+            public static final String DESCRIPTION = "description";
+
+            /**
+             * Indexed value of {@link ExifInterface#TAG_EXPOSURE_TIME}
+             * extracted from this media item.
+             */
+            @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+            public static final String EXPOSURE_TIME = "exposure_time";
+
+            /**
+             * Indexed value of {@link ExifInterface#TAG_F_NUMBER}
+             * extracted from this media item.
+             */
+            @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+            public static final String F_NUMBER = "f_number";
+
+            /**
+             * Indexed value of {@link ExifInterface#TAG_ISO_SPEED_RATINGS}
+             * extracted from this media item.
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String ISO = "iso";
+
+            /**
+             * Indexed value of {@link ExifInterface#TAG_SCENE_CAPTURE_TYPE}
+             * extracted from this media item.
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String SCENE_CAPTURE_TYPE = "scene_capture_type";
+        }
+
+        public static final class Media implements ImageColumns {
+            /**
+             * @deprecated all queries should be performed through
+             *             {@link ContentResolver} directly, which offers modern
+             *             features like {@link CancellationSignal}.
+             */
+            @Deprecated
+            public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) {
+                return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER);
+            }
+
+            /**
+             * @deprecated all queries should be performed through
+             *             {@link ContentResolver} directly, which offers modern
+             *             features like {@link CancellationSignal}.
+             */
+            @Deprecated
+            public static final Cursor query(ContentResolver cr, Uri uri, String[] projection,
+                    String where, String orderBy) {
+                return cr.query(uri, projection, where,
+                                             null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
+            }
+
+            /**
+             * @deprecated all queries should be performed through
+             *             {@link ContentResolver} directly, which offers modern
+             *             features like {@link CancellationSignal}.
+             */
+            @Deprecated
+            public static final Cursor query(ContentResolver cr, Uri uri, String[] projection,
+                    String selection, String [] selectionArgs, String orderBy) {
+                return cr.query(uri, projection, selection,
+                        selectionArgs, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
+            }
+
+            /**
+             * Retrieves an image for the given url as a {@link Bitmap}.
+             *
+             * @param cr The content resolver to use
+             * @param url The url of the image
+             * @deprecated loading of images should be performed through
+             *             {@link ImageDecoder#createSource(ContentResolver, Uri)},
+             *             which offers modern features like
+             *             {@link PostProcessor}.
+             */
+            @Deprecated
+            public static final Bitmap getBitmap(ContentResolver cr, Uri url)
+                    throws FileNotFoundException, IOException {
+                InputStream input = cr.openInputStream(url);
+                Bitmap bitmap = BitmapFactory.decodeStream(input);
+                input.close();
+                return bitmap;
+            }
+
+            /**
+             * Insert an image and create a thumbnail for it.
+             *
+             * @param cr The content resolver to use
+             * @param imagePath The path to the image to insert
+             * @param name The name of the image
+             * @param description The description of the image
+             * @return The URL to the newly created image
+             * @deprecated inserting of images should be performed using
+             *             {@link MediaColumns#IS_PENDING}, which offers richer
+             *             control over lifecycle.
+             */
+            @Deprecated
+            public static final String insertImage(ContentResolver cr, String imagePath,
+                    String name, String description) throws FileNotFoundException {
+                final Bitmap source;
+                try {
+                    source = ImageDecoder
+                            .decodeBitmap(ImageDecoder.createSource(new File(imagePath)));
+                } catch (IOException e) {
+                    throw new FileNotFoundException(e.getMessage());
+                }
+                return insertImage(cr, source, name, description);
+            }
+
+            /**
+             * Insert an image and create a thumbnail for it.
+             *
+             * @param cr The content resolver to use
+             * @param source The stream to use for the image
+             * @param title The name of the image
+             * @param description The description of the image
+             * @return The URL to the newly created image, or <code>null</code> if the image failed to be stored
+             *              for any reason.
+             * @deprecated inserting of images should be performed using
+             *             {@link MediaColumns#IS_PENDING}, which offers richer
+             *             control over lifecycle.
+             */
+            @Deprecated
+            public static final String insertImage(ContentResolver cr, Bitmap source, String title,
+                    String description) {
+                if (TextUtils.isEmpty(title)) title = "Image";
+
+                final long now = System.currentTimeMillis();
+                final ContentValues values = new ContentValues();
+                values.put(MediaColumns.DISPLAY_NAME, title);
+                values.put(MediaColumns.MIME_TYPE, "image/jpeg");
+                values.put(MediaColumns.DATE_ADDED, now / 1000);
+                values.put(MediaColumns.DATE_MODIFIED, now / 1000);
+                values.put(MediaColumns.IS_PENDING, 1);
+
+                final Uri uri = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
+                try {
+                    try (OutputStream out = cr.openOutputStream(uri)) {
+                        source.compress(Bitmap.CompressFormat.JPEG, 90, out);
+                    }
+
+                    // Everything went well above, publish it!
+                    values.clear();
+                    values.put(MediaColumns.IS_PENDING, 0);
+                    cr.update(uri, values, null, null);
+                    return uri.toString();
+                } catch (Exception e) {
+                    Log.w(TAG, "Failed to insert image", e);
+                    cr.delete(uri, null, null);
+                    return null;
+                }
+            }
+
+            /**
+             * Get the content:// style URI for the image media table on the
+             * given volume.
+             *
+             * @param volumeName the name of the volume to get the URI for
+             * @return the URI to the image media table on the given volume
+             */
+            public static Uri getContentUri(String volumeName) {
+                return AUTHORITY_URI.buildUpon().appendPath(volumeName).appendPath("images")
+                        .appendPath("media").build();
+            }
+
+            /**
+             * Get the content:// style URI for a single row in the images table
+             * on the given volume.
+             *
+             * @param volumeName the name of the volume to get the URI for
+             * @param id the image to get the URI for
+             * @return the URI to the images table on the given volume
+             */
+            public static @NonNull Uri getContentUri(@NonNull String volumeName, long id) {
+                return ContentUris.withAppendedId(getContentUri(volumeName), id);
+            }
+
+            /**
+             * The content:// style URI for the internal storage.
+             */
+            public static final Uri INTERNAL_CONTENT_URI =
+                    getContentUri("internal");
+
+            /**
+             * The content:// style URI for the "primary" external storage
+             * volume.
+             */
+            public static final Uri EXTERNAL_CONTENT_URI =
+                    getContentUri("external");
+
+            /**
+             * The MIME type of this directory of
+             * images.  Note that each entry in this directory will have a standard
+             * image MIME type as appropriate -- for example, image/jpeg.
+             */
+            public static final String CONTENT_TYPE = "vnd.android.cursor.dir/image";
+
+            /**
+             * The default sort order for this table
+             */
+            public static final String DEFAULT_SORT_ORDER = ImageColumns.BUCKET_DISPLAY_NAME;
+        }
+
+        /**
+         * This class provides utility methods to obtain thumbnails for various
+         * {@link Images} items.
+         *
+         * @deprecated Callers should migrate to using
+         *             {@link ContentResolver#loadThumbnail}, since it offers
+         *             richer control over requested thumbnail sizes and
+         *             cancellation behavior.
+         */
+        @Deprecated
+        public static class Thumbnails implements BaseColumns {
+            /**
+             * @deprecated all queries should be performed through
+             *             {@link ContentResolver} directly, which offers modern
+             *             features like {@link CancellationSignal}.
+             */
+            @Deprecated
+            public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) {
+                return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER);
+            }
+
+            /**
+             * @deprecated all queries should be performed through
+             *             {@link ContentResolver} directly, which offers modern
+             *             features like {@link CancellationSignal}.
+             */
+            @Deprecated
+            public static final Cursor queryMiniThumbnails(ContentResolver cr, Uri uri, int kind,
+                    String[] projection) {
+                return cr.query(uri, projection, "kind = " + kind, null, DEFAULT_SORT_ORDER);
+            }
+
+            /**
+             * @deprecated all queries should be performed through
+             *             {@link ContentResolver} directly, which offers modern
+             *             features like {@link CancellationSignal}.
+             */
+            @Deprecated
+            public static final Cursor queryMiniThumbnail(ContentResolver cr, long origId, int kind,
+                    String[] projection) {
+                return cr.query(EXTERNAL_CONTENT_URI, projection,
+                        IMAGE_ID + " = " + origId + " AND " + KIND + " = " +
+                        kind, null, null);
+            }
+
+            /**
+             * Cancel any outstanding {@link #getThumbnail} requests, causing
+             * them to return by throwing a {@link OperationCanceledException}.
+             * <p>
+             * This method has no effect on
+             * {@link ContentResolver#loadThumbnail} calls, since they provide
+             * their own {@link CancellationSignal}.
+             *
+             * @deprecated Callers should migrate to using
+             *             {@link ContentResolver#loadThumbnail}, since it
+             *             offers richer control over requested thumbnail sizes
+             *             and cancellation behavior.
+             */
+            @Deprecated
+            public static void cancelThumbnailRequest(ContentResolver cr, long origId) {
+                final Uri uri = ContentUris.withAppendedId(
+                        Images.Media.EXTERNAL_CONTENT_URI, origId);
+                InternalThumbnails.cancelThumbnail(cr, uri);
+            }
+
+            /**
+             * Return thumbnail representing a specific image item. If a
+             * thumbnail doesn't exist, this method will block until it's
+             * generated. Callers are responsible for their own in-memory
+             * caching of returned values.
+             *
+             * As of {@link android.os.Build.VERSION_CODES#Q}, this output
+             * of the thumbnail has correct rotation, don't need to rotate
+             * it again.
+             *
+             * @param imageId the image item to obtain a thumbnail for.
+             * @param kind optimal thumbnail size desired.
+             * @return decoded thumbnail, or {@code null} if problem was
+             *         encountered.
+             * @deprecated Callers should migrate to using
+             *             {@link ContentResolver#loadThumbnail}, since it
+             *             offers richer control over requested thumbnail sizes
+             *             and cancellation behavior.
+             */
+            @Deprecated
+            public static Bitmap getThumbnail(ContentResolver cr, long imageId, int kind,
+                    BitmapFactory.Options options) {
+                final Uri uri = ContentUris.withAppendedId(
+                        Images.Media.EXTERNAL_CONTENT_URI, imageId);
+                return InternalThumbnails.getThumbnail(cr, uri, kind, options);
+            }
+
+            /**
+             * Cancel any outstanding {@link #getThumbnail} requests, causing
+             * them to return by throwing a {@link OperationCanceledException}.
+             * <p>
+             * This method has no effect on
+             * {@link ContentResolver#loadThumbnail} calls, since they provide
+             * their own {@link CancellationSignal}.
+             *
+             * @deprecated Callers should migrate to using
+             *             {@link ContentResolver#loadThumbnail}, since it
+             *             offers richer control over requested thumbnail sizes
+             *             and cancellation behavior.
+             */
+            @Deprecated
+            public static void cancelThumbnailRequest(ContentResolver cr, long origId,
+                    long groupId) {
+                cancelThumbnailRequest(cr, origId);
+            }
+
+            /**
+             * Return thumbnail representing a specific image item. If a
+             * thumbnail doesn't exist, this method will block until it's
+             * generated. Callers are responsible for their own in-memory
+             * caching of returned values.
+             *
+             * As of {@link android.os.Build.VERSION_CODES#Q}, this output
+             * of the thumbnail has correct rotation, don't need to rotate
+             * it again.
+             *
+             * @param imageId the image item to obtain a thumbnail for.
+             * @param kind optimal thumbnail size desired.
+             * @return decoded thumbnail, or {@code null} if problem was
+             *         encountered.
+             * @deprecated Callers should migrate to using
+             *             {@link ContentResolver#loadThumbnail}, since it
+             *             offers richer control over requested thumbnail sizes
+             *             and cancellation behavior.
+             */
+            @Deprecated
+            public static Bitmap getThumbnail(ContentResolver cr, long imageId, long groupId,
+                    int kind, BitmapFactory.Options options) {
+                return getThumbnail(cr, imageId, kind, options);
+            }
+
+            /**
+             * Get the content:// style URI for the image media table on the
+             * given volume.
+             *
+             * @param volumeName the name of the volume to get the URI for
+             * @return the URI to the image media table on the given volume
+             */
+            public static Uri getContentUri(String volumeName) {
+                return AUTHORITY_URI.buildUpon().appendPath(volumeName).appendPath("images")
+                        .appendPath("thumbnails").build();
+            }
+
+            /**
+             * The content:// style URI for the internal storage.
+             */
+            public static final Uri INTERNAL_CONTENT_URI =
+                    getContentUri("internal");
+
+            /**
+             * The content:// style URI for the "primary" external storage
+             * volume.
+             */
+            public static final Uri EXTERNAL_CONTENT_URI =
+                    getContentUri("external");
+
+            /**
+             * The default sort order for this table
+             */
+            public static final String DEFAULT_SORT_ORDER = "image_id ASC";
+
+            /**
+             * Path to the thumbnail file on disk.
+             * <p>
+             * Note that apps may not have filesystem permissions to directly
+             * access this path. Instead of trying to open this path directly,
+             * apps should use
+             * {@link ContentResolver#openFileDescriptor(Uri, String)} to gain
+             * access.
+             *
+             * As of {@link android.os.Build.VERSION_CODES#Q}, this thumbnail
+             * has correct rotation, don't need to rotate it again.
+             *
+             * @deprecated Apps may not have filesystem permissions to directly
+             *             access this path. Instead of trying to open this path
+             *             directly, apps should use
+             *             {@link ContentResolver#loadThumbnail}
+             *             to gain access.
+             */
+            @Deprecated
+            @Column(Cursor.FIELD_TYPE_STRING)
+            public static final String DATA = "_data";
+
+            /**
+             * The original image for the thumbnal
+             */
+            @Column(Cursor.FIELD_TYPE_INTEGER)
+            public static final String IMAGE_ID = "image_id";
+
+            /**
+             * The kind of the thumbnail
+             */
+            @Column(Cursor.FIELD_TYPE_INTEGER)
+            public static final String KIND = "kind";
+
+            public static final int MINI_KIND = ThumbnailConstants.MINI_KIND;
+            public static final int FULL_SCREEN_KIND = ThumbnailConstants.FULL_SCREEN_KIND;
+            public static final int MICRO_KIND = ThumbnailConstants.MICRO_KIND;
+
+            /**
+             * Return the typical {@link Size} (in pixels) used internally when
+             * the given thumbnail kind is requested.
+             *
+             * @deprecated Callers should migrate to using
+             *             {@link ContentResolver#loadThumbnail}, since it
+             *             offers richer control over requested thumbnail sizes
+             *             and cancellation behavior.
+             */
+            @Deprecated
+            public static @NonNull Size getKindSize(int kind) {
+                return ThumbnailConstants.getKindSize(kind);
+            }
+
+            /**
+             * The blob raw data of thumbnail
+             *
+             * @deprecated this column never existed internally, and could never
+             *             have returned valid data.
+             */
+            @Deprecated
+            @Column(Cursor.FIELD_TYPE_BLOB)
+            public static final String THUMB_DATA = "thumb_data";
+
+            /**
+             * The width of the thumbnal
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String WIDTH = "width";
+
+            /**
+             * The height of the thumbnail
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String HEIGHT = "height";
+        }
+    }
+
+    /**
+     * Collection of all media with MIME type of {@code audio/*}.
+     */
+    public static final class Audio {
+        /**
+         * Audio metadata columns.
+         */
+        public interface AudioColumns extends MediaColumns {
+
+            /**
+             * A non human readable key calculated from the TITLE, used for
+             * searching, sorting and grouping
+             *
+             * @see Audio#keyFor(String)
+             * @deprecated These keys are generated using
+             *             {@link java.util.Locale#ROOT}, which means they don't
+             *             reflect locale-specific sorting preferences. To apply
+             *             locale-specific sorting preferences, use
+             *             {@link ContentResolver#QUERY_ARG_SQL_SORT_ORDER} with
+             *             {@code COLLATE LOCALIZED}, or
+             *             {@link ContentResolver#QUERY_ARG_SORT_LOCALE}.
+             */
+            @Deprecated
+            @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+            public static final String TITLE_KEY = "title_key";
+
+            /** @removed promoted to parent interface */
+            public static final String DURATION = "duration";
+
+            /**
+             * The position within the audio item at which playback should be
+             * resumed.
+             */
+            @DurationMillisLong
+            @Column(Cursor.FIELD_TYPE_INTEGER)
+            public static final String BOOKMARK = "bookmark";
+
+            /**
+             * The id of the artist who created the audio file, if any
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String ARTIST_ID = "artist_id";
+
+            /** @removed promoted to parent interface */
+            public static final String ARTIST = "artist";
+
+            /**
+             * The artist credited for the album that contains the audio file
+             * @hide
+             */
+            @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+            public static final String ALBUM_ARTIST = "album_artist";
+
+            /**
+             * A non human readable key calculated from the ARTIST, used for
+             * searching, sorting and grouping
+             *
+             * @see Audio#keyFor(String)
+             * @deprecated These keys are generated using
+             *             {@link java.util.Locale#ROOT}, which means they don't
+             *             reflect locale-specific sorting preferences. To apply
+             *             locale-specific sorting preferences, use
+             *             {@link ContentResolver#QUERY_ARG_SQL_SORT_ORDER} with
+             *             {@code COLLATE LOCALIZED}, or
+             *             {@link ContentResolver#QUERY_ARG_SORT_LOCALE}.
+             */
+            @Deprecated
+            @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+            public static final String ARTIST_KEY = "artist_key";
+
+            /** @removed promoted to parent interface */
+            public static final String COMPOSER = "composer";
+
+            /**
+             * The id of the album the audio file is from, if any
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String ALBUM_ID = "album_id";
+
+            /** @removed promoted to parent interface */
+            public static final String ALBUM = "album";
+
+            /**
+             * A non human readable key calculated from the ALBUM, used for
+             * searching, sorting and grouping
+             *
+             * @see Audio#keyFor(String)
+             * @deprecated These keys are generated using
+             *             {@link java.util.Locale#ROOT}, which means they don't
+             *             reflect locale-specific sorting preferences. To apply
+             *             locale-specific sorting preferences, use
+             *             {@link ContentResolver#QUERY_ARG_SQL_SORT_ORDER} with
+             *             {@code COLLATE LOCALIZED}, or
+             *             {@link ContentResolver#QUERY_ARG_SORT_LOCALE}.
+             */
+            @Deprecated
+            @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+            public static final String ALBUM_KEY = "album_key";
+
+            /**
+             * The track number of this song on the album, if any.
+             * This number encodes both the track number and the
+             * disc number. For multi-disc sets, this number will
+             * be 1xxx for tracks on the first disc, 2xxx for tracks
+             * on the second disc, etc.
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String TRACK = "track";
+
+            /**
+             * The year the audio file was recorded, if any
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String YEAR = "year";
+
+            /**
+             * Non-zero if the audio file is music
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String IS_MUSIC = "is_music";
+
+            /**
+             * Non-zero if the audio file is a podcast
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String IS_PODCAST = "is_podcast";
+
+            /**
+             * Non-zero if the audio file may be a ringtone
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String IS_RINGTONE = "is_ringtone";
+
+            /**
+             * Non-zero if the audio file may be an alarm
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String IS_ALARM = "is_alarm";
+
+            /**
+             * Non-zero if the audio file may be a notification sound
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String IS_NOTIFICATION = "is_notification";
+
+            /**
+             * Non-zero if the audio file is an audiobook
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String IS_AUDIOBOOK = "is_audiobook";
+
+            /**
+             * The id of the genre the audio file is from, if any
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String GENRE_ID = "genre_id";
+
+            /**
+             * The genre of the audio file, if any.
+             */
+            @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+            public static final String GENRE = "genre";
+
+            /**
+             * A non human readable key calculated from the GENRE, used for
+             * searching, sorting and grouping
+             *
+             * @see Audio#keyFor(String)
+             * @deprecated These keys are generated using
+             *             {@link java.util.Locale#ROOT}, which means they don't
+             *             reflect locale-specific sorting preferences. To apply
+             *             locale-specific sorting preferences, use
+             *             {@link ContentResolver#QUERY_ARG_SQL_SORT_ORDER} with
+             *             {@code COLLATE LOCALIZED}, or
+             *             {@link ContentResolver#QUERY_ARG_SORT_LOCALE}.
+             */
+            @Deprecated
+            @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+            public static final String GENRE_KEY = "genre_key";
+
+            /**
+             * The resource URI of a localized title, if any.
+             * <p>
+             * Conforms to this pattern:
+             * <ul>
+             * <li>Scheme: {@link ContentResolver#SCHEME_ANDROID_RESOURCE}
+             * <li>Authority: Package Name of ringtone title provider
+             * <li>First Path Segment: Type of resource (must be "string")
+             * <li>Second Path Segment: Resource ID of title
+             * </ul>
+             */
+            @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+            public static final String TITLE_RESOURCE_URI = "title_resource_uri";
+        }
+
+        private static final Pattern PATTERN_TRIM_BEFORE = Pattern.compile(
+                "(?i)(^(the|an|a) |,\\s*(the|an|a)$|[^\\w\\s]|^\\s+|\\s+$)");
+        private static final Pattern PATTERN_TRIM_AFTER = Pattern.compile(
+                "(^(00)+|(00)+$)");
+
+        /**
+         * Converts a user-visible string into a "key" that can be used for
+         * grouping, sorting, and searching.
+         *
+         * @return Opaque token that should not be parsed or displayed to users.
+         * @deprecated These keys are generated using
+         *             {@link java.util.Locale#ROOT}, which means they don't
+         *             reflect locale-specific sorting preferences. To apply
+         *             locale-specific sorting preferences, use
+         *             {@link ContentResolver#QUERY_ARG_SQL_SORT_ORDER} with
+         *             {@code COLLATE LOCALIZED}, or
+         *             {@link ContentResolver#QUERY_ARG_SORT_LOCALE}.
+         */
+        @Deprecated
+        public static @Nullable String keyFor(@Nullable String name) {
+            if (TextUtils.isEmpty(name)) return "";
+
+            if (UNKNOWN_STRING.equals(name)) {
+                return "01";
+            }
+
+            final boolean sortFirst = name.startsWith("\001");
+
+            name = PATTERN_TRIM_BEFORE.matcher(name).replaceAll("");
+            if (TextUtils.isEmpty(name)) return "";
+
+            final Collator c = Collator.getInstance(Locale.ROOT);
+            c.setStrength(Collator.PRIMARY);
+            name = encodeToString(c.getCollationKey(name).toByteArray());
+
+            name = PATTERN_TRIM_AFTER.matcher(name).replaceAll("");
+            if (sortFirst) {
+                name = "01" + name;
+            }
+            return name;
+        }
+
+        private static String encodeToString(byte[] bytes) {
+            final StringBuilder sb = new StringBuilder();
+            for (byte b : bytes) {
+                sb.append(String.format("%02x", b));
+            }
+            return sb.toString();
+        }
+
+        public static final class Media implements AudioColumns {
+            /**
+             * Get the content:// style URI for the audio media table on the
+             * given volume.
+             *
+             * @param volumeName the name of the volume to get the URI for
+             * @return the URI to the audio media table on the given volume
+             */
+            public static Uri getContentUri(String volumeName) {
+                return AUTHORITY_URI.buildUpon().appendPath(volumeName).appendPath("audio")
+                        .appendPath("media").build();
+            }
+
+            /**
+             * Get the content:// style URI for a single row in the audio table
+             * on the given volume.
+             *
+             * @param volumeName the name of the volume to get the URI for
+             * @param id the audio to get the URI for
+             * @return the URI to the audio table on the given volume
+             */
+            public static @NonNull Uri getContentUri(@NonNull String volumeName, long id) {
+                return ContentUris.withAppendedId(getContentUri(volumeName), id);
+            }
+
+            /**
+             * Get the content:// style URI for the given audio media file.
+             *
+             * @deprecated Apps may not have filesystem permissions to directly
+             *             access this path.
+             */
+            @Deprecated
+            public static @Nullable Uri getContentUriForPath(@NonNull String path) {
+                return getContentUri(getVolumeName(new File(path)));
+            }
+
+            /**
+             * The content:// style URI for the internal storage.
+             */
+            public static final Uri INTERNAL_CONTENT_URI =
+                    getContentUri("internal");
+
+            /**
+             * The content:// style URI for the "primary" external storage
+             * volume.
+             */
+            public static final Uri EXTERNAL_CONTENT_URI =
+                    getContentUri("external");
+
+            /**
+             * The MIME type for this table.
+             */
+            public static final String CONTENT_TYPE = "vnd.android.cursor.dir/audio";
+
+            /**
+             * The MIME type for an audio track.
+             */
+            public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/audio";
+
+            /**
+             * The default sort order for this table
+             */
+            public static final String DEFAULT_SORT_ORDER = TITLE_KEY;
+
+            /**
+             * Activity Action: Start SoundRecorder application.
+             * <p>Input: nothing.
+             * <p>Output: An uri to the recorded sound stored in the Media Library
+             * if the recording was successful.
+             * May also contain the extra EXTRA_MAX_BYTES.
+             * @see #EXTRA_MAX_BYTES
+             */
+            @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+            public static final String RECORD_SOUND_ACTION =
+                    "android.provider.MediaStore.RECORD_SOUND";
+
+            /**
+             * The name of the Intent-extra used to define a maximum file size for
+             * a recording made by the SoundRecorder application.
+             *
+             * @see #RECORD_SOUND_ACTION
+             */
+             public static final String EXTRA_MAX_BYTES =
+                    "android.provider.MediaStore.extra.MAX_BYTES";
+        }
+
+        /**
+         * Audio genre metadata columns.
+         */
+        public interface GenresColumns {
+            /**
+             * The name of the genre
+             */
+            @Column(Cursor.FIELD_TYPE_STRING)
+            public static final String NAME = "name";
+        }
+
+        /**
+         * Contains all genres for audio files
+         */
+        public static final class Genres implements BaseColumns, GenresColumns {
+            /**
+             * Get the content:// style URI for the audio genres table on the
+             * given volume.
+             *
+             * @param volumeName the name of the volume to get the URI for
+             * @return the URI to the audio genres table on the given volume
+             */
+            public static Uri getContentUri(String volumeName) {
+                return AUTHORITY_URI.buildUpon().appendPath(volumeName).appendPath("audio")
+                        .appendPath("genres").build();
+            }
+
+            /**
+             * Get the content:// style URI for querying the genres of an audio file.
+             *
+             * @param volumeName the name of the volume to get the URI for
+             * @param audioId the ID of the audio file for which to retrieve the genres
+             * @return the URI to for querying the genres for the audio file
+             * with the given the volume and audioID
+             */
+            public static Uri getContentUriForAudioId(String volumeName, int audioId) {
+                return ContentUris.withAppendedId(Audio.Media.getContentUri(volumeName), audioId)
+                        .buildUpon().appendPath("genres").build();
+            }
+
+            /**
+             * The content:// style URI for the internal storage.
+             */
+            public static final Uri INTERNAL_CONTENT_URI =
+                    getContentUri("internal");
+
+            /**
+             * The content:// style URI for the "primary" external storage
+             * volume.
+             */
+            public static final Uri EXTERNAL_CONTENT_URI =
+                    getContentUri("external");
+
+            /**
+             * The MIME type for this table.
+             */
+            public static final String CONTENT_TYPE = "vnd.android.cursor.dir/genre";
+
+            /**
+             * The MIME type for entries in this table.
+             */
+            public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/genre";
+
+            /**
+             * The default sort order for this table
+             */
+            public static final String DEFAULT_SORT_ORDER = NAME;
+
+            /**
+             * Sub-directory of each genre containing all members.
+             */
+            public static final class Members implements AudioColumns {
+
+                public static final Uri getContentUri(String volumeName, long genreId) {
+                    return ContentUris
+                            .withAppendedId(Audio.Genres.getContentUri(volumeName), genreId)
+                            .buildUpon().appendPath("members").build();
+                }
+
+                /**
+                 * A subdirectory of each genre containing all member audio files.
+                 */
+                public static final String CONTENT_DIRECTORY = "members";
+
+                /**
+                 * The default sort order for this table
+                 */
+                public static final String DEFAULT_SORT_ORDER = TITLE_KEY;
+
+                /**
+                 * The ID of the audio file
+                 */
+                @Column(Cursor.FIELD_TYPE_INTEGER)
+                public static final String AUDIO_ID = "audio_id";
+
+                /**
+                 * The ID of the genre
+                 */
+                @Column(Cursor.FIELD_TYPE_INTEGER)
+                public static final String GENRE_ID = "genre_id";
+            }
+        }
+
+        /**
+         * Audio playlist metadata columns.
+         */
+        public interface PlaylistsColumns extends MediaColumns {
+            /**
+             * The name of the playlist
+             */
+            @Column(Cursor.FIELD_TYPE_STRING)
+            public static final String NAME = "name";
+
+            /**
+             * Path to the playlist file on disk.
+             * <p>
+             * Note that apps may not have filesystem permissions to directly
+             * access this path. Instead of trying to open this path directly,
+             * apps should use
+             * {@link ContentResolver#openFileDescriptor(Uri, String)} to gain
+             * access.
+             *
+             * @deprecated Apps may not have filesystem permissions to directly
+             *             access this path. Instead of trying to open this path
+             *             directly, apps should use
+             *             {@link ContentResolver#openFileDescriptor(Uri, String)}
+             *             to gain access.
+             */
+            @Deprecated
+            @Column(Cursor.FIELD_TYPE_STRING)
+            public static final String DATA = "_data";
+
+            /**
+             * The time the media item was first added.
+             */
+            @CurrentTimeSecondsLong
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String DATE_ADDED = "date_added";
+
+            /**
+             * The time the media item was last modified.
+             */
+            @CurrentTimeSecondsLong
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String DATE_MODIFIED = "date_modified";
+        }
+
+        /**
+         * Contains playlists for audio files
+         */
+        public static final class Playlists implements BaseColumns,
+                PlaylistsColumns {
+            /**
+             * Get the content:// style URI for the audio playlists table on the
+             * given volume.
+             *
+             * @param volumeName the name of the volume to get the URI for
+             * @return the URI to the audio playlists table on the given volume
+             */
+            public static Uri getContentUri(String volumeName) {
+                return AUTHORITY_URI.buildUpon().appendPath(volumeName).appendPath("audio")
+                        .appendPath("playlists").build();
+            }
+
+            /**
+             * The content:// style URI for the internal storage.
+             */
+            public static final Uri INTERNAL_CONTENT_URI =
+                    getContentUri("internal");
+
+            /**
+             * The content:// style URI for the "primary" external storage
+             * volume.
+             */
+            public static final Uri EXTERNAL_CONTENT_URI =
+                    getContentUri("external");
+
+            /**
+             * The MIME type for this table.
+             */
+            public static final String CONTENT_TYPE = "vnd.android.cursor.dir/playlist";
+
+            /**
+             * The MIME type for entries in this table.
+             */
+            public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/playlist";
+
+            /**
+             * The default sort order for this table
+             */
+            public static final String DEFAULT_SORT_ORDER = NAME;
+
+            /**
+             * Sub-directory of each playlist containing all members.
+             */
+            public static final class Members implements AudioColumns {
+                public static final Uri getContentUri(String volumeName, long playlistId) {
+                    return ContentUris
+                            .withAppendedId(Audio.Playlists.getContentUri(volumeName), playlistId)
+                            .buildUpon().appendPath("members").build();
+                }
+
+                /**
+                 * Convenience method to move a playlist item to a new location
+                 * @param res The content resolver to use
+                 * @param playlistId The numeric id of the playlist
+                 * @param from The position of the item to move
+                 * @param to The position to move the item to
+                 * @return true on success
+                 */
+                public static final boolean moveItem(ContentResolver res,
+                        long playlistId, int from, int to) {
+                    Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external",
+                            playlistId)
+                            .buildUpon()
+                            .appendEncodedPath(String.valueOf(from))
+                            .appendQueryParameter("move", "true")
+                            .build();
+                    ContentValues values = new ContentValues();
+                    values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, to);
+                    return res.update(uri, values, null, null) != 0;
+                }
+
+                /**
+                 * The ID within the playlist.
+                 */
+                @Column(Cursor.FIELD_TYPE_INTEGER)
+                public static final String _ID = "_id";
+
+                /**
+                 * A subdirectory of each playlist containing all member audio
+                 * files.
+                 */
+                public static final String CONTENT_DIRECTORY = "members";
+
+                /**
+                 * The ID of the audio file
+                 */
+                @Column(Cursor.FIELD_TYPE_INTEGER)
+                public static final String AUDIO_ID = "audio_id";
+
+                /**
+                 * The ID of the playlist
+                 */
+                @Column(Cursor.FIELD_TYPE_INTEGER)
+                public static final String PLAYLIST_ID = "playlist_id";
+
+                /**
+                 * The order of the songs in the playlist
+                 */
+                @Column(Cursor.FIELD_TYPE_INTEGER)
+                public static final String PLAY_ORDER = "play_order";
+
+                /**
+                 * The default sort order for this table
+                 */
+                public static final String DEFAULT_SORT_ORDER = PLAY_ORDER;
+            }
+        }
+
+        /**
+         * Audio artist metadata columns.
+         */
+        public interface ArtistColumns {
+            /**
+             * The artist who created the audio file, if any
+             */
+            @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+            public static final String ARTIST = "artist";
+
+            /**
+             * A non human readable key calculated from the ARTIST, used for
+             * searching, sorting and grouping
+             *
+             * @see Audio#keyFor(String)
+             * @deprecated These keys are generated using
+             *             {@link java.util.Locale#ROOT}, which means they don't
+             *             reflect locale-specific sorting preferences. To apply
+             *             locale-specific sorting preferences, use
+             *             {@link ContentResolver#QUERY_ARG_SQL_SORT_ORDER} with
+             *             {@code COLLATE LOCALIZED}, or
+             *             {@link ContentResolver#QUERY_ARG_SORT_LOCALE}.
+             */
+            @Deprecated
+            @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+            public static final String ARTIST_KEY = "artist_key";
+
+            /**
+             * The number of albums in the database for this artist
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String NUMBER_OF_ALBUMS = "number_of_albums";
+
+            /**
+             * The number of albums in the database for this artist
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String NUMBER_OF_TRACKS = "number_of_tracks";
+        }
+
+        /**
+         * Contains artists for audio files
+         */
+        public static final class Artists implements BaseColumns, ArtistColumns {
+            /**
+             * Get the content:// style URI for the artists table on the
+             * given volume.
+             *
+             * @param volumeName the name of the volume to get the URI for
+             * @return the URI to the audio artists table on the given volume
+             */
+            public static Uri getContentUri(String volumeName) {
+                return AUTHORITY_URI.buildUpon().appendPath(volumeName).appendPath("audio")
+                        .appendPath("artists").build();
+            }
+
+            /**
+             * The content:// style URI for the internal storage.
+             */
+            public static final Uri INTERNAL_CONTENT_URI =
+                    getContentUri("internal");
+
+            /**
+             * The content:// style URI for the "primary" external storage
+             * volume.
+             */
+            public static final Uri EXTERNAL_CONTENT_URI =
+                    getContentUri("external");
+
+            /**
+             * The MIME type for this table.
+             */
+            public static final String CONTENT_TYPE = "vnd.android.cursor.dir/artists";
+
+            /**
+             * The MIME type for entries in this table.
+             */
+            public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/artist";
+
+            /**
+             * The default sort order for this table
+             */
+            public static final String DEFAULT_SORT_ORDER = ARTIST_KEY;
+
+            /**
+             * Sub-directory of each artist containing all albums on which
+             * a song by the artist appears.
+             */
+            public static final class Albums implements AlbumColumns {
+                public static final Uri getContentUri(String volumeName,long artistId) {
+                    return ContentUris
+                            .withAppendedId(Audio.Artists.getContentUri(volumeName), artistId)
+                            .buildUpon().appendPath("albums").build();
+                }
+            }
+        }
+
+        /**
+         * Audio album metadata columns.
+         */
+        public interface AlbumColumns {
+
+            /**
+             * The id for the album
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String ALBUM_ID = "album_id";
+
+            /**
+             * The album on which the audio file appears, if any
+             */
+            @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+            public static final String ALBUM = "album";
+
+            /**
+             * The ID of the artist whose songs appear on this album.
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String ARTIST_ID = "artist_id";
+
+            /**
+             * The name of the artist whose songs appear on this album.
+             */
+            @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+            public static final String ARTIST = "artist";
+
+            /**
+             * A non human readable key calculated from the ARTIST, used for
+             * searching, sorting and grouping
+             *
+             * @see Audio#keyFor(String)
+             * @deprecated These keys are generated using
+             *             {@link java.util.Locale#ROOT}, which means they don't
+             *             reflect locale-specific sorting preferences. To apply
+             *             locale-specific sorting preferences, use
+             *             {@link ContentResolver#QUERY_ARG_SQL_SORT_ORDER} with
+             *             {@code COLLATE LOCALIZED}, or
+             *             {@link ContentResolver#QUERY_ARG_SORT_LOCALE}.
+             */
+            @Deprecated
+            @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+            public static final String ARTIST_KEY = "artist_key";
+
+            /**
+             * The number of songs on this album
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String NUMBER_OF_SONGS = "numsongs";
+
+            /**
+             * This column is available when getting album info via artist,
+             * and indicates the number of songs on the album by the given
+             * artist.
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String NUMBER_OF_SONGS_FOR_ARTIST = "numsongs_by_artist";
+
+            /**
+             * The year in which the earliest songs
+             * on this album were released. This will often
+             * be the same as {@link #LAST_YEAR}, but for compilation albums
+             * they might differ.
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String FIRST_YEAR = "minyear";
+
+            /**
+             * The year in which the latest songs
+             * on this album were released. This will often
+             * be the same as {@link #FIRST_YEAR}, but for compilation albums
+             * they might differ.
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String LAST_YEAR = "maxyear";
+
+            /**
+             * A non human readable key calculated from the ALBUM, used for
+             * searching, sorting and grouping
+             *
+             * @see Audio#keyFor(String)
+             * @deprecated These keys are generated using
+             *             {@link java.util.Locale#ROOT}, which means they don't
+             *             reflect locale-specific sorting preferences. To apply
+             *             locale-specific sorting preferences, use
+             *             {@link ContentResolver#QUERY_ARG_SQL_SORT_ORDER} with
+             *             {@code COLLATE LOCALIZED}, or
+             *             {@link ContentResolver#QUERY_ARG_SORT_LOCALE}.
+             */
+            @Deprecated
+            @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+            public static final String ALBUM_KEY = "album_key";
+
+            /**
+             * Cached album art.
+             *
+             * @deprecated Apps may not have filesystem permissions to directly
+             *             access this path. Instead of trying to open this path
+             *             directly, apps should use
+             *             {@link ContentResolver#loadThumbnail}
+             *             to gain access.
+             */
+            @Deprecated
+            @Column(Cursor.FIELD_TYPE_STRING)
+            public static final String ALBUM_ART = "album_art";
+        }
+
+        /**
+         * Contains artists for audio files
+         */
+        public static final class Albums implements BaseColumns, AlbumColumns {
+            /**
+             * Get the content:// style URI for the albums table on the
+             * given volume.
+             *
+             * @param volumeName the name of the volume to get the URI for
+             * @return the URI to the audio albums table on the given volume
+             */
+            public static Uri getContentUri(String volumeName) {
+                return AUTHORITY_URI.buildUpon().appendPath(volumeName).appendPath("audio")
+                        .appendPath("albums").build();
+            }
+
+            /**
+             * The content:// style URI for the internal storage.
+             */
+            public static final Uri INTERNAL_CONTENT_URI =
+                    getContentUri("internal");
+
+            /**
+             * The content:// style URI for the "primary" external storage
+             * volume.
+             */
+            public static final Uri EXTERNAL_CONTENT_URI =
+                    getContentUri("external");
+
+            /**
+             * The MIME type for this table.
+             */
+            public static final String CONTENT_TYPE = "vnd.android.cursor.dir/albums";
+
+            /**
+             * The MIME type for entries in this table.
+             */
+            public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/album";
+
+            /**
+             * The default sort order for this table
+             */
+            public static final String DEFAULT_SORT_ORDER = ALBUM_KEY;
+        }
+
+        public static final class Radio {
+            /**
+             * The MIME type for entries in this table.
+             */
+            public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/radio";
+
+            // Not instantiable.
+            private Radio() { }
+        }
+
+        /**
+         * This class provides utility methods to obtain thumbnails for various
+         * {@link Audio} items.
+         *
+         * @deprecated Callers should migrate to using
+         *             {@link ContentResolver#loadThumbnail}, since it offers
+         *             richer control over requested thumbnail sizes and
+         *             cancellation behavior.
+         * @hide
+         */
+        @Deprecated
+        public static class Thumbnails implements BaseColumns {
+            /**
+             * Path to the thumbnail file on disk.
+             * <p>
+             * Note that apps may not have filesystem permissions to directly
+             * access this path. Instead of trying to open this path directly,
+             * apps should use
+             * {@link ContentResolver#openFileDescriptor(Uri, String)} to gain
+             * access.
+             *
+             * @deprecated Apps may not have filesystem permissions to directly
+             *             access this path. Instead of trying to open this path
+             *             directly, apps should use
+             *             {@link ContentResolver#loadThumbnail}
+             *             to gain access.
+             */
+            @Deprecated
+            @Column(Cursor.FIELD_TYPE_STRING)
+            public static final String DATA = "_data";
+
+            @Column(Cursor.FIELD_TYPE_INTEGER)
+            public static final String ALBUM_ID = "album_id";
+        }
+    }
+
+    /**
+     * Collection of all media with MIME type of {@code video/*}.
+     */
+    public static final class Video {
+
+        /**
+         * The default sort order for this table.
+         */
+        public static final String DEFAULT_SORT_ORDER = MediaColumns.DISPLAY_NAME;
+
+        /**
+         * @deprecated all queries should be performed through
+         *             {@link ContentResolver} directly, which offers modern
+         *             features like {@link CancellationSignal}.
+         */
+        @Deprecated
+        public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) {
+            return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER);
+        }
+
+        /**
+         * Video metadata columns.
+         */
+        public interface VideoColumns extends MediaColumns {
+            /** @removed promoted to parent interface */
+            public static final String DURATION = "duration";
+            /** @removed promoted to parent interface */
+            public static final String ARTIST = "artist";
+            /** @removed promoted to parent interface */
+            public static final String ALBUM = "album";
+            /** @removed promoted to parent interface */
+            public static final String RESOLUTION = "resolution";
+
+            /**
+             * The description of the video recording
+             */
+            @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
+            public static final String DESCRIPTION = "description";
+
+            /**
+             * Whether the video should be published as public or private
+             */
+            @Column(Cursor.FIELD_TYPE_INTEGER)
+            public static final String IS_PRIVATE = "isprivate";
+
+            /**
+             * The user-added tags associated with a video
+             */
+            @Column(Cursor.FIELD_TYPE_STRING)
+            public static final String TAGS = "tags";
+
+            /**
+             * The YouTube category of the video
+             */
+            @Column(Cursor.FIELD_TYPE_STRING)
+            public static final String CATEGORY = "category";
+
+            /**
+             * The language of the video
+             */
+            @Column(Cursor.FIELD_TYPE_STRING)
+            public static final String LANGUAGE = "language";
+
+            /**
+             * The latitude where the video was captured.
+             *
+             * @deprecated location details are no longer indexed for privacy
+             *             reasons, and this value is now always {@code null}.
+             *             You can still manually obtain location metadata using
+             *             {@link ExifInterface#getLatLong(float[])}.
+             */
+            @Deprecated
+            @Column(value = Cursor.FIELD_TYPE_FLOAT, readOnly = true)
+            public static final String LATITUDE = "latitude";
+
+            /**
+             * The longitude where the video was captured.
+             *
+             * @deprecated location details are no longer indexed for privacy
+             *             reasons, and this value is now always {@code null}.
+             *             You can still manually obtain location metadata using
+             *             {@link ExifInterface#getLatLong(float[])}.
+             */
+            @Deprecated
+            @Column(value = Cursor.FIELD_TYPE_FLOAT, readOnly = true)
+            public static final String LONGITUDE = "longitude";
+
+            /** @removed promoted to parent interface */
+            public static final String DATE_TAKEN = "datetaken";
+
+            /**
+             * The mini thumb id.
+             *
+             * @deprecated all thumbnails should be obtained via
+             *             {@link MediaStore.Images.Thumbnails#getThumbnail}, as this
+             *             value is no longer supported.
+             */
+            @Deprecated
+            @Column(Cursor.FIELD_TYPE_INTEGER)
+            public static final String MINI_THUMB_MAGIC = "mini_thumb_magic";
+
+            /** @removed promoted to parent interface */
+            public static final String BUCKET_ID = "bucket_id";
+            /** @removed promoted to parent interface */
+            public static final String BUCKET_DISPLAY_NAME = "bucket_display_name";
+            /** @removed promoted to parent interface */
+            public static final String GROUP_ID = "group_id";
+
+            /**
+             * The position within the video item at which playback should be
+             * resumed.
+             */
+            @DurationMillisLong
+            @Column(Cursor.FIELD_TYPE_INTEGER)
+            public static final String BOOKMARK = "bookmark";
+
+            /**
+             * The color standard of this media file, if available.
+             *
+             * @see MediaFormat#COLOR_STANDARD_BT709
+             * @see MediaFormat#COLOR_STANDARD_BT601_PAL
+             * @see MediaFormat#COLOR_STANDARD_BT601_NTSC
+             * @see MediaFormat#COLOR_STANDARD_BT2020
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String COLOR_STANDARD = "color_standard";
+
+            /**
+             * The color transfer of this media file, if available.
+             *
+             * @see MediaFormat#COLOR_TRANSFER_LINEAR
+             * @see MediaFormat#COLOR_TRANSFER_SDR_VIDEO
+             * @see MediaFormat#COLOR_TRANSFER_ST2084
+             * @see MediaFormat#COLOR_TRANSFER_HLG
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String COLOR_TRANSFER = "color_transfer";
+
+            /**
+             * The color range of this media file, if available.
+             *
+             * @see MediaFormat#COLOR_RANGE_LIMITED
+             * @see MediaFormat#COLOR_RANGE_FULL
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String COLOR_RANGE = "color_range";
+        }
+
+        public static final class Media implements VideoColumns {
+            /**
+             * Get the content:// style URI for the video media table on the
+             * given volume.
+             *
+             * @param volumeName the name of the volume to get the URI for
+             * @return the URI to the video media table on the given volume
+             */
+            public static Uri getContentUri(String volumeName) {
+                return AUTHORITY_URI.buildUpon().appendPath(volumeName).appendPath("video")
+                        .appendPath("media").build();
+            }
+
+            /**
+             * Get the content:// style URI for a single row in the videos table
+             * on the given volume.
+             *
+             * @param volumeName the name of the volume to get the URI for
+             * @param id the video to get the URI for
+             * @return the URI to the videos table on the given volume
+             */
+            public static @NonNull Uri getContentUri(@NonNull String volumeName, long id) {
+                return ContentUris.withAppendedId(getContentUri(volumeName), id);
+            }
+
+            /**
+             * The content:// style URI for the internal storage.
+             */
+            public static final Uri INTERNAL_CONTENT_URI =
+                    getContentUri("internal");
+
+            /**
+             * The content:// style URI for the "primary" external storage
+             * volume.
+             */
+            public static final Uri EXTERNAL_CONTENT_URI =
+                    getContentUri("external");
+
+            /**
+             * The MIME type for this table.
+             */
+            public static final String CONTENT_TYPE = "vnd.android.cursor.dir/video";
+
+            /**
+             * The default sort order for this table
+             */
+            public static final String DEFAULT_SORT_ORDER = TITLE;
+        }
+
+        /**
+         * This class provides utility methods to obtain thumbnails for various
+         * {@link Video} items.
+         *
+         * @deprecated Callers should migrate to using
+         *             {@link ContentResolver#loadThumbnail}, since it offers
+         *             richer control over requested thumbnail sizes and
+         *             cancellation behavior.
+         */
+        @Deprecated
+        public static class Thumbnails implements BaseColumns {
+            /**
+             * Cancel any outstanding {@link #getThumbnail} requests, causing
+             * them to return by throwing a {@link OperationCanceledException}.
+             * <p>
+             * This method has no effect on
+             * {@link ContentResolver#loadThumbnail} calls, since they provide
+             * their own {@link CancellationSignal}.
+             *
+             * @deprecated Callers should migrate to using
+             *             {@link ContentResolver#loadThumbnail}, since it
+             *             offers richer control over requested thumbnail sizes
+             *             and cancellation behavior.
+             */
+            @Deprecated
+            public static void cancelThumbnailRequest(ContentResolver cr, long origId) {
+                final Uri uri = ContentUris.withAppendedId(
+                        Video.Media.EXTERNAL_CONTENT_URI, origId);
+                InternalThumbnails.cancelThumbnail(cr, uri);
+            }
+
+            /**
+             * Return thumbnail representing a specific video item. If a
+             * thumbnail doesn't exist, this method will block until it's
+             * generated. Callers are responsible for their own in-memory
+             * caching of returned values.
+             *
+             * @param videoId the video item to obtain a thumbnail for.
+             * @param kind optimal thumbnail size desired.
+             * @return decoded thumbnail, or {@code null} if problem was
+             *         encountered.
+             * @deprecated Callers should migrate to using
+             *             {@link ContentResolver#loadThumbnail}, since it
+             *             offers richer control over requested thumbnail sizes
+             *             and cancellation behavior.
+             */
+            @Deprecated
+            public static Bitmap getThumbnail(ContentResolver cr, long videoId, int kind,
+                    BitmapFactory.Options options) {
+                final Uri uri = ContentUris.withAppendedId(
+                        Video.Media.EXTERNAL_CONTENT_URI, videoId);
+                return InternalThumbnails.getThumbnail(cr, uri, kind, options);
+            }
+
+            /**
+             * Cancel any outstanding {@link #getThumbnail} requests, causing
+             * them to return by throwing a {@link OperationCanceledException}.
+             * <p>
+             * This method has no effect on
+             * {@link ContentResolver#loadThumbnail} calls, since they provide
+             * their own {@link CancellationSignal}.
+             *
+             * @deprecated Callers should migrate to using
+             *             {@link ContentResolver#loadThumbnail}, since it
+             *             offers richer control over requested thumbnail sizes
+             *             and cancellation behavior.
+             */
+            @Deprecated
+            public static void cancelThumbnailRequest(ContentResolver cr, long videoId,
+                    long groupId) {
+                cancelThumbnailRequest(cr, videoId);
+            }
+
+            /**
+             * Return thumbnail representing a specific video item. If a
+             * thumbnail doesn't exist, this method will block until it's
+             * generated. Callers are responsible for their own in-memory
+             * caching of returned values.
+             *
+             * @param videoId the video item to obtain a thumbnail for.
+             * @param kind optimal thumbnail size desired.
+             * @return decoded thumbnail, or {@code null} if problem was
+             *         encountered.
+             * @deprecated Callers should migrate to using
+             *             {@link ContentResolver#loadThumbnail}, since it
+             *             offers richer control over requested thumbnail sizes
+             *             and cancellation behavior.
+             */
+            @Deprecated
+            public static Bitmap getThumbnail(ContentResolver cr, long videoId, long groupId,
+                    int kind, BitmapFactory.Options options) {
+                return getThumbnail(cr, videoId, kind, options);
+            }
+
+            /**
+             * Get the content:// style URI for the image media table on the
+             * given volume.
+             *
+             * @param volumeName the name of the volume to get the URI for
+             * @return the URI to the image media table on the given volume
+             */
+            public static Uri getContentUri(String volumeName) {
+                return AUTHORITY_URI.buildUpon().appendPath(volumeName).appendPath("video")
+                        .appendPath("thumbnails").build();
+            }
+
+            /**
+             * The content:// style URI for the internal storage.
+             */
+            public static final Uri INTERNAL_CONTENT_URI =
+                    getContentUri("internal");
+
+            /**
+             * The content:// style URI for the "primary" external storage
+             * volume.
+             */
+            public static final Uri EXTERNAL_CONTENT_URI =
+                    getContentUri("external");
+
+            /**
+             * The default sort order for this table
+             */
+            public static final String DEFAULT_SORT_ORDER = "video_id ASC";
+
+            /**
+             * Path to the thumbnail file on disk.
+             *
+             * @deprecated Apps may not have filesystem permissions to directly
+             *             access this path. Instead of trying to open this path
+             *             directly, apps should use
+             *             {@link ContentResolver#openFileDescriptor(Uri, String)}
+             *             to gain access.
+             */
+            @Deprecated
+            @Column(Cursor.FIELD_TYPE_STRING)
+            public static final String DATA = "_data";
+
+            /**
+             * The original image for the thumbnal
+             */
+            @Column(Cursor.FIELD_TYPE_INTEGER)
+            public static final String VIDEO_ID = "video_id";
+
+            /**
+             * The kind of the thumbnail
+             */
+            @Column(Cursor.FIELD_TYPE_INTEGER)
+            public static final String KIND = "kind";
+
+            public static final int MINI_KIND = ThumbnailConstants.MINI_KIND;
+            public static final int FULL_SCREEN_KIND = ThumbnailConstants.FULL_SCREEN_KIND;
+            public static final int MICRO_KIND = ThumbnailConstants.MICRO_KIND;
+
+            /**
+             * Return the typical {@link Size} (in pixels) used internally when
+             * the given thumbnail kind is requested.
+             *
+             * @deprecated Callers should migrate to using
+             *             {@link ContentResolver#loadThumbnail}, since it
+             *             offers richer control over requested thumbnail sizes
+             *             and cancellation behavior.
+             */
+            @Deprecated
+            public static @NonNull Size getKindSize(int kind) {
+                return ThumbnailConstants.getKindSize(kind);
+            }
+
+            /**
+             * The width of the thumbnal
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String WIDTH = "width";
+
+            /**
+             * The height of the thumbnail
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+            public static final String HEIGHT = "height";
+        }
+    }
+
+    /**
+     * Return list of all specific volume names that make up
+     * {@link #VOLUME_EXTERNAL}. This includes a unique volume name for each
+     * shared storage device that is currently attached, which typically
+     * includes {@link MediaStore#VOLUME_EXTERNAL_PRIMARY}.
+     * <p>
+     * Each specific volume name can be passed to APIs like
+     * {@link MediaStore.Images.Media#getContentUri(String)} to interact with
+     * media on that storage device.
+     */
+    public static @NonNull Set<String> getExternalVolumeNames(@NonNull Context context) {
+        final StorageManager sm = context.getSystemService(StorageManager.class);
+        final Set<String> res = new ArraySet<>();
+        for (StorageVolume sv : sm.getStorageVolumes()) {
+            Log.v(TAG, "Examining volume " + sv.getId() + " with name "
+                    + sv.getMediaStoreVolumeName() + " and state " + sv.getState());
+            switch (sv.getState()) {
+                case Environment.MEDIA_MOUNTED:
+                case Environment.MEDIA_MOUNTED_READ_ONLY: {
+                    final String volumeName = sv.getMediaStoreVolumeName();
+                    if (volumeName != null) {
+                        res.add(volumeName);
+                    }
+                    break;
+                }
+            }
+        }
+        return res;
+    }
+
+    /**
+     * Return list of all recent volume names that have been part of
+     * {@link #VOLUME_EXTERNAL}.
+     * <p>
+     * These volume names are not currently mounted, but they're likely to
+     * reappear in the future, so apps are encouraged to preserve any indexed
+     * metadata related to these volumes to optimize user experiences.
+     * <p>
+     * Each specific volume name can be passed to APIs like
+     * {@link MediaStore.Images.Media#getContentUri(String)} to interact with
+     * media on that storage device.
+     */
+    public static @NonNull Set<String> getRecentExternalVolumeNames(@NonNull Context context) {
+        final StorageManager sm = context.getSystemService(StorageManager.class);
+        final Set<String> res = new ArraySet<>();
+        for (StorageVolume sv : sm.getRecentStorageVolumes()) {
+            final String volumeName = sv.getMediaStoreVolumeName();
+            if (volumeName != null) {
+                res.add(volumeName);
+            }
+        }
+        return res;
+    }
+
+    /**
+     * Return the volume name that the given {@link Uri} references.
+     */
+    public static @NonNull String getVolumeName(@NonNull Uri uri) {
+        final List<String> segments = uri.getPathSegments();
+        switch (uri.getAuthority()) {
+            case AUTHORITY:
+            case AUTHORITY_LEGACY: {
+                if (segments != null && segments.size() > 0) {
+                    return segments.get(0);
+                }
+            }
+        }
+        throw new IllegalArgumentException("Missing volume name: " + uri);
+    }
+
+    /** {@hide} */
+    public static @NonNull String checkArgumentVolumeName(@NonNull String volumeName) {
+        if (TextUtils.isEmpty(volumeName)) {
+            throw new IllegalArgumentException();
+        }
+
+        if (VOLUME_INTERNAL.equals(volumeName)) {
+            return volumeName;
+        } else if (VOLUME_EXTERNAL.equals(volumeName)) {
+            return volumeName;
+        } else if (VOLUME_EXTERNAL_PRIMARY.equals(volumeName)) {
+            return volumeName;
+        } else if (VOLUME_DEMO.equals(volumeName)) {
+            return volumeName;
+        }
+
+        // When not one of the well-known values above, it must be a hex UUID
+        for (int i = 0; i < volumeName.length(); i++) {
+            final char c = volumeName.charAt(i);
+            if (('a' <= c && c <= 'f') || ('0' <= c && c <= '9') || (c == '-')) {
+                continue;
+            } else {
+                throw new IllegalArgumentException("Invalid volume name: " + volumeName);
+            }
+        }
+        return volumeName;
+    }
+
+    /**
+     * Uri for querying the state of the media scanner.
+     */
+    public static Uri getMediaScannerUri() {
+        return AUTHORITY_URI.buildUpon().appendPath("none").appendPath("media_scanner").build();
+    }
+
+    /**
+     * Name of current volume being scanned by the media scanner.
+     */
+    public static final String MEDIA_SCANNER_VOLUME = "volume";
+
+    /**
+     * Name of the file signaling the media scanner to ignore media in the containing directory
+     * and its subdirectories. Developers should use this to avoid application graphics showing
+     * up in the Gallery and likewise prevent application sounds and music from showing up in
+     * the Music app.
+     */
+    public static final String MEDIA_IGNORE_FILENAME = ".nomedia";
+
+    /**
+     * Return an opaque version string describing the {@link MediaStore} state.
+     * <p>
+     * Applications that import data from {@link MediaStore} into their own
+     * caches can use this to detect that {@link MediaStore} has undergone
+     * substantial changes, and that data should be rescanned.
+     * <p>
+     * No other assumptions should be made about the meaning of the version.
+     * <p>
+     * This method returns the version for
+     * {@link MediaStore#VOLUME_EXTERNAL_PRIMARY}; to obtain a version for a
+     * different volume, use {@link #getVersion(Context, String)}.
+     */
+    public static @NonNull String getVersion(@NonNull Context context) {
+        return getVersion(context, VOLUME_EXTERNAL_PRIMARY);
+    }
+
+    /**
+     * Return an opaque version string describing the {@link MediaStore} state.
+     * <p>
+     * Applications that import data from {@link MediaStore} into their own
+     * caches can use this to detect that {@link MediaStore} has undergone
+     * substantial changes, and that data should be rescanned.
+     * <p>
+     * No other assumptions should be made about the meaning of the version.
+     *
+     * @param volumeName specific volume to obtain an opaque version string for.
+     *            Must be one of the values returned from
+     *            {@link #getExternalVolumeNames(Context)}.
+     */
+    public static @NonNull String getVersion(@NonNull Context context, @NonNull String volumeName) {
+        final ContentResolver resolver = context.getContentResolver();
+        try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) {
+            final Bundle in = new Bundle();
+            in.putString(Intent.EXTRA_TEXT, volumeName);
+            final Bundle out = client.call(GET_VERSION_CALL, null, in);
+            return out.getString(Intent.EXTRA_TEXT);
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Return the latest generation value for the given volume.
+     * <p>
+     * Generation numbers are useful for apps that are attempting to quickly
+     * identify exactly which media items have been added or changed since a
+     * previous point in time. Generation numbers are monotonically increasing
+     * over time, and can be safely arithmetically compared.
+     * <p>
+     * Detecting media changes using generation numbers is more robust than
+     * using {@link MediaColumns#DATE_ADDED} or
+     * {@link MediaColumns#DATE_MODIFIED}, since those values may change in
+     * unexpected ways when apps use {@link File#setLastModified(long)} or when
+     * the system clock is set incorrectly.
+     * <p>
+     * Note that before comparing these detailed generation values, you should
+     * first confirm that the overall version hasn't changed by checking
+     * {@link MediaStore#getVersion(Context, String)}, since that indicates when
+     * a more radical change has occurred. If the overall version changes, you
+     * should assume that generation numbers have been reset and perform a full
+     * synchronization pass.
+     *
+     * @param volumeName specific volume to obtain an generation value for. Must
+     *            be one of the values returned from
+     *            {@link #getExternalVolumeNames(Context)}.
+     * @see MediaColumns#GENERATION_ADDED
+     * @see MediaColumns#GENERATION_MODIFIED
+     */
+    public static long getGeneration(@NonNull Context context, @NonNull String volumeName) {
+        return getGeneration(context.getContentResolver(), volumeName);
+    }
+
+    /** {@hide} */
+    public static long getGeneration(@NonNull ContentResolver resolver,
+            @NonNull String volumeName) {
+        final Bundle in = new Bundle();
+        in.putString(Intent.EXTRA_TEXT, volumeName);
+        final Bundle out = resolver.call(AUTHORITY, GET_GENERATION_CALL, null, in);
+        return out.getLong(Intent.EXTRA_INDEX);
+    }
+
+    /**
+     * Return a {@link DocumentsProvider} Uri that is an equivalent to the given
+     * {@link MediaStore} Uri.
+     * <p>
+     * This allows apps with Storage Access Framework permissions to convert
+     * between {@link MediaStore} and {@link DocumentsProvider} Uris that refer
+     * to the same underlying item. Note that this method doesn't grant any new
+     * permissions; callers must already hold permissions obtained with
+     * {@link Intent#ACTION_OPEN_DOCUMENT} or related APIs.
+     *
+     * @param mediaUri The {@link MediaStore} Uri to convert.
+     * @return An equivalent {@link DocumentsProvider} Uri. Returns {@code null}
+     *         if no equivalent was found.
+     * @see #getMediaUri(Context, Uri)
+     */
+    public static @Nullable Uri getDocumentUri(@NonNull Context context, @NonNull Uri mediaUri) {
+        final ContentResolver resolver = context.getContentResolver();
+        final List<UriPermission> uriPermissions = resolver.getPersistedUriPermissions();
+
+        try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) {
+            final Bundle in = new Bundle();
+            in.putParcelable(EXTRA_URI, mediaUri);
+            in.putParcelableArrayList(EXTRA_URI_PERMISSIONS, new ArrayList<>(uriPermissions));
+            final Bundle out = client.call(GET_DOCUMENT_URI_CALL, null, in);
+            return out.getParcelable(EXTRA_URI);
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Return a {@link MediaStore} Uri that is an equivalent to the given
+     * {@link DocumentsProvider} Uri.
+     * <p>
+     * This allows apps with Storage Access Framework permissions to convert
+     * between {@link MediaStore} and {@link DocumentsProvider} Uris that refer
+     * to the same underlying item. Note that this method doesn't grant any new
+     * permissions; callers must already hold permissions obtained with
+     * {@link Intent#ACTION_OPEN_DOCUMENT} or related APIs.
+     *
+     * @param documentUri The {@link DocumentsProvider} Uri to convert.
+     * @return An equivalent {@link MediaStore} Uri. Returns {@code null} if no
+     *         equivalent was found.
+     * @see #getDocumentUri(Context, Uri)
+     */
+    public static @Nullable Uri getMediaUri(@NonNull Context context, @NonNull Uri documentUri) {
+        final ContentResolver resolver = context.getContentResolver();
+        final List<UriPermission> uriPermissions = resolver.getPersistedUriPermissions();
+
+        try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) {
+            final Bundle in = new Bundle();
+            in.putParcelable(EXTRA_URI, documentUri);
+            in.putParcelableArrayList(EXTRA_URI_PERMISSIONS, new ArrayList<>(uriPermissions));
+            final Bundle out = client.call(GET_MEDIA_URI_CALL, null, in);
+            return out.getParcelable(EXTRA_URI);
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    /** {@hide} */
+    public static void resolvePlaylistMembers(@NonNull ContentResolver resolver,
+            @NonNull Uri playlistUri) {
+        final Bundle in = new Bundle();
+        in.putParcelable(EXTRA_URI, playlistUri);
+        resolver.call(AUTHORITY, RESOLVE_PLAYLIST_MEMBERS_CALL, null, in);
+    }
+
+    /** {@hide} */
+    public static void runIdleMaintenance(@NonNull ContentResolver resolver) {
+        resolver.call(AUTHORITY, RUN_IDLE_MAINTENANCE_CALL, null, null);
+    }
+
+    /**
+     * Block until any pending operations have finished, such as
+     * {@link #scanFile} or {@link #scanVolume} requests.
+     *
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    @WorkerThread
+    public static void waitForIdle(@NonNull ContentResolver resolver) {
+        resolver.call(AUTHORITY, WAIT_FOR_IDLE_CALL, null, null);
+    }
+
+    /**
+     * Perform a blocking scan of the given {@link File}, returning the
+     * {@link Uri} of the scanned file.
+     *
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    @WorkerThread
+    @SuppressLint("StreamFiles")
+    public static @NonNull Uri scanFile(@NonNull ContentResolver resolver, @NonNull File file) {
+        final Bundle out = resolver.call(AUTHORITY, SCAN_FILE_CALL, file.getAbsolutePath(), null);
+        return out.getParcelable(Intent.EXTRA_STREAM);
+    }
+
+    /**
+     * Perform a blocking scan of the given storage volume.
+     *
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    @WorkerThread
+    public static void scanVolume(@NonNull ContentResolver resolver, @NonNull String volumeName) {
+        resolver.call(AUTHORITY, SCAN_VOLUME_CALL, volumeName, null);
+    }
+}
diff --git a/apex/testing/Android.bp b/apex/testing/Android.bp
new file mode 100644
index 0000000..2f6cc4f
--- /dev/null
+++ b/apex/testing/Android.bp
@@ -0,0 +1,11 @@
+apex_test {
+    name: "test_com.android.mediaprovider",
+    visibility: [
+        "//system/apex/tests",
+    ],
+    defaults: ["com.android.mediaprovider-defaults"],
+    manifest: "test_manifest.json",
+    // Test APEX, should never be installed
+    installable: false,
+    apps: ["MediaProvider"],
+}
diff --git a/apex/testing/test_manifest.json b/apex/testing/test_manifest.json
new file mode 100644
index 0000000..8ec7fba
--- /dev/null
+++ b/apex/testing/test_manifest.json
@@ -0,0 +1,4 @@
+{
+  "name": "com.android.mediaprovider",
+  "version": 2147483647
+}
diff --git a/deploy.sh b/deploy.sh
new file mode 100755
index 0000000..e6edcce
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,15 @@
+set -e
+
+# Build both our APK and APEX combined together
+./build/soong/soong_ui.bash --make-mode -j64 MediaProviderLegacy com.google.android.mediaprovider
+
+# Push our updated APEX to device, then force apexd to remount it
+adb shell stop
+adb remount
+adb sync
+adb shell umount /apex/com.android.mediaprovider*
+adb shell setprop apexd.status '""'
+adb shell setprop ctl.restart apexd
+adb shell rm -rf /system/priv-app/MediaProvider
+adb shell rm -rf /system/priv-app/MediaProviderGoogle
+adb shell start
diff --git a/errorprone/Android.bp b/errorprone/Android.bp
new file mode 100644
index 0000000..317c081
--- /dev/null
+++ b/errorprone/Android.bp
@@ -0,0 +1,25 @@
+
+java_plugin {
+    name: "error_prone_mediaprovider",
+
+    static_libs: [
+        "error_prone_mediaprovider_lib",
+    ],
+}
+
+java_library_host {
+    name: "error_prone_mediaprovider_lib",
+
+    srcs: ["src/**/*.java"],
+
+    static_libs: [
+        "//external/error_prone:error_prone_core",
+        "//external/dagger2:dagger2-auto-service",
+    ],
+
+    plugins: [
+        "//external/dagger2:dagger2-auto-service",
+    ],
+
+    javacflags: ["-verbose"],
+}
diff --git a/errorprone/src/com/android/providers/media/LocaleRootChecker.java b/errorprone/src/com/android/providers/media/LocaleRootChecker.java
new file mode 100644
index 0000000..95794d8
--- /dev/null
+++ b/errorprone/src/com/android/providers/media/LocaleRootChecker.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media;
+
+import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
+
+import com.google.auto.service.AutoService;
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker;
+import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
+import com.google.errorprone.matchers.ChildMultiMatcher;
+import com.google.errorprone.matchers.Description;
+import com.google.errorprone.matchers.FieldMatchers;
+import com.google.errorprone.matchers.Matcher;
+import com.google.errorprone.matchers.Matchers;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.MethodInvocationTree;
+
+@AutoService(BugChecker.class)
+@BugPattern(
+    name = "MediaProviderLocaleRoot",
+    summary = "Verifies that all case-altering methods use Locale.ROOT",
+    severity = WARNING)
+public final class LocaleRootChecker extends BugChecker implements MethodInvocationTreeMatcher {
+    private static final Matcher<ExpressionTree> STRING_TO_UPPER_CASE =
+            Matchers.instanceMethod().onExactClass("java.lang.String").named("toUpperCase");
+    private static final Matcher<ExpressionTree> STRING_TO_LOWER_CASE =
+            Matchers.instanceMethod().onExactClass("java.lang.String").named("toLowerCase");
+    private static final Matcher<ExpressionTree> LOCALE_ROOT =
+            FieldMatchers.staticField("java.util.Locale", "ROOT");
+
+    private static final Matcher<ExpressionTree> MISSING_LOCALE_ROOT = Matchers.anyOf(
+            Matchers.methodInvocation(
+                    STRING_TO_UPPER_CASE,
+                    ChildMultiMatcher.MatchType.ALL,
+                    Matchers.anyOf(Matchers.nothing(), Matchers.not(LOCALE_ROOT))),
+            Matchers.methodInvocation(
+                    STRING_TO_LOWER_CASE,
+                    ChildMultiMatcher.MatchType.ALL,
+                    Matchers.anyOf(Matchers.nothing(), Matchers.not(LOCALE_ROOT)))
+            );
+
+    @Override
+    public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+        if (MISSING_LOCALE_ROOT.matches(tree, state)) {
+            return buildDescription(tree)
+                    .setMessage("All case-altering methods must declare Locale.ROOT")
+                    .build();
+        }
+        return Description.NO_MATCH;
+    }
+}
diff --git a/errorprone/src/com/android/providers/media/MimeTypeChecker.java b/errorprone/src/com/android/providers/media/MimeTypeChecker.java
new file mode 100644
index 0000000..2b7cb3d
--- /dev/null
+++ b/errorprone/src/com/android/providers/media/MimeTypeChecker.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media;
+
+import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
+
+import com.google.auto.service.AutoService;
+import com.google.errorprone.BugPattern;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.bugpatterns.BugChecker;
+import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
+import com.google.errorprone.bugpatterns.BugChecker.SwitchTreeMatcher;
+import com.google.errorprone.matchers.Description;
+import com.google.errorprone.matchers.Matcher;
+import com.google.errorprone.matchers.Matchers;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.source.tree.SwitchTree;
+
+import java.util.function.Predicate;
+
+@AutoService(BugChecker.class)
+@BugPattern(
+    name = "MediaProviderMimeType",
+    summary = "Verifies that all MIME type operations are case-insensitive",
+    severity = WARNING)
+public final class MimeTypeChecker extends BugChecker
+        implements MethodInvocationTreeMatcher, SwitchTreeMatcher {
+    private static final String MESSAGE = "MIME type comparisons must be case-insensitive";
+
+    private static final Matcher<ExpressionTree> STRING_EQUALS =
+            Matchers.instanceMethod().onExactClass("java.lang.String").named("equals");
+    private static final Matcher<ExpressionTree> STRING_STARTS_WITH =
+            Matchers.instanceMethod().onExactClass("java.lang.String").named("startsWith");
+    private static final Matcher<ExpressionTree> STRING_REGION_MATCHES =
+            Matchers.instanceMethod().onExactClass("java.lang.String").named("regionMatches");
+    private static final Matcher<ExpressionTree> OBJECTS_EQUALS =
+            Matchers.staticMethod().onClass("java.util.Objects").named("equals");
+
+    @Override
+    public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
+        if (STRING_EQUALS.matches(tree, state) || STRING_STARTS_WITH.matches(tree, state)
+                || OBJECTS_EQUALS.matches(tree, state)) {
+            if (MIME_WITHOUT_CASE_FOLDING.matches(tree.getMethodSelect(), state)
+                    || tree.getArguments().stream().anyMatch(MIME_WITHOUT_CASE_FOLDING)) {
+                return buildDescription(tree).setMessage(MESSAGE).build();
+            }
+        }
+        if (STRING_REGION_MATCHES.matches(tree, state)) {
+            if (!Matchers.booleanLiteral(true).matches(tree.getArguments().get(0), state)) {
+                return buildDescription(tree).setMessage(MESSAGE).build();
+            }
+        }
+        return Description.NO_MATCH;
+    }
+
+    @Override
+    public Description matchSwitch(SwitchTree tree, VisitorState state) {
+        if (MIME_WITHOUT_CASE_FOLDING.matches(tree.getExpression(), state)) {
+            return buildDescription(tree).setMessage(MESSAGE).build();
+        }
+        return Description.NO_MATCH;
+    }
+
+    private static final MimeWithoutCaseFoldingMatcher MIME_WITHOUT_CASE_FOLDING =
+            new MimeWithoutCaseFoldingMatcher();
+
+    private static class MimeWithoutCaseFoldingMatcher
+            implements Matcher<ExpressionTree>, Predicate<ExpressionTree> {
+        @Override
+        public boolean matches(ExpressionTree tree, VisitorState state) {
+            // This is a pretty rough way to match raw names, but it works
+            final String string = tree.toString();
+            return string.toLowerCase().contains("mime") && !string.contains("toUpperCase")
+                    && !string.contains("toLowerCase");
+        }
+
+        @Override
+        public boolean test(ExpressionTree tree) {
+            return matches(tree, null);
+        }
+    }
+}
diff --git a/errorprone/src/com/google/errorprone/matchers/FieldMatchers.java b/errorprone/src/com/google/errorprone/matchers/FieldMatchers.java
new file mode 100644
index 0000000..46f0fb2
--- /dev/null
+++ b/errorprone/src/com/google/errorprone/matchers/FieldMatchers.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2018 The Error Prone Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.errorprone.matchers;
+
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.util.ASTHelpers;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.ImportTree;
+import com.sun.tools.javac.code.Symbol;
+import com.sun.tools.javac.code.Symbol.ClassSymbol;
+import javax.annotation.Nullable;
+
+// TODO(glorioso): this likely wants to be a fluent interface like MethodMatchers.
+// Ex: [staticField()|instanceField()]
+//         .[onClass(String)|onAnyClass|onClassMatching]
+//         .[named(String)|withAnyName|withNameMatching]
+/** Static utility methods for creating {@link Matcher}s for detecting references to fields. */
+public final class FieldMatchers {
+  private FieldMatchers() {}
+
+  public static Matcher<ExpressionTree> anyFieldInClass(String className) {
+    return new FieldReferenceMatcher() {
+      @Override
+      boolean classIsAppropriate(ClassSymbol classSymbol) {
+        return classSymbol.getQualifiedName().contentEquals(className);
+      }
+
+      @Override
+      boolean fieldSymbolIsAppropriate(Symbol symbol) {
+        return true;
+      }
+    };
+  }
+
+  public static Matcher<ExpressionTree> staticField(String className, String fieldName) {
+    return new FieldReferenceMatcher() {
+      @Override
+      boolean classIsAppropriate(ClassSymbol classSymbol) {
+        return classSymbol.getQualifiedName().contentEquals(className);
+      }
+
+      @Override
+      boolean fieldSymbolIsAppropriate(Symbol symbol) {
+        return symbol.isStatic() && symbol.getSimpleName().contentEquals(fieldName);
+      }
+    };
+  }
+
+  public static Matcher<ExpressionTree> instanceField(String className, String fieldName) {
+    return new FieldReferenceMatcher() {
+      @Override
+      boolean classIsAppropriate(ClassSymbol classSymbol) {
+        return classSymbol.getQualifiedName().contentEquals(className);
+      }
+
+      @Override
+      boolean fieldSymbolIsAppropriate(Symbol symbol) {
+        return !symbol.isStatic() && symbol.getSimpleName().contentEquals(fieldName);
+      }
+    };
+  }
+
+  private abstract static class FieldReferenceMatcher implements Matcher<ExpressionTree> {
+    @Override
+    public boolean matches(ExpressionTree expressionTree, VisitorState state) {
+      return isSymbolFieldInAppropriateClass(ASTHelpers.getSymbol(expressionTree))
+          // Don't match if this is part of a static import tree, since they will get the finding
+          // on any usage of the field in their source.
+          && ASTHelpers.findEnclosingNode(state.getPath(), ImportTree.class) == null;
+    }
+
+    private boolean isSymbolFieldInAppropriateClass(@Nullable Symbol symbol) {
+      if (symbol == null) {
+        return false;
+      }
+      return symbol.getKind().isField()
+          && fieldSymbolIsAppropriate(symbol)
+          && classIsAppropriate(symbol.owner.enclClass());
+    }
+
+    abstract boolean fieldSymbolIsAppropriate(Symbol symbol);
+
+    abstract boolean classIsAppropriate(ClassSymbol classSymbol);
+  }
+}
diff --git a/gen_strings.py b/gen_strings.py
new file mode 100755
index 0000000..99bba8a
--- /dev/null
+++ b/gen_strings.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Helper script to generate tedious strings.xml permutations
+"""
+
+from string import Template
+
+verbs = ["write","trash","untrash","delete"]
+datas = [("audio","audio file"),("video","video"),("image","photo"),("generic","item")]
+
+print '''
+<!-- ========================= BEGIN AUTO-GENERATED BY gen_strings.py ========================= -->'''
+
+for verb in verbs:
+    verblabel = verb
+    if verb == "write":
+        verblabel = "modify"
+
+    verblabelcaps = verblabel[0].upper() + verblabel[1:]
+    if verb == "trash":
+        verblabelcaps = "Move to trash"
+    if verb == "untrash":
+        verblabelcaps = "Move out of trash"
+
+    print '''
+<!-- ========================= %s STRINGS ========================= -->
+''' % (verb.upper())
+    for data, datalabel in datas:
+        if verb == "trash":
+            print Template('''
+<!-- Dialog title asking if user will allow $verb permission to the $data item displayed below this string. [CHAR LIMIT=128] -->
+<plurals name="permission_${verb}_${data}">
+    <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move this $datalabel to trash?</item>
+    <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move <xliff:g id="count" example="42">^2</xliff:g> ${datalabel}s to trash?</item>
+</plurals>
+''').substitute(vars()).strip("\n")
+
+        elif verb == "untrash":
+            print Template('''
+<!-- Dialog title asking if user will allow $verb permission to the $data item displayed below this string. [CHAR LIMIT=128] -->
+<plurals name="permission_${verb}_${data}">
+    <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move this $datalabel out of trash?</item>
+    <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to move <xliff:g id="count" example="42">^2</xliff:g> ${datalabel}s out of trash?</item>
+</plurals>
+''').substitute(vars()).strip("\n")
+
+        else:
+            print Template('''
+<!-- Dialog title asking if user will allow $verb permission to the $data item displayed below this string. [CHAR LIMIT=128] -->
+<plurals name="permission_${verb}_${data}">
+    <item quantity="one">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to $verblabel this $datalabel?</item>
+    <item quantity="other">Allow <xliff:g id="app_name" example="Gmail">^1</xliff:g> to $verblabel <xliff:g id="count" example="42">^2</xliff:g> ${datalabel}s?</item>
+</plurals>
+''').substitute(vars()).strip("\n")
+
+print '''
+<!-- ========================= END AUTO-GENERATED BY gen_strings.py ========================= -->
+'''
diff --git a/jni/Android.bp b/jni/Android.bp
new file mode 100644
index 0000000..280ddcd
--- /dev/null
+++ b/jni/Android.bp
@@ -0,0 +1,167 @@
+//
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+cc_library_shared {
+    name: "libfuse_jni",
+
+    srcs: [
+        "jni_init.cpp",
+        "com_android_providers_media_FuseDaemon.cpp",
+        "FuseDaemon.cpp",
+        "FuseUtils.cpp",
+        "MediaProviderWrapper.cpp",
+        "ReaddirHelper.cpp",
+        "RedactionInfo.cpp",
+        "node.cpp"
+    ],
+
+    header_libs: [
+        "libnativehelper_header_only",
+    ],
+
+    export_include_dirs: ["include"],
+
+    shared_libs: [
+        "liblog",
+        "libfuse",
+        "libandroid"
+    ],
+
+    static_libs: [
+        "libbase_ndk",
+    ],
+
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+        "-Wno-unused-variable",
+        "-Wthread-safety",
+
+        "-D_FILE_OFFSET_BITS=64",
+        "-DFUSE_USE_VERSION=34",
+    ],
+
+    tidy: true,
+    tidy_checks: [
+        "-google-runtime-int",
+    ],
+
+    sdk_version: "current",
+    stl: "c++_static",
+}
+
+cc_test {
+    name: "fuse_node_test",
+    test_suites: ["device-tests", "mts"],
+    test_config: "fuse_node_test.xml",
+
+    compile_multilib: "both",
+    multilib: {
+        lib32: { suffix: "32", },
+        lib64: { suffix: "64", },
+    },
+
+    srcs: [
+        "node_test.cpp",
+        "node.cpp",
+        "ReaddirHelper.cpp",
+        "RedactionInfo.cpp",
+    ],
+
+    local_include_dirs: ["include"],
+
+    static_libs: [
+        "libbase_ndk",
+    ],
+
+    shared_libs: [
+        "liblog",
+    ],
+
+    tidy: true,
+
+    sdk_version: "current",
+    stl: "c++_static",
+}
+
+cc_test {
+    name: "RedactionInfoTest",
+    test_suites: ["device-tests", "mts"],
+    test_config: "RedactionInfoTest.xml",
+
+    compile_multilib: "both",
+    multilib: {
+        lib32: { suffix: "32", },
+        lib64: { suffix: "64", },
+    },
+
+    srcs: [
+        "RedactionInfoTest.cpp",
+        "RedactionInfo.cpp",
+    ],
+
+    local_include_dirs: ["include"],
+
+    static_libs: [
+        "libbase_ndk",
+    ],
+
+    shared_libs: [
+        "liblog",
+    ],
+
+    tidy: true,
+
+    sdk_version: "current",
+    stl: "c++_static",
+}
+
+cc_test {
+    name: "FuseUtilsTest",
+    test_suites: ["device-tests", "mts"],
+    test_config: "FuseUtilsTest.xml",
+
+    compile_multilib: "both",
+    multilib: {
+        lib32: { suffix: "32", },
+        lib64: { suffix: "64", },
+    },
+
+    srcs: [
+        "FuseUtilsTest.cpp",
+        "FuseUtils.cpp",
+    ],
+
+    header_libs: [
+        "libnativehelper_header_only",
+    ],
+
+    local_include_dirs: ["include"],
+
+    static_libs: [
+        "libbase_ndk",
+    ],
+
+    shared_libs: [
+        "liblog",
+    ],
+
+    tidy: true,
+
+    sdk_version: "current",
+    stl: "c++_static",
+}
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
new file mode 100644
index 0000000..c651d5e
--- /dev/null
+++ b/jni/FuseDaemon.cpp
@@ -0,0 +1,1827 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#define ATRACE_TAG ATRACE_TAG_APP
+#define LOG_TAG "FuseDaemon"
+#define LIBFUSE_LOG_TAG "libfuse"
+
+#include "FuseDaemon.h"
+
+#include <android-base/logging.h>
+#include <android-base/properties.h>
+#include <android/log.h>
+#include <android/trace.h>
+#include <ctype.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <fuse_i.h>
+#include <fuse_log.h>
+#include <fuse_lowlevel.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <linux/fuse.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/inotify.h>
+#include <sys/mman.h>
+#include <sys/mount.h>
+#include <sys/param.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/statvfs.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/uio.h>
+#include <unistd.h>
+
+#include <iostream>
+#include <list>
+#include <map>
+#include <mutex>
+#include <queue>
+#include <regex>
+#include <thread>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+#include "MediaProviderWrapper.h"
+#include "libfuse_jni/FuseUtils.h"
+#include "libfuse_jni/ReaddirHelper.h"
+#include "libfuse_jni/RedactionInfo.h"
+#include "node-inl.h"
+
+using mediaprovider::fuse::DirectoryEntry;
+using mediaprovider::fuse::dirhandle;
+using mediaprovider::fuse::handle;
+using mediaprovider::fuse::node;
+using mediaprovider::fuse::RedactionInfo;
+using std::list;
+using std::string;
+using std::vector;
+
+// logging macros to avoid duplication.
+#define TRACE_NODE(__node, __req)                                                  \
+    LOG(VERBOSE) << __FUNCTION__ << " : " << #__node << " = [" << get_name(__node) \
+                 << "] (uid=" << __req->ctx.uid << ") "
+
+#define ATRACE_NAME(name) ScopedTrace ___tracer(name)
+#define ATRACE_CALL() ATRACE_NAME(__FUNCTION__)
+
+class ScopedTrace {
+  public:
+    explicit inline ScopedTrace(const char *name) {
+      ATrace_beginSection(name);
+    }
+
+    inline ~ScopedTrace() {
+      ATrace_endSection();
+    }
+};
+
+const bool IS_OS_DEBUGABLE = android::base::GetIntProperty("ro.debuggable", 0);
+
+#define FUSE_UNKNOWN_INO 0xffffffff
+
+// Stolen from: android_filesystem_config.h
+#define AID_APP_START 10000
+
+constexpr size_t MAX_READ_SIZE = 128 * 1024;
+// Stolen from: UserHandle#getUserId
+constexpr int PER_USER_RANGE = 100000;
+
+// Regex copied from FileUtils.java in MediaProvider, but without media directory.
+const std::regex PATTERN_OWNED_PATH(
+    "^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb|sandbox)/([^/]+)(/?.*)?",
+    std::regex_constants::icase);
+
+/*
+ * In order to avoid double caching with fuse, call fadvise on the file handles
+ * in the underlying file system. However, if this is done on every read/write,
+ * the fadvises cause a very significant slowdown in tests (specifically fio
+ * seq_write). So call fadvise on the file handles with the most reads/writes
+ * only after a threshold is passed.
+ */
+class FAdviser {
+  public:
+    FAdviser() : thread_(MessageLoop, this), total_size_(0) {}
+
+    ~FAdviser() {
+        SendMessage(Message::quit);
+        thread_.join();
+    }
+
+    void Record(int fd, size_t size) { SendMessage(Message::record, fd, size); }
+
+    void Close(int fd) { SendMessage(Message::close, fd); }
+
+  private:
+    struct Message {
+        enum Type { record, close, quit };
+        Type type;
+        int fd;
+        size_t size;
+    };
+
+    void RecordImpl(int fd, size_t size) {
+        total_size_ += size;
+
+        // Find or create record in files_
+        // Remove record from sizes_ if it exists, adjusting size appropriately
+        auto file = files_.find(fd);
+        if (file != files_.end()) {
+            auto old_size = file->second;
+            size += old_size->first;
+            sizes_.erase(old_size);
+        } else {
+            file = files_.insert(Files::value_type(fd, sizes_.end())).first;
+        }
+
+        // Now (re) insert record in sizes_
+        auto new_size = sizes_.insert(Sizes::value_type(size, fd));
+        file->second = new_size;
+
+        if (total_size_ < threshold_) return;
+
+        LOG(INFO) << "Threshold exceeded - fadvising " << total_size_;
+        while (!sizes_.empty() && total_size_ > target_) {
+            auto size = --sizes_.end();
+            total_size_ -= size->first;
+            posix_fadvise(size->second, 0, 0, POSIX_FADV_DONTNEED);
+            files_.erase(size->second);
+            sizes_.erase(size);
+        }
+        LOG(INFO) << "Threshold now " << total_size_;
+    }
+
+    void CloseImpl(int fd) {
+        auto file = files_.find(fd);
+        if (file == files_.end()) return;
+
+        total_size_ -= file->second->first;
+        sizes_.erase(file->second);
+        files_.erase(file);
+    }
+
+    void MessageLoopImpl() {
+        while (1) {
+            Message message;
+
+            {
+                std::unique_lock<std::mutex> lock(mutex_);
+                cv_.wait(lock, [this] { return !queue_.empty(); });
+                message = queue_.front();
+                queue_.pop();
+            }
+
+            switch (message.type) {
+                case Message::record:
+                    RecordImpl(message.fd, message.size);
+                    break;
+
+                case Message::close:
+                    CloseImpl(message.fd);
+                    break;
+
+                case Message::quit:
+                    return;
+            }
+        }
+    }
+
+    static int MessageLoop(FAdviser* ptr) {
+        ptr->MessageLoopImpl();
+        return 0;
+    }
+
+    void SendMessage(Message::Type type, int fd = -1, size_t size = 0) {
+        {
+            std::unique_lock<std::mutex> lock(mutex_);
+            Message message = {type, fd, size};
+            queue_.push(message);
+        }
+        cv_.notify_one();
+    }
+
+    std::mutex mutex_;
+    std::condition_variable cv_;
+    std::queue<Message> queue_;
+    std::thread thread_;
+
+    typedef std::multimap<size_t, int> Sizes;
+    typedef std::map<int, Sizes::iterator> Files;
+
+    Files files_;
+    Sizes sizes_;
+    size_t total_size_;
+
+    const size_t threshold_ = 64 * 1024 * 1024;
+    const size_t target_ = 32 * 1024 * 1024;
+};
+
+/* Single FUSE mount */
+struct fuse {
+    explicit fuse(const std::string& _path)
+        : path(_path),
+          tracker(mediaprovider::fuse::NodeTracker(&lock)),
+          root(node::CreateRoot(_path, &lock, &tracker)),
+          mp(0),
+          zero_addr(0) {}
+
+    inline bool IsRoot(const node* node) const { return node == root; }
+
+    inline string GetEffectiveRootPath() {
+        if (path.find("/storage/emulated", 0) == 0) {
+            return path + "/" + std::to_string(getuid() / PER_USER_RANGE);
+        }
+        return path;
+    }
+
+    // Note that these two (FromInode / ToInode) conversion wrappers are required
+    // because fuse_lowlevel_ops documents that the root inode is always one
+    // (see FUSE_ROOT_ID in fuse_lowlevel.h). There are no particular requirements
+    // on any of the other inodes in the FS.
+    inline node* FromInode(__u64 inode) {
+        if (inode == FUSE_ROOT_ID) {
+            return root;
+        }
+
+        return node::FromInode(inode, &tracker);
+    }
+
+    inline __u64 ToInode(node* node) const {
+        if (IsRoot(node)) {
+            return FUSE_ROOT_ID;
+        }
+
+        return node::ToInode(node);
+    }
+
+    std::recursive_mutex lock;
+    const string path;
+    // The Inode tracker associated with this FUSE instance.
+    mediaprovider::fuse::NodeTracker tracker;
+    node* const root;
+    struct fuse_session* se;
+
+    /*
+     * Used to make JNI calls to MediaProvider.
+     * Responsibility of freeing this object falls on corresponding
+     * FuseDaemon object.
+     */
+    mediaprovider::fuse::MediaProviderWrapper* mp;
+
+    /*
+     * Points to a range of zeroized bytes, used by pf_read to represent redacted ranges.
+     * The memory is read only and should never be modified.
+     */
+    /* const */ char* zero_addr;
+
+    FAdviser fadviser;
+
+    std::atomic_bool* active;
+};
+
+static inline string get_name(node* n) {
+    if (n) {
+        std::string name = IS_OS_DEBUGABLE ? "real_path: " + n->BuildPath() + " " : "";
+        name += "node_path: " + n->BuildSafePath();
+        return name;
+    }
+    return "?";
+}
+
+static inline __u64 ptr_to_id(void* ptr) {
+    return (__u64)(uintptr_t) ptr;
+}
+
+/*
+ * Set an F_RDLCK or F_WRLCKK on fd with fcntl(2).
+ *
+ * This is called before the MediaProvider returns fd from the lower file
+ * system to an app over the ContentResolver interface. This allows us
+ * check with is_file_locked if any reference to that fd is still open.
+ */
+static int set_file_lock(int fd, bool for_read, const std::string& path) {
+    std::string lock_str = (for_read ? "read" : "write");
+
+    struct flock fl{};
+    fl.l_type = for_read ? F_RDLCK : F_WRLCK;
+    fl.l_whence = SEEK_SET;
+
+    int res = fcntl(fd, F_OFD_SETLK, &fl);
+    if (res) {
+        PLOG(WARNING) << "Failed to set lock: " << lock_str;
+        return res;
+    }
+    return res;
+}
+
+/*
+ * Check if an F_RDLCK or F_WRLCK is set on fd with fcntl(2).
+ *
+ * This is used to determine if the MediaProvider has given an fd to the lower fs to an app over
+ * the ContentResolver interface. Before that happens, we always call set_file_lock on the file
+ * allowing us to know if any reference to that fd is still open here.
+ *
+ * Returns true if fd may have a lock, false otherwise
+ */
+static bool is_file_locked(int fd, const std::string& path) {
+    struct flock fl{};
+    fl.l_type = F_WRLCK;
+    fl.l_whence = SEEK_SET;
+
+    int res = fcntl(fd, F_OFD_GETLK, &fl);
+    if (res) {
+        PLOG(WARNING) << "Failed to check lock";
+        // Assume worst
+        return true;
+    }
+    bool locked = fl.l_type != F_UNLCK;
+    return locked;
+}
+
+static struct fuse* get_fuse(fuse_req_t req) {
+    return reinterpret_cast<struct fuse*>(fuse_req_userdata(req));
+}
+
+static bool is_package_owned_path(const string& path, const string& fuse_path) {
+    if (path.rfind(fuse_path, 0) != 0) {
+        return false;
+    }
+    return std::regex_match(path, PATTERN_OWNED_PATH);
+}
+
+// See fuse_lowlevel.h fuse_lowlevel_notify_inval_entry for how to call this safetly without
+// deadlocking the kernel
+static void fuse_inval(fuse_session* se, fuse_ino_t parent_ino, fuse_ino_t child_ino,
+                       const string& child_name, const string& path) {
+    if (mediaprovider::fuse::containsMount(path, std::to_string(getuid() / PER_USER_RANGE))) {
+        LOG(WARNING) << "Ignoring attempt to invalidate dentry for FUSE mounts";
+        return;
+    }
+
+    if (fuse_lowlevel_notify_inval_entry(se, parent_ino, child_name.c_str(), child_name.size())) {
+        // Invalidating the dentry can fail if there's no dcache entry, however, there may still
+        // be cached attributes, so attempt to invalidate those by invalidating the inode
+        fuse_lowlevel_notify_inval_inode(se, child_ino, 0, 0);
+    }
+}
+
+static double get_timeout(struct fuse* fuse, const string& path, bool should_inval) {
+    string media_path = fuse->GetEffectiveRootPath() + "/Android/media";
+    if (should_inval || path.find(media_path, 0) == 0 || is_package_owned_path(path, fuse->path)) {
+        // We set dentry timeout to 0 for the following reasons:
+        // 1. Case-insensitive lookups need to invalidate other case-insensitive dentry matches
+        // 2. Installd might delete Android/media/<package> dirs when app data is cleared.
+        // This can leave a stale entry in the kernel dcache, and break subsequent creation of the
+        // dir via FUSE.
+        // 3. With app data isolation enabled, app A should not guess existence of app B from the
+        // Android/{data,obb}/<package> paths, hence we prevent the kernel from caching that
+        // information.
+        return 0;
+    }
+    return std::numeric_limits<double>::max();
+}
+
+static node* make_node_entry(fuse_req_t req, node* parent, const string& name, const string& path,
+                             struct fuse_entry_param* e, int* error_code) {
+    struct fuse* fuse = get_fuse(req);
+    const struct fuse_ctx* ctx = fuse_req_ctx(req);
+    node* node;
+
+    memset(e, 0, sizeof(*e));
+    if (lstat(path.c_str(), &e->attr) < 0) {
+        *error_code = errno;
+        return NULL;
+    }
+
+    bool should_inval = false;
+    node = parent->LookupChildByName(name, true /* acquire */);
+    if (!node) {
+        node = ::node::Create(parent, name, &fuse->lock, &fuse->tracker);
+    } else if (!mediaprovider::fuse::containsMount(path, std::to_string(getuid() / PER_USER_RANGE))) {
+        should_inval = true;
+        // Only invalidate a path if it does not contain mount.
+        // Invalidate both names to ensure there's no dentry left in the kernel after the following
+        // operations:
+        // 1) touch foo, touch FOO, unlink *foo*
+        // 2) touch foo, touch FOO, unlink *FOO*
+        // Invalidating lookup_name fixes (1) and invalidating node_name fixes (2)
+        // |should_inval| invalidates lookup_name by using 0 timeout below and we explicitly
+        // invalidate node_name if different case
+        // Note that we invalidate async otherwise we will deadlock the kernel
+        if (name != node->GetName()) {
+            std::thread t([=]() {
+                fuse_inval(fuse->se, fuse->ToInode(parent), fuse->ToInode(node), node->GetName(),
+                           path);
+            });
+            t.detach();
+        }
+    }
+    TRACE_NODE(node, req);
+
+    // This FS is not being exported via NFS so just a fixed generation number
+    // for now. If we do need this, we need to increment the generation ID each
+    // time the fuse daemon restarts because that's what it takes for us to
+    // reuse inode numbers.
+    e->generation = 0;
+    e->ino = fuse->ToInode(node);
+    e->entry_timeout = get_timeout(fuse, path, should_inval);
+    e->attr_timeout = is_package_owned_path(path, fuse->path) || should_inval
+                              ? 0
+                              : std::numeric_limits<double>::max();
+
+    return node;
+}
+
+static inline bool is_requesting_write(int flags) {
+    return flags & (O_WRONLY | O_RDWR);
+}
+
+namespace mediaprovider {
+namespace fuse {
+
+/**
+ * Function implementations
+ *
+ * These implement the various functions in fuse_lowlevel_ops
+ *
+ */
+
+static void pf_init(void* userdata, struct fuse_conn_info* conn) {
+    // We don't want a getattr request with every read request
+    conn->want &= ~FUSE_CAP_AUTO_INVAL_DATA & ~FUSE_CAP_READDIRPLUS_AUTO;
+    unsigned mask = (FUSE_CAP_SPLICE_WRITE | FUSE_CAP_SPLICE_MOVE | FUSE_CAP_SPLICE_READ |
+                     FUSE_CAP_ASYNC_READ | FUSE_CAP_ATOMIC_O_TRUNC | FUSE_CAP_WRITEBACK_CACHE |
+                     FUSE_CAP_EXPORT_SUPPORT | FUSE_CAP_FLOCK_LOCKS);
+    conn->want |= conn->capable & mask;
+    conn->max_read = MAX_READ_SIZE;
+
+    struct fuse* fuse = reinterpret_cast<struct fuse*>(userdata);
+    fuse->active->store(true, std::memory_order_release);
+}
+
+static void pf_destroy(void* userdata) {
+    struct fuse* fuse = reinterpret_cast<struct fuse*>(userdata);
+    LOG(INFO) << "DESTROY " << fuse->path;
+
+    node::DeleteTree(fuse->root);
+}
+
+// Return true if the path is accessible for that uid.
+static bool is_app_accessible_path(MediaProviderWrapper* mp, const string& path, uid_t uid) {
+    if (uid < AID_APP_START) {
+        return true;
+    }
+
+    if (path == "/storage/emulated") {
+        // Apps should never refer to /storage/emulated - they should be using the user-spcific
+        // subdirs, eg /storage/emulated/0
+        return false;
+    }
+
+    std::smatch match;
+    if (std::regex_match(path, match, PATTERN_OWNED_PATH)) {
+        const std::string& pkg = match[1];
+        // .nomedia is not a valid package. .nomedia always exists in /Android/data directory,
+        // and it's not an external file/directory of any package
+        if (pkg == ".nomedia") {
+            return true;
+        }
+        if (!mp->IsUidForPackage(pkg, uid)) {
+            PLOG(WARNING) << "Invalid other package file access from " << pkg << "(: " << path;
+            return false;
+        }
+    }
+    return true;
+}
+
+static std::regex storage_emulated_regex("^\\/storage\\/emulated\\/([0-9]+)");
+static node* do_lookup(fuse_req_t req, fuse_ino_t parent, const char* name,
+                       struct fuse_entry_param* e, int* error_code) {
+    struct fuse* fuse = get_fuse(req);
+    node* parent_node = fuse->FromInode(parent);
+    if (!parent_node) {
+        *error_code = ENOENT;
+        return nullptr;
+    }
+    string parent_path = parent_node->BuildPath();
+    // We should always allow lookups on the root, because failing them could cause
+    // bind mounts to be invalidated.
+    if (!fuse->IsRoot(parent_node) && !is_app_accessible_path(fuse->mp, parent_path, req->ctx.uid)) {
+        *error_code = ENOENT;
+        return nullptr;
+    }
+
+    string child_path = parent_path + "/" + name;
+
+    TRACE_NODE(parent_node, req);
+
+    std::smatch match;
+    std::regex_search(child_path, match, storage_emulated_regex);
+    if (match.size() == 2 && std::to_string(getuid() / PER_USER_RANGE) != match[1].str()) {
+        // Ensure the FuseDaemon user id matches the user id in requested path
+        *error_code = EPERM;
+        return nullptr;
+    }
+    return make_node_entry(req, parent_node, name, child_path, e, error_code);
+}
+
+static void pf_lookup(fuse_req_t req, fuse_ino_t parent, const char* name) {
+    ATRACE_CALL();
+    struct fuse_entry_param e;
+
+    int error_code = 0;
+    if (do_lookup(req, parent, name, &e, &error_code)) {
+        fuse_reply_entry(req, &e);
+    } else {
+        CHECK(error_code != 0);
+        fuse_reply_err(req, error_code);
+    }
+}
+
+static void do_forget(fuse_req_t req, struct fuse* fuse, fuse_ino_t ino, uint64_t nlookup) {
+    node* node = fuse->FromInode(ino);
+    TRACE_NODE(node, req);
+    if (node) {
+        // This is a narrowing conversion from an unsigned 64bit to a 32bit value. For
+        // some reason we only keep 32 bit refcounts but the kernel issues
+        // forget requests with a 64 bit counter.
+        node->Release(static_cast<uint32_t>(nlookup));
+    }
+}
+
+static void pf_forget(fuse_req_t req, fuse_ino_t ino, uint64_t nlookup) {
+    // Always allow to forget so no need to check is_app_accessible_path()
+    ATRACE_CALL();
+    node* node;
+    struct fuse* fuse = get_fuse(req);
+
+    do_forget(req, fuse, ino, nlookup);
+    fuse_reply_none(req);
+}
+
+static void pf_forget_multi(fuse_req_t req,
+                            size_t count,
+                            struct fuse_forget_data* forgets) {
+    ATRACE_CALL();
+    struct fuse* fuse = get_fuse(req);
+
+    for (int i = 0; i < count; i++) {
+        do_forget(req, fuse, forgets[i].ino, forgets[i].nlookup);
+    }
+    fuse_reply_none(req);
+}
+
+static void pf_getattr(fuse_req_t req,
+                       fuse_ino_t ino,
+                       struct fuse_file_info* fi) {
+    ATRACE_CALL();
+    struct fuse* fuse = get_fuse(req);
+    node* node = fuse->FromInode(ino);
+    if (!node) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+    string path = node->BuildPath();
+    if (!is_app_accessible_path(fuse->mp, path, req->ctx.uid)) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+    TRACE_NODE(node, req);
+
+    struct stat s;
+    memset(&s, 0, sizeof(s));
+    if (lstat(path.c_str(), &s) < 0) {
+        fuse_reply_err(req, errno);
+    } else {
+        fuse_reply_attr(req, &s, is_package_owned_path(path, fuse->path) ?
+                0 : std::numeric_limits<double>::max());
+    }
+}
+
+static void pf_setattr(fuse_req_t req,
+                       fuse_ino_t ino,
+                       struct stat* attr,
+                       int to_set,
+                       struct fuse_file_info* fi) {
+    ATRACE_CALL();
+    struct fuse* fuse = get_fuse(req);
+    node* node = fuse->FromInode(ino);
+    if (!node) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+    string path = node->BuildPath();
+    if (!is_app_accessible_path(fuse->mp, path, req->ctx.uid)) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+
+    int fd = -1;
+    if (fi) {
+        // If we have a file_info, setattr was called with an fd so use the fd instead of path
+        handle* h = reinterpret_cast<handle*>(fi->fh);
+        fd = h->fd;
+    } else {
+        const struct fuse_ctx* ctx = fuse_req_ctx(req);
+        int status = fuse->mp->IsOpenAllowed(path, ctx->uid, true);
+        if (status) {
+            fuse_reply_err(req, EACCES);
+            return;
+        }
+    }
+    struct timespec times[2];
+    TRACE_NODE(node, req);
+
+    /* XXX: incomplete implementation on purpose.
+     * chmod/chown should NEVER be implemented.*/
+
+    if ((to_set & FUSE_SET_ATTR_SIZE)) {
+        int res = 0;
+        if (fd == -1) {
+            res = truncate64(path.c_str(), attr->st_size);
+        } else {
+            res = ftruncate64(fd, attr->st_size);
+        }
+
+        if (res < 0) {
+            fuse_reply_err(req, errno);
+            return;
+        }
+    }
+
+    /* Handle changing atime and mtime.  If FATTR_ATIME_and FATTR_ATIME_NOW
+     * are both set, then set it to the current time.  Else, set it to the
+     * time specified in the request.  Same goes for mtime.  Use utimensat(2)
+     * as it allows ATIME and MTIME to be changed independently, and has
+     * nanosecond resolution which fuse also has.
+     */
+    if (to_set & (FATTR_ATIME | FATTR_MTIME)) {
+        times[0].tv_nsec = UTIME_OMIT;
+        times[1].tv_nsec = UTIME_OMIT;
+        if (to_set & FATTR_ATIME) {
+            if (to_set & FATTR_ATIME_NOW) {
+                times[0].tv_nsec = UTIME_NOW;
+            } else {
+                times[0] = attr->st_atim;
+            }
+        }
+
+        if (to_set & FATTR_MTIME) {
+            if (to_set & FATTR_MTIME_NOW) {
+                times[1].tv_nsec = UTIME_NOW;
+            } else {
+                times[1] = attr->st_mtim;
+            }
+        }
+
+        TRACE_NODE(node, req);
+        int res = 0;
+        if (fd == -1) {
+            res = utimensat(-1, path.c_str(), times, 0);
+        } else {
+            res = futimens(fd, times);
+        }
+
+        if (res < 0) {
+            fuse_reply_err(req, errno);
+            return;
+        }
+    }
+
+    lstat(path.c_str(), attr);
+    fuse_reply_attr(req, attr, is_package_owned_path(path, fuse->path) ?
+            0 : std::numeric_limits<double>::max());
+}
+
+static void pf_canonical_path(fuse_req_t req, fuse_ino_t ino)
+{
+    struct fuse* fuse = get_fuse(req);
+    node* node = fuse->FromInode(ino);
+    string path = node ? node->BuildPath() : "";
+
+    if (node && is_app_accessible_path(fuse->mp, path, req->ctx.uid)) {
+        // TODO(b/147482155): Check that uid has access to |path| and its contents
+        fuse_reply_canonical_path(req, path.c_str());
+        return;
+    }
+    fuse_reply_err(req, ENOENT);
+}
+
+static void pf_mknod(fuse_req_t req,
+                     fuse_ino_t parent,
+                     const char* name,
+                     mode_t mode,
+                     dev_t rdev) {
+    ATRACE_CALL();
+    struct fuse* fuse = get_fuse(req);
+    node* parent_node = fuse->FromInode(parent);
+    if (!parent_node) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+    string parent_path = parent_node->BuildPath();
+    if (!is_app_accessible_path(fuse->mp, parent_path, req->ctx.uid)) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+
+    TRACE_NODE(parent_node, req);
+
+    const string child_path = parent_path + "/" + name;
+
+    mode = (mode & (~0777)) | 0664;
+    if (mknod(child_path.c_str(), mode, rdev) < 0) {
+        fuse_reply_err(req, errno);
+        return;
+    }
+
+    int error_code = 0;
+    struct fuse_entry_param e;
+    if (make_node_entry(req, parent_node, name, child_path, &e, &error_code)) {
+        fuse_reply_entry(req, &e);
+    } else {
+        CHECK(error_code != 0);
+        fuse_reply_err(req, error_code);
+    }
+}
+
+static void pf_mkdir(fuse_req_t req,
+                     fuse_ino_t parent,
+                     const char* name,
+                     mode_t mode) {
+    ATRACE_CALL();
+    struct fuse* fuse = get_fuse(req);
+    node* parent_node = fuse->FromInode(parent);
+    if (!parent_node) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+    const struct fuse_ctx* ctx = fuse_req_ctx(req);
+    const string parent_path = parent_node->BuildPath();
+    if (!is_app_accessible_path(fuse->mp, parent_path, ctx->uid)) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+
+    TRACE_NODE(parent_node, req);
+
+    const string child_path = parent_path + "/" + name;
+
+    int status = fuse->mp->IsCreatingDirAllowed(child_path, ctx->uid);
+    if (status) {
+        fuse_reply_err(req, status);
+        return;
+    }
+
+    mode = (mode & (~0777)) | 0775;
+    if (mkdir(child_path.c_str(), mode) < 0) {
+        fuse_reply_err(req, errno);
+        return;
+    }
+
+    int error_code = 0;
+    struct fuse_entry_param e;
+    if (make_node_entry(req, parent_node, name, child_path, &e, &error_code)) {
+        fuse_reply_entry(req, &e);
+    } else {
+        CHECK(error_code != 0);
+        fuse_reply_err(req, error_code);
+    }
+}
+
+static void pf_unlink(fuse_req_t req, fuse_ino_t parent, const char* name) {
+    ATRACE_CALL();
+    struct fuse* fuse = get_fuse(req);
+    node* parent_node = fuse->FromInode(parent);
+    if (!parent_node) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+    const struct fuse_ctx* ctx = fuse_req_ctx(req);
+    const string parent_path = parent_node->BuildPath();
+    if (!is_app_accessible_path(fuse->mp, parent_path, ctx->uid)) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+
+    TRACE_NODE(parent_node, req);
+
+    const string child_path = parent_path + "/" + name;
+
+    int status = fuse->mp->DeleteFile(child_path, ctx->uid);
+    if (status) {
+        fuse_reply_err(req, status);
+        return;
+    }
+
+    node* child_node = parent_node->LookupChildByName(name, false /* acquire */);
+    TRACE_NODE(child_node, req);
+    if (child_node) {
+        child_node->SetDeleted();
+    }
+
+    fuse_reply_err(req, 0);
+}
+
+static void pf_rmdir(fuse_req_t req, fuse_ino_t parent, const char* name) {
+    ATRACE_CALL();
+    struct fuse* fuse = get_fuse(req);
+    node* parent_node = fuse->FromInode(parent);
+    if (!parent_node) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+    const string parent_path = parent_node->BuildPath();
+    if (!is_app_accessible_path(fuse->mp, parent_path, req->ctx.uid)) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+    TRACE_NODE(parent_node, req);
+
+    const string child_path = parent_path + "/" + name;
+
+    int status = fuse->mp->IsDeletingDirAllowed(child_path, req->ctx.uid);
+    if (status) {
+        fuse_reply_err(req, status);
+        return;
+    }
+
+    if (rmdir(child_path.c_str()) < 0) {
+        fuse_reply_err(req, errno);
+        return;
+    }
+
+    node* child_node = parent_node->LookupChildByName(name, false /* acquire */);
+    TRACE_NODE(child_node, req);
+    if (child_node) {
+        child_node->SetDeleted();
+    }
+
+    fuse_reply_err(req, 0);
+}
+/*
+static void pf_symlink(fuse_req_t req, const char* link, fuse_ino_t parent,
+                         const char* name)
+{
+    cout << "TODO:" << __func__;
+}
+*/
+static int do_rename(fuse_req_t req, fuse_ino_t parent, const char* name, fuse_ino_t new_parent,
+                     const char* new_name, unsigned int flags) {
+    ATRACE_CALL();
+    struct fuse* fuse = get_fuse(req);
+
+    if (flags != 0) {
+        return EINVAL;
+    }
+
+    node* old_parent_node = fuse->FromInode(parent);
+    if (!old_parent_node) return ENOENT;
+    const struct fuse_ctx* ctx = fuse_req_ctx(req);
+    const string old_parent_path = old_parent_node->BuildPath();
+    if (!is_app_accessible_path(fuse->mp, old_parent_path, ctx->uid)) {
+        return ENOENT;
+    }
+
+    node* new_parent_node = fuse->FromInode(new_parent);
+    if (!new_parent_node) return ENOENT;
+    const string new_parent_path = new_parent_node->BuildPath();
+    if (!is_app_accessible_path(fuse->mp, new_parent_path, ctx->uid)) {
+        return ENOENT;
+    }
+
+    if (!old_parent_node || !new_parent_node) {
+        return ENOENT;
+    } else if (parent == new_parent && name == new_name) {
+        // No rename required.
+        return 0;
+    }
+
+    TRACE_NODE(old_parent_node, req);
+    TRACE_NODE(new_parent_node, req);
+
+    node* child_node = old_parent_node->LookupChildByName(name, true /* acquire */);
+    TRACE_NODE(child_node, req) << "old_child";
+
+    const string old_child_path = child_node->BuildPath();
+    const string new_child_path = new_parent_path + "/" + new_name;
+
+    // TODO(b/147408834): Check ENOTEMPTY & EEXIST error conditions before JNI call.
+    const int res = fuse->mp->Rename(old_child_path, new_child_path, req->ctx.uid);
+    // TODO(b/145663158): Lookups can go out of sync if file/directory is actually moved but
+    // EFAULT/EIO is reported due to JNI exception.
+    if (res == 0) {
+        child_node->Rename(new_name, new_parent_node);
+    }
+    TRACE_NODE(child_node, req) << "new_child";
+
+    child_node->Release(1);
+    return res;
+}
+
+static void pf_rename(fuse_req_t req, fuse_ino_t parent, const char* name, fuse_ino_t new_parent,
+                      const char* new_name, unsigned int flags) {
+    int res = do_rename(req, parent, name, new_parent, new_name, flags);
+    fuse_reply_err(req, res);
+}
+
+/*
+static void pf_link(fuse_req_t req, fuse_ino_t ino, fuse_ino_t new_parent,
+                      const char* new_name)
+{
+    cout << "TODO:" << __func__;
+}
+*/
+
+static handle* create_handle_for_node(struct fuse* fuse, const string& path, int fd, node* node,
+                                      const RedactionInfo* ri) {
+    std::lock_guard<std::recursive_mutex> guard(fuse->lock);
+    // We don't want to use the FUSE VFS cache in two cases:
+    // 1. When redaction is needed because app A with EXIF access might access
+    // a region that should have been redacted for app B without EXIF access, but app B on
+    // a subsequent read, will be able to see the EXIF data because the read request for
+    // that region will be served from cache and not get to the FUSE daemon
+    // 2. When the file has a read or write lock on it. This means that the MediaProvider
+    // has given an fd to the lower file system to an app. There are two cases where using
+    // the cache in this case can be a problem:
+    // a. Writing to a FUSE fd with caching enabled will use the write-back cache and a
+    // subsequent read from the lower fs fd will not see the write.
+    // b. Reading from a FUSE fd with caching enabled may not see the latest writes using
+    // the lower fs fd because those writes did not go through the FUSE layer and reads from
+    // FUSE after that write may be served from cache
+    bool direct_io = ri->isRedactionNeeded() || is_file_locked(fd, path);
+
+    handle* h = new handle(fd, ri, !direct_io);
+    node->AddHandle(h);
+    return h;
+}
+
+static void pf_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) {
+    ATRACE_CALL();
+    struct fuse* fuse = get_fuse(req);
+    node* node = fuse->FromInode(ino);
+    if (!node) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+    const struct fuse_ctx* ctx = fuse_req_ctx(req);
+    const string path = node->BuildPath();
+    if (!is_app_accessible_path(fuse->mp, path, ctx->uid)) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+
+    TRACE_NODE(node, req) << (is_requesting_write(fi->flags) ? "write" : "read");
+
+    if (fi->flags & O_DIRECT) {
+        fi->flags &= ~O_DIRECT;
+        fi->direct_io = true;
+    }
+
+    int status = fuse->mp->IsOpenAllowed(path, ctx->uid, is_requesting_write(fi->flags));
+    if (status) {
+        fuse_reply_err(req, status);
+        return;
+    }
+
+    // With the writeback cache enabled, FUSE may generate READ requests even for files that
+    // were opened O_WRONLY; so make sure we open it O_RDWR instead.
+    int open_flags = fi->flags;
+    if (open_flags & O_WRONLY) {
+        open_flags &= ~O_WRONLY;
+        open_flags |= O_RDWR;
+    }
+
+    if (open_flags & O_APPEND) {
+        open_flags &= ~O_APPEND;
+    }
+
+    const int fd = open(path.c_str(), open_flags);
+    if (fd < 0) {
+        fuse_reply_err(req, errno);
+        return;
+    }
+
+    // We don't redact if the caller was granted write permission for this file
+    std::unique_ptr<RedactionInfo> ri;
+    if (is_requesting_write(fi->flags)) {
+        ri = std::make_unique<RedactionInfo>();
+    } else {
+        ri = fuse->mp->GetRedactionInfo(path, req->ctx.uid, req->ctx.pid);
+    }
+
+    if (!ri) {
+        close(fd);
+        fuse_reply_err(req, EFAULT);
+        return;
+    }
+
+    handle* h = create_handle_for_node(fuse, path, fd, node, ri.release());
+    fi->fh = ptr_to_id(h);
+    fi->keep_cache = 1;
+    fi->direct_io = !h->cached;
+    fuse_reply_open(req, fi);
+}
+
+static void do_read(fuse_req_t req, size_t size, off_t off, struct fuse_file_info* fi) {
+    handle* h = reinterpret_cast<handle*>(fi->fh);
+    struct fuse_bufvec buf = FUSE_BUFVEC_INIT(size);
+
+    buf.buf[0].fd = h->fd;
+    buf.buf[0].pos = off;
+    buf.buf[0].flags =
+            (enum fuse_buf_flags) (FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK);
+
+    fuse_reply_data(req, &buf, (enum fuse_buf_copy_flags) 0);
+}
+
+static bool range_contains(const RedactionRange& rr, off_t off) {
+    return rr.first <= off && off <= rr.second;
+}
+
+/**
+ * Sets the parameters for a fuse_buf that reads from memory, including flags.
+ * Makes buf->mem point to an already mapped region of zeroized memory.
+ * This memory is read only.
+ */
+static void create_mem_fuse_buf(size_t size, fuse_buf* buf, struct fuse* fuse) {
+    buf->size = size;
+    buf->mem = fuse->zero_addr;
+    buf->flags = static_cast<fuse_buf_flags>(0 /*read from fuse_buf.mem*/);
+    buf->pos = -1;
+    buf->fd = -1;
+}
+
+/**
+ * Sets the parameters for a fuse_buf that reads from file, including flags.
+ */
+static void create_file_fuse_buf(size_t size, off_t pos, int fd, fuse_buf* buf) {
+    buf->size = size;
+    buf->fd = fd;
+    buf->pos = pos;
+    buf->flags = static_cast<fuse_buf_flags>(FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK);
+    buf->mem = nullptr;
+}
+
+static void do_read_with_redaction(fuse_req_t req, size_t size, off_t off, fuse_file_info* fi) {
+    handle* h = reinterpret_cast<handle*>(fi->fh);
+    auto overlapping_rr = h->ri->getOverlappingRedactionRanges(size, off);
+
+    if (overlapping_rr->size() <= 0) {
+        // no relevant redaction ranges for this request
+        do_read(req, size, off, fi);
+        return;
+    }
+    // the number of buffers we need, if the read doesn't start or end with
+    //  a redaction range.
+    int num_bufs = overlapping_rr->size() * 2 + 1;
+    if (overlapping_rr->front().first <= off) {
+        // the beginning of the read request is redacted
+        num_bufs--;
+    }
+    if (overlapping_rr->back().second >= off + size) {
+        // the end of the read request is redacted
+        num_bufs--;
+    }
+    auto bufvec_ptr = std::unique_ptr<fuse_bufvec, decltype(free)*>{
+            reinterpret_cast<fuse_bufvec*>(
+                    malloc(sizeof(fuse_bufvec) + (num_bufs - 1) * sizeof(fuse_buf))),
+            free};
+    fuse_bufvec& bufvec = *bufvec_ptr;
+
+    // initialize bufvec
+    bufvec.count = num_bufs;
+    bufvec.idx = 0;
+    bufvec.off = 0;
+
+    int rr_idx = 0;
+    off_t start = off;
+    // Add a dummy redaction range to make sure we don't go out of vector
+    // limits when computing the end of the last non-redacted range.
+    // This ranges is invalid because its starting point is larger than it's ending point.
+    overlapping_rr->push_back(RedactionRange(LLONG_MAX, LLONG_MAX - 1));
+
+    for (int i = 0; i < num_bufs; ++i) {
+        off_t end;
+        if (range_contains(overlapping_rr->at(rr_idx), start)) {
+            // Handle a redacted range
+            // end should be the end of the redacted range, but can't be out of
+            // the read request bounds
+            end = std::min(static_cast<off_t>(off + size - 1), overlapping_rr->at(rr_idx).second);
+            create_mem_fuse_buf(/*size*/ end - start + 1, &(bufvec.buf[i]), get_fuse(req));
+            ++rr_idx;
+        } else {
+            // Handle a non-redacted range
+            // end should be right before the next redaction range starts or
+            // the end of the read request
+            end = std::min(static_cast<off_t>(off + size - 1),
+                    overlapping_rr->at(rr_idx).first - 1);
+            create_file_fuse_buf(/*size*/ end - start + 1, start, h->fd, &(bufvec.buf[i]));
+        }
+        start = end + 1;
+    }
+
+    fuse_reply_data(req, &bufvec, static_cast<fuse_buf_copy_flags>(0));
+}
+
+static void pf_read(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
+                    struct fuse_file_info* fi) {
+    ATRACE_CALL();
+    handle* h = reinterpret_cast<handle*>(fi->fh);
+    struct fuse* fuse = get_fuse(req);
+
+    fuse->fadviser.Record(h->fd, size);
+
+    if (h->ri->isRedactionNeeded()) {
+        do_read_with_redaction(req, size, off, fi);
+    } else {
+        do_read(req, size, off, fi);
+    }
+}
+
+/*
+static void pf_write(fuse_req_t req, fuse_ino_t ino, const char* buf,
+                       size_t size, off_t off, struct fuse_file_info* fi)
+{
+    cout << "TODO:" << __func__;
+}
+*/
+
+static void pf_write_buf(fuse_req_t req,
+                         fuse_ino_t ino,
+                         struct fuse_bufvec* bufv,
+                         off_t off,
+                         struct fuse_file_info* fi) {
+    ATRACE_CALL();
+    handle* h = reinterpret_cast<handle*>(fi->fh);
+    struct fuse_bufvec buf = FUSE_BUFVEC_INIT(fuse_buf_size(bufv));
+    ssize_t size;
+    struct fuse* fuse = get_fuse(req);
+
+    buf.buf[0].fd = h->fd;
+    buf.buf[0].pos = off;
+    buf.buf[0].flags =
+            (enum fuse_buf_flags) (FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK);
+    size = fuse_buf_copy(&buf, bufv, (enum fuse_buf_copy_flags) 0);
+
+    if (size < 0)
+        fuse_reply_err(req, -size);
+    else {
+        fuse_reply_write(req, size);
+        fuse->fadviser.Record(h->fd, size);
+    }
+}
+// Haven't tested this one. Not sure what calls it.
+#if 0
+static void pf_copy_file_range(fuse_req_t req, fuse_ino_t ino_in,
+                                 off_t off_in, struct fuse_file_info* fi_in,
+                                 fuse_ino_t ino_out, off_t off_out,
+                                 struct fuse_file_info* fi_out, size_t len,
+                                 int flags)
+{
+    handle* h_in = reinterpret_cast<handle *>(fi_in->fh);
+    handle* h_out = reinterpret_cast<handle *>(fi_out->fh);
+    struct fuse_bufvec buf_in = FUSE_BUFVEC_INIT(len);
+    struct fuse_bufvec buf_out = FUSE_BUFVEC_INIT(len);
+    ssize_t size;
+
+    buf_in.buf[0].fd = h_in->fd;
+    buf_in.buf[0].pos = off_in;
+    buf_in.buf[0].flags = (enum fuse_buf_flags)(FUSE_BUF_IS_FD|FUSE_BUF_FD_SEEK);
+
+    buf_out.buf[0].fd = h_out->fd;
+    buf_out.buf[0].pos = off_out;
+    buf_out.buf[0].flags = (enum fuse_buf_flags)(FUSE_BUF_IS_FD|FUSE_BUF_FD_SEEK);
+    size = fuse_buf_copy(&buf_out, &buf_in, (enum fuse_buf_copy_flags) 0);
+
+    if (size < 0) {
+        fuse_reply_err(req, -size);
+    }
+
+    fuse_reply_write(req, size);
+}
+#endif
+static void pf_flush(fuse_req_t req,
+                     fuse_ino_t ino,
+                     struct fuse_file_info* fi) {
+    ATRACE_CALL();
+    struct fuse* fuse = get_fuse(req);
+    TRACE_NODE(nullptr, req) << "noop";
+    fuse_reply_err(req, 0);
+}
+
+static void pf_release(fuse_req_t req,
+                       fuse_ino_t ino,
+                       struct fuse_file_info* fi) {
+    ATRACE_CALL();
+    struct fuse* fuse = get_fuse(req);
+
+    node* node = fuse->FromInode(ino);
+    handle* h = reinterpret_cast<handle*>(fi->fh);
+    TRACE_NODE(node, req);
+
+    fuse->fadviser.Close(h->fd);
+    if (node) {
+        node->DestroyHandle(h);
+    }
+
+    fuse_reply_err(req, 0);
+}
+
+static int do_sync_common(int fd, bool datasync) {
+    int res = datasync ? fdatasync(fd) : fsync(fd);
+
+    if (res == -1) return errno;
+    return 0;
+}
+
+static void pf_fsync(fuse_req_t req,
+                     fuse_ino_t ino,
+                     int datasync,
+                     struct fuse_file_info* fi) {
+    ATRACE_CALL();
+    handle* h = reinterpret_cast<handle*>(fi->fh);
+    int err = do_sync_common(h->fd, datasync);
+
+    fuse_reply_err(req, err);
+}
+
+static void pf_fsyncdir(fuse_req_t req,
+                        fuse_ino_t ino,
+                        int datasync,
+                        struct fuse_file_info* fi) {
+    dirhandle* h = reinterpret_cast<dirhandle*>(fi->fh);
+    int err = do_sync_common(dirfd(h->d), datasync);
+
+    fuse_reply_err(req, err);
+}
+
+static void pf_opendir(fuse_req_t req,
+                       fuse_ino_t ino,
+                       struct fuse_file_info* fi) {
+    ATRACE_CALL();
+    struct fuse* fuse = get_fuse(req);
+    node* node = fuse->FromInode(ino);
+    if (!node) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+    const struct fuse_ctx* ctx = fuse_req_ctx(req);
+    const string path = node->BuildPath();
+    if (!is_app_accessible_path(fuse->mp, path, ctx->uid)) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+
+    TRACE_NODE(node, req);
+
+    int status = fuse->mp->IsOpendirAllowed(path, ctx->uid, /* forWrite */ false);
+    if (status) {
+        fuse_reply_err(req, status);
+        return;
+    }
+
+    DIR* dir = opendir(path.c_str());
+    if (!dir) {
+        fuse_reply_err(req, errno);
+        return;
+    }
+
+    dirhandle* h = new dirhandle(dir);
+    node->AddDirHandle(h);
+
+    fi->fh = ptr_to_id(h);
+    fuse_reply_open(req, fi);
+}
+
+#define READDIR_BUF 8192LU
+
+static void do_readdir_common(fuse_req_t req,
+                              fuse_ino_t ino,
+                              size_t size,
+                              off_t off,
+                              struct fuse_file_info* fi,
+                              bool plus) {
+    struct fuse* fuse = get_fuse(req);
+    dirhandle* h = reinterpret_cast<dirhandle*>(fi->fh);
+    size_t len = std::min<size_t>(size, READDIR_BUF);
+    char buf[READDIR_BUF];
+    size_t used = 0;
+    std::shared_ptr<DirectoryEntry> de;
+
+    struct fuse_entry_param e;
+    size_t entry_size = 0;
+
+    node* node = fuse->FromInode(ino);
+    if (!node) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+    const string path = node->BuildPath();
+    if (!is_app_accessible_path(fuse->mp, path, req->ctx.uid)) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+
+    TRACE_NODE(node, req);
+    // Get all directory entries from MediaProvider on first readdir() call of
+    // directory handle. h->next_off = 0 indicates that current readdir() call
+    // is first readdir() call for the directory handle, Avoid multiple JNI calls
+    // for single directory handle.
+    if (h->next_off == 0) {
+        h->de = fuse->mp->GetDirectoryEntries(req->ctx.uid, path, h->d);
+    }
+    // If the last entry in the previous readdir() call was rejected due to
+    // buffer capacity constraints, update directory offset to start from
+    // previously rejected entry. Directory offset can also change if there was
+    // a seekdir() on the given directory handle.
+    if (off != h->next_off) {
+        h->next_off = off;
+    }
+    const int num_directory_entries = h->de.size();
+    // Check for errors. Any error/exception occurred while obtaining directory
+    // entries will be indicated by marking first directory entry name as empty
+    // string. In the erroneous case corresponding d_type will hold error number.
+    if (num_directory_entries && h->de[0]->d_name.empty()) {
+        fuse_reply_err(req, h->de[0]->d_type);
+        return;
+    }
+
+    while (h->next_off < num_directory_entries) {
+        de = h->de[h->next_off];
+        entry_size = 0;
+        h->next_off++;
+        if (plus) {
+            int error_code = 0;
+            if (do_lookup(req, ino, de->d_name.c_str(), &e, &error_code)) {
+                entry_size = fuse_add_direntry_plus(req, buf + used, len - used, de->d_name.c_str(),
+                                                    &e, h->next_off);
+            } else {
+                // Ignore lookup errors on
+                // 1. non-existing files returned from MediaProvider database.
+                // 2. path that doesn't match FuseDaemon UID and calling uid.
+                if (error_code == ENOENT || error_code == EPERM || error_code == EACCES) continue;
+                fuse_reply_err(req, error_code);
+                return;
+            }
+        } else {
+            // This should never happen because we have readdir_plus enabled without adaptive
+            // readdir_plus, FUSE_CAP_READDIRPLUS_AUTO
+            LOG(WARNING) << "Handling plain readdir for " << de->d_name << ". Invalid d_ino";
+            e.attr.st_ino = FUSE_UNKNOWN_INO;
+            e.attr.st_mode = de->d_type << 12;
+            entry_size = fuse_add_direntry(req, buf + used, len - used, de->d_name.c_str(), &e.attr,
+                                           h->next_off);
+        }
+        // If buffer in fuse_add_direntry[_plus] is not large enough then
+        // the entry is not added to buffer but the size of the entry is still
+        // returned. Check available buffer size + returned entry size is less
+        // than actual buffer size to confirm entry is added to buffer.
+        if (used + entry_size > len) {
+            // When an entry is rejected, lookup called by readdir_plus will not be tracked by
+            // kernel. Call forget on the rejected node to decrement the reference count.
+            if (plus) {
+                do_forget(req, fuse, e.ino, 1);
+            }
+            break;
+        }
+        used += entry_size;
+    }
+    fuse_reply_buf(req, buf, used);
+}
+
+static void pf_readdir(fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
+                       struct fuse_file_info* fi) {
+    ATRACE_CALL();
+    do_readdir_common(req, ino, size, off, fi, false);
+}
+
+static void pf_readdirplus(fuse_req_t req,
+                           fuse_ino_t ino,
+                           size_t size,
+                           off_t off,
+                           struct fuse_file_info* fi) {
+    ATRACE_CALL();
+    do_readdir_common(req, ino, size, off, fi, true);
+}
+
+static void pf_releasedir(fuse_req_t req,
+                          fuse_ino_t ino,
+                          struct fuse_file_info* fi) {
+    ATRACE_CALL();
+    struct fuse* fuse = get_fuse(req);
+
+    node* node = fuse->FromInode(ino);
+
+    dirhandle* h = reinterpret_cast<dirhandle*>(fi->fh);
+    TRACE_NODE(node, req);
+    if (node) {
+        node->DestroyDirHandle(h);
+    }
+
+    fuse_reply_err(req, 0);
+}
+
+static void pf_statfs(fuse_req_t req, fuse_ino_t ino) {
+    ATRACE_CALL();
+    struct statvfs st;
+    struct fuse* fuse = get_fuse(req);
+
+    if (statvfs(fuse->root->GetName().c_str(), &st))
+        fuse_reply_err(req, errno);
+    else
+        fuse_reply_statfs(req, &st);
+}
+/*
+static void pf_setxattr(fuse_req_t req, fuse_ino_t ino, const char* name,
+                          const char* value, size_t size, int flags)
+{
+    cout << "TODO:" << __func__;
+}
+
+static void pf_getxattr(fuse_req_t req, fuse_ino_t ino, const char* name,
+                          size_t size)
+{
+    cout << "TODO:" << __func__;
+}
+
+static void pf_listxattr(fuse_req_t req, fuse_ino_t ino, size_t size)
+{
+    cout << "TODO:" << __func__;
+}
+
+static void pf_removexattr(fuse_req_t req, fuse_ino_t ino, const char* name)
+{
+    cout << "TODO:" << __func__;
+}*/
+
+static void pf_access(fuse_req_t req, fuse_ino_t ino, int mask) {
+    ATRACE_CALL();
+    struct fuse* fuse = get_fuse(req);
+
+    node* node = fuse->FromInode(ino);
+    if (!node) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+    const string path = node->BuildPath();
+    if (!is_app_accessible_path(fuse->mp, path, req->ctx.uid)) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+    TRACE_NODE(node, req);
+
+    // exists() checks are always allowed.
+    if (mask == F_OK) {
+        int res = access(path.c_str(), F_OK);
+        fuse_reply_err(req, res ? errno : 0);
+        return;
+    }
+    struct stat stat;
+    if (lstat(path.c_str(), &stat)) {
+        // File doesn't exist
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+
+    // For read and write permission checks we go to MediaProvider.
+    int status = 0;
+    bool for_write = mask & W_OK;
+    bool is_directory = S_ISDIR(stat.st_mode);
+    if (is_directory) {
+        status = fuse->mp->IsOpendirAllowed(path, req->ctx.uid, for_write);
+    } else {
+        if (mask & X_OK) {
+            // Fuse is mounted with MS_NOEXEC.
+            fuse_reply_err(req, EACCES);
+            return;
+        }
+
+        status = fuse->mp->IsOpenAllowed(path, req->ctx.uid, for_write);
+    }
+
+    fuse_reply_err(req, status);
+}
+
+static void pf_create(fuse_req_t req,
+                      fuse_ino_t parent,
+                      const char* name,
+                      mode_t mode,
+                      struct fuse_file_info* fi) {
+    ATRACE_CALL();
+    struct fuse* fuse = get_fuse(req);
+    node* parent_node = fuse->FromInode(parent);
+    if (!parent_node) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+    const string parent_path = parent_node->BuildPath();
+    if (!is_app_accessible_path(fuse->mp, parent_path, req->ctx.uid)) {
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+
+    TRACE_NODE(parent_node, req);
+
+    const string child_path = parent_path + "/" + name;
+
+    int mp_return_code = fuse->mp->InsertFile(child_path.c_str(), req->ctx.uid);
+    if (mp_return_code) {
+        fuse_reply_err(req, mp_return_code);
+        return;
+    }
+
+    // With the writeback cache enabled, FUSE may generate READ requests even for files that
+    // were opened O_WRONLY; so make sure we open it O_RDWR instead.
+    int open_flags = fi->flags;
+    if (open_flags & O_WRONLY) {
+        open_flags &= ~O_WRONLY;
+        open_flags |= O_RDWR;
+    }
+
+    if (open_flags & O_APPEND) {
+        open_flags &= ~O_APPEND;
+    }
+
+    mode = (mode & (~0777)) | 0664;
+    int fd = open(child_path.c_str(), open_flags, mode);
+    if (fd < 0) {
+        int error_code = errno;
+        // We've already inserted the file into the MP database before the
+        // failed open(), so that needs to be rolled back here.
+        fuse->mp->DeleteFile(child_path.c_str(), req->ctx.uid);
+        fuse_reply_err(req, error_code);
+        return;
+    }
+
+    int error_code = 0;
+    struct fuse_entry_param e;
+    node* node = make_node_entry(req, parent_node, name, child_path, &e, &error_code);
+    TRACE_NODE(node, req);
+    if (!node) {
+        CHECK(error_code != 0);
+        fuse_reply_err(req, error_code);
+        return;
+    }
+
+    // Let MediaProvider know we've created a new file
+    fuse->mp->OnFileCreated(child_path);
+
+    // TODO(b/147274248): Assume there will be no EXIF to redact.
+    // This prevents crashing during reads but can be a security hole if a malicious app opens an fd
+    // to the file before all the EXIF content is written. We could special case reads before the
+    // first close after a file has just been created.
+    handle* h = create_handle_for_node(fuse, child_path, fd, node, new RedactionInfo());
+    fi->fh = ptr_to_id(h);
+    fi->keep_cache = 1;
+    fi->direct_io = !h->cached;
+    fuse_reply_create(req, &e, fi);
+}
+/*
+static void pf_getlk(fuse_req_t req, fuse_ino_t ino,
+                       struct fuse_file_info* fi, struct flock* lock)
+{
+    cout << "TODO:" << __func__;
+}
+
+static void pf_setlk(fuse_req_t req, fuse_ino_t ino,
+                       struct fuse_file_info* fi,
+                       struct flock* lock, int sleep)
+{
+    cout << "TODO:" << __func__;
+}
+
+static void pf_bmap(fuse_req_t req, fuse_ino_t ino, size_t blocksize,
+                      uint64_t idx)
+{
+    cout << "TODO:" << __func__;
+}
+
+static void pf_ioctl(fuse_req_t req, fuse_ino_t ino, unsigned int cmd,
+                       void* arg, struct fuse_file_info* fi, unsigned flags,
+                       const void* in_buf, size_t in_bufsz, size_t out_bufsz)
+{
+    cout << "TODO:" << __func__;
+}
+
+static void pf_poll(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi,
+                      struct fuse_pollhandle* ph)
+{
+    cout << "TODO:" << __func__;
+}
+
+static void pf_retrieve_reply(fuse_req_t req, void* cookie, fuse_ino_t ino,
+                                off_t offset, struct fuse_bufvec* bufv)
+{
+    cout << "TODO:" << __func__;
+}
+
+static void pf_flock(fuse_req_t req, fuse_ino_t ino,
+                       struct fuse_file_info* fi, int op)
+{
+    cout << "TODO:" << __func__;
+}
+
+static void pf_fallocate(fuse_req_t req, fuse_ino_t ino, int mode,
+                       off_t offset, off_t length, struct fuse_file_info* fi)
+{
+    cout << "TODO:" << __func__;
+}
+*/
+
+static struct fuse_lowlevel_ops ops{
+    .init = pf_init, .destroy = pf_destroy, .lookup = pf_lookup, .forget = pf_forget,
+    .getattr = pf_getattr, .setattr = pf_setattr, .canonical_path = pf_canonical_path,
+    .mknod = pf_mknod, .mkdir = pf_mkdir, .unlink = pf_unlink, .rmdir = pf_rmdir,
+    /*.symlink = pf_symlink,*/
+    .rename = pf_rename,
+    /*.link = pf_link,*/
+    .open = pf_open, .read = pf_read,
+    /*.write = pf_write,*/
+    .flush = pf_flush,
+    .release = pf_release, .fsync = pf_fsync, .opendir = pf_opendir, .readdir = pf_readdir,
+    .releasedir = pf_releasedir, .fsyncdir = pf_fsyncdir, .statfs = pf_statfs,
+    /*.setxattr = pf_setxattr,
+    .getxattr = pf_getxattr,
+    .listxattr = pf_listxattr,
+    .removexattr = pf_removexattr,*/
+    .access = pf_access, .create = pf_create,
+    /*.getlk = pf_getlk,
+    .setlk = pf_setlk,
+    .bmap = pf_bmap,
+    .ioctl = pf_ioctl,
+    .poll = pf_poll,*/
+    .write_buf = pf_write_buf,
+    /*.retrieve_reply = pf_retrieve_reply,*/
+    .forget_multi = pf_forget_multi,
+    /*.flock = pf_flock,
+    .fallocate = pf_fallocate,*/
+    .readdirplus = pf_readdirplus,
+    /*.copy_file_range = pf_copy_file_range,*/
+};
+
+static struct fuse_loop_config config = {
+        .clone_fd = 1,
+        .max_idle_threads = 10,
+};
+
+static std::unordered_map<enum fuse_log_level, enum android_LogPriority> fuse_to_android_loglevel({
+    {FUSE_LOG_EMERG, ANDROID_LOG_FATAL},
+    {FUSE_LOG_ALERT, ANDROID_LOG_ERROR},
+    {FUSE_LOG_CRIT, ANDROID_LOG_ERROR},
+    {FUSE_LOG_ERR, ANDROID_LOG_ERROR},
+    {FUSE_LOG_WARNING, ANDROID_LOG_WARN},
+    {FUSE_LOG_NOTICE, ANDROID_LOG_INFO},
+    {FUSE_LOG_INFO, ANDROID_LOG_DEBUG},
+    {FUSE_LOG_DEBUG, ANDROID_LOG_VERBOSE},
+    });
+
+static void fuse_logger(enum fuse_log_level level, const char* fmt, va_list ap) {
+    __android_log_vprint(fuse_to_android_loglevel.at(level), LIBFUSE_LOG_TAG, fmt, ap);
+}
+
+bool FuseDaemon::ShouldOpenWithFuse(int fd, bool for_read, const std::string& path) {
+    bool use_fuse = false;
+
+    if (active.load(std::memory_order_acquire)) {
+        std::lock_guard<std::recursive_mutex> guard(fuse->lock);
+        const node* node = node::LookupAbsolutePath(fuse->root, path);
+        if (node && node->HasCachedHandle()) {
+            use_fuse = true;
+        } else {
+            // If we are unable to set a lock, we should use fuse since we can't track
+            // when all fd references (including dups) are closed. This can happen when
+            // we try to set a write lock twice on the same file
+            use_fuse = set_file_lock(fd, for_read, path);
+        }
+    } else {
+        LOG(WARNING) << "FUSE daemon is inactive. Cannot open file with FUSE";
+    }
+
+    return use_fuse;
+}
+
+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;
+        fuse_ino_t child;
+        {
+            std::lock_guard<std::recursive_mutex> guard(fuse->lock);
+            const node* node = node::LookupAbsolutePath(fuse->root, path);
+            if (node) {
+                name = node->GetName();
+                child = fuse->ToInode(const_cast<class node*>(node));
+                parent = fuse->ToInode(node->GetParent());
+            }
+        }
+
+        if (!name.empty()) {
+            fuse_inval(fuse->se, parent, child, name, path);
+        }
+    } else {
+        LOG(WARNING) << "FUSE daemon is inactive. Cannot invalidate dentry";
+    }
+}
+
+FuseDaemon::FuseDaemon(JNIEnv* env, jobject mediaProvider) : mp(env, mediaProvider),
+                                                             active(false), fuse(nullptr) {}
+
+bool FuseDaemon::IsStarted() const {
+    return active.load(std::memory_order_acquire);
+}
+
+void FuseDaemon::Start(android::base::unique_fd fd, const std::string& path) {
+    android::base::SetDefaultTag(LOG_TAG);
+
+    struct fuse_args args;
+    struct fuse_cmdline_opts opts;
+
+    struct stat stat;
+
+    if (lstat(path.c_str(), &stat)) {
+        PLOG(ERROR) << "ERROR: failed to stat source " << path;
+        return;
+    }
+
+    if (!S_ISDIR(stat.st_mode)) {
+        PLOG(ERROR) << "ERROR: source is not a directory";
+        return;
+    }
+
+    args = FUSE_ARGS_INIT(0, nullptr);
+    if (fuse_opt_add_arg(&args, path.c_str()) || fuse_opt_add_arg(&args, "-odebug") ||
+        fuse_opt_add_arg(&args, ("-omax_read=" + std::to_string(MAX_READ_SIZE)).c_str())) {
+        LOG(ERROR) << "ERROR: failed to set options";
+        return;
+    }
+
+    struct fuse fuse_default(path);
+    fuse_default.mp = &mp;
+    // fuse_default is stack allocated, but it's safe to save it as an instance variable because
+    // this method blocks and FuseDaemon#active tells if we are currently blocking
+    fuse = &fuse_default;
+
+    // Used by pf_read: redacted ranges are represented by zeroized ranges of bytes,
+    // so we mmap the maximum length of redacted ranges in the beginning and save memory allocations
+    // on each read.
+    fuse_default.zero_addr = static_cast<char*>(mmap(
+            NULL, MAX_READ_SIZE, PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, /*fd*/ -1, /*off*/ 0));
+    if (fuse_default.zero_addr == MAP_FAILED) {
+        LOG(FATAL) << "mmap failed - could not start fuse! errno = " << errno;
+    }
+
+    // Custom logging for libfuse
+    if (android::base::GetBoolProperty("persist.sys.fuse.log", false)) {
+        fuse_set_log_func(fuse_logger);
+    }
+
+    struct fuse_session
+            * se = fuse_session_new(&args, &ops, sizeof(ops), &fuse_default);
+    if (!se) {
+        PLOG(ERROR) << "Failed to create session ";
+        return;
+    }
+    fuse_default.se = se;
+    fuse_default.active = &active;
+    se->fd = fd.release();  // libfuse owns the FD now
+    se->mountpoint = strdup(path.c_str());
+
+    // Single thread. Useful for debugging
+    // fuse_session_loop(se);
+    // Multi-threaded
+    LOG(INFO) << "Starting fuse...";
+    fuse_session_loop_mt(se, &config);
+    fuse->active->store(false, std::memory_order_release);
+    LOG(INFO) << "Ending fuse...";
+
+    if (munmap(fuse_default.zero_addr, MAX_READ_SIZE)) {
+        PLOG(ERROR) << "munmap failed!";
+    }
+
+    fuse_opt_free_args(&args);
+    fuse_session_destroy(se);
+    LOG(INFO) << "Ended fuse";
+    return;
+}
+} //namespace fuse
+}  // namespace mediaprovider
diff --git a/jni/FuseDaemon.h b/jni/FuseDaemon.h
new file mode 100644
index 0000000..3c4b947
--- /dev/null
+++ b/jni/FuseDaemon.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specic language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEDIAPROVIDER_JNI_FUSEDAEMON_H_
+#define MEDIAPROVIDER_JNI_FUSEDAEMON_H_
+
+#include <memory>
+#include <string>
+
+#include <android-base/unique_fd.h>
+
+#include "MediaProviderWrapper.h"
+#include "jni.h"
+
+struct fuse;
+namespace mediaprovider {
+namespace fuse {
+class FuseDaemon final {
+  public:
+    FuseDaemon(JNIEnv* env, jobject mediaProvider);
+
+    ~FuseDaemon() = default;
+
+    /**
+     * Start the FUSE daemon loop that will handle filesystem calls.
+     */
+    void Start(android::base::unique_fd fd, const std::string& path);
+
+    /**
+     * Checks if the FUSE daemon is started.
+     */
+    bool IsStarted() const;
+
+    /**
+     * Check if file should be opened with FUSE
+     */
+    bool ShouldOpenWithFuse(int fd, bool for_read, const std::string& path);
+
+    /**
+     * Invalidate FUSE VFS dentry cache entry for path
+     */
+    void InvalidateFuseDentryCache(const std::string& path);
+
+  private:
+    FuseDaemon(const FuseDaemon&) = delete;
+    void operator=(const FuseDaemon&) = delete;
+    MediaProviderWrapper mp;
+    std::atomic_bool active;
+    struct ::fuse* fuse;
+};
+
+}  // namespace fuse
+}  // namespace mediaprovider
+
+#endif  // MEDIAPROVIDER_JNI_FUSEDAEMON_H_
diff --git a/jni/FuseUtils.cpp b/jni/FuseUtils.cpp
new file mode 100644
index 0000000..7829888
--- /dev/null
+++ b/jni/FuseUtils.cpp
@@ -0,0 +1,54 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#define LOG_TAG "FuseUtils"
+
+#include "include/libfuse_jni/FuseUtils.h"
+
+#include <string>
+#include <vector>
+
+#include "android-base/strings.h"
+
+using std::string;
+
+namespace mediaprovider {
+namespace fuse {
+
+bool containsMount(const string& path, const string& userid) {
+    // This method is called from lookup, so it's called rather frequently.
+    // Hence, we avoid concatenating the strings and we use 3 separate suffixes.
+
+    static const string prefix = "/storage/emulated/";
+    if (!android::base::StartsWithIgnoreCase(path, prefix)) {
+        return false;
+    }
+
+    const string& rest_of_path = path.substr(prefix.length());
+    if (!android::base::StartsWithIgnoreCase(rest_of_path, userid)) {
+        return false;
+    }
+
+    static const string android_suffix = "/Android";
+    static const string data_suffix = "/Android/data";
+    static const string obb_suffix = "/Android/obb";
+
+    const string& path_suffix = rest_of_path.substr(userid.length());
+    return android::base::EqualsIgnoreCase(path_suffix, android_suffix) ||
+           android::base::EqualsIgnoreCase(path_suffix, data_suffix) ||
+           android::base::EqualsIgnoreCase(path_suffix, obb_suffix);
+}
+
+}  // namespace fuse
+}  // namespace mediaprovider
diff --git a/jni/FuseUtilsTest.cpp b/jni/FuseUtilsTest.cpp
new file mode 100644
index 0000000..d9d28e6
--- /dev/null
+++ b/jni/FuseUtilsTest.cpp
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specic language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "FuseUtilsTest"
+
+#include "libfuse_jni/FuseUtils.h"
+
+#include <gtest/gtest.h>
+
+using namespace mediaprovider::fuse;
+
+TEST(FuseUtilsTest, testContainsMount_isTrueForAndroidDataObb) {
+    EXPECT_TRUE(containsMount("/storage/emulated/1234/Android", "1234"));
+    EXPECT_TRUE(containsMount("/storage/emulated/1234/Android/data", "1234"));
+    EXPECT_TRUE(containsMount("/storage/emulated/1234/Android/obb", "1234"));
+}
+
+TEST(FuseUtilsTest, testContainsMount) {
+    EXPECT_FALSE(containsMount("/random/path", "1234"));
+    EXPECT_FALSE(containsMount("/storage/abc-123", "1234"));
+    EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/data/and/more", "1234"));
+}
+
+TEST(FuseUtilsTest, testContainsMount_isCaseInsensitive) {
+    EXPECT_TRUE(containsMount("/storage/emulated/1234/android", "1234"));
+    EXPECT_TRUE(containsMount("/storage/emulated/1234/Android/Data", "1234"));
+    EXPECT_TRUE(containsMount("/storage/emulated/1234/ANDroid/dATa", "1234"));
+    EXPECT_TRUE(containsMount("/storage/emulated/1234/ANDROID/OBB", "1234"));
+    EXPECT_TRUE(containsMount("/Storage/EMULATED/1234/Android/obb", "1234"));
+}
+
+TEST(FuseUtilsTest, testContainsMount_isCaseInsensitiveForUserid) {
+    EXPECT_TRUE(containsMount("/storage/emulated/UserId/Android", "UserId"));
+    EXPECT_TRUE(containsMount("/storage/emulated/userid/Android/obb", "Userid"));
+    EXPECT_TRUE(containsMount("/storage/emulated/Userid/Android/obb", "userid"));
+}
+
+TEST(FuseUtilsTest, testContainsMount_isFalseForPathWithAdditionalSlash) {
+    EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/", "1234"));
+    EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/data/", "1234"));
+    EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/obb/", "1234"));
+
+    EXPECT_FALSE(containsMount("//storage/emulated/1234/Android", "1234"));
+    EXPECT_FALSE(containsMount("/storage/emulated//1234/Android/data", "1234"));
+    EXPECT_FALSE(containsMount("/storage/emulated/1234//Android/data", "1234"));
+}
+
+TEST(FuseUtilsTest, testContainsMount_isFalseForPathWithWrongUserid) {
+    EXPECT_FALSE(containsMount("/storage/emulated/11234/Android", "1234"));
+    EXPECT_FALSE(containsMount("/storage/emulated/0/Android/data", "1234"));
+    EXPECT_FALSE(containsMount("/storage/emulated/12345/Android/obb", "1234"));
+    EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/obb", "5678"));
+}
diff --git a/jni/FuseUtilsTest.xml b/jni/FuseUtilsTest.xml
new file mode 100644
index 0000000..46fccac
--- /dev/null
+++ b/jni/FuseUtilsTest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<!-- Note: this is derived from the autogenerated configuration. We require
+           root support. -->
+<configuration description="Runs FuseUtilsTest">
+    <option name="test-suite-tag" value="mts" />
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="FuseUtilsTest->/data/local/tmp/FuseUtilsTest" />
+        <option name="append-bitness" value="true" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+    <test class="com.android.tradefed.testtype.GTest" >
+        <option name="native-test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="FuseUtilsTest" />
+        <option name="runtime-hint" value="10m" />
+        <!-- test-timeout unit is ms, value = 10 min -->
+        <option name="native-test-timeout" value="600000" />
+    </test>
+
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.mediaprovider" />
+    </object>
+</configuration>
diff --git a/jni/MediaProviderWrapper.cpp b/jni/MediaProviderWrapper.cpp
new file mode 100644
index 0000000..04c2503
--- /dev/null
+++ b/jni/MediaProviderWrapper.cpp
@@ -0,0 +1,477 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specic language governing permissions and
+ * limitations under the License.
+ */
+
+#include "MediaProviderWrapper.h"
+#include "libfuse_jni/ReaddirHelper.h"
+
+#include <android-base/logging.h>
+#include <android-base/properties.h>
+#include <jni.h>
+#include <nativehelper/scoped_local_ref.h>
+#include <nativehelper/scoped_primitive_array.h>
+#include <nativehelper/scoped_utf_chars.h>
+
+#include <pthread.h>
+
+#include <mutex>
+#include <unordered_map>
+
+namespace mediaprovider {
+namespace fuse {
+using android::base::GetBoolProperty;
+using std::string;
+
+namespace {
+
+constexpr const char* kPropRedactionEnabled = "persist.sys.fuse.redaction-enabled";
+
+constexpr uid_t ROOT_UID = 0;
+constexpr uid_t SHELL_UID = 2000;
+
+/** Private helper functions **/
+
+inline bool shouldBypassMediaProvider(uid_t uid) {
+    return uid == SHELL_UID || uid == ROOT_UID;
+}
+
+static bool CheckForJniException(JNIEnv* env) {
+    if (env->ExceptionCheck()) {
+        env->ExceptionDescribe();
+        env->ExceptionClear();
+        return true;
+    }
+    return false;
+}
+
+std::unique_ptr<RedactionInfo> getRedactionInfoInternal(JNIEnv* env, jobject media_provider_object,
+                                                        jmethodID mid_get_redaction_ranges,
+                                                        uid_t uid, pid_t tid, const string& path) {
+    ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
+    ScopedLongArrayRO redaction_ranges(
+            env, static_cast<jlongArray>(env->CallObjectMethod(
+                         media_provider_object, mid_get_redaction_ranges, j_path.get(), uid, tid)));
+
+    if (CheckForJniException(env)) {
+        return nullptr;
+    }
+
+    std::unique_ptr<RedactionInfo> ri;
+    if (redaction_ranges.size() % 2) {
+        LOG(ERROR) << "Error while calculating redaction ranges: array length is uneven";
+    } else if (redaction_ranges.size() > 0) {
+        ri = std::make_unique<RedactionInfo>(redaction_ranges.size() / 2, redaction_ranges.get());
+    } else {
+        // No ranges to redact
+        ri = std::make_unique<RedactionInfo>();
+    }
+
+    return ri;
+}
+
+int insertFileInternal(JNIEnv* env, jobject media_provider_object, jmethodID mid_insert_file,
+                       const string& path, uid_t uid) {
+    ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
+    int res = env->CallIntMethod(media_provider_object, mid_insert_file, j_path.get(), uid);
+
+    if (CheckForJniException(env)) {
+        return EFAULT;
+    }
+    return res;
+}
+
+int deleteFileInternal(JNIEnv* env, jobject media_provider_object, jmethodID mid_delete_file,
+                       const string& path, uid_t uid) {
+    ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
+    int res = env->CallIntMethod(media_provider_object, mid_delete_file, j_path.get(), uid);
+
+    if (CheckForJniException(env)) {
+        return EFAULT;
+    }
+    return res;
+}
+
+int isOpenAllowedInternal(JNIEnv* env, jobject media_provider_object, jmethodID mid_is_open_allowed,
+                          const string& path, uid_t uid, bool for_write) {
+    ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
+    int res = env->CallIntMethod(media_provider_object, mid_is_open_allowed, j_path.get(), uid,
+                                 for_write);
+
+    if (CheckForJniException(env)) {
+        return EFAULT;
+    }
+    return res;
+}
+
+void scanFileInternal(JNIEnv* env, jobject media_provider_object, jmethodID mid_scan_file,
+                      const string& path) {
+    ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
+    env->CallVoidMethod(media_provider_object, mid_scan_file, j_path.get());
+    CheckForJniException(env);
+}
+
+int isMkdirOrRmdirAllowedInternal(JNIEnv* env, jobject media_provider_object,
+                                  jmethodID mid_is_mkdir_or_rmdir_allowed, const string& path,
+                                  uid_t uid, bool forCreate) {
+    ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
+    int res = env->CallIntMethod(media_provider_object, mid_is_mkdir_or_rmdir_allowed, j_path.get(),
+                                 uid, forCreate);
+
+    if (CheckForJniException(env)) {
+        return EFAULT;
+    }
+    return res;
+}
+
+int isOpendirAllowedInternal(JNIEnv* env, jobject media_provider_object,
+                             jmethodID mid_is_opendir_allowed, const string& path, uid_t uid,
+                             bool forWrite) {
+    ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
+    int res = env->CallIntMethod(media_provider_object, mid_is_opendir_allowed, j_path.get(), uid,
+                                 forWrite);
+
+    if (CheckForJniException(env)) {
+        return EFAULT;
+    }
+    return res;
+}
+
+bool isUidForPackageInternal(JNIEnv* env, jobject media_provider_object,
+                             jmethodID mid_is_uid_for_package, const string& pkg, uid_t uid) {
+    ScopedLocalRef<jstring> j_pkg(env, env->NewStringUTF(pkg.c_str()));
+    bool res = env->CallBooleanMethod(media_provider_object, mid_is_uid_for_package, j_pkg.get(),
+            uid);
+
+    if (CheckForJniException(env)) {
+        return false;
+    }
+    return res;
+}
+
+std::vector<std::shared_ptr<DirectoryEntry>> getFilesInDirectoryInternal(
+        JNIEnv* env, jobject media_provider_object, jmethodID mid_get_files_in_dir, uid_t uid,
+        const string& path) {
+    std::vector<std::shared_ptr<DirectoryEntry>> directory_entries;
+    ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
+
+    ScopedLocalRef<jobjectArray> files_list(
+            env, static_cast<jobjectArray>(env->CallObjectMethod(
+                         media_provider_object, mid_get_files_in_dir, j_path.get(), uid)));
+
+    if (CheckForJniException(env)) {
+        directory_entries.push_back(std::make_shared<DirectoryEntry>("", EFAULT));
+        return directory_entries;
+    }
+
+    int de_count = env->GetArrayLength(files_list.get());
+    if (de_count == 1) {
+        ScopedLocalRef<jstring> j_d_name(env,
+                                         (jstring)env->GetObjectArrayElement(files_list.get(), 0));
+        ScopedUtfChars d_name(env, j_d_name.get());
+        if (d_name.c_str() == nullptr) {
+            LOG(ERROR) << "Error reading file name returned from MediaProvider at index " << 0;
+            directory_entries.push_back(std::make_shared<DirectoryEntry>("", EFAULT));
+            return directory_entries;
+        } else if (d_name.c_str()[0] == '\0') {
+            // Calling package has no storage permissions.
+            directory_entries.push_back(std::make_shared<DirectoryEntry>("", EPERM));
+            return directory_entries;
+        }
+    }
+
+    for (int i = 0; i < de_count; i++) {
+        ScopedLocalRef<jstring> j_d_name(env,
+                                         (jstring)env->GetObjectArrayElement(files_list.get(), i));
+        ScopedUtfChars d_name(env, j_d_name.get());
+
+        if (d_name.c_str() == nullptr) {
+            LOG(ERROR) << "Error reading file name returned from MediaProvider at index " << i;
+            directory_entries.resize(0);
+            directory_entries.push_back(std::make_shared<DirectoryEntry>("", EFAULT));
+            break;
+        }
+        directory_entries.push_back(std::make_shared<DirectoryEntry>(d_name.c_str(), DT_REG));
+    }
+    return directory_entries;
+}
+
+int renameInternal(JNIEnv* env, jobject media_provider_object, jmethodID mid_rename,
+                   const string& old_path, const string& new_path, uid_t uid) {
+    ScopedLocalRef<jstring> j_old_path(env, env->NewStringUTF(old_path.c_str()));
+    ScopedLocalRef<jstring> j_new_path(env, env->NewStringUTF(new_path.c_str()));
+    int res = env->CallIntMethod(media_provider_object, mid_rename, j_old_path.get(),
+                                 j_new_path.get(), uid);
+
+    if (CheckForJniException(env)) {
+        return EFAULT;
+    }
+    return res;
+}
+
+void onFileCreatedInternal(JNIEnv* env, jobject media_provider_object,
+                           jmethodID mid_on_file_created, const string& path) {
+    ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
+
+    env->CallVoidMethod(media_provider_object, mid_on_file_created, j_path.get());
+    CheckForJniException(env);
+    return;
+}
+
+}  // namespace
+/*****************************************************************************************/
+/******************************* Public API Implementation *******************************/
+/*****************************************************************************************/
+
+JavaVM* MediaProviderWrapper::gJavaVm = nullptr;
+pthread_key_t MediaProviderWrapper::gJniEnvKey;
+
+void MediaProviderWrapper::OneTimeInit(JavaVM* vm) {
+    gJavaVm = vm;
+    CHECK(gJavaVm != nullptr);
+
+    pthread_key_create(&MediaProviderWrapper::gJniEnvKey,
+                       MediaProviderWrapper::DetachThreadFunction);
+}
+
+MediaProviderWrapper::MediaProviderWrapper(JNIEnv* env, jobject media_provider) {
+    if (!media_provider) {
+        LOG(FATAL) << "MediaProvider is null!";
+    }
+
+    media_provider_object_ = reinterpret_cast<jobject>(env->NewGlobalRef(media_provider));
+    media_provider_class_ = env->FindClass("com/android/providers/media/MediaProvider");
+    if (!media_provider_class_) {
+        LOG(FATAL) << "Could not find class MediaProvider";
+    }
+    media_provider_class_ = reinterpret_cast<jclass>(env->NewGlobalRef(media_provider_class_));
+
+    // Cache methods - Before calling a method, make sure you cache it here
+    mid_get_redaction_ranges_ = CacheMethod(env, "getRedactionRanges", "(Ljava/lang/String;II)[J",
+                                            /*is_static*/ false);
+    mid_insert_file_ = CacheMethod(env, "insertFileIfNecessary", "(Ljava/lang/String;I)I",
+                                   /*is_static*/ false);
+    mid_delete_file_ = CacheMethod(env, "deleteFile", "(Ljava/lang/String;I)I", /*is_static*/ false);
+    mid_is_open_allowed_ = CacheMethod(env, "isOpenAllowed", "(Ljava/lang/String;IZ)I",
+                                       /*is_static*/ false);
+    mid_scan_file_ = CacheMethod(env, "scanFile", "(Ljava/lang/String;)V",
+                                 /*is_static*/ false);
+    mid_is_mkdir_or_rmdir_allowed_ = CacheMethod(env, "isDirectoryCreationOrDeletionAllowed",
+                                                 "(Ljava/lang/String;IZ)I", /*is_static*/ false);
+    mid_is_opendir_allowed_ = CacheMethod(env, "isOpendirAllowed", "(Ljava/lang/String;IZ)I",
+                                          /*is_static*/ false);
+    mid_get_files_in_dir_ =
+            CacheMethod(env, "getFilesInDirectory", "(Ljava/lang/String;I)[Ljava/lang/String;",
+                        /*is_static*/ false);
+    mid_rename_ = CacheMethod(env, "rename", "(Ljava/lang/String;Ljava/lang/String;I)I",
+                              /*is_static*/ false);
+    mid_is_uid_for_package_ = CacheMethod(env, "isUidForPackage", "(Ljava/lang/String;I)Z",
+                              /*is_static*/ false);
+    mid_on_file_created_ = CacheMethod(env, "onFileCreated", "(Ljava/lang/String;)V",
+                                       /*is_static*/ false);
+}
+
+MediaProviderWrapper::~MediaProviderWrapper() {
+    JNIEnv* env = MaybeAttachCurrentThread();
+    env->DeleteGlobalRef(media_provider_object_);
+    env->DeleteGlobalRef(media_provider_class_);
+}
+
+std::unique_ptr<RedactionInfo> MediaProviderWrapper::GetRedactionInfo(const string& path, uid_t uid,
+                                                                      pid_t tid) {
+    if (shouldBypassMediaProvider(uid) || !GetBoolProperty(kPropRedactionEnabled, true)) {
+        return std::make_unique<RedactionInfo>();
+    }
+
+    // Default value in case JNI thread was being terminated, causes the read to fail.
+    std::unique_ptr<RedactionInfo> res = nullptr;
+
+    JNIEnv* env = MaybeAttachCurrentThread();
+    auto ri = getRedactionInfoInternal(env, media_provider_object_, mid_get_redaction_ranges_, uid,
+                                       tid, path);
+    res = std::move(ri);
+
+    return res;
+}
+
+int MediaProviderWrapper::InsertFile(const string& path, uid_t uid) {
+    if (uid == ROOT_UID) {
+        return 0;
+    }
+
+    JNIEnv* env = MaybeAttachCurrentThread();
+    return insertFileInternal(env, media_provider_object_, mid_insert_file_, path, uid);
+}
+
+int MediaProviderWrapper::DeleteFile(const string& path, uid_t uid) {
+    if (uid == ROOT_UID) {
+        int res = unlink(path.c_str());
+        return res;
+    }
+
+    JNIEnv* env = MaybeAttachCurrentThread();
+    return deleteFileInternal(env, media_provider_object_, mid_delete_file_, path, uid);
+}
+
+int MediaProviderWrapper::IsOpenAllowed(const string& path, uid_t uid, bool for_write) {
+    if (shouldBypassMediaProvider(uid)) {
+        return 0;
+    }
+
+    JNIEnv* env = MaybeAttachCurrentThread();
+    return isOpenAllowedInternal(env, media_provider_object_, mid_is_open_allowed_, path, uid,
+                                 for_write);
+}
+
+void MediaProviderWrapper::ScanFile(const string& path) {
+    JNIEnv* env = MaybeAttachCurrentThread();
+    scanFileInternal(env, media_provider_object_, mid_scan_file_, path);
+}
+
+int MediaProviderWrapper::IsCreatingDirAllowed(const string& path, uid_t uid) {
+    if (shouldBypassMediaProvider(uid)) {
+        return 0;
+    }
+
+    JNIEnv* env = MaybeAttachCurrentThread();
+    return isMkdirOrRmdirAllowedInternal(env, media_provider_object_,
+                                         mid_is_mkdir_or_rmdir_allowed_, path, uid,
+                                         /*forCreate*/ true);
+}
+
+int MediaProviderWrapper::IsDeletingDirAllowed(const string& path, uid_t uid) {
+    if (shouldBypassMediaProvider(uid)) {
+        return 0;
+    }
+
+    JNIEnv* env = MaybeAttachCurrentThread();
+    return isMkdirOrRmdirAllowedInternal(env, media_provider_object_,
+                                         mid_is_mkdir_or_rmdir_allowed_, path, uid,
+                                         /*forCreate*/ false);
+}
+
+std::vector<std::shared_ptr<DirectoryEntry>> MediaProviderWrapper::GetDirectoryEntries(
+        uid_t uid, const string& path, DIR* dirp) {
+    // Default value in case JNI thread was being terminated
+    std::vector<std::shared_ptr<DirectoryEntry>> res;
+    if (shouldBypassMediaProvider(uid)) {
+        addDirectoryEntriesFromLowerFs(dirp, /* filter */ nullptr, &res);
+        return res;
+    }
+
+    JNIEnv* env = MaybeAttachCurrentThread();
+    res = getFilesInDirectoryInternal(env, media_provider_object_, mid_get_files_in_dir_, uid, path);
+
+    const int res_size = res.size();
+    if (res_size && res[0]->d_name[0] == '/') {
+        // Path is unknown to MediaProvider, get files and directories from lower file system.
+        res.resize(0);
+        addDirectoryEntriesFromLowerFs(dirp, /* filter */ nullptr, &res);
+    } else if (res_size == 0 || !res[0]->d_name.empty()) {
+        // add directory names from lower file system.
+        addDirectoryEntriesFromLowerFs(dirp, /* filter */ &isDirectory, &res);
+    }
+    return res;
+}
+
+int MediaProviderWrapper::IsOpendirAllowed(const string& path, uid_t uid, bool forWrite) {
+    if (shouldBypassMediaProvider(uid)) {
+        return 0;
+    }
+
+    JNIEnv* env = MaybeAttachCurrentThread();
+    return isOpendirAllowedInternal(env, media_provider_object_, mid_is_opendir_allowed_, path, uid,
+                                    forWrite);
+}
+
+bool MediaProviderWrapper::IsUidForPackage(const string& pkg, uid_t uid) {
+    if (shouldBypassMediaProvider(uid)) {
+        return true;
+    }
+
+    JNIEnv* env = MaybeAttachCurrentThread();
+    return isUidForPackageInternal(env, media_provider_object_, mid_is_uid_for_package_, pkg, uid);
+}
+
+int MediaProviderWrapper::Rename(const string& old_path, const string& new_path, uid_t uid) {
+    // Rename from SHELL_UID should go through MediaProvider to update database rows, so only bypass
+    // MediaProvider for ROOT_UID.
+    if (uid == ROOT_UID) {
+        int res = rename(old_path.c_str(), new_path.c_str());
+        if (res != 0) res = -errno;
+        return res;
+    }
+
+    JNIEnv* env = MaybeAttachCurrentThread();
+    return renameInternal(env, media_provider_object_, mid_rename_, old_path, new_path, uid);
+}
+
+void MediaProviderWrapper::OnFileCreated(const string& path) {
+    JNIEnv* env = MaybeAttachCurrentThread();
+
+    return onFileCreatedInternal(env, media_provider_object_, mid_on_file_created_, path);
+}
+
+/*****************************************************************************************/
+/******************************** Private member functions *******************************/
+/*****************************************************************************************/
+
+/**
+ * Finds MediaProvider method and adds it to methods map so it can be quickly called later.
+ */
+jmethodID MediaProviderWrapper::CacheMethod(JNIEnv* env, const char method_name[],
+                                            const char signature[], bool is_static) {
+    jmethodID mid;
+    string actual_method_name(method_name);
+    actual_method_name.append("ForFuse");
+    if (is_static) {
+        mid = env->GetStaticMethodID(media_provider_class_, actual_method_name.c_str(), signature);
+    } else {
+        mid = env->GetMethodID(media_provider_class_, actual_method_name.c_str(), signature);
+    }
+    if (!mid) {
+        // SHOULD NOT HAPPEN!
+        LOG(FATAL) << "Error caching method: " << method_name << signature;
+    }
+    return mid;
+}
+
+void MediaProviderWrapper::DetachThreadFunction(void* unused) {
+    int detach = gJavaVm->DetachCurrentThread();
+    CHECK_EQ(detach, 0);
+}
+
+JNIEnv* MediaProviderWrapper::MaybeAttachCurrentThread() {
+    // We could use pthread_getspecific here as that's likely quicker but
+    // that would result in wrong behaviour for threads that don't need to
+    // be attached (e.g, those that were created in managed code).
+    JNIEnv* env = nullptr;
+    if (gJavaVm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_4) == JNI_OK) {
+        return env;
+    }
+
+    // This thread is currently unattached, so it must not have any TLS
+    // value. Note that we don't really care about the actual value we store
+    // in TLS -- we only care about the value destructor being called, which
+    // will happen as long as the key is not null.
+    CHECK(pthread_getspecific(gJniEnvKey) == nullptr);
+    CHECK_EQ(gJavaVm->AttachCurrentThread(&env, nullptr), 0);
+    CHECK(env != nullptr);
+
+    pthread_setspecific(gJniEnvKey, env);
+    return env;
+}
+
+}  // namespace fuse
+}  // namespace mediaprovider
diff --git a/jni/MediaProviderWrapper.h b/jni/MediaProviderWrapper.h
new file mode 100644
index 0000000..1f74acc
--- /dev/null
+++ b/jni/MediaProviderWrapper.h
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specic language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEDIAPROVIDER_FUSE_MEDIAPROVIDERWRAPPER_H_
+#define MEDIAPROVIDER_FUSE_MEDIAPROVIDERWRAPPER_H_
+
+#include <jni.h>
+#include <sys/types.h>
+
+#include <dirent.h>
+#include <atomic>
+#include <condition_variable>
+#include <functional>
+#include <mutex>
+#include <queue>
+#include <string>
+#include <thread>
+
+#include "libfuse_jni/ReaddirHelper.h"
+#include "libfuse_jni/RedactionInfo.h"
+
+namespace mediaprovider {
+namespace fuse {
+
+/**
+ * Class that wraps MediaProvider.java and all of the needed JNI calls to make
+ * interaction with MediaProvider easier.
+ */
+class MediaProviderWrapper final {
+  public:
+    MediaProviderWrapper(JNIEnv* env, jobject media_provider);
+    ~MediaProviderWrapper();
+
+    /**
+     * Computes and returns the RedactionInfo for a given file and UID.
+     *
+     * @param uid UID of the app requesting the read
+     * @param path path of the requested file
+     * @return RedactionInfo on success, nullptr on failure to calculate
+     * redaction ranges (e.g. exception was thrown in Java world)
+     */
+    std::unique_ptr<RedactionInfo> GetRedactionInfo(const std::string& path, uid_t uid, pid_t tid);
+
+    /**
+     * Inserts a new entry for the given path and UID.
+     *
+     * @param path the path of the file to be created
+     * @param uid UID of the calling app
+     * @return 0 if the operation succeeded,
+     * or errno error code if operation fails.
+     */
+    int InsertFile(const std::string& path, uid_t uid);
+
+    /**
+     * Delete the file denoted by the given path on behalf of the given UID.
+     *
+     * @param path the path of the file to be deleted
+     * @param uid UID of the calling app
+     * @return 0 upon success, or errno error code if operation fails.
+     */
+    int DeleteFile(const std::string& path, uid_t uid);
+
+    /**
+     * Gets directory entries for given path from MediaProvider database and lower file system
+     *
+     * @param uid UID of the calling app.
+     * @param path Relative path of the directory.
+     * @param dirp Pointer to directory stream, used to query lower file system.
+     * @return DirectoryEntries with list of directory entries on success.
+     * File names in a directory are obtained from MediaProvider. If a path is unknown to
+     * MediaProvider, file names are obtained from lower file system. All directory names in the
+     * given directory are obtained from lower file system.
+     * An empty string in first directory entry name indicates the error occurred while obtaining
+     * directory entries, directory entry type will hold the corresponding errno information.
+     */
+    std::vector<std::shared_ptr<DirectoryEntry>> GetDirectoryEntries(uid_t uid,
+                                                                     const std::string& path,
+                                                                     DIR* dirp);
+
+    /**
+     * Determines if the given UID is allowed to open the file denoted by the given path.
+     *
+     * @param path the path of the file to be opened
+     * @param uid UID of the calling app
+     * @param for_write specifies if the file is to be opened for write
+     * @return 0 upon success or errno value upon failure.
+     */
+    int IsOpenAllowed(const std::string& path, uid_t uid, bool for_write);
+
+    /**
+     * Potentially triggers a scan of the file before closing it and reconciles it with the
+     * MediaProvider database.
+     *
+     * @param path the path of the file to be scanned
+     */
+    void ScanFile(const std::string& path);
+
+    /**
+     * Determines if the given UID is allowed to create a directory with the given path.
+     *
+     * @param path the path of the directory to be created
+     * @param uid UID of the calling app
+     * @return 0 if it's allowed, or errno error code if operation isn't allowed.
+     */
+    int IsCreatingDirAllowed(const std::string& path, uid_t uid);
+
+    /**
+     * Determines if the given UID is allowed to delete the directory with the given path.
+     *
+     * @param path the path of the directory to be deleted
+     * @param uid UID of the calling app
+     * @return 0 if it's allowed, or errno error code if operation isn't allowed.
+     */
+    int IsDeletingDirAllowed(const std::string& path, uid_t uid);
+
+    /**
+     * Determines if the given UID is allowed to open the directory with the given path.
+     *
+     * @param path the path of the directory to be opened
+     * @param uid UID of the calling app
+     * @param forWrite if it's a write access
+     * @return 0 if it's allowed, or errno error code if operation isn't allowed.
+     */
+    int IsOpendirAllowed(const std::string& path, uid_t uid, bool forWrite);
+
+    /**
+     * Determines if the given package name matches its uid.
+     *
+     * @param pkg the package name of the app
+     * @param uid UID of the app
+     * @return true if it matches, otherwise return false.
+     */
+    bool IsUidForPackage(const std::string& pkg, uid_t uid);
+
+    /**
+     * Renames a file or directory to new path.
+     *
+     * @param old_path path of the file or directory to be renamed.
+     * @param new_path new path of the file or directory to be renamed.
+     * @param uid UID of the calling app.
+     * @return 0 if rename is successful, errno if one of the rename fails. If return
+     * value is 0, it's guaranteed that file/directory is moved to new_path. For any other errno
+     * except EFAULT/EIO, it's guaranteed that file/directory is not renamed.
+     */
+    int Rename(const std::string& old_path, const std::string& new_path, uid_t uid);
+
+    /**
+     * Called whenever a file has been created through FUSE.
+     *
+     * @param path path of the file that has been created.
+     */
+    void OnFileCreated(const std::string& path);
+
+    /**
+     * Initializes per-process static variables associated with the lifetime of
+     * a managed runtime.
+     */
+    static void OneTimeInit(JavaVM* vm);
+
+    /** TLS Key to map a given thread to its JNIEnv. */
+    static pthread_key_t gJniEnvKey;
+
+  private:
+    jclass media_provider_class_;
+    jobject media_provider_object_;
+    /** Cached MediaProvider method IDs **/
+    jmethodID mid_get_redaction_ranges_;
+    jmethodID mid_insert_file_;
+    jmethodID mid_delete_file_;
+    jmethodID mid_is_open_allowed_;
+    jmethodID mid_scan_file_;
+    jmethodID mid_is_mkdir_or_rmdir_allowed_;
+    jmethodID mid_is_opendir_allowed_;
+    jmethodID mid_get_files_in_dir_;
+    jmethodID mid_rename_;
+    jmethodID mid_is_uid_for_package_;
+    jmethodID mid_on_file_created_;
+
+    /**
+     * Auxiliary for caching MediaProvider methods.
+     */
+    jmethodID CacheMethod(JNIEnv* env, const char method_name[], const char signature[],
+                          bool is_static);
+
+    // Attaches the current thread (if necessary) and returns the JNIEnv
+    // associated with it.
+    static JNIEnv* MaybeAttachCurrentThread();
+    // Destructor function for a given native thread. Called precisely once
+    // by the pthreads library.
+    static void DetachThreadFunction(void* unused);
+
+    static JavaVM* gJavaVm;
+};
+
+}  // namespace fuse
+}  // namespace mediaprovider
+
+#endif  // MEDIAPROVIDER_FUSE_MEDIAPROVIDERWRAPPER_H_
diff --git a/jni/ReaddirHelper.cpp b/jni/ReaddirHelper.cpp
new file mode 100644
index 0000000..1fd1e7b
--- /dev/null
+++ b/jni/ReaddirHelper.cpp
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#define LOG_TAG "ReaddirHelper"
+
+#include "libfuse_jni/ReaddirHelper.h"
+#include <android-base/logging.h>
+#include <sys/types.h>
+
+namespace mediaprovider {
+namespace fuse {
+namespace {
+
+inline bool is_dot_or_dotdot(const char* name) {
+    return name && name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'));
+}
+
+}  // namespace
+
+bool isDirectory(const dirent& entry) {
+    if (entry.d_type == DT_DIR) return true;
+    return false;
+}
+
+void addDirectoryEntriesFromLowerFs(DIR* dirp, bool (*const filter)(const dirent&),
+        std::vector<std::shared_ptr<DirectoryEntry>>* directory_entries) {
+    while (1) {
+        errno = 0;
+        const struct dirent* entry = readdir(dirp);
+        if (entry == nullptr) {
+            if (errno) {
+                PLOG(ERROR) << "DEBUG: readdir(): readdir failed with %d" << errno;
+                directory_entries->resize(0);
+                directory_entries->push_back(std::make_shared<DirectoryEntry>("", errno));
+            }
+            break;
+        }
+        // Ignore '.' & '..' to maintain consistency with directory entries
+        // returned by MediaProvider.
+        if (is_dot_or_dotdot(entry->d_name)) continue;
+        if (filter == nullptr || filter(*entry)) {
+            directory_entries->push_back(
+                    std::make_shared<DirectoryEntry>(entry->d_name, entry->d_type));
+        }
+    }
+}
+
+}  // namespace fuse
+}  // namespace mediaprovider
diff --git a/jni/RedactionInfo.cpp b/jni/RedactionInfo.cpp
new file mode 100644
index 0000000..17de22e
--- /dev/null
+++ b/jni/RedactionInfo.cpp
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specic language governing permissions and
+ * limitations under the License.
+ */
+
+#include "include/libfuse_jni/RedactionInfo.h"
+
+using std::unique_ptr;
+using std::vector;
+
+namespace mediaprovider {
+namespace fuse {
+
+/**
+ * Merges any overlapping ranges into 1 range.
+ *
+ * Given ranges should be sorted, and they remain sorted.
+ */
+static void mergeOverlappingRedactionRanges(vector<RedactionRange>& ranges) {
+    int newRangesSize = ranges.size();
+    for (int i = 0; i < ranges.size() - 1; ++i) {
+        if (ranges[i].second >= ranges[i + 1].first) {
+            ranges[i + 1].first = ranges[i].first;
+            ranges[i + 1].second = std::max(ranges[i].second, ranges[i + 1].second);
+            // Invalidate the redundant range
+            ranges[i].first = LONG_MAX;
+            ranges[i].second = LONG_MAX;
+            newRangesSize--;
+        }
+    }
+    if (newRangesSize < ranges.size()) {
+        // Move invalid ranges to end of array
+        std::sort(ranges.begin(), ranges.end());
+        ranges.resize(newRangesSize);
+    }
+}
+
+/**
+ * Determine whether the read request overlaps with the redaction ranges
+ * defined by the given RedactionInfo.
+ *
+ * This function assumes redaction_ranges_ within RedactionInfo is sorted.
+ */
+bool RedactionInfo::hasOverlapWithReadRequest(size_t size, off64_t off) const {
+    if (!isRedactionNeeded() || off > redaction_ranges_.back().second ||
+        off + size < redaction_ranges_.front().first) {
+        return false;
+    }
+    return true;
+}
+
+/**
+ * Sets the redaction ranges in RedactionInfo, sort the ranges and merge
+ * overlapping ranges.
+ */
+void RedactionInfo::processRedactionRanges(int redaction_ranges_num,
+                                           const off64_t* redaction_ranges) {
+    redaction_ranges_.resize(redaction_ranges_num);
+    for (int i = 0; i < redaction_ranges_num; ++i) {
+        redaction_ranges_[i].first = static_cast<off64_t>(redaction_ranges[2 * i]);
+        redaction_ranges_[i].second = static_cast<off64_t>(redaction_ranges[2 * i + 1]);
+    }
+    std::sort(redaction_ranges_.begin(), redaction_ranges_.end());
+    mergeOverlappingRedactionRanges(redaction_ranges_);
+}
+
+int RedactionInfo::size() const {
+    return redaction_ranges_.size();
+}
+
+bool RedactionInfo::isRedactionNeeded() const {
+    return size() > 0;
+}
+
+RedactionInfo::RedactionInfo(int redaction_ranges_num, const off64_t* redaction_ranges) {
+    if (redaction_ranges == 0) return;
+    processRedactionRanges(redaction_ranges_num, redaction_ranges);
+}
+
+unique_ptr<vector<RedactionRange>> RedactionInfo::getOverlappingRedactionRanges(size_t size,
+                                                                                off64_t off) const {
+    if (hasOverlapWithReadRequest(size, off)) {
+        auto first_redaction = redaction_ranges_.end();
+        auto last_redaction = redaction_ranges_.end();
+        for (auto iter = redaction_ranges_.begin(); iter != redaction_ranges_.end(); ++iter) {
+            const RedactionRange& rr = *iter;
+            // Look for the first range that overlaps with the read request
+            if (first_redaction == redaction_ranges_.end() && off <= rr.second &&
+                off + size >= rr.first) {
+                first_redaction = iter;
+            } else if (first_redaction != redaction_ranges_.end() && off + size < rr.first) {
+                // Once we're in the read request range, we start checking if
+                // we're out of it so we can return the result to the caller
+                break;
+            }
+            last_redaction = iter;
+        }
+        if (first_redaction != redaction_ranges_.end()) {
+            return std::make_unique<vector<RedactionRange>>(first_redaction, last_redaction + 1);
+        }
+    }
+    return std::make_unique<vector<RedactionRange>>();
+}
+}  // namespace fuse
+}  // namespace mediaprovider
diff --git a/jni/RedactionInfoTest.cpp b/jni/RedactionInfoTest.cpp
new file mode 100644
index 0000000..9d98058
--- /dev/null
+++ b/jni/RedactionInfoTest.cpp
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specic language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "RedactionInfoTest"
+
+#include <gtest/gtest.h>
+
+#include <memory>
+#include <vector>
+
+#include "libfuse_jni/RedactionInfo.h"
+
+using namespace mediaprovider::fuse;
+
+using std::unique_ptr;
+using std::vector;
+
+unique_ptr<vector<RedactionRange>> createRedactionRangeVector(int num_rr, off64_t* rr) {
+    auto res = std::make_unique<vector<RedactionRange>>();
+    for (int i = 0; i < num_rr; ++i) {
+        res->push_back(RedactionRange(rr[2 * i], rr[2 * i + 1]));
+    }
+    return res;
+}
+
+/**
+ * Test the case where there are no redaction ranges.
+ */
+TEST(RedactionInfoTest, testNoRedactionRanges) {
+    RedactionInfo info(0, nullptr);
+    EXPECT_EQ(0, info.size());
+    EXPECT_EQ(false, info.isRedactionNeeded());
+
+    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 1000, /*off*/ 1000);
+    EXPECT_EQ(0, overlapping_rr->size());
+}
+
+/**
+ * Test the case where there is 1 redaction range.
+ */
+TEST(RedactionInfoTest, testSingleRedactionRange) {
+    off64_t ranges[2] = {
+            1,
+            10,
+    };
+    RedactionInfo info(1, ranges);
+    EXPECT_EQ(1, info.size());
+    EXPECT_EQ(true, info.isRedactionNeeded());
+    // Overlapping ranges
+    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 1000, /*off*/ 0);
+    EXPECT_EQ(*(createRedactionRangeVector(1, ranges)), *overlapping_rr);
+
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 5, /*off*/ 0);
+    EXPECT_EQ(*(createRedactionRangeVector(1, ranges)), *overlapping_rr);
+
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 5, /*off*/ 5);
+    EXPECT_EQ(*(createRedactionRangeVector(1, ranges)), *overlapping_rr);
+
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 10, /*off*/ 1);
+    EXPECT_EQ(*(createRedactionRangeVector(1, ranges)), *overlapping_rr);
+
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 1, /*off*/ 1);
+    EXPECT_EQ(*(createRedactionRangeVector(1, ranges)), *overlapping_rr);
+
+    // Non-overlapping range
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 100, /*off*/ 11);
+    EXPECT_EQ(*(createRedactionRangeVector(0, nullptr)), *overlapping_rr);
+
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 1, /*off*/ 11);
+    EXPECT_EQ(*(createRedactionRangeVector(0, nullptr)), *overlapping_rr);
+}
+
+/**
+ * Test the case where the redaction ranges don't require sorting or merging
+ */
+TEST(RedactionInfoTest, testSortedAndNonOverlappingRedactionRanges) {
+    off64_t ranges[6] = {
+            1, 10, 15, 21, 32, 40,
+    };
+
+    RedactionInfo info = RedactionInfo(3, ranges);
+    EXPECT_EQ(3, info.size());
+    EXPECT_EQ(true, info.isRedactionNeeded());
+
+    // Read request strictly contains all ranges: [0, 49]
+    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 50, /*off*/ 0);
+    off64_t expected1[] = {
+            1, 10, 15, 21, 32, 40,
+    };
+    EXPECT_EQ(*(createRedactionRangeVector(3, expected1)), *overlapping_rr);
+
+    // Read request strictly contains a subset of the ranges: [15, 40]
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 26, /*off*/ 15);
+    off64_t expected2[] = {
+            15,
+            21,
+            32,
+            40,
+    };
+    EXPECT_EQ(*(createRedactionRangeVector(2, expected2)), *overlapping_rr);
+
+    // Read request intersects with a subset of the ranges" [16, 32]
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 17, /*off*/ 16);
+    EXPECT_EQ(*(createRedactionRangeVector(2, expected2)), *overlapping_rr);
+}
+
+/**
+ * Test the case where the redaction ranges require sorting
+ */
+TEST(RedactionInfoTest, testSortRedactionRanges) {
+    off64_t ranges[6] = {
+            1, 10, 32, 40, 15, 21,
+    };
+
+    RedactionInfo info = RedactionInfo(3, ranges);
+    EXPECT_EQ(3, info.size());
+    EXPECT_EQ(true, info.isRedactionNeeded());
+
+    // Read request strictly contains all ranges: [0, 49]
+    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 50, /*off*/ 0);
+    off64_t expected1[] = {
+            1, 10, 15, 21, 32, 40,
+    };
+    EXPECT_EQ(*(createRedactionRangeVector(3, expected1)), *overlapping_rr);
+
+    // Read request strictly contains a subset of the ranges: [15, 40]
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 26, /*off*/ 15);
+    off64_t expected2[] = {
+            15,
+            21,
+            32,
+            40,
+    };
+    EXPECT_EQ(*(createRedactionRangeVector(2, expected2)), *overlapping_rr);
+
+    // Read request intersects with a subset of the ranges" [16, 32]
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 17, /*off*/ 16);
+    EXPECT_EQ(*(createRedactionRangeVector(2, expected2)), *overlapping_rr);
+}
+
+/**
+ * Test the case where the redaction ranges require sorting or merging
+ */
+TEST(RedactionInfoTest, testSortAndMergeRedactionRanges) {
+    off64_t ranges[8] = {
+            35, 40, 1, 10, 32, 35, 15, 21,
+    };
+
+    RedactionInfo info = RedactionInfo(4, ranges);
+    EXPECT_EQ(3, info.size());
+    EXPECT_EQ(true, info.isRedactionNeeded());
+
+    // Read request strictly contains all ranges: [0, 49]
+    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 50, /*off*/ 0);
+    off64_t expected1[] = {
+            1, 10, 15, 21, 32, 40,
+    };
+    EXPECT_EQ(*(createRedactionRangeVector(3, expected1)), *overlapping_rr);
+
+    // Read request strictly contains a subset of the ranges: [15, 40]
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 26, /*off*/ 15);
+    off64_t expected2[] = {
+            15,
+            21,
+            32,
+            40,
+    };
+    EXPECT_EQ(*(createRedactionRangeVector(2, expected2)), *overlapping_rr);
+
+    // Read request intersects with a subset of the ranges" [16, 32]
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 17, /*off*/ 16);
+    EXPECT_EQ(*(createRedactionRangeVector(2, expected2)), *overlapping_rr);
+}
+
+/**
+ * Test the case where the redaction ranges all merge into the first range
+ */
+TEST(RedactionInfoTest, testMergeAllRangesIntoTheFirstRange) {
+    off64_t ranges[10] = {
+            1, 100, 2, 99, 3, 98, 4, 97, 3, 15,
+    };
+
+    RedactionInfo info = RedactionInfo(5, ranges);
+    EXPECT_EQ(1, info.size());
+    EXPECT_EQ(true, info.isRedactionNeeded());
+
+    // Read request equals the range: [1, 100]
+    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 100, /*off*/ 1);
+    off64_t expected[] = {1, 100};
+    EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
+
+    // Read request is contained in the range: [15, 40]
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 26, /*off*/ 15);
+    EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
+
+    // Read request that strictly contains all of the redaction ranges
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 1000, /*off*/ 0);
+    EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
+}
+
+/**
+ * Test the case where the redaction ranges all merge into the last range
+ */
+TEST(RedactionInfoTest, testMergeAllRangesIntoTheLastRange) {
+    off64_t ranges[10] = {
+            4, 96, 3, 97, 2, 98, 1, 99, 0, 100,
+    };
+
+    RedactionInfo info = RedactionInfo(5, ranges);
+    EXPECT_EQ(1, info.size());
+    EXPECT_EQ(true, info.isRedactionNeeded());
+
+    // Read request equals the range: [0, 100]
+    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 100, /*off*/ 0);
+    off64_t expected[] = {0, 100};
+    EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
+
+    // Read request is contained in the range: [15, 40]
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 26, /*off*/ 15);
+    EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
+
+    // Read request that strictly contains all of the redaction ranges
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 1000, /*off*/ 0);
+    EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
+}
+
+/**
+ * Test the case where the redaction ranges progressively merge
+ */
+TEST(RedactionInfoTest, testMergeAllRangesProgressively) {
+    off64_t ranges[10] = {
+            1, 11, 2, 12, 3, 13, 4, 14, 5, 15,
+    };
+
+    RedactionInfo info = RedactionInfo(5, ranges);
+    EXPECT_EQ(1, info.size());
+    EXPECT_EQ(true, info.isRedactionNeeded());
+
+    // Read request equals the range: [1, 15]
+    auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 15, /*off*/ 1);
+    off64_t expected[] = {1, 15};
+    EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
+
+    // Read request is contained in the range: [2, 12]
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 10, /*off*/ 2);
+    EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
+
+    // Read request that strictly contains all of the redaction ranges
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 100, /*off*/ 0);
+    EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
+
+    off64_t reverse_rr[10] = {
+            5, 15, 4, 14, 3, 13, 2, 12, 1, 11,
+    };
+
+    RedactionInfo reverse_info = RedactionInfo(5, reverse_rr);
+    EXPECT_EQ(1, info.size());
+    EXPECT_EQ(true, info.isRedactionNeeded());
+
+    // Read request equals the range: [1, 15]
+    overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 15, /*off*/ 1);
+    EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
+}
diff --git a/jni/RedactionInfoTest.xml b/jni/RedactionInfoTest.xml
new file mode 100644
index 0000000..cd00a9a
--- /dev/null
+++ b/jni/RedactionInfoTest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<!-- Note: this is derived from the autogenerated configuration. We require
+           root support. -->
+<configuration description="Runs RedactionInfoTest">
+    <option name="test-suite-tag" value="mts" />
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="RedactionInfoTest->/data/local/tmp/RedactionInfoTest" />
+        <option name="append-bitness" value="true" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+    <test class="com.android.tradefed.testtype.GTest" >
+        <option name="native-test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="RedactionInfoTest" />
+        <option name="runtime-hint" value="10m" />
+        <!-- test-timeout unit is ms, value = 10 min -->
+        <option name="native-test-timeout" value="600000" />
+    </test>
+
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.mediaprovider" />
+    </object>
+</configuration>
diff --git a/jni/TEST_MAPPING b/jni/TEST_MAPPING
new file mode 100644
index 0000000..5ee1bc6
--- /dev/null
+++ b/jni/TEST_MAPPING
@@ -0,0 +1,13 @@
+{
+  "presubmit": [
+    {
+      "name": "FuseUtilsTest"
+    },
+    {
+      "name": "RedactionInfoTest"
+    },
+    {
+      "name": "fuse_node_test"
+    }
+  ]
+}
diff --git a/jni/com_android_providers_media_FuseDaemon.cpp b/jni/com_android_providers_media_FuseDaemon.cpp
new file mode 100644
index 0000000..3a65696
--- /dev/null
+++ b/jni/com_android_providers_media_FuseDaemon.cpp
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specic language governing permissions and
+ * limitations under the License.
+ */
+
+// Need to use LOGE_EX.
+#define LOG_TAG "FuseDaemonJNI"
+
+#include <nativehelper/scoped_utf_chars.h>
+
+#include <string>
+
+#include "FuseDaemon.h"
+#include "MediaProviderWrapper.h"
+#include "android-base/logging.h"
+#include "android-base/unique_fd.h"
+
+namespace mediaprovider {
+namespace {
+
+constexpr const char* CLASS_NAME = "com/android/providers/media/fuse/FuseDaemon";
+static jclass gFuseDaemonClass;
+
+jlong com_android_providers_media_FuseDaemon_new(JNIEnv* env, jobject self,
+                                                 jobject media_provider) {
+    LOG(DEBUG) << "Creating the FUSE daemon...";
+    return reinterpret_cast<jlong>(new fuse::FuseDaemon(env, media_provider));
+}
+
+void com_android_providers_media_FuseDaemon_start(JNIEnv* env, jobject self, jlong java_daemon,
+                                                  jint fd, jstring java_path) {
+    LOG(DEBUG) << "Starting the FUSE daemon...";
+    fuse::FuseDaemon* const daemon = reinterpret_cast<fuse::FuseDaemon*>(java_daemon);
+
+    android::base::unique_fd ufd(fd);
+
+    ScopedUtfChars utf_chars_path(env, java_path);
+    if (!utf_chars_path.c_str()) {
+        return;
+    }
+
+    daemon->Start(std::move(ufd), utf_chars_path.c_str());
+}
+
+bool com_android_providers_media_FuseDaemon_is_started(JNIEnv* env, jobject self,
+                                                       jlong java_daemon) {
+    LOG(DEBUG) << "Checking if FUSE daemon started...";
+    const fuse::FuseDaemon* daemon = reinterpret_cast<fuse::FuseDaemon*>(java_daemon);
+    return daemon->IsStarted();
+}
+
+void com_android_providers_media_FuseDaemon_delete(JNIEnv* env, jobject self, jlong java_daemon) {
+    LOG(DEBUG) << "Destroying the FUSE daemon...";
+    fuse::FuseDaemon* const daemon = reinterpret_cast<fuse::FuseDaemon*>(java_daemon);
+    delete daemon;
+}
+
+jboolean com_android_providers_media_FuseDaemon_should_open_with_fuse(JNIEnv* env, jobject self,
+                                                                      jlong java_daemon,
+                                                                      jstring java_path,
+                                                                      jboolean for_read, jint fd) {
+    fuse::FuseDaemon* const daemon = reinterpret_cast<fuse::FuseDaemon*>(java_daemon);
+    if (daemon) {
+        ScopedUtfChars utf_chars_path(env, java_path);
+        if (!utf_chars_path.c_str()) {
+            // TODO(b/145741852): Throw exception
+            return JNI_FALSE;
+        }
+
+        return daemon->ShouldOpenWithFuse(fd, for_read, utf_chars_path.c_str());
+    }
+    // TODO(b/145741852): Throw exception
+    return JNI_FALSE;
+}
+
+void com_android_providers_media_FuseDaemon_invalidate_fuse_dentry_cache(JNIEnv* env, jobject self,
+                                                                         jlong java_daemon,
+                                                                         jstring java_path) {
+    fuse::FuseDaemon* const daemon = reinterpret_cast<fuse::FuseDaemon*>(java_daemon);
+    if (daemon) {
+        ScopedUtfChars utf_chars_path(env, java_path);
+        if (!utf_chars_path.c_str()) {
+            // TODO(b/145741152): Throw exception
+            return;
+        }
+
+        CHECK(pthread_getspecific(fuse::MediaProviderWrapper::gJniEnvKey) == nullptr);
+        daemon->InvalidateFuseDentryCache(utf_chars_path.c_str());
+    }
+    // TODO(b/145741152): Throw exception
+}
+
+bool com_android_providers_media_FuseDaemon_is_fuse_thread(JNIEnv* env, jclass clazz) {
+    return pthread_getspecific(fuse::MediaProviderWrapper::gJniEnvKey) != nullptr;
+}
+
+const JNINativeMethod methods[] = {
+        {"native_new", "(Lcom/android/providers/media/MediaProvider;)J",
+         reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_new)},
+        {"native_start", "(JILjava/lang/String;)V",
+         reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_start)},
+        {"native_delete", "(J)V",
+         reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_delete)},
+        {"native_should_open_with_fuse", "(JLjava/lang/String;ZI)Z",
+         reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_should_open_with_fuse)},
+        {"native_is_fuse_thread", "()Z",
+         reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_is_fuse_thread)},
+        {"native_is_started", "(J)Z",
+         reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_is_started)},
+        {"native_invalidate_fuse_dentry_cache", "(JLjava/lang/String;)V",
+         reinterpret_cast<void*>(
+                 com_android_providers_media_FuseDaemon_invalidate_fuse_dentry_cache)}};
+}  // namespace
+
+void register_android_providers_media_FuseDaemon(JavaVM* vm, JNIEnv* env) {
+    gFuseDaemonClass = static_cast<jclass>(env->NewGlobalRef(env->FindClass(CLASS_NAME)));
+
+    if (gFuseDaemonClass == nullptr) {
+        LOG(FATAL) << "Unable to find class : " << CLASS_NAME;
+    }
+
+    if (env->RegisterNatives(gFuseDaemonClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) {
+        LOG(FATAL) << "Unable to register native methods";
+    }
+
+    fuse::MediaProviderWrapper::OneTimeInit(vm);
+}
+}  // namespace mediaprovider
diff --git a/jni/fuse_node_test.xml b/jni/fuse_node_test.xml
new file mode 100644
index 0000000..512b01d
--- /dev/null
+++ b/jni/fuse_node_test.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<!-- Note: this is derived from the autogenerated configuration. We require
+           root support. -->
+<configuration description="Runs fuse_node_test">
+    <option name="test-suite-tag" value="mts" />
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="fuse_node_test->/data/local/tmp/fuse_node_test" />
+        <option name="append-bitness" value="true" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+    <test class="com.android.tradefed.testtype.GTest" >
+        <option name="native-test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="fuse_node_test" />
+        <option name="runtime-hint" value="10m" />
+        <!-- test-timeout unit is ms, value = 10 min -->
+        <option name="native-test-timeout" value="600000" />
+    </test>
+
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.mediaprovider" />
+    </object>
+</configuration>
diff --git a/jni/include/libfuse_jni/FuseUtils.h b/jni/include/libfuse_jni/FuseUtils.h
new file mode 100644
index 0000000..88044db
--- /dev/null
+++ b/jni/include/libfuse_jni/FuseUtils.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specic language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEDIAPROVIDER_JNI_UTILS_H_
+#define MEDIAPROVIDER_JNI_UTILS_H_
+
+#include <string>
+
+namespace mediaprovider {
+namespace fuse {
+
+/**
+ * Returns true if the given path (ignoring case) is mounted for the given
+ * userid. Mounted paths are:
+ * "/storage/emulated/<userid>/Android"
+ * "/storage/emulated/<userid>/Android/data"
+ * "/storage/emulated/<userid>/Android/obb" *
+ */
+bool containsMount(const std::string& path, const std::string& userid);
+
+}  // namespace fuse
+}  // namespace mediaprovider
+
+#endif  // MEDIAPROVIDER_JNI_UTILS_H_
diff --git a/jni/include/libfuse_jni/ReaddirHelper.h b/jni/include/libfuse_jni/ReaddirHelper.h
new file mode 100644
index 0000000..31a9aab
--- /dev/null
+++ b/jni/include/libfuse_jni/ReaddirHelper.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEDIA_PROVIDER_FUSE_READDIR_HELPER_H
+#define MEDIA_PROVIDER_FUSE_READDIR_HELPER_H
+
+#include <dirent.h>
+#include <string>
+#include <vector>
+
+namespace mediaprovider {
+namespace fuse {
+
+/**
+ * Holds a directory entry.
+ *
+ * DirectoryEntry object holds information about the directory entry such as
+ * name and type of the file or directory.
+ */
+struct DirectoryEntry {
+    /**
+     * Create a directory entry.
+     *
+     * @param name directory entry name.
+     * @param type directory entry type. Directory entry type corresponds to
+     * d_type of dirent structure defined in dirent.h
+     */
+    DirectoryEntry(const std::string& name, int type) : d_name(name), d_type(type) {}
+    const std::string d_name;
+    const int d_type;
+};
+
+/**
+ * Adds directory entries from lower file system to the list.
+ *
+ * If a filter is specified, directory entries must satisfy the given filter. If filter is null,
+ * all directory entries(except '.' & '..') are returned.
+ */
+void addDirectoryEntriesFromLowerFs(DIR* dirp, bool (*const filter)(const dirent&),
+        std::vector<std::shared_ptr<DirectoryEntry>>* directory_entries);
+
+/**
+ * Checks if the given dirent is directory.
+ */
+bool isDirectory(const dirent& entry);
+
+}  // namespace fuse
+}  // namespace mediaprovider
+#endif
diff --git a/jni/include/libfuse_jni/RedactionInfo.h b/jni/include/libfuse_jni/RedactionInfo.h
new file mode 100644
index 0000000..5218e28
--- /dev/null
+++ b/jni/include/libfuse_jni/RedactionInfo.h
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specic language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEDIA_PROVIDER_FUSE_REDACTIONINFO_H_
+#define MEDIA_PROVIDER_FUSE_REDACTIONINFO_H_
+
+#include <vector>
+
+namespace mediaprovider {
+namespace fuse {
+
+/**
+ * Type that represents a single redaction range within a file.
+ * first is the offset of the first byte in the redaction range within the file
+ * second is the offset of the last byte in the redaction range within the file
+ * Ranges are inclusive.
+ */
+typedef std::pair<off64_t, off64_t> RedactionRange;
+
+class RedactionInfo {
+  public:
+    /**
+     * Constructs a new instance of RedactionInfo based on the given redaction
+     * ranges.
+     *
+     * @param redaction_ranges_num number of redaction ranges, essentially HALF
+     * the size of the array redaction_ranges. If there are no redaction ranges,
+     * this value must be set to 0.
+     *
+     * @param redaction_ranges array that defines the redaction ranges in
+     * the following way:
+     * redaction ranges n = [ redaction_ranges[2n], redaction_ranges[2n+1]]
+     * This means that this array's length should be TWICE the value of
+     * redaction_ranges_num
+     */
+    RedactionInfo(int redaction_ranges_num, const off64_t* redaction_ranges);
+    /**
+     * Constructs a new instance of RedactionInfo with no redaction ranges.
+     */
+    RedactionInfo() = default;
+    /**
+     * Calls d'tor for redactionRanges (vector).
+     */
+    ~RedactionInfo() = default;
+    /**
+     * Calculates the redaction ranges that overlap with a given read request.
+     * The read request is defined by its size and the offset of its first byte.
+     *
+     * <p>The returned ranges are guaranteed to be:
+     *     * Non-overlapping (with each other)
+     *     * Sorted in an ascending order of offset
+     *
+     * @param size size of the read request
+     * @param off offset of the first byte of the read request
+     * @return unique_ptr to a vector of RedactionRanges. If there are no
+     * relevant redaction ranges, the vector will be empty.
+     */
+    std::unique_ptr<std::vector<RedactionRange>> getOverlappingRedactionRanges(size_t size,
+                                                                               off64_t off) const;
+    /**
+     * Returns whether any ranges need to be redacted.
+     */
+    bool isRedactionNeeded() const;
+    /**
+     * Returns number of redaction ranges.
+     */
+    int size() const;
+
+  private:
+    std::vector<RedactionRange> redaction_ranges_;
+    void processRedactionRanges(int redaction_ranges_num, const off64_t* redaction_ranges);
+    bool hasOverlapWithReadRequest(size_t size, off64_t off) const;
+};
+
+}  // namespace fuse
+}  // namespace mediaprovider
+
+#endif  // MEDIA_PROVIDER_FUSE_REDACTIONINFO_H_
diff --git a/jni/jni_init.cpp b/jni/jni_init.cpp
new file mode 100644
index 0000000..d59e871
--- /dev/null
+++ b/jni/jni_init.cpp
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "FuseDaemon"
+
+#include "android-base/logging.h"
+#include "jni.h"
+
+namespace mediaprovider {
+int register_android_providers_media_FuseDaemon(JavaVM* vm, JNIEnv* env);
+}
+
+extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
+    JNIEnv* env;
+    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+        LOG(ERROR) << "ERROR: GetEnv failed";
+        return JNI_ERR;
+    }
+
+    mediaprovider::register_android_providers_media_FuseDaemon(vm, env);
+
+    return JNI_VERSION_1_6;
+}
diff --git a/jni/node-inl.h b/jni/node-inl.h
new file mode 100644
index 0000000..d6ad0ad
--- /dev/null
+++ b/jni/node-inl.h
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specic language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEDIA_PROVIDER_JNI_NODE_INL_H_
+#define MEDIA_PROVIDER_JNI_NODE_INL_H_
+
+#include <android-base/logging.h>
+
+#include <list>
+#include <memory>
+#include <mutex>
+#include <sstream>
+#include <string>
+#include <unordered_set>
+#include <vector>
+
+#include "libfuse_jni/ReaddirHelper.h"
+#include "libfuse_jni/RedactionInfo.h"
+
+class NodeTest;
+
+namespace mediaprovider {
+namespace fuse {
+
+struct handle {
+    explicit handle(int fd, const RedactionInfo* ri, bool cached) : fd(fd), ri(ri), cached(cached) {
+        CHECK(ri != nullptr);
+    }
+
+    const int fd;
+    const std::unique_ptr<const RedactionInfo> ri;
+    const bool cached;
+
+    ~handle() { close(fd); }
+};
+
+struct dirhandle {
+    explicit dirhandle(DIR* dir) : d(dir), next_off(0) { CHECK(dir != nullptr); }
+
+    DIR* const d;
+    off_t next_off;
+    // Fuse readdir() is called multiple times based on the size of the buffer and
+    // number of directory entries in the given directory. 'de' holds the list
+    // of directory entries for the directory handle and this list is available
+    // across subsequent readdir() calls for the same directory handle.
+    std::vector<std::shared_ptr<DirectoryEntry>> de;
+
+    ~dirhandle() { closedir(d); }
+};
+
+// Whether inode tracking is enabled or not. When enabled, we maintain a
+// separate mapping from inode numbers to "live" nodes so we can detect when
+// we receive a request to a node that has been deleted.
+static constexpr bool kEnableInodeTracking = true;
+
+class node;
+
+// Tracks the set of active nodes associated with a FUSE instance so that we
+// can assert that we only ever return an active node in response to a lookup.
+class NodeTracker {
+  public:
+    explicit NodeTracker(std::recursive_mutex* lock) : lock_(lock) {}
+
+    void CheckTracked(__u64 ino) const {
+        if (kEnableInodeTracking) {
+            const node* node = reinterpret_cast<const class node*>(ino);
+            std::lock_guard<std::recursive_mutex> guard(*lock_);
+            CHECK(active_nodes_.find(node) != active_nodes_.end());
+        }
+    }
+
+    void NodeDeleted(const node* node) {
+        if (kEnableInodeTracking) {
+            std::lock_guard<std::recursive_mutex> guard(*lock_);
+            LOG(DEBUG) << "Node: " << reinterpret_cast<uintptr_t>(node) << " deleted.";
+
+            CHECK(active_nodes_.find(node) != active_nodes_.end());
+            active_nodes_.erase(node);
+        }
+    }
+
+    void NodeCreated(const node* node) {
+        if (kEnableInodeTracking) {
+            std::lock_guard<std::recursive_mutex> guard(*lock_);
+            LOG(DEBUG) << "Node: " << reinterpret_cast<uintptr_t>(node) << " created.";
+
+            CHECK(active_nodes_.find(node) == active_nodes_.end());
+            active_nodes_.insert(node);
+        }
+    }
+
+  private:
+    std::recursive_mutex* lock_;
+    std::unordered_set<const node*> active_nodes_;
+};
+
+class node {
+  public:
+    // Creates a new node with the specified parent, name and lock.
+    static node* Create(node* parent, const std::string& name, std::recursive_mutex* lock,
+                        NodeTracker* tracker) {
+        // Place the entire constructor under a critical section to make sure
+        // node creation, tracking (if enabled) and the addition to a parent are
+        // atomic.
+        std::lock_guard<std::recursive_mutex> guard(*lock);
+        return new node(parent, name, lock, tracker);
+    }
+
+    // Creates a new root node. Root nodes have no parents by definition
+    // and their "name" must signify an absolute path.
+    static node* CreateRoot(const std::string& path, std::recursive_mutex* lock,
+                            NodeTracker* tracker) {
+        std::lock_guard<std::recursive_mutex> guard(*lock);
+        node* root = new node(nullptr, path, lock, tracker);
+
+        // The root always has one extra reference to avoid it being
+        // accidentally collected.
+        root->Acquire();
+        return root;
+    }
+
+    // Maps an inode to its associated node.
+    static inline node* FromInode(__u64 ino, const NodeTracker* tracker) {
+        tracker->CheckTracked(ino);
+        return reinterpret_cast<node*>(static_cast<uintptr_t>(ino));
+    }
+
+    // Maps a node to its associated inode.
+    static __u64 ToInode(node* node) {
+        return static_cast<__u64>(reinterpret_cast<uintptr_t>(node));
+    }
+
+    // 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.
+    bool Release(uint32_t count) {
+        std::lock_guard<std::recursive_mutex> guard(*lock_);
+        if (refcount_ >= count) {
+            refcount_ -= count;
+            if (refcount_ == 0) {
+                delete this;
+                return true;
+            }
+        } else {
+            LOG(ERROR) << "Mismatched reference count: refcount_ = " << this->refcount_
+                       << " ,count = " << count;
+        }
+
+        return false;
+    }
+
+    // Builds the full path associated with this node, including all path segments
+    // associated with its descendants.
+    std::string BuildPath() const;
+
+    // Builds the full PII safe path associated with this node, including all path segments
+    // associated with its descendants.
+    std::string BuildSafePath() 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_);
+
+        const char* name_char = name.c_str();
+        for (node* child : children_) {
+            const std::string& child_name = child->GetName();
+            if (!strcasecmp(name_char, child_name.c_str()) && !child->deleted_) {
+                if (acquire) {
+                    child->Acquire();
+                }
+
+                return child;
+            }
+        }
+        return nullptr;
+    }
+
+    // Marks this node as deleted. It is still associated with its parent, and
+    // all open handles etc. to this node are preserved until its refcount goes
+    // to zero.
+    void SetDeleted() {
+        std::lock_guard<std::recursive_mutex> guard(*lock_);
+
+        deleted_ = true;
+    }
+
+    void Rename(const std::string& name, node* new_parent) {
+        std::lock_guard<std::recursive_mutex> guard(*lock_);
+
+        name_ = name;
+        if (new_parent != parent_) {
+            RemoveFromParent();
+            AddToParent(new_parent);
+        }
+    }
+
+    const std::string& GetName() const {
+        std::lock_guard<std::recursive_mutex> guard(*lock_);
+        return name_;
+    }
+
+    node* GetParent() const {
+        std::lock_guard<std::recursive_mutex> guard(*lock_);
+        return parent_;
+    }
+
+    inline void AddHandle(handle* h) {
+        std::lock_guard<std::recursive_mutex> guard(*lock_);
+        handles_.emplace_back(std::unique_ptr<handle>(h));
+    }
+
+    void DestroyHandle(handle* h) {
+        std::lock_guard<std::recursive_mutex> guard(*lock_);
+
+        auto comp = [h](const std::unique_ptr<handle>& ptr) { return ptr.get() == h; };
+        auto it = std::find_if(handles_.begin(), handles_.end(), comp);
+        CHECK(it != handles_.end());
+        handles_.erase(it);
+    }
+
+    bool HasCachedHandle() const {
+        std::lock_guard<std::recursive_mutex> guard(*lock_);
+
+        for (const auto& handle : handles_) {
+            if (handle->cached) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    inline void AddDirHandle(dirhandle* d) {
+        std::lock_guard<std::recursive_mutex> guard(*lock_);
+
+        dirhandles_.emplace_back(std::unique_ptr<dirhandle>(d));
+    }
+
+    void DestroyDirHandle(dirhandle* d) {
+        std::lock_guard<std::recursive_mutex> guard(*lock_);
+
+        auto comp = [d](const std::unique_ptr<dirhandle>& ptr) { return ptr.get() == d; };
+        auto it = std::find_if(dirhandles_.begin(), dirhandles_.end(), comp);
+        CHECK(it != dirhandles_.end());
+        dirhandles_.erase(it);
+    }
+
+    // Deletes the tree of nodes rooted at |tree|.
+    static void DeleteTree(node* tree);
+
+    // Looks up an absolute path rooted at |root|, or nullptr if no such path
+    // through the hierarchy exists.
+    static const node* LookupAbsolutePath(const node* root, const std::string& absolute_path);
+
+  private:
+    node(node* parent, const std::string& name, std::recursive_mutex* lock, NodeTracker* tracker)
+        : name_(name),
+          refcount_(0),
+          parent_(nullptr),
+          deleted_(false),
+          lock_(lock),
+          tracker_(tracker) {
+        tracker_->NodeCreated(this);
+        Acquire();
+        // This is a special case for the root node. All other nodes will have a
+        // non-null parent.
+        if (parent != nullptr) {
+            AddToParent(parent);
+        }
+    }
+
+    // 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_);
+        // This method assumes this node is currently unparented.
+        CHECK(parent_ == nullptr);
+        // Check that the new parent isn't nullptr either.
+        CHECK(parent != nullptr);
+
+        parent_ = parent;
+        parent_->children_.push_back(this);
+
+        // TODO(narayan, zezeozue): It's unclear why we need to call Acquire on the
+        // parent node when we're adding a child to it.
+        parent_->Acquire();
+    }
+
+    // Removes this node from its current parent, and set its parent to nullptr.
+    void RemoveFromParent() {
+        std::lock_guard<std::recursive_mutex> guard(*lock_);
+
+        if (parent_ != nullptr) {
+            std::list<node*>& children = parent_->children_;
+            std::list<node*>::iterator it = std::find(children.begin(), children.end(), this);
+
+            CHECK(it != children.end());
+            children.erase(it);
+            parent_->Release(1);
+            parent_ = nullptr;
+        }
+    }
+
+    // A helper function to recursively construct the absolute path of a given node.
+    // If |safe| is true, builds a PII safe path instead
+    void BuildPathForNodeRecursive(bool safe, const node* node, std::stringstream* path) const;
+
+    // The name of this node. Non-const because it can change during renames.
+    std::string name_;
+    // The reference count for this node. Guarded by |lock_|.
+    uint32_t refcount_;
+    // List of children of this node. All of them contain a back reference
+    // to their parent. Guarded by |lock_|.
+    std::list<node*> children_;
+    // Containing directory for this node. Guarded by |lock_|.
+    node* parent_;
+    // List of file handles associated with this node. Guarded by |lock_|.
+    std::vector<std::unique_ptr<handle>> handles_;
+    // List of directory handles associated with this node. Guarded by |lock_|.
+    std::vector<std::unique_ptr<dirhandle>> dirhandles_;
+    bool deleted_;
+    std::recursive_mutex* lock_;
+
+    NodeTracker* const tracker_;
+
+    ~node() {
+        RemoveFromParent();
+
+        handles_.clear();
+        dirhandles_.clear();
+
+        tracker_->NodeDeleted(this);
+    }
+
+    friend class ::NodeTest;
+};
+
+}  // namespace fuse
+}  // namespace mediaprovider
+
+#endif  // MEDIA_PROVIDER_JNI_MODE_INL_H_
diff --git a/jni/node.cpp b/jni/node.cpp
new file mode 100644
index 0000000..e17a9e8
--- /dev/null
+++ b/jni/node.cpp
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specic language governing permissions and
+ * limitations under the License.
+ */
+
+#include "node-inl.h"
+
+static std::vector<std::string> GetPathSegments(int segment_start, const std::string& path) {
+    std::vector<std::string> segments;
+    int segment_end = path.find_first_of('/', segment_start);
+
+    while (segment_end != std::string::npos) {
+        if (segment_end == segment_start) {
+            // First character is '/' ignore
+            segment_end = path.find_first_of('/', ++segment_start);
+            continue;
+        }
+
+        segments.push_back(path.substr(segment_start, segment_end - segment_start));
+        segment_start = segment_end + 1;
+        segment_end = path.find_first_of('/', segment_start);
+    }
+    if (segment_start < path.size()) {
+        segments.push_back(path.substr(segment_start));
+    }
+    return segments;
+}
+
+namespace mediaprovider {
+namespace fuse {
+
+// Assumes that |node| has at least one child.
+void node::BuildPathForNodeRecursive(bool safe, const node* node, std::stringstream* path) const {
+    if (node->parent_) {
+        BuildPathForNodeRecursive(safe, node->parent_, path);
+    }
+
+    if (safe && node->parent_) {
+        (*path) << reinterpret_cast<uintptr_t>(node);
+    } else {
+        (*path) << node->GetName();
+    }
+  
+    if (node != this) {
+        // Must not add a '/' to the last segment
+        (*path) << "/";
+    }
+}
+
+std::string node::BuildPath() const {
+    std::lock_guard<std::recursive_mutex> guard(*lock_);
+    std::stringstream path;
+
+    BuildPathForNodeRecursive(false, this, &path);
+    return path.str();
+}
+
+std::string node::BuildSafePath() const {
+    std::lock_guard<std::recursive_mutex> guard(*lock_);
+    std::stringstream path;
+
+    BuildPathForNodeRecursive(true, this, &path);
+    return path.str();
+}
+
+const node* node::LookupAbsolutePath(const node* root, const std::string& absolute_path) {
+    if (absolute_path.find(root->GetName()) != 0) {
+        return nullptr;
+    }
+
+    std::vector<std::string> segments = GetPathSegments(root->GetName().size(), absolute_path);
+
+    std::lock_guard<std::recursive_mutex> guard(*root->lock_);
+
+    const node* node = root;
+    for (const std::string& segment : segments) {
+        node = node->LookupChildByName(segment, false /* acquire */);
+        if (!node) {
+            return nullptr;
+        }
+    }
+    return node;
+}
+
+void node::DeleteTree(node* tree) {
+    std::lock_guard<std::recursive_mutex> guard(*tree->lock_);
+
+    if (tree) {
+        // Make a copy of the list of children because calling Delete tree
+        // will modify the list of children, which will cause issues while
+        // iterating over them.
+        std::vector<node*> children(tree->children_.begin(), tree->children_.end());
+        for (node* child : children) {
+            DeleteTree(child);
+        }
+
+        CHECK(tree->children_.empty());
+        delete tree;
+    }
+}
+
+}  // namespace fuse
+}  // namespace mediaprovider
diff --git a/jni/node_test.cpp b/jni/node_test.cpp
new file mode 100644
index 0000000..098bb28
--- /dev/null
+++ b/jni/node_test.cpp
@@ -0,0 +1,241 @@
+#include <gtest/gtest.h>
+
+#include "node-inl.h"
+
+#include <algorithm>
+#include <memory>
+#include <mutex>
+
+using mediaprovider::fuse::dirhandle;
+using mediaprovider::fuse::handle;
+using mediaprovider::fuse::node;
+using mediaprovider::fuse::NodeTracker;
+
+// Listed as a friend class to struct node so it can observe implementation
+// details if required. The only implementation detail that is worth writing
+// tests around at the moment is the reference count.
+class NodeTest : public ::testing::Test {
+  public:
+    NodeTest() : tracker_(NodeTracker(&lock_)) {}
+
+    uint32_t GetRefCount(node* node) { return node->refcount_; }
+
+    std::recursive_mutex lock_;
+    NodeTracker tracker_;
+
+    // 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) {
+        return unique_node_ptr(node::Create(parent, path, &lock_, &tracker_), &NodeTest::destroy);
+    }
+};
+
+TEST_F(NodeTest, TestCreate) {
+    unique_node_ptr node = CreateNode(nullptr, "/path");
+
+    ASSERT_EQ("/path", node->GetName());
+    ASSERT_EQ(1, GetRefCount(node.get()));
+    ASSERT_FALSE(node->HasCachedHandle());
+}
+
+TEST_F(NodeTest, TestCreate_withParent) {
+    unique_node_ptr parent = CreateNode(nullptr, "/path");
+    ASSERT_EQ(1, GetRefCount(parent.get()));
+
+    // Adding a child to a parent node increments its refcount.
+    unique_node_ptr child = CreateNode(parent.get(), "subdir");
+    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", false /* acquire */));
+    ASSERT_EQ(1, GetRefCount(child.get()));
+}
+
+TEST_F(NodeTest, TestRelease) {
+    node* node = node::Create(nullptr, "/path", &lock_, &tracker_);
+    acquire(node);
+    acquire(node);
+    ASSERT_EQ(3, GetRefCount(node));
+
+    ASSERT_FALSE(node->Release(1));
+    ASSERT_EQ(2, GetRefCount(node));
+
+    // A Release that makes refcount go negative should be a no-op.
+    ASSERT_FALSE(node->Release(10000));
+    ASSERT_EQ(2, GetRefCount(node));
+
+    // Finally, let the refcount go to zero.
+    ASSERT_TRUE(node->Release(2));
+}
+
+TEST_F(NodeTest, TestRenameWithName) {
+    unique_node_ptr parent = CreateNode(nullptr, "/path");
+
+    unique_node_ptr child = CreateNode(parent.get(), "subdir");
+    ASSERT_EQ(2, GetRefCount(parent.get()));
+    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", 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) {
+    unique_node_ptr parent1 = CreateNode(nullptr, "/path1");
+    unique_node_ptr parent2 = CreateNode(nullptr, "/path2");
+
+    unique_node_ptr child = CreateNode(parent1.get(), "subdir");
+    ASSERT_EQ(2, GetRefCount(parent1.get()));
+    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", false /* acquire */));
+
+    ASSERT_EQ(2, GetRefCount(parent2.get()));
+    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) {
+    unique_node_ptr parent1 = CreateNode(nullptr, "/path1");
+    unique_node_ptr parent2 = CreateNode(nullptr, "/path2");
+
+    unique_node_ptr child = CreateNode(parent1.get(), "subdir");
+    ASSERT_EQ(2, GetRefCount(parent1.get()));
+    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", 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", false /* acquire */));
+
+    ASSERT_EQ("/path2/subdir_new", child->BuildPath());
+    ASSERT_EQ(1, GetRefCount(child.get()));
+}
+
+TEST_F(NodeTest, TestBuildPath) {
+    unique_node_ptr parent = CreateNode(nullptr, "/path");
+    ASSERT_EQ("/path", parent->BuildPath());
+
+    unique_node_ptr child = CreateNode(parent.get(), "subdir");
+    ASSERT_EQ("/path/subdir", child->BuildPath());
+
+    unique_node_ptr child2 = CreateNode(parent.get(), "subdir2");
+    ASSERT_EQ("/path/subdir2", child2->BuildPath());
+
+    unique_node_ptr subchild = CreateNode(child2.get(), "subsubdir");
+    ASSERT_EQ("/path/subdir2/subsubdir", subchild->BuildPath());
+}
+
+TEST_F(NodeTest, TestSetDeleted) {
+    unique_node_ptr parent = CreateNode(nullptr, "/path");
+    unique_node_ptr child = CreateNode(parent.get(), "subdir");
+
+    ASSERT_EQ(child.get(), parent->LookupChildByName("subdir", false /* acquire */));
+    child->SetDeleted();
+    ASSERT_EQ(nullptr, parent->LookupChildByName("subdir", false /* acquire */));
+}
+
+TEST_F(NodeTest, DeleteTree) {
+    unique_node_ptr parent = CreateNode(nullptr, "/path");
+
+    // This is the tree that we intend to delete.
+    node* child = node::Create(parent.get(), "subdir", &lock_, &tracker_);
+    node::Create(child, "s1", &lock_, &tracker_);
+    node* subchild2 = node::Create(child, "s2", &lock_, &tracker_);
+    node::Create(subchild2, "sc2", &lock_, &tracker_);
+
+    ASSERT_EQ(child, parent->LookupChildByName("subdir", false /* acquire */));
+    node::DeleteTree(child);
+    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", 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) {
+    unique_node_ptr parent = CreateNode(nullptr, "/path");
+    unique_node_ptr child = CreateNode(parent.get(), "subdir");
+    unique_node_ptr child2 = CreateNode(parent.get(), "subdir2");
+    unique_node_ptr subchild = CreateNode(child2.get(), "subsubdir");
+
+    ASSERT_EQ(parent.get(), node::LookupAbsolutePath(parent.get(), "/path"));
+    ASSERT_EQ(parent.get(), node::LookupAbsolutePath(parent.get(), "/path/"));
+    ASSERT_EQ(nullptr, node::LookupAbsolutePath(parent.get(), "/path2"));
+
+    ASSERT_EQ(child.get(), node::LookupAbsolutePath(parent.get(), "/path/subdir"));
+    ASSERT_EQ(child.get(), node::LookupAbsolutePath(parent.get(), "/path/subdir/"));
+    // TODO(narayan): Are the two cases below intentional behaviour ?
+    ASSERT_EQ(child.get(), node::LookupAbsolutePath(parent.get(), "/path//subdir"));
+    ASSERT_EQ(child.get(), node::LookupAbsolutePath(parent.get(), "/path///subdir"));
+
+    ASSERT_EQ(child2.get(), node::LookupAbsolutePath(parent.get(), "/path/subdir2"));
+    ASSERT_EQ(child2.get(), node::LookupAbsolutePath(parent.get(), "/path/subdir2/"));
+
+    ASSERT_EQ(nullptr, node::LookupAbsolutePath(parent.get(), "/path/subdir3/"));
+
+    ASSERT_EQ(subchild.get(), node::LookupAbsolutePath(parent.get(), "/path/subdir2/subsubdir"));
+    ASSERT_EQ(nullptr, node::LookupAbsolutePath(parent.get(), "/path/subdir/subsubdir"));
+}
+
+TEST_F(NodeTest, AddDestroyHandle) {
+    unique_node_ptr node = CreateNode(nullptr, "/path");
+
+    handle* h = new handle(-1, new mediaprovider::fuse::RedactionInfo, true /* cached */);
+    node->AddHandle(h);
+    ASSERT_TRUE(node->HasCachedHandle());
+
+    node->DestroyHandle(h);
+    ASSERT_FALSE(node->HasCachedHandle());
+
+    // Should all crash the process as the handle is no longer associated with
+    // the node in question.
+    EXPECT_DEATH(node->DestroyHandle(h), "");
+    EXPECT_DEATH(node->DestroyHandle(nullptr), "");
+    std::unique_ptr<handle> h2(
+            new handle(-1, new mediaprovider::fuse::RedactionInfo, true /* cached */));
+    EXPECT_DEATH(node->DestroyHandle(h2.get()), "");
+}
+
+TEST_F(NodeTest, CaseInsensitive) {
+    unique_node_ptr parent = CreateNode(nullptr, "/path");
+    unique_node_ptr mixed_child = CreateNode(parent.get(), "cHiLd");
+
+    node* upper_child = parent->LookupChildByName("CHILD", false /* acquire */);
+    node* lower_child = parent->LookupChildByName("child", false /* acquire */);
+
+    ASSERT_EQ(mixed_child.get(), lower_child);
+    ASSERT_EQ(mixed_child.get(), upper_child);
+}
diff --git a/legacy/Android.bp b/legacy/Android.bp
new file mode 100644
index 0000000..203ee5a
--- /dev/null
+++ b/legacy/Android.bp
@@ -0,0 +1,23 @@
+
+android_app {
+    name: "MediaProviderLegacy",
+    manifest: "AndroidManifest.xml",
+
+    static_libs: [
+        "androidx.appcompat_appcompat",
+        "androidx.core_core",
+        "guava",
+    ],
+
+    libs: ["app-compat-annotations"],
+
+    srcs: [
+        "src/**/*.aidl",
+        "src/**/*.java",
+        ":mediaprovider-database-sources",
+    ],
+
+    certificate: "media",
+    privileged: true,
+    sdk_version: "system_current",
+}
diff --git a/legacy/AndroidManifest.xml b/legacy/AndroidManifest.xml
new file mode 100644
index 0000000..2396f58
--- /dev/null
+++ b/legacy/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.providers.media"
+        android:sharedUserId="android.media"
+        android:versionCode="1024">
+
+    <!-- This "legacy" instance is retained on the device to preserve the
+         database contents before MediaProvider was migrated into a
+         Mainline module.  This ensures that we can reconstruct information
+         such as IDs and other user-generated content. -->
+
+    <application
+            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"
+                android:authorities="media_legacy"
+                android:exported="true"
+                android:permission="android.permission.WRITE_MEDIA_STORAGE" />
+    </application>
+</manifest>
diff --git a/legacy/src/com/android/providers/media/LegacyMediaProvider.java b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
new file mode 100644
index 0000000..e921155
--- /dev/null
+++ b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media;
+
+import static com.android.providers.media.DatabaseHelper.EXTERNAL_DATABASE_NAME;
+import static com.android.providers.media.DatabaseHelper.INTERNAL_DATABASE_NAME;
+
+import android.content.ContentProvider;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.content.UriMatcher;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.provider.MediaStore.MediaColumns;
+import android.util.ArraySet;
+
+import androidx.annotation.NonNull;
+
+import com.android.providers.media.util.Logging;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Very limited subset of {@link MediaProvider} which only surfaces
+ * {@link android.provider.MediaStore.Files} data.
+ */
+public class LegacyMediaProvider extends ContentProvider {
+    private DatabaseHelper mInternalDatabase;
+    private DatabaseHelper mExternalDatabase;
+
+    public static final String START_LEGACY_MIGRATION_CALL = "start_legacy_migration";
+    public static final String FINISH_LEGACY_MIGRATION_CALL = "finish_legacy_migration";
+
+    @Override
+    public void attachInfo(Context context, ProviderInfo info) {
+        // Sanity check our setup
+        if (!info.exported) {
+            throw new SecurityException("Provider must be exported");
+        }
+        if (!android.Manifest.permission.WRITE_MEDIA_STORAGE.equals(info.readPermission)
+                || !android.Manifest.permission.WRITE_MEDIA_STORAGE.equals(info.writePermission)) {
+            throw new SecurityException("Provider must be protected by WRITE_MEDIA_STORAGE");
+        }
+
+        super.attachInfo(context, info);
+    }
+
+    @Override
+    public boolean onCreate() {
+        final Context context = getContext();
+
+        final File persistentDir = context.getDir("logs", Context.MODE_PRIVATE);
+        Logging.initPersistent(persistentDir);
+
+        mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME,
+                true, false, true, null, null, null, null, null);
+        mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME,
+                false, false, true, null, null, null, null, null);
+
+        return true;
+    }
+
+    private @NonNull DatabaseHelper getDatabaseForUri(Uri uri) {
+        final String volumeName = MediaStore.getVolumeName(uri);
+        switch (volumeName) {
+            case MediaStore.VOLUME_INTERNAL:
+                return Objects.requireNonNull(mInternalDatabase, "Missing internal database");
+            default:
+                return Objects.requireNonNull(mExternalDatabase, "Missing external database");
+        }
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        final String appendedSelection = getAppendedSelection(selection, uri);
+        final DatabaseHelper helper = getDatabaseForUri(uri);
+        return helper.runWithoutTransaction((db) -> {
+            return db.query(getTableName(uri), projection, appendedSelection, selectionArgs,
+                    null, null, sortOrder);
+        });
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
+                throws OperationApplicationException {
+        // Open transactions on databases for requested volumes
+        final Set<DatabaseHelper> transactions = new ArraySet<>();
+        try {
+            for (ContentProviderOperation op : operations) {
+                final DatabaseHelper helper = getDatabaseForUri(op.getUri());
+                if (!transactions.contains(helper)) {
+                    helper.beginTransaction();
+                    transactions.add(helper);
+                }
+            }
+
+            final ContentProviderResult[] result = super.applyBatch(operations);
+            for (DatabaseHelper helper : transactions) {
+                helper.setTransactionSuccessful();
+            }
+            return result;
+        } finally {
+            for (DatabaseHelper helper : transactions) {
+                helper.endTransaction();
+            }
+        }
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        if (!uri.getBooleanQueryParameter("silent", false)) {
+            try {
+                final File file = new File(values.getAsString(MediaColumns.DATA));
+                file.getParentFile().mkdirs();
+                file.createNewFile();
+            } catch (IOException e) {
+                throw new IllegalStateException(e);
+            }
+        }
+
+        final DatabaseHelper helper = getDatabaseForUri(uri);
+        final long id = helper.runWithTransaction((db) -> {
+            return db.insert(getTableName(uri), null, values);
+        });
+        return ContentUris.withAppendedId(uri, id);
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+
+    private static final int AUDIO_PLAYLISTS_ID_MEMBERS = 112;
+    private static final int FILES_ID = 701;
+    private static final UriMatcher BASIC_URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+    static {
+        final UriMatcher basicUriMatcher = BASIC_URI_MATCHER;
+        basicUriMatcher.addURI(MediaStore.AUTHORITY_LEGACY, "*/audio/playlists/#/members",
+                AUDIO_PLAYLISTS_ID_MEMBERS);
+        basicUriMatcher.addURI(MediaStore.AUTHORITY_LEGACY, "*/file/#", FILES_ID);
+    };
+
+    private static String getAppendedSelection(String selection, Uri uri) {
+        String whereClause = "";
+        final int match = BASIC_URI_MATCHER.match(uri);
+        switch (match) {
+            case AUDIO_PLAYLISTS_ID_MEMBERS:
+                whereClause = "playlist_id=" + uri.getPathSegments().get(3);
+                break;
+            case FILES_ID:
+                whereClause = "_id=" + uri.getPathSegments().get(2);
+                break;
+            default:
+                // No additional whereClause required
+        }
+        if (selection == null || selection.isEmpty()) {
+            return whereClause;
+        } else if (whereClause.isEmpty()) {
+            return selection;
+        } else {
+            return  whereClause + " AND " + selection;
+        }
+    }
+
+    private static String getTableName(Uri uri) {
+        final int playlistMatch = BASIC_URI_MATCHER.match(uri);
+        if (playlistMatch == AUDIO_PLAYLISTS_ID_MEMBERS) {
+            return "audio_playlists_map";
+        } else {
+            // Return the "files" table by default for all other Uris.
+            return "files";
+        }
+    }
+
+    @Override
+    public Bundle call(String authority, String method, String arg, Bundle extras) {
+        switch (method) {
+            case START_LEGACY_MIGRATION_CALL: {
+                // Nice to know, but nothing actionable
+                break;
+            }
+            case FINISH_LEGACY_MIGRATION_CALL: {
+                // We're only going to hear this once, since we've either
+                // successfully migrated legacy data, or we're never going to
+                // try again, so it's time to clean things up
+                final String volumeName = arg;
+                switch (volumeName) {
+                    case MediaStore.VOLUME_INTERNAL: {
+                        mInternalDatabase.close();
+                        getContext().deleteDatabase(INTERNAL_DATABASE_NAME);
+                        break;
+                    }
+                    default: {
+                        mExternalDatabase.close();
+                        getContext().deleteDatabase(EXTERNAL_DATABASE_NAME);
+                        break;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        Logging.dumpPersistent(writer);
+    }
+}
diff --git a/logging.sh b/logging.sh
new file mode 100755
index 0000000..fd2a3a5
--- /dev/null
+++ b/logging.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+level=$1
+
+if [ $level == "on" ] || [ $level == "extreme" ]
+then
+    adb shell setprop log.tag.MediaProvider VERBOSE
+    adb shell setprop log.tag.ModernMediaScanner VERBOSE
+    adb shell setprop log.tag.FuseDaemon DEBUG
+    adb shell setprop log.tag.libfuse DEBUG
+else
+    adb shell setprop log.tag.MediaProvider INFO
+    adb shell setprop log.tag.ModernMediaScanner INFO
+    adb shell setprop log.tag.FuseDaemon INFO
+    adb shell setprop log.tag.libfuse INFO
+fi
+
+if [ $level == "extreme" ]
+then
+    adb shell setprop log.tag.SQLiteQueryBuilder VERBOSE
+    adb shell setprop log.tag.FuseDaemon VERBOSE
+    adb shell setprop log.tag.libfuse VERBOSE
+    adb shell setprop persist.sys.fuse.log true
+else
+    adb shell setprop log.tag.SQLiteQueryBuilder INFO
+    adb shell setprop log.tag.FuseDaemon INFO
+    adb shell setprop log.tag.libfuse INFO
+    adb shell setprop persist.sys.fuse.log false
+fi
+
+# Kill process to kick new settings into place
+adb shell am force-stop com.android.providers.media
+adb shell am force-stop com.android.providers.media.module
+adb shell am force-stop com.google.android.providers.media.module
diff --git a/res/color/thumb_more_tint.xml b/res/color/thumb_more_tint.xml
new file mode 100644
index 0000000..3205bf9
--- /dev/null
+++ b/res/color/thumb_more_tint.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:color="?android:attr/colorAccent"
+        android:alpha="0.6" />
+</selector>
diff --git a/res/drawable/ic_add.xml b/res/drawable/ic_add.xml
deleted file mode 100644
index 22b3fe9..0000000
--- a/res/drawable/ic_add.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-    Copyright (C) 2016 The Android Open Source Project
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-         http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="24.0dp"
-        android:height="24.0dp"
-        android:viewportWidth="48.0"
-        android:viewportHeight="48.0">
-    <path
-        android:fillColor="?android:attr/colorAccent"
-        android:pathData="M38.0,26.0L26.0,26.0l0.0,12.0l-4.0,0.0L22.0,26.0L10.0,26.0l0.0,-4.0l12.0,0.0L22.0,10.0l4.0,0.0l0.0,12.0l12.0,0.0l0.0,4.0z"/>
-</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_add_padded.xml b/res/drawable/ic_add_padded.xml
deleted file mode 100644
index c376867..0000000
--- a/res/drawable/ic_add_padded.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<!--
-    Copyright (C) 2017 The Android Open Source Project
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-         http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
--->
-
-<inset xmlns:android="http://schemas.android.com/apk/res/android"
-        android:drawable="@drawable/ic_add"
-        android:insetTop="4dp"
-        android:insetRight="4dp"
-        android:insetBottom="4dp"
-        android:insetLeft="4dp"/>
diff --git a/res/drawable/ic_warning.xml b/res/drawable/ic_warning.xml
new file mode 100644
index 0000000..2163203
--- /dev/null
+++ b/res/drawable/ic_warning.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24"
+        android:viewportWidth="24">
+    <path
+        android:fillColor="#1A73E8"
+        android:pathData="M12 5.99L19.53 19H4.47L12 5.99M12 2L1 21h22L12 2zm1 14h-2v2h2v-2zm0-6h-2v4h2v-4z" />
+</vector>
\ No newline at end of file
diff --git a/res/drawable/thumb_clip.xml b/res/drawable/thumb_clip.xml
new file mode 100644
index 0000000..f075e41
--- /dev/null
+++ b/res/drawable/thumb_clip.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+        android:shape="rectangle">
+    <solid android:color="@color/thumb_gray_color"/>
+    <corners android:radius="4dp" />
+</shape>
diff --git a/res/layout-watch/add_new_sound_item.xml b/res/layout-watch/add_new_sound_item.xml
deleted file mode 100644
index 6f91d77..0000000
--- a/res/layout-watch/add_new_sound_item.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2017 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<!--
-     Currently, no file manager app on watch could handle ACTION_GET_CONTENT intent.
-     Make the visibility to "gone" to prevent failures.
- -->
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:minHeight="?android:attr/listPreferredItemHeightSmall"
-        android:textAppearance="?android:attr/textAppearanceMedium"
-        android:text="@null"
-        android:textColor="?android:attr/colorAccent"
-        android:gravity="center_vertical"
-        android:paddingStart="?android:attr/listPreferredItemPaddingStart"
-        android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
-        android:drawableStart="@drawable/ic_add_padded"
-        android:drawablePadding="8dp"
-        android:ellipsize="marquee"
-        android:visibility="gone" />
diff --git a/res/layout-watch/radio_with_work_badge.xml b/res/layout-watch/radio_with_work_badge.xml
deleted file mode 100644
index 0e11621..0000000
--- a/res/layout-watch/radio_with_work_badge.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<com.android.providers.media.CheckedListItem xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="fill_parent"
-    android:layout_height="wrap_content"
-    android:gravity="center_vertical"
-    android:background="?android:attr/selectableItemBackground"
-    >
-
-    <CheckedTextView
-        android:id="@+id/checked_text_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:minHeight="?android:attr/listPreferredItemHeightSmall"
-        android:textAppearance="?android:attr/textAppearanceMedium"
-        android:textColor="?android:attr/textColorAlertDialogListItem"
-        android:gravity="center_vertical"
-        android:paddingStart="?android:attr/listPreferredItemPaddingStart"
-        android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
-        android:drawableStart="?android:attr/listChoiceIndicatorSingle"
-        android:drawablePadding="8dp"
-        android:ellipsize="marquee"
-        android:layout_toLeftOf="@+id/work_icon"
-        android:maxLines="3" />
-
-    <ImageView
-        android:id="@id/work_icon"
-        android:layout_width="18dp"
-        android:layout_height="18dp"
-        android:layout_alignParentRight="true"
-        android:layout_centerVertical="true"
-        android:scaleType="centerCrop"
-        android:layout_marginRight="20dp" />
-</com.android.providers.media.CheckedListItem>
diff --git a/res/layout/add_new_sound_item.xml b/res/layout/add_new_sound_item.xml
deleted file mode 100644
index 14421c9..0000000
--- a/res/layout/add_new_sound_item.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2016 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="fill_parent"
-    android:layout_height="wrap_content"
-    android:gravity="center_vertical"
-    android:background="?android:attr/selectableItemBackground">
-
-<ImageView
-        android:layout_width="24dp"
-        android:layout_height="24dp"
-        android:layout_alignParentRight="true"
-        android:layout_centerVertical="true"
-        android:scaleType="centerCrop"
-        android:layout_marginRight="24dp"
-        android:layout_marginLeft="24dp"
-        android:src="@drawable/ic_add" />
-
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
-        android:id="@+id/add_new_sound_text"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:minHeight="?android:attr/listPreferredItemHeightSmall"
-        android:text="@null"
-        android:textColor="?android:attr/colorAccent"
-        android:textAppearance="?android:attr/textAppearanceMedium"
-        android:maxLines="3"
-        android:gravity="center_vertical"
-        android:paddingEnd="?android:attr/dialogPreferredPadding"
-        android:drawablePadding="20dp"
-        android:ellipsize="marquee" />
-</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/cache_clearing_dialog.xml b/res/layout/cache_clearing_dialog.xml
new file mode 100644
index 0000000..06f97b8
--- /dev/null
+++ b/res/layout/cache_clearing_dialog.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright 2020, The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+
+<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:theme="@style/CacheClearingAlertDialogTheme"
+        android:paddingStart="?android:attr/dialogPreferredPadding"
+        android:paddingEnd="?android:attr/dialogPreferredPadding"
+        android:orientation="vertical">
+    <ImageView
+        android:adjustViewBounds="true"
+        android:layout_gravity="bottom|center_horizontal"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_marginTop="20dp"
+        android:scaleType="fitCenter"
+        android:src="@drawable/ic_warning"
+        android:contentDescription="@null"/>
+
+    <TextView
+        android:id="@+id/dialog_title"
+        style="@style/CacheClearingAlertDialogTitle"/>
+</LinearLayout>
diff --git a/res/layout/permission_body.xml b/res/layout/permission_body.xml
new file mode 100644
index 0000000..6282c7c
--- /dev/null
+++ b/res/layout/permission_body.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingTop="12dp"
+    android:paddingBottom="12dp"
+    android:orientation="vertical">
+
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
+        android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd"
+        android:orientation="horizontal">
+        <ImageView
+            android:id="@+id/thumb1"
+            android:layout_width="@dimen/permission_thumb_size"
+            android:layout_height="@dimen/permission_thumb_size"
+            android:layout_marginEnd="@dimen/permission_thumb_margin"
+            android:background="@drawable/thumb_clip"
+            android:scaleType="centerCrop"
+            android:visibility="gone" />
+        <ImageView
+            android:id="@+id/thumb2"
+            android:layout_width="@dimen/permission_thumb_size"
+            android:layout_height="@dimen/permission_thumb_size"
+            android:layout_marginEnd="@dimen/permission_thumb_margin"
+            android:background="@drawable/thumb_clip"
+            android:scaleType="centerCrop"
+            android:visibility="gone" />
+        <ImageView
+            android:id="@+id/thumb3"
+            android:layout_width="@dimen/permission_thumb_size"
+            android:layout_height="@dimen/permission_thumb_size"
+            android:layout_marginEnd="@dimen/permission_thumb_margin"
+            android:background="@drawable/thumb_clip"
+            android:scaleType="centerCrop"
+            android:visibility="gone" />
+        <FrameLayout
+            android:id="@+id/thumb_more_container"
+            android:layout_width="@dimen/permission_thumb_size"
+            android:layout_height="@dimen/permission_thumb_size"
+            android:visibility="gone">
+            <ImageView
+                android:id="@+id/thumb_more"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="@drawable/thumb_clip"
+                android:scaleType="centerCrop"
+                android:tint="@color/thumb_more_tint" />
+            <TextView
+                android:id="@+id/thumb_more_text"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:gravity="center"
+                android:textColor="?android:attr/textColorPrimaryInverse" />
+        </FrameLayout>
+    </LinearLayout>
+
+    <ImageView
+        android:id="@+id/thumb_full"
+        android:layout_width="match_parent"
+        android:layout_height="200dp"
+        android:scaleType="centerCrop"
+        android:src="@color/thumb_gray_color"
+        android:visibility="gone" />
+
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="?android:attr/listPreferredItemPaddingStart"
+        android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd"
+        android:orientation="vertical">
+        <TextView
+            android:id="@+id/list"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="12dp"
+            android:visibility="gone" />
+        <TextView
+            android:id="@+id/message"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="12dp"
+            android:visibility="gone" />
+    </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/radio_with_work_badge.xml b/res/layout/radio_with_work_badge.xml
deleted file mode 100644
index e7d37ea..0000000
--- a/res/layout/radio_with_work_badge.xml
+++ /dev/null
@@ -1,48 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<com.android.providers.media.CheckedListItem xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="fill_parent"
-    android:layout_height="wrap_content"
-    android:gravity="center_vertical"
-    android:background="?android:attr/selectableItemBackground"
-    >
-
-    <CheckedTextView
-        android:id="@+id/checked_text_view"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:minHeight="?android:attr/listPreferredItemHeightSmall"
-        android:textAppearance="?android:attr/textAppearanceMedium"
-        android:textColor="?android:attr/textColorAlertDialogListItem"
-        android:gravity="center_vertical"
-        android:paddingStart="20dp"
-        android:paddingEnd="?android:attr/dialogPreferredPadding"
-        android:drawableStart="?android:attr/listChoiceIndicatorSingle"
-        android:drawablePadding="20dp"
-        android:ellipsize="marquee"
-        android:layout_toLeftOf="@+id/work_icon"
-        android:maxLines="3" />
-
-    <ImageView
-        android:id="@id/work_icon"
-        android:layout_width="18dp"
-        android:layout_height="18dp"
-        android:layout_alignParentRight="true"
-        android:layout_centerVertical="true"
-        android:scaleType="centerCrop"
-        android:layout_marginRight="20dp" />
-</com.android.providers.media.CheckedListItem>
diff --git a/res/raw/default_alarm_alert.ogg b/res/raw/default_alarm_alert.ogg
deleted file mode 100644
index e69de29..0000000
--- a/res/raw/default_alarm_alert.ogg
+++ /dev/null
diff --git a/res/raw/default_notification_sound.ogg b/res/raw/default_notification_sound.ogg
deleted file mode 100644
index e69de29..0000000
--- a/res/raw/default_notification_sound.ogg
+++ /dev/null
diff --git a/res/raw/default_ringtone.ogg b/res/raw/default_ringtone.ogg
deleted file mode 100644
index e69de29..0000000
--- a/res/raw/default_ringtone.ogg
+++ /dev/null
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index 666ba23..99778ca 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -20,32 +20,90 @@
     <string name="storage_description" msgid="4081716890357580107">"Plaaslike berging"</string>
     <string name="app_label" msgid="9035307001052716210">"Mediaberging"</string>
     <string name="artist_label" msgid="8105600993099120273">"Kunstenaar"</string>
-    <string name="ringtone_default" msgid="1744846699922263043">"Verstekluitoon"</string>
-    <string name="notification_sound_default" msgid="6541609166469990135">"Verstekkennisgewingklank"</string>
-    <string name="alarm_sound_default" msgid="5488847775252870314">"Verstekwekkerklank"</string>
+    <string name="unknown" msgid="2059049215682829375">"Onbekend"</string>
     <string name="root_images" msgid="5861633549189045666">"Prente"</string>
     <string name="root_videos" msgid="8792703517064649453">"Video\'s"</string>
     <string name="root_audio" msgid="3505830755201326018">"Oudio"</string>
+    <string name="root_documents" msgid="3829103301363849237">"Dokumente"</string>
     <string name="permission_required" msgid="1460820436132943754">"Toestemming word vereis om hierdie item te wysig of uit te vee."</string>
     <string name="permission_required_action" msgid="706370952366113539">"Gaan voort"</string>
-    <string name="permission_audio" msgid="344061911159388398">"Laat "<b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" toe om hierdie musiek te wysig of uit te vee?"</string>
-    <string name="permission_video" msgid="8188037202573267740">"Laat "<b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" toe om hierdie video te wysig of uit te vee?"</string>
-    <string name="permission_images" msgid="6711726334380945124">"Laat "<b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" toe om hierdie prent te wysig of uit te vee?"</string>
     <string name="grant_dialog_button_allow" msgid="1644287024501033471">"Laat toe"</string>
     <string name="grant_dialog_button_deny" msgid="6190589471415815741">"Weier"</string>
-    <string name="sound_name_awaken" msgid="5266892392848526147">"Ontwaak"</string>
-    <string name="sound_name_bounce" msgid="8771447635446665231">"Opspring"</string>
-    <string name="sound_name_drip" msgid="1744684469020662152">"Drup"</string>
-    <string name="sound_name_gallop" msgid="2664454314532060876">"Gallop"</string>
-    <string name="sound_name_nudge" msgid="5445751598250698244">"Stamp"</string>
-    <string name="sound_name_orbit" msgid="4623457897813255481">"Wentelbaan"</string>
-    <string name="sound_name_rise" msgid="2200258555031675806">"Opstaan"</string>
-    <string name="sound_name_sway" msgid="348448316663085643">"Slinger"</string>
-    <string name="sound_name_wag" msgid="2730733083078126563">"Swaai"</string>
-    <string name="add_ringtone_text" msgid="8077717303037760418">"Voeg luitoon by"</string>
-    <string name="add_alarm_text" msgid="2440919366091720815">"Voeg wekker by"</string>
-    <string name="add_notification_text" msgid="5013822215567515665">"Voeg kennisgewing by"</string>
-    <string name="delete_ringtone_text" msgid="2963662097583300181">"Vee uit"</string>
-    <string name="unable_to_add_ringtone" msgid="6256097521165711269">"Kan nie gepasmaakte luitoon byvoeg nie"</string>
-    <string name="unable_to_delete_ringtone" msgid="2258180073859253495">"Kan nie gepasmaakte luitoon uitvee nie"</string>
+    <plurals name="permission_more_thumb" formatted="false" msgid="4392079224649478923">
+      <item quantity="other">+<xliff:g id="COUNT_1">^1</xliff:g></item>
+      <item quantity="one">+<xliff:g id="COUNT_0">^1</xliff:g></item>
+    </plurals>
+    <plurals name="permission_more_text" formatted="false" msgid="7291997297174507324">
+      <item quantity="other">Plus <xliff:g id="COUNT_1">^1</xliff:g> bykomende items</item>
+      <item quantity="one">Plus <xliff:g id="COUNT_0">^1</xliff:g> bykomende item</item>
+    </plurals>
+    <string name="cache_clearing_dialog_title" msgid="4672878017407595782">"Vee tydelike programlêers uit?"</string>
+    <string name="cache_clearing_dialog_text" msgid="7057784635111940957">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> wil \'n paar tydelike lêers uitvee. Dit kan verhoogde batterygebruik of sellulêre data tot gevolg hê."</string>
+    <string name="cache_clearing_in_progress_title" msgid="6902220064511664209">"Vee tans tydelike programlêers uit …"</string>
+    <string name="allow" msgid="8885707816848569619">"Laat toe"</string>
+    <string name="deny" msgid="6040983710442068936">"Weier"</string>
+    <plurals name="permission_write_audio" formatted="false" msgid="8914759422381305478">
+      <item quantity="other">Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> oudiolêers te wysig?</item>
+      <item quantity="one">Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie oudiolêer te wysig?</item>
+    </plurals>
+    <plurals name="permission_write_video" formatted="false" msgid="1098082003326873084">
+      <item quantity="other">Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> video\'s te wysig?</item>
+      <item quantity="one">Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie video te wysig?</item>
+    </plurals>
+    <plurals name="permission_write_image" formatted="false" msgid="748745548893845892">
+      <item quantity="other">Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> foto\'s te wysig?</item>
+      <item quantity="one">Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie foto te wysig?</item>
+    </plurals>
+    <plurals name="permission_write_generic" formatted="false" msgid="3270172714743671779">
+      <item quantity="other">Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> items te wysig?</item>
+      <item quantity="one">Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie item te wysig?</item>
+    </plurals>
+    <plurals name="permission_trash_audio" formatted="false" msgid="8907813869381755423">
+      <item quantity="other">Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> oudiolêers na die asblik toe te skuif?</item>
+      <item quantity="one">Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie oudiolêer na die asblik toe te skuif?</item>
+    </plurals>
+    <plurals name="permission_trash_video" formatted="false" msgid="4672871911555787438">
+      <item quantity="other">Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> video\'s na die asblik toe te skuif?</item>
+      <item quantity="one">Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie video na die asblik toe skuif?</item>
+    </plurals>
+    <plurals name="permission_trash_image" formatted="false" msgid="6400475304599873227">
+      <item quantity="other">Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> foto\'s na die asblik toe te skuif?</item>
+      <item quantity="one">Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie foto na die asblik toe skuif?</item>
+    </plurals>
+    <plurals name="permission_trash_generic" formatted="false" msgid="3814167365075039711">
+      <item quantity="other">Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> items na die asblik toe te skuif?</item>
+      <item quantity="one">Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie item na die asblik toe skuif?</item>
+    </plurals>
+    <plurals name="permission_untrash_audio" formatted="false" msgid="7795265980168966321">
+      <item quantity="other">Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> oudiolêers uit die asblik uit te skuif?</item>
+      <item quantity="one">Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie oudiolêer uit die asblik uit te skuif?</item>
+    </plurals>
+    <plurals name="permission_untrash_video" formatted="false" msgid="332894888445508879">
+      <item quantity="other">Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> video\'s uit die asblik uit te skuif?</item>
+      <item quantity="one">Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie video uit die asblik uit te skuif?</item>
+    </plurals>
+    <plurals name="permission_untrash_image" formatted="false" msgid="7024071378733595056">
+      <item quantity="other">Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> foto\'s uit die asblik uit te skuif?</item>
+      <item quantity="one">Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie foto uit die asblik uit te skuif?</item>
+    </plurals>
+    <plurals name="permission_untrash_generic" formatted="false" msgid="6872817093731198374">
+      <item quantity="other">Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> items uit die asblik uit te skuif?</item>
+      <item quantity="one">Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie item uit die asblik uit te skuif?</item>
+    </plurals>
+    <plurals name="permission_delete_audio" formatted="false" msgid="6848547621165184719">
+      <item quantity="other">Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> oudiolêers uit te vee?</item>
+      <item quantity="one">Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie oudiolêer uit te vee?</item>
+    </plurals>
+    <plurals name="permission_delete_video" formatted="false" msgid="1251942606336748563">
+      <item quantity="other">Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> video\'s uit te vee?</item>
+      <item quantity="one">Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie video uit te vee?</item>
+    </plurals>
+    <plurals name="permission_delete_image" formatted="false" msgid="2303409455224710111">
+      <item quantity="other">Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> foto\'s uit te vee?</item>
+      <item quantity="one">Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie foto uit te vee?</item>
+    </plurals>
+    <plurals name="permission_delete_generic" formatted="false" msgid="1412218850351841181">
+      <item quantity="other">Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> items uit te vee?</item>
+      <item quantity="one">Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie item uit te vee?</item>
+    </plurals>
 </resources>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 3bf2224..f8400b6 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -20,32 +20,90 @@
     <string name="storage_description" msgid="4081716890357580107">"አካባቢያዊ ማከማቻ"</string>
     <string name="app_label" msgid="9035307001052716210">"ማህደረ መረጃ ማከማቻ"</string>
     <string name="artist_label" msgid="8105600993099120273">"አርቲስት"</string>
-    <string name="ringtone_default" msgid="1744846699922263043">"ነባሪ የደወል ቅላጼ"</string>
-    <string name="notification_sound_default" msgid="6541609166469990135">"ነባሪ የማሳወቂያ ድምጽ"</string>
-    <string name="alarm_sound_default" msgid="5488847775252870314">"ነባሪ የማንቂያ ድምፅ"</string>
+    <string name="unknown" msgid="2059049215682829375">"የማይታወቅ"</string>
     <string name="root_images" msgid="5861633549189045666">"ምስሎች"</string>
     <string name="root_videos" msgid="8792703517064649453">"ቪዲዮዎች"</string>
     <string name="root_audio" msgid="3505830755201326018">"ኦዲዮ"</string>
+    <string name="root_documents" msgid="3829103301363849237">"ሰነዶች"</string>
     <string name="permission_required" msgid="1460820436132943754">"ይህን ንጥል ለማሻሻል ወይም ለመሰረዝ ፈቃድ ያስፈልጋል።"</string>
     <string name="permission_required_action" msgid="706370952366113539">"ቀጥል"</string>
-    <string name="permission_audio" msgid="344061911159388398"><b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" ይህን ሙዚቃ እንዲቀይር ወይም እንዲሰርዝ ይፈቀድለት?"</string>
-    <string name="permission_video" msgid="8188037202573267740"><b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" ይህን ቪዲዮ እንዲቀይሩ ወይም እንዲሰርዙ ይፈቀድለት?"</string>
-    <string name="permission_images" msgid="6711726334380945124"><b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" ይህን ምስል እንዲቀይር ወይም እንዲሰርዝ ይፈቀድለት?"</string>
     <string name="grant_dialog_button_allow" msgid="1644287024501033471">"ፍቀድ"</string>
     <string name="grant_dialog_button_deny" msgid="6190589471415815741">"ከልክል"</string>
-    <string name="sound_name_awaken" msgid="5266892392848526147">"ንቃ"</string>
-    <string name="sound_name_bounce" msgid="8771447635446665231">"አንጥር"</string>
-    <string name="sound_name_drip" msgid="1744684469020662152">"አንጠባጥብ"</string>
-    <string name="sound_name_gallop" msgid="2664454314532060876">"ጋልብ"</string>
-    <string name="sound_name_nudge" msgid="5445751598250698244">"ገፋ አድርግ"</string>
-    <string name="sound_name_orbit" msgid="4623457897813255481">"ምሕዋር"</string>
-    <string name="sound_name_rise" msgid="2200258555031675806">"ተነሳ"</string>
-    <string name="sound_name_sway" msgid="348448316663085643">"ተወዛወዝ"</string>
-    <string name="sound_name_wag" msgid="2730733083078126563">"አወዛውዝ"</string>
-    <string name="add_ringtone_text" msgid="8077717303037760418">"የጥሪ ቅላጼ አክል"</string>
-    <string name="add_alarm_text" msgid="2440919366091720815">"የማንቂያ ደውል አክል"</string>
-    <string name="add_notification_text" msgid="5013822215567515665">"ማሳወቂያን አክል"</string>
-    <string name="delete_ringtone_text" msgid="2963662097583300181">"ሰርዝ"</string>
-    <string name="unable_to_add_ringtone" msgid="6256097521165711269">"ብጁ የጥሪ ቅላጼን ማከል አልተቻለም"</string>
-    <string name="unable_to_delete_ringtone" msgid="2258180073859253495">"ብጁ የጥሪ ቅላጼን መሰረዝ አልተቻለም"</string>
+    <plurals name="permission_more_thumb" formatted="false" msgid="4392079224649478923">
+      <item quantity="one">+<xliff:g id="COUNT_1">^1</xliff:g></item>
+      <item quantity="other">+<xliff:g id="COUNT_1">^1</xliff:g></item>
+    </plurals>
+    <plurals name="permission_more_text" formatted="false" msgid="7291997297174507324">
+      <item quantity="one">እንዲሁም <xliff:g id="COUNT_1">^1</xliff:g> ተጨማሪ ንጥሎች</item>
+      <item quantity="other">እንዲሁም <xliff:g id="COUNT_1">^1</xliff:g> ተጨማሪ ንጥሎች</item>
+    </plurals>
+    <string name="cache_clearing_dialog_title" msgid="4672878017407595782">"ጊዜያዊ የመተግበሪያ ፋይሎች ይጽዱ?"</string>
+    <string name="cache_clearing_dialog_text" msgid="7057784635111940957">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> አንዳንድ ጊዜያዊ ፋይሎችን ማጽዳት ይፈልጋል። ይህ የበለጠ የባትሪ ኃይል ወይም የተንቀሳቃሽ ስልክ ውሂብ ፍጆታን ሊጨምር ይችላል።"</string>
+    <string name="cache_clearing_in_progress_title" msgid="6902220064511664209">"ጊዜያዊ የመተግበሪያ ፋይሎችን በማጽዳት ላይ…"</string>
+    <string name="allow" msgid="8885707816848569619">"ፍቀድ"</string>
+    <string name="deny" msgid="6040983710442068936">"ከልክል"</string>
+    <plurals name="permission_write_audio" formatted="false" msgid="8914759422381305478">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ኦዲዮ ፋይሎችን እንዲቀይር ይፈቀድለት?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ኦዲዮ ፋይሎችን እንዲቀይር ይፈቀድለት?</item>
+    </plurals>
+    <plurals name="permission_write_video" formatted="false" msgid="1098082003326873084">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ቪዲዮዎችን እንዲቀይር ይፈቀድለት?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ቪዲዮዎችን እንዲቀይር ይፈቀድለት?</item>
+    </plurals>
+    <plurals name="permission_write_image" formatted="false" msgid="748745548893845892">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ፎቶዎችን እንዲቀይር ይፈቀድለት?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ፎቶዎችን እንዲቀይር ይፈቀድለት?</item>
+    </plurals>
+    <plurals name="permission_write_generic" formatted="false" msgid="3270172714743671779">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ንጥሎችን እንዲቀይር ይፈቀድለት?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ንጥሎችን እንዲቀይር ይፈቀድለት?</item>
+    </plurals>
+    <plurals name="permission_trash_audio" formatted="false" msgid="8907813869381755423">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ኦዲዮ ፋይሎችን ወደ መጣያ እንዲወስድ ይፈቀድለት?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ኦዲዮ ፋይሎችን ወደ መጣያ እንዲወስድ ይፈቀድለት?</item>
+    </plurals>
+    <plurals name="permission_trash_video" formatted="false" msgid="4672871911555787438">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ቪዲዮዎችን ወደ መጣያ እንዲወስድ ይፈቀድለት?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ቪዲዮዎችን ወደ መጣያ እንዲወስድ ይፈቀድለት?</item>
+    </plurals>
+    <plurals name="permission_trash_image" formatted="false" msgid="6400475304599873227">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ፎቶዎችን ወደ መጣያ እንዲወስድ ይፈቀድለት?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ፎቶዎችን ወደ መጣያ እንዲወስድ ይፈቀድለት?</item>
+    </plurals>
+    <plurals name="permission_trash_generic" formatted="false" msgid="3814167365075039711">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ንጥሎችን ወደ መጣያ እንዲወስድ ይፈቀድለት?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ንጥሎችን ወደ መጣያ እንዲወስድ ይፈቀድለት?</item>
+    </plurals>
+    <plurals name="permission_untrash_audio" formatted="false" msgid="7795265980168966321">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ኦዲዮ ፋይሎችን ከመጣያ እንዲያስወጣ ይፈቀድለት?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ኦዲዮ ፋይሎችን ከመጣያ እንዲያስወጣ ይፈቀድለት?</item>
+    </plurals>
+    <plurals name="permission_untrash_video" formatted="false" msgid="332894888445508879">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ቪዲዮዎችን ከመጣያ እንዲያስወጣ ይፈቀድለት?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ቪዲዮዎችን ከመጣያ እንዲያስወጣ ይፈቀድለት?</item>
+    </plurals>
+    <plurals name="permission_untrash_image" formatted="false" msgid="7024071378733595056">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ፎቶዎችን ከመጣያ እንዲያስወጣ ይፈቀድለት?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ፎቶዎችን ከመጣያ እንዲያስወጣ ይፈቀድለት?</item>
+    </plurals>
+    <plurals name="permission_untrash_generic" formatted="false" msgid="6872817093731198374">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ንጥሎችን ከመጣያ እንዲያስወጣ ይፈቀድለት?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ንጥሎችን ከመጣያ እንዲያስወጣ ይፈቀድለት?</item>
+    </plurals>
+    <plurals name="permission_delete_audio" formatted="false" msgid="6848547621165184719">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> የኦዲዮ ፋይሎችን እንዲሰረዝ ይፈቀድለት?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> የኦዲዮ ፋይሎችን እንዲሰረዝ ይፈቀድለት?</item>
+    </plurals>
+    <plurals name="permission_delete_video" formatted="false" msgid="1251942606336748563">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ቪዲዮዎችን እንዲሰረዝ ይፈቀድለት?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ቪዲዮዎችን እንዲሰረዝ ይፈቀድለት?</item>
+    </plurals>
+    <plurals name="permission_delete_image" formatted="false" msgid="2303409455224710111">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ፎቶዎችን እንዲሰረዝ ይፈቀድለት?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ፎቶዎችን እንዲሰረዝ ይፈቀድለት?</item>
+    </plurals>
+    <plurals name="permission_delete_generic" formatted="false" msgid="1412218850351841181">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ንጥሎችን እንዲሰረዝ ይፈቀድለት?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ንጥሎችን እንዲሰረዝ ይፈቀድለት?</item>
+    </plurals>
 </resources>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index c8698f1..dd3b567 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -20,32 +20,162 @@
     <string name="storage_description" msgid="4081716890357580107">"التخزين المحلي"</string>
     <string name="app_label" msgid="9035307001052716210">"تخزين الوسائط"</string>
     <string name="artist_label" msgid="8105600993099120273">"الفنان"</string>
-    <string name="ringtone_default" msgid="1744846699922263043">"نغمة الرنين التلقائية"</string>
-    <string name="notification_sound_default" msgid="6541609166469990135">"الصوت التلقائي للإشعارات"</string>
-    <string name="alarm_sound_default" msgid="5488847775252870314">"الصوت التلقائي للمنبّه"</string>
+    <string name="unknown" msgid="2059049215682829375">"غير معروف"</string>
     <string name="root_images" msgid="5861633549189045666">"الصور"</string>
     <string name="root_videos" msgid="8792703517064649453">"الفيديوهات"</string>
     <string name="root_audio" msgid="3505830755201326018">"صوتيات"</string>
+    <string name="root_documents" msgid="3829103301363849237">"المستندات"</string>
     <string name="permission_required" msgid="1460820436132943754">"مطلوب الحصول على إذن لتعديل هذا العنصر أو حذفه."</string>
     <string name="permission_required_action" msgid="706370952366113539">"متابعة"</string>
-    <string name="permission_audio" msgid="344061911159388398">"هل تريد السماح لتطبيق "<b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" بتعديل ملف الموسيقى هذا أو حذفه؟"</string>
-    <string name="permission_video" msgid="8188037202573267740">"هل تريد السماح لتطبيق "<b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" بتعديل هذا الفيديو أو حذفه؟"</string>
-    <string name="permission_images" msgid="6711726334380945124">"هل تريد السماح لتطبيق "<b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" بتعديل هذه الصورة أو حذفها؟"</string>
     <string name="grant_dialog_button_allow" msgid="1644287024501033471">"سماح"</string>
     <string name="grant_dialog_button_deny" msgid="6190589471415815741">"رفض"</string>
-    <string name="sound_name_awaken" msgid="5266892392848526147">"إيقاظ"</string>
-    <string name="sound_name_bounce" msgid="8771447635446665231">"ارتداد"</string>
-    <string name="sound_name_drip" msgid="1744684469020662152">"تَقَطُّر"</string>
-    <string name="sound_name_gallop" msgid="2664454314532060876">"عَدْو الفَرَس"</string>
-    <string name="sound_name_nudge" msgid="5445751598250698244">"وكزة"</string>
-    <string name="sound_name_orbit" msgid="4623457897813255481">"مدار"</string>
-    <string name="sound_name_rise" msgid="2200258555031675806">"ارتفاع"</string>
-    <string name="sound_name_sway" msgid="348448316663085643">"تأرجُح"</string>
-    <string name="sound_name_wag" msgid="2730733083078126563">"اهتزاز"</string>
-    <string name="add_ringtone_text" msgid="8077717303037760418">"إضافة نغمة رنين"</string>
-    <string name="add_alarm_text" msgid="2440919366091720815">"إضافة منبه"</string>
-    <string name="add_notification_text" msgid="5013822215567515665">"إضافة إشعار"</string>
-    <string name="delete_ringtone_text" msgid="2963662097583300181">"حذف"</string>
-    <string name="unable_to_add_ringtone" msgid="6256097521165711269">"يتعذر إضافة نغمة رنين مخصصة"</string>
-    <string name="unable_to_delete_ringtone" msgid="2258180073859253495">"يتعذر حذف نغمة الرنين المخصصة"</string>
+    <plurals name="permission_more_thumb" formatted="false" msgid="4392079224649478923">
+      <item quantity="zero">+<xliff:g id="COUNT_1">^1</xliff:g></item>
+      <item quantity="two">+<xliff:g id="COUNT_1">^1</xliff:g></item>
+      <item quantity="few">+<xliff:g id="COUNT_1">^1</xliff:g></item>
+      <item quantity="many">+<xliff:g id="COUNT_1">^1</xliff:g></item>
+      <item quantity="other">+<xliff:g id="COUNT_1">^1</xliff:g></item>
+      <item quantity="one">+<xliff:g id="COUNT_0">^1</xliff:g></item>
+    </plurals>
+    <plurals name="permission_more_text" formatted="false" msgid="7291997297174507324">
+      <item quantity="zero">و<xliff:g id="COUNT_1">^1</xliff:g> عنصر إضافي</item>
+      <item quantity="two">وعنصران إضافيان (<xliff:g id="COUNT_1">^1</xliff:g>)</item>
+      <item quantity="few">و<xliff:g id="COUNT_1">^1</xliff:g> عناصر إضافية</item>
+      <item quantity="many">و<xliff:g id="COUNT_1">^1</xliff:g> عنصرًا إضافيًا</item>
+      <item quantity="other">و<xliff:g id="COUNT_1">^1</xliff:g> عنصر إضافي</item>
+      <item quantity="one">وعنصر إضافي واحد (<xliff:g id="COUNT_0">^1</xliff:g>)</item>
+    </plurals>
+    <string name="cache_clearing_dialog_title" msgid="4672878017407595782">"هل تريد محو ملفات التطبيق المؤقتة؟"</string>
+    <string name="cache_clearing_dialog_text" msgid="7057784635111940957">"يريد تطبيق <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> محو بعض الملفات المؤقتة. قد يؤدي هذا إلى زيادة استهلاك شحن البطارية أو بيانات شبكة الجوّال."</string>
+    <string name="cache_clearing_in_progress_title" msgid="6902220064511664209">"جارٍ محو ملفات التطبيق المؤقتة…"</string>
+    <string name="allow" msgid="8885707816848569619">"سماح"</string>
+    <string name="deny" msgid="6040983710442068936">"رفض"</string>
+    <plurals name="permission_write_audio" formatted="false" msgid="8914759422381305478">
+      <item quantity="zero">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> ملف صوتي؟</item>
+      <item quantity="two">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل ملفين صوتيين (<xliff:g id="COUNT">^2</xliff:g>)؟</item>
+      <item quantity="few">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> ملفات صوتية؟</item>
+      <item quantity="many">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> ملفًا صوتيًا؟</item>
+      <item quantity="other">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> ملف صوتي؟</item>
+      <item quantity="one">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_0">^1</xliff:g> بتعديل هذا الملف الصوتي؟</item>
+    </plurals>
+    <plurals name="permission_write_video" formatted="false" msgid="1098082003326873084">
+      <item quantity="zero">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> فيديو؟</item>
+      <item quantity="two">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل فيديوهين (<xliff:g id="COUNT">^2</xliff:g>)؟</item>
+      <item quantity="few">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> فيديوهات؟</item>
+      <item quantity="many">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> فيديو؟</item>
+      <item quantity="other">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> فيديو؟</item>
+      <item quantity="one">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_0">^1</xliff:g> بتعديل هذا الفيديو؟</item>
+    </plurals>
+    <plurals name="permission_write_image" formatted="false" msgid="748745548893845892">
+      <item quantity="zero">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> صورة؟</item>
+      <item quantity="two">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل صورتين (<xliff:g id="COUNT">^2</xliff:g>)؟</item>
+      <item quantity="few">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> صور؟</item>
+      <item quantity="many">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> صورة؟</item>
+      <item quantity="other">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> صورة؟</item>
+      <item quantity="one">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_0">^1</xliff:g> بتعديل هذه الصورة؟</item>
+    </plurals>
+    <plurals name="permission_write_generic" formatted="false" msgid="3270172714743671779">
+      <item quantity="zero">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> عنصر؟</item>
+      <item quantity="two">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل عنصرين (<xliff:g id="COUNT">^2</xliff:g>)؟</item>
+      <item quantity="few">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> عناصر؟</item>
+      <item quantity="many">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> عنصرًا؟</item>
+      <item quantity="other">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> عنصر؟</item>
+      <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="zero">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> ملف صوتي إلى المهملات؟</item>
+      <item quantity="two">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل ملفين صوتيين (<xliff:g id="COUNT">^2</xliff:g>) إلى المهملات؟</item>
+      <item quantity="few">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> ملفات صوتية إلى المهملات؟</item>
+      <item quantity="many">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> ملفًا صوتية إلى المهملات؟</item>
+      <item quantity="other">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</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="zero">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> فيديو إلى المهملات؟</item>
+      <item quantity="two">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل فيديوهين (<xliff:g id="COUNT">^2</xliff:g>) إلى المهملات؟</item>
+      <item quantity="few">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> فيديوهات إلى المهملات؟</item>
+      <item quantity="many">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> فيديو إلى المهملات؟</item>
+      <item quantity="other">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</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="zero">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> صورة إلى المهملات؟</item>
+      <item quantity="two">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل صورتين (<xliff:g id="COUNT">^2</xliff:g>) إلى المهملات؟</item>
+      <item quantity="few">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> صور إلى المهملات؟</item>
+      <item quantity="many">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> صورة إلى المهملات؟</item>
+      <item quantity="other">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</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="zero">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> عنصر إلى المهملات؟</item>
+      <item quantity="two">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل عنصرين (<xliff:g id="COUNT">^2</xliff:g>) إلى المهملات؟</item>
+      <item quantity="few">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> عناصر إلى المهملات؟</item>
+      <item quantity="many">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> عنصرًا إلى المهملات؟</item>
+      <item quantity="other">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</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="zero">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> ملف صوتي خارج المهملات؟</item>
+      <item quantity="two">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل ملفين صوتيين (<xliff:g id="COUNT">^2</xliff:g>) خارج المهملات؟</item>
+      <item quantity="few">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> ملفات صوتية خارج المهملات؟</item>
+      <item quantity="many">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> ملفًا صوتيًا خارج المهملات؟</item>
+      <item quantity="other">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</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="zero">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> فيديو خارج المهملات؟</item>
+      <item quantity="two">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل فيديوهين (<xliff:g id="COUNT">^2</xliff:g>) خارج المهملات؟</item>
+      <item quantity="few">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> فيديوهات خارج المهملات؟</item>
+      <item quantity="many">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> فيديو خارج المهملات؟</item>
+      <item quantity="other">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</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="zero">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> صورة خارج المهملات؟</item>
+      <item quantity="two">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل صورتين (<xliff:g id="COUNT">^2</xliff:g>) خارج المهملات؟</item>
+      <item quantity="few">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> صور خارج المهملات؟</item>
+      <item quantity="many">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> صورة خارج المهملات؟</item>
+      <item quantity="other">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</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="zero">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> عنصر خارج المهملات؟</item>
+      <item quantity="two">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل عنصرين (<xliff:g id="COUNT">^2</xliff:g>) خارج المهملات؟</item>
+      <item quantity="few">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> عناصر خارج المهملات؟</item>
+      <item quantity="many">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</xliff:g> عنصرًا خارج المهملات؟</item>
+      <item quantity="other">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بنقل <xliff:g id="COUNT">^2</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="zero">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف <xliff:g id="COUNT">^2</xliff:g> ملف صوتي؟</item>
+      <item quantity="two">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف ملفين صوتيين (<xliff:g id="COUNT">^2</xliff:g>)؟</item>
+      <item quantity="few">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف <xliff:g id="COUNT">^2</xliff:g> ملفات صوتية؟</item>
+      <item quantity="many">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف <xliff:g id="COUNT">^2</xliff:g> ملفًا صوتيًا؟</item>
+      <item quantity="other">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف <xliff:g id="COUNT">^2</xliff:g> ملف صوتي؟</item>
+      <item quantity="one">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_0">^1</xliff:g> بحذف هذا الملف الصوتي؟</item>
+    </plurals>
+    <plurals name="permission_delete_video" formatted="false" msgid="1251942606336748563">
+      <item quantity="zero">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف <xliff:g id="COUNT">^2</xliff:g> فيديو؟</item>
+      <item quantity="two">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف فيديوهين (<xliff:g id="COUNT">^2</xliff:g>)؟</item>
+      <item quantity="few">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف <xliff:g id="COUNT">^2</xliff:g> فيديوهات؟</item>
+      <item quantity="many">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف <xliff:g id="COUNT">^2</xliff:g> فيديو؟</item>
+      <item quantity="other">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف <xliff:g id="COUNT">^2</xliff:g> فيديو؟</item>
+      <item quantity="one">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_0">^1</xliff:g> بحذف هذا الفيديو؟</item>
+    </plurals>
+    <plurals name="permission_delete_image" formatted="false" msgid="2303409455224710111">
+      <item quantity="zero">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف <xliff:g id="COUNT">^2</xliff:g> صورة؟</item>
+      <item quantity="two">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف صورتين (<xliff:g id="COUNT">^2</xliff:g>)؟</item>
+      <item quantity="few">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف <xliff:g id="COUNT">^2</xliff:g> صور؟</item>
+      <item quantity="many">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف <xliff:g id="COUNT">^2</xliff:g> صورة؟</item>
+      <item quantity="other">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف <xliff:g id="COUNT">^2</xliff:g> صورة؟</item>
+      <item quantity="one">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_0">^1</xliff:g> بحذف صورة واحدة؟</item>
+    </plurals>
+    <plurals name="permission_delete_generic" formatted="false" msgid="1412218850351841181">
+      <item quantity="zero">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف <xliff:g id="COUNT">^2</xliff:g> عنصر؟</item>
+      <item quantity="two">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف عنصرين (<xliff:g id="COUNT">^2</xliff:g>)؟</item>
+      <item quantity="few">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف <xliff:g id="COUNT">^2</xliff:g> عناصر؟</item>
+      <item quantity="many">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف <xliff:g id="COUNT">^2</xliff:g> عنصرًا؟</item>
+      <item quantity="other">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بحذف <xliff:g id="COUNT">^2</xliff:g> عنصر؟</item>
+      <item quantity="one">هل تريد السماح لتطبيق <xliff:g id="APP_NAME_0">^1</xliff:g> بحذف هذا العنصر؟</item>
+    </plurals>
 </resources>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index 6cc857c..d6c697e 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -20,32 +20,90 @@
     <string name="storage_description" msgid="4081716890357580107">"স্থানীয় সঞ্চয়াগাৰ"</string>
     <string name="app_label" msgid="9035307001052716210">"মিডিয়া সঞ্চয়াগাৰ"</string>
     <string name="artist_label" msgid="8105600993099120273">"শিল্পী"</string>
-    <string name="ringtone_default" msgid="1744846699922263043">"ডিফ\'ল্ট ৰিংট\'ন"</string>
-    <string name="notification_sound_default" msgid="6541609166469990135">"জাননীৰ ডিফ\'ল্ট ধ্বনি"</string>
-    <string name="alarm_sound_default" msgid="5488847775252870314">"এলাৰ্মৰ ডিফ\'ল্ট ধ্বনি"</string>
+    <string name="unknown" msgid="2059049215682829375">"অজ্ঞাত"</string>
     <string name="root_images" msgid="5861633549189045666">"প্ৰতিচ্ছবিসমূহ"</string>
     <string name="root_videos" msgid="8792703517064649453">"ভিডিঅ\'সমূহ"</string>
     <string name="root_audio" msgid="3505830755201326018">"অডিঅ’"</string>
+    <string name="root_documents" msgid="3829103301363849237">"নথিপত্র"</string>
     <string name="permission_required" msgid="1460820436132943754">"এই বস্তুটো সংশোধন কৰিবলৈ বা মচিবলৈ অনুমতিৰ প্ৰয়োজন।"</string>
     <string name="permission_required_action" msgid="706370952366113539">"অব্যাহত ৰাখক"</string>
-    <string name="permission_audio" msgid="344061911159388398"><b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>"ক এই সংগীতটো সংশোধন কৰিবলৈ বা মচিবলৈ অনুমতি দিবনে?"</string>
-    <string name="permission_video" msgid="8188037202573267740"><b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>"ক এই ভিডিঅ’টো সংশোধন কৰিবলৈ বা মচিবলৈ অনুমতি দিবনে?"</string>
-    <string name="permission_images" msgid="6711726334380945124"><b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>"ক এই ছবিখন সংশোধন কৰিবলৈ বা মচিবলৈ অনুমতি দিবনে?"</string>
     <string name="grant_dialog_button_allow" msgid="1644287024501033471">"অনুমতি দিয়ক"</string>
     <string name="grant_dialog_button_deny" msgid="6190589471415815741">"অস্বীকাৰ কৰক"</string>
-    <string name="sound_name_awaken" msgid="5266892392848526147">"জাগি উঠাৰ ধ্বনি"</string>
-    <string name="sound_name_bounce" msgid="8771447635446665231">"বাউঞ্চ ধ্বনি"</string>
-    <string name="sound_name_drip" msgid="1744684469020662152">"ড্ৰিপ ধ্বনি"</string>
-    <string name="sound_name_gallop" msgid="2664454314532060876">"গেল\'প ধ্বনি"</string>
-    <string name="sound_name_nudge" msgid="5445751598250698244">"নাজ ধ্বনি"</string>
-    <string name="sound_name_orbit" msgid="4623457897813255481">"অৰবিট ধ্বনি"</string>
-    <string name="sound_name_rise" msgid="2200258555031675806">"ৰাইজ ধ্বনি"</string>
-    <string name="sound_name_sway" msgid="348448316663085643">"শ্ব\'ৱে ধ্বনি"</string>
-    <string name="sound_name_wag" msgid="2730733083078126563">"ৱাগ ধ্বনি"</string>
-    <string name="add_ringtone_text" msgid="8077717303037760418">"ৰিংট\'ন যোগ কৰক"</string>
-    <string name="add_alarm_text" msgid="2440919366091720815">"এলাৰ্ম যোগ কৰক"</string>
-    <string name="add_notification_text" msgid="5013822215567515665">"জাননী যোগ কৰক"</string>
-    <string name="delete_ringtone_text" msgid="2963662097583300181">"মচক"</string>
-    <string name="unable_to_add_ringtone" msgid="6256097521165711269">"নিজৰ উপযোগিতা অনুযায়ী তৈয়াৰ কৰা ৰিংট\'ন যোগ কৰিব পৰা নগ\'ল"</string>
-    <string name="unable_to_delete_ringtone" msgid="2258180073859253495">"নিজৰ উপযোগিতা অনুযায়ী তৈয়াৰ কৰা ৰিংট\'ন মচিব পৰা নগ\'ল"</string>
+    <plurals name="permission_more_thumb" formatted="false" msgid="4392079224649478923">
+      <item quantity="one">+<xliff:g id="COUNT_1">^1</xliff:g> টা</item>
+      <item quantity="other">+<xliff:g id="COUNT_1">^1</xliff:g> টা</item>
+    </plurals>
+    <plurals name="permission_more_text" formatted="false" msgid="7291997297174507324">
+      <item quantity="one">আৰু <xliff:g id="COUNT_1">^1</xliff:g> টা অতিৰিক্ত বস্তু</item>
+      <item quantity="other">আৰু <xliff:g id="COUNT_1">^1</xliff:g> টা অতিৰিক্ত বস্তু</item>
+    </plurals>
+    <string name="cache_clearing_dialog_title" msgid="4672878017407595782">"অস্থায়ী এপ্‌ ফাইলসমূহ মচিবনে?"</string>
+    <string name="cache_clearing_dialog_text" msgid="7057784635111940957">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g>এ কিছুমান অস্থায়ী ফাইল মচিব বিচাৰিছে। ইয়াৰ ফলত বেটাৰী অথবা চেলুলাৰ ডেটাৰ ব্যৱহাৰ বৃদ্ধি হ’ব পাৰে।"</string>
+    <string name="cache_clearing_in_progress_title" msgid="6902220064511664209">"অস্থায়ী এপ্‌ ফাইলসমূহ মচি থকা হৈছে…"</string>
+    <string name="allow" msgid="8885707816848569619">"অনুমতি দিয়ক"</string>
+    <string name="deny" msgid="6040983710442068936">"অস্বীকাৰ কৰক"</string>
+    <plurals name="permission_write_audio" formatted="false" msgid="8914759422381305478">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা অডিঅ’ ফাইল সংশোধন কৰিবলৈ অনুমতি দিবনে?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা অডিঅ’ ফাইল সংশোধন কৰিবলৈ অনুমতি দিবনে?</item>
+    </plurals>
+    <plurals name="permission_write_video" formatted="false" msgid="1098082003326873084">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা ভিডিঅ’ সংশোধন কৰিবলৈ অনুমতি দিবনে?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা ভিডিঅ’ সংশোধন কৰিবলৈ অনুমতি দিবনে?</item>
+    </plurals>
+    <plurals name="permission_write_image" formatted="false" msgid="748745548893845892">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> খন ফট’ সংশোধন কৰিবলৈ অনুমতি দিবনে?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> খন ফট’ সংশোধন কৰিবলৈ অনুমতি দিবনে?</item>
+    </plurals>
+    <plurals name="permission_write_generic" formatted="false" msgid="3270172714743671779">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা বস্তু সংশোধন কৰিবলৈ অনুমতি দিবনে?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা বস্তু সংশোধন কৰিবলৈ অনুমতি দিবনে?</item>
+    </plurals>
+    <plurals name="permission_trash_audio" formatted="false" msgid="8907813869381755423">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা অডিঅ’ ফাইল ট্ৰেশ্বলৈ স্থানান্তৰ কৰিবলৈ অনুমতি দিবনে?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা অডিঅ’ ফাইল ট্ৰেশ্বলৈ স্থানান্তৰ কৰিবলৈ অনুমতি দিবনে?</item>
+    </plurals>
+    <plurals name="permission_trash_video" formatted="false" msgid="4672871911555787438">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা ভিডিঅ’ ট্ৰেশ্বলৈ স্থানান্তৰ কৰিবলৈ অনুমতি দিবনে?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা ভিডিঅ’ ট্ৰেশ্বলৈ স্থানান্তৰ কৰিবলৈ অনুমতি দিবনে?</item>
+    </plurals>
+    <plurals name="permission_trash_image" formatted="false" msgid="6400475304599873227">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> খন ফট’ ট্ৰেশ্বলৈ স্থানান্তৰ কৰিবলৈ অনুমতি দিবনে?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> খন ফট’ ট্ৰেশ্বলৈ স্থানান্তৰ কৰিবলৈ অনুমতি দিবনে?</item>
+    </plurals>
+    <plurals name="permission_trash_generic" formatted="false" msgid="3814167365075039711">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা বস্তু ট্ৰেশ্বলৈ স্থানান্তৰ কৰিবলৈ অনুমতি দিবনে?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা বস্তু ট্ৰেশ্বলৈ স্থানান্তৰ কৰিবলৈ অনুমতি দিবনে?</item>
+    </plurals>
+    <plurals name="permission_untrash_audio" formatted="false" msgid="7795265980168966321">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা অডিঅ’ ফাইল ট্ৰেশ্বৰ পৰা বাহিৰলৈ স্থানান্তৰ কৰিবলৈ অনুমতি দিবনে?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা অডিঅ’ ফাইল ট্ৰেশ্বৰ পৰা বাহিৰলৈ স্থানান্তৰ কৰিবলৈ অনুমতি দিবনে?</item>
+    </plurals>
+    <plurals name="permission_untrash_video" formatted="false" msgid="332894888445508879">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা ভিডিঅ’ ট্ৰেশ্বৰ পৰা বাহিৰলৈ স্থানান্তৰ কৰিবলৈ অনুমতি দিবনে?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা ভিডিঅ’ ট্ৰেশ্বৰ পৰা বাহিৰলৈ স্থানান্তৰ কৰিবলৈ অনুমতি দিবনে?</item>
+    </plurals>
+    <plurals name="permission_untrash_image" formatted="false" msgid="7024071378733595056">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> খন ফট’ ট্ৰেশ্বৰ পৰা বাহিৰলৈ স্থানান্তৰ কৰিবলৈ অনুমতি দিবনে?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> খন ফট’ ট্ৰেশ্বৰ পৰা বাহিৰলৈ স্থানান্তৰ কৰিবলৈ অনুমতি দিবনে?</item>
+    </plurals>
+    <plurals name="permission_untrash_generic" formatted="false" msgid="6872817093731198374">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা বস্তু ট্ৰেশ্বৰ পৰা বাহিৰলৈ স্থানান্তৰ কৰিবলৈ অনুমতি দিবনে?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা বস্তু ট্ৰেশ্বৰ পৰা বাহিৰলৈ স্থানান্তৰ কৰিবলৈ অনুমতি দিবনে?</item>
+    </plurals>
+    <plurals name="permission_delete_audio" formatted="false" msgid="6848547621165184719">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা অডিঅ’ ফাইল মচিবলৈ অনুমতি দিবনে?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা অডিঅ’ ফাইল মচিবলৈ অনুমতি দিবনে?</item>
+    </plurals>
+    <plurals name="permission_delete_video" formatted="false" msgid="1251942606336748563">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা ভিডিঅ’ মচিবলৈ অনুমতি দিবনে?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা ভিডিঅ’ মচিবলৈ অনুমতি দিবনে?</item>
+    </plurals>
+    <plurals name="permission_delete_image" formatted="false" msgid="2303409455224710111">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> খন ফট’ মচিবলৈ অনুমতি দিবনে?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> খন ফট’ মচিবলৈ অনুমতি দিবনে?</item>
+    </plurals>
+    <plurals name="permission_delete_generic" formatted="false" msgid="1412218850351841181">
+      <item quantity="one"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা বস্তু মচিবলৈ অনুমতি দিবনে?</item>
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা বস্তু মচিবলৈ অনুমতি দিবনে?</item>
+    </plurals>
 </resources>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index c283a4b..4a278d7 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -20,32 +20,90 @@
     <string name="storage_description" msgid="4081716890357580107">"Yerli yaddaş"</string>
     <string name="app_label" msgid="9035307001052716210">"Media Yaddaşı"</string>
     <string name="artist_label" msgid="8105600993099120273">"Sənətçi"</string>
-    <string name="ringtone_default" msgid="1744846699922263043">"Defolt rinqton"</string>
-    <string name="notification_sound_default" msgid="6541609166469990135">"Defolt bildiriş səsi"</string>
-    <string name="alarm_sound_default" msgid="5488847775252870314">"Defolt siqnal səsi"</string>
+    <string name="unknown" msgid="2059049215682829375">"Naməlum"</string>
     <string name="root_images" msgid="5861633549189045666">"Təsvirlər"</string>
     <string name="root_videos" msgid="8792703517064649453">"Videolar"</string>
     <string name="root_audio" msgid="3505830755201326018">"Audio"</string>
+    <string name="root_documents" msgid="3829103301363849237">"Sənədlər"</string>
     <string name="permission_required" msgid="1460820436132943754">"Bu elementi dəyişmək və ya silmək üçün icazə tələb edilir."</string>
     <string name="permission_required_action" msgid="706370952366113539">"Davam edin"</string>
-    <string name="permission_audio" msgid="344061911159388398">"Bu musiqini dəyişmək və ya silmək üçün "<b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" tətbiqinə icazə verilsin?"</string>
-    <string name="permission_video" msgid="8188037202573267740">"Bu videonu dəyişmək və ya silmək üçün "<b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" tətbiqinə icazə verilsin?"</string>
-    <string name="permission_images" msgid="6711726334380945124">"Bu şəkli dəyişmək və ya silmək üçün "<b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" tətbiqinə icazə verilsin?"</string>
     <string name="grant_dialog_button_allow" msgid="1644287024501033471">"İcazə verin"</string>
     <string name="grant_dialog_button_deny" msgid="6190589471415815741">"Rədd edin"</string>
-    <string name="sound_name_awaken" msgid="5266892392848526147">"Oyatma"</string>
-    <string name="sound_name_bounce" msgid="8771447635446665231">"Sıçrayış"</string>
-    <string name="sound_name_drip" msgid="1744684469020662152">"Damcılatma"</string>
-    <string name="sound_name_gallop" msgid="2664454314532060876">"Çaparaq"</string>
-    <string name="sound_name_nudge" msgid="5445751598250698244">"Tərpətmə"</string>
-    <string name="sound_name_orbit" msgid="4623457897813255481">"Orbit"</string>
-    <string name="sound_name_rise" msgid="2200258555031675806">"Ucalan tempdə"</string>
-    <string name="sound_name_sway" msgid="348448316663085643">"Yellənmə"</string>
-    <string name="sound_name_wag" msgid="2730733083078126563">"Fırlatma"</string>
-    <string name="add_ringtone_text" msgid="8077717303037760418">"Zəng səsi daxil edin"</string>
-    <string name="add_alarm_text" msgid="2440919366091720815">"Siqnal əlavə edin"</string>
-    <string name="add_notification_text" msgid="5013822215567515665">"Bildiriş əlavə edin"</string>
-    <string name="delete_ringtone_text" msgid="2963662097583300181">"Silin"</string>
-    <string name="unable_to_add_ringtone" msgid="6256097521165711269">"Fərdi zəng səsi əlavə etmək mümkün deyil"</string>
-    <string name="unable_to_delete_ringtone" msgid="2258180073859253495">"Fərdi zəng səsini silmək mümkün deyil"</string>
+    <plurals name="permission_more_thumb" formatted="false" msgid="4392079224649478923">
+      <item quantity="other">+<xliff:g id="COUNT_1">^1</xliff:g></item>
+      <item quantity="one">+<xliff:g id="COUNT_0">^1</xliff:g></item>
+    </plurals>
+    <plurals name="permission_more_text" formatted="false" msgid="7291997297174507324">
+      <item quantity="other">Üstəgəl <xliff:g id="COUNT_1">^1</xliff:g> əlavə element</item>
+      <item quantity="one">Üstəgəl <xliff:g id="COUNT_0">^1</xliff:g> əlavə element</item>
+    </plurals>
+    <string name="cache_clearing_dialog_title" msgid="4672878017407595782">"Müvəqqəti tətbiq faylları silinsin?"</string>
+    <string name="cache_clearing_dialog_text" msgid="7057784635111940957">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> bəzi müvəqqəti faylları silmək istəyir. Bu, batareya və ya mobil data istifadəsinin artmasına səbəb ola bilər."</string>
+    <string name="cache_clearing_in_progress_title" msgid="6902220064511664209">"Müvəqqəti tətbiq faylları silinir…"</string>
+    <string name="allow" msgid="8885707816848569619">"İcazə verin"</string>
+    <string name="deny" msgid="6040983710442068936">"Rədd edin"</string>
+    <plurals name="permission_write_audio" formatted="false" msgid="8914759422381305478">
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> tətbiqinə <xliff:g id="COUNT">^2</xliff:g> audio fayla dəyişiklik etmək icazəsi verilsin?</item>
+      <item quantity="one"><xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu audio fayla dəyişiklik etmək icazəsi verilsin?</item>
+    </plurals>
+    <plurals name="permission_write_video" formatted="false" msgid="1098082003326873084">
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> tətbiqinə <xliff:g id="COUNT">^2</xliff:g> videoya dəyişiklik etmək icazəsi verilsin?</item>
+      <item quantity="one"><xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu videoya dəyişiklik etmək icazəsi verilsin?</item>
+    </plurals>
+    <plurals name="permission_write_image" formatted="false" msgid="748745548893845892">
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> tətbiqinə <xliff:g id="COUNT">^2</xliff:g> fotoya dəyişiklik etmək icazəsi verilsin?</item>
+      <item quantity="one"><xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu fotoya dəyişiklik etmək icazəsi verilsin?</item>
+    </plurals>
+    <plurals name="permission_write_generic" formatted="false" msgid="3270172714743671779">
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> tətbiqinə <xliff:g id="COUNT">^2</xliff:g> elementə dəyişiklik etmək icazəsi verilsin?</item>
+      <item quantity="one"><xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu elementə dəyişiklik etmək icazəsi verilsin?</item>
+    </plurals>
+    <plurals name="permission_trash_audio" formatted="false" msgid="8907813869381755423">
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> tətbiqinə <xliff:g id="COUNT">^2</xliff:g> audio faylı zibil qutusuna köçürmək icazəsi verilsin?</item>
+      <item quantity="one"><xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu audio faylı zibil qutusuna köçürmək icazəsi verilsin?</item>
+    </plurals>
+    <plurals name="permission_trash_video" formatted="false" msgid="4672871911555787438">
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> tətbiqinə <xliff:g id="COUNT">^2</xliff:g> videonu zibil qutusuna köçürmək icazəsi verilsin?</item>
+      <item quantity="one"><xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu videonu zibil qutusuna köçürmək icazəsi verilsin?</item>
+    </plurals>
+    <plurals name="permission_trash_image" formatted="false" msgid="6400475304599873227">
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> tətbiqinə <xliff:g id="COUNT">^2</xliff:g> fotonu zibil qutusuna köçürmək icazəsi verilsin?</item>
+      <item quantity="one"><xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu fotonu zibil qutusuna köçürmək icazəsi verilsin?</item>
+    </plurals>
+    <plurals name="permission_trash_generic" formatted="false" msgid="3814167365075039711">
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> tətbiqinə <xliff:g id="COUNT">^2</xliff:g> elementi zibil qutusuna köçürmək icazəsi verilsin?</item>
+      <item quantity="one"><xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu elementi zibil qutusuna köçürmək icazəsi verilsin?</item>
+    </plurals>
+    <plurals name="permission_untrash_audio" formatted="false" msgid="7795265980168966321">
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> tətbiqinə <xliff:g id="COUNT">^2</xliff:g> audio faylı zibil qutusundan çıxarmaq icazəsi verilsin?</item>
+      <item quantity="one"><xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu audio faylı zibil qutusundan çıxarmaq icazəsi verilsin?</item>
+    </plurals>
+    <plurals name="permission_untrash_video" formatted="false" msgid="332894888445508879">
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> tətbiqinə <xliff:g id="COUNT">^2</xliff:g> videonu zibil qutusundan çıxarmaq icazəsi verilsin?</item>
+      <item quantity="one"><xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu videonu zibil qutusundan çıxarmaq icazəsi verilsin?</item>
+    </plurals>
+    <plurals name="permission_untrash_image" formatted="false" msgid="7024071378733595056">
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> tətbiqinə <xliff:g id="COUNT">^2</xliff:g> fotonu zibil qutusundan çıxarmaq icazəsi verilsin?</item>
+      <item quantity="one"><xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu fotonu zibil qutusundan çıxarmaq icazəsi verilsin?</item>
+    </plurals>
+    <plurals name="permission_untrash_generic" formatted="false" msgid="6872817093731198374">
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> tətbiqinə <xliff:g id="COUNT">^2</xliff:g> elementi zibil qutusundan çıxarmaq icazəsi verilsin?</item>
+      <item quantity="one"><xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu elementi zibil qutusundan çıxarmaq icazəsi verilsin?</item>
+    </plurals>
+    <plurals name="permission_delete_audio" formatted="false" msgid="6848547621165184719">
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> tətbiqinə <xliff:g id="COUNT">^2</xliff:g> audio faylı silmək icazəsi verilsin?</item>
+      <item quantity="one"><xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu audio faylı silmək icazəsi verilsin?</item>
+    </plurals>
+    <plurals name="permission_delete_video" formatted="false" msgid="1251942606336748563">
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> tətbiqinə <xliff:g id="COUNT">^2</xliff:g> videonu silmək icazəsi verilsin?</item>
+      <item quantity="one"><xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu videonu silmək icazəsi verilsin?</item>
+    </plurals>
+    <plurals name="permission_delete_image" formatted="false" msgid="2303409455224710111">
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> tətbiqinə <xliff:g id="COUNT">^2</xliff:g> fotonu silmək icazəsi verilsin?</item>
+      <item quantity="one"><xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu fotonu silmək icazəsi verilsin?</item>
+    </plurals>
+    <plurals name="permission_delete_generic" formatted="false" msgid="1412218850351841181">
+      <item quantity="other"><xliff:g id="APP_NAME_1">^1</xliff:g> tətbiqinə <xliff:g id="COUNT">^2</xliff:g> elementi silmək icazəsi verilsin?</item>
+      <item quantity="one"><xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu elementi silmək icazəsi verilsin?</item>
+    </plurals>
 </resources>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index 884bde2..6a9a757 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -20,32 +20,108 @@
     <string name="storage_description" msgid="4081716890357580107">"Lokalni memorijski prostor"</string>
     <string name="app_label" msgid="9035307001052716210">"Memorijski prostor za medije"</string>
     <string name="artist_label" msgid="8105600993099120273">"Izvođač"</string>
-    <string name="ringtone_default" msgid="1744846699922263043">"Podrazumevana melodija zvona"</string>
-    <string name="notification_sound_default" msgid="6541609166469990135">"Podraz. zvuk obaveštenja"</string>
-    <string name="alarm_sound_default" msgid="5488847775252870314">"Podrazumevani zvuk alarma"</string>
+    <string name="unknown" msgid="2059049215682829375">"Nepoznato"</string>
     <string name="root_images" msgid="5861633549189045666">"Slike"</string>
     <string name="root_videos" msgid="8792703517064649453">"Video snimci"</string>
     <string name="root_audio" msgid="3505830755201326018">"Zvuk"</string>
+    <string name="root_documents" msgid="3829103301363849237">"Dokumenti"</string>
     <string name="permission_required" msgid="1460820436132943754">"Potrebna je dozvola za izmenu ili brisanje ove stavke."</string>
     <string name="permission_required_action" msgid="706370952366113539">"Nastavi"</string>
-    <string name="permission_audio" msgid="344061911159388398">"Želite li da dozvolite da "<b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" menja ili briše ovu muziku?"</string>
-    <string name="permission_video" msgid="8188037202573267740">"Želite li da dozvolite da "<b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" menja ili briše ovaj video?"</string>
-    <string name="permission_images" msgid="6711726334380945124">"Želite li da dozvolite da "<b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" menja ili briše ovu sliku?"</string>
     <string name="grant_dialog_button_allow" msgid="1644287024501033471">"Dozvoli"</string>
     <string name="grant_dialog_button_deny" msgid="6190589471415815741">"Odbij"</string>
-    <string name="sound_name_awaken" msgid="5266892392848526147">"probuđen"</string>
-    <string name="sound_name_bounce" msgid="8771447635446665231">"skok"</string>
-    <string name="sound_name_drip" msgid="1744684469020662152">"kapanje"</string>
-    <string name="sound_name_gallop" msgid="2664454314532060876">"galop"</string>
-    <string name="sound_name_nudge" msgid="5445751598250698244">"gurkanje"</string>
-    <string name="sound_name_orbit" msgid="4623457897813255481">"kruženje"</string>
-    <string name="sound_name_rise" msgid="2200258555031675806">"uspon"</string>
-    <string name="sound_name_sway" msgid="348448316663085643">"njihanje"</string>
-    <string name="sound_name_wag" msgid="2730733083078126563">"mahanje"</string>
-    <string name="add_ringtone_text" msgid="8077717303037760418">"Dodaj melodiju zvona"</string>
-    <string name="add_alarm_text" msgid="2440919366091720815">"Dodajte alarm"</string>
-    <string name="add_notification_text" msgid="5013822215567515665">"Dodajte obaveštenje"</string>
-    <string name="delete_ringtone_text" msgid="2963662097583300181">"Izbriši"</string>
-    <string name="unable_to_add_ringtone" msgid="6256097521165711269">"Dodavanje prilagođene melodije zvona nije uspelo"</string>
-    <string name="unable_to_delete_ringtone" msgid="2258180073859253495">"Brisanje prilagođene melodije zvona nije uspelo"</string>
+    <plurals name="permission_more_thumb" formatted="false" msgid="4392079224649478923">
+      <item quantity="one">i još <xliff:g id="COUNT_1">^1</xliff:g></item>
+      <item quantity="few">i još <xliff:g id="COUNT_1">^1</xliff:g></item>
+      <item quantity="other">i još <xliff:g id="COUNT_1">^1</xliff:g></item>
+    </plurals>
+    <plurals name="permission_more_text" formatted="false" msgid="7291997297174507324">
+      <item quantity="one">I još <xliff:g id="COUNT_1">^1</xliff:g> stavka</item>
+      <item quantity="few">I još <xliff:g id="COUNT_1">^1</xliff:g> stavke</item>
+      <item quantity="other">I još <xliff:g id="COUNT_1">^1</xliff:g> stavki</item>
+    </plurals>
+    <string name="cache_clearing_dialog_title" msgid="4672878017407595782">"Želite li da obrišete privremene datoteke aplikacija?"</string>
+    <string name="cache_clearing_dialog_text" msgid="7057784635111940957">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> želi da obriše neke privremene datoteke. Ovo može da dovede do povećane potrošnje baterije ili mobilnih podataka."</string>
+    <string name="cache_clearing_in_progress_title" msgid="6902220064511664209">"Brišu se privremene datoteke aplikacija…"</string>
+    <string name="allow" msgid="8885707816848569619">"Dozvoli"</string>
+    <string name="deny" msgid="6040983710442068936">"Odbij"</string>
+    <plurals name="permission_write_audio" formatted="false" msgid="8914759422381305478">
+      <item quantity="one">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> audio datoteku?</item>
+      <item quantity="few">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> audio datoteke?</item>
+      <item quantity="other">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> audio datoteka?</item>
+    </plurals>
+    <plurals name="permission_write_video" formatted="false" msgid="1098082003326873084">
+      <item quantity="one">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> video?</item>
+      <item quantity="few">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> video snimka?</item>
+      <item quantity="other">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> video snimaka?</item>
+    </plurals>
+    <plurals name="permission_write_image" formatted="false" msgid="748745548893845892">
+      <item quantity="one">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> sliku?</item>
+      <item quantity="few">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> slike?</item>
+      <item quantity="other">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> slika?</item>
+    </plurals>
+    <plurals name="permission_write_generic" formatted="false" msgid="3270172714743671779">
+      <item quantity="one">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> stavku?</item>
+      <item quantity="few">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> stavke?</item>
+      <item quantity="other">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> stavki?</item>
+    </plurals>
+    <plurals name="permission_trash_audio" formatted="false" msgid="8907813869381755423">
+      <item quantity="one">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> audio datoteku u otpad?</item>
+      <item quantity="few">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> audio datoteke u otpad?</item>
+      <item quantity="other">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> audio datoteka u otpad?</item>
+    </plurals>
+    <plurals name="permission_trash_video" formatted="false" msgid="4672871911555787438">
+      <item quantity="one">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> video u otpad?</item>
+      <item quantity="few">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> video snimka u otpad?</item>
+      <item quantity="other">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> video snimaka u otpad?</item>
+    </plurals>
+    <plurals name="permission_trash_image" formatted="false" msgid="6400475304599873227">
+      <item quantity="one">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> sliku u otpad?</item>
+      <item quantity="few">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> slike u otpad?</item>
+      <item quantity="other">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> slika u otpad?</item>
+    </plurals>
+    <plurals name="permission_trash_generic" formatted="false" msgid="3814167365075039711">
+      <item quantity="one">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> stavku u otpad?</item>
+      <item quantity="few">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> stavke u otpad?</item>
+      <item quantity="other">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> stavki u otpad?</item>
+    </plurals>
+    <plurals name="permission_untrash_audio" formatted="false" msgid="7795265980168966321">
+      <item quantity="one">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> audio datoteku iz otpada?</item>
+      <item quantity="few">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> audio datoteke iz otpada?</item>
+      <item quantity="other">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> audio datoteka iz otpada?</item>
+    </plurals>
+    <plurals name="permission_untrash_video" formatted="false" msgid="332894888445508879">
+      <item quantity="one">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> video iz otpada?</item>
+      <item quantity="few">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> video snimka iz otpada?</item>
+      <item quantity="other">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> video snimaka iz otpada?</item>
+    </plurals>
+    <plurals name="permission_untrash_image" formatted="false" msgid="7024071378733595056">
+      <item quantity="one">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> sliku iz otpada?</item>
+      <item quantity="few">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> slike iz otpada?</item>
+      <item quantity="other">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> slika iz otpada?</item>
+    </plurals>
+    <plurals name="permission_untrash_generic" formatted="false" msgid="6872817093731198374">
+      <item quantity="one">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> stavku iz otpada?</item>
+      <item quantity="few">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> stavke iz otpada?</item>
+      <item quantity="other">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> stavki iz otpada?</item>
+    </plurals>
+    <plurals name="permission_delete_audio" formatted="false" msgid="6848547621165184719">
+      <item quantity="one">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> audio datoteku?</item>
+      <item quantity="few">Želite li da dozvolite <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> audio datoteke?</item>
+      <item quantity="other">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> audio datoteka?</item>
+    </plurals>
+    <plurals name="permission_delete_video" formatted="false" msgid="1251942606336748563">
+      <item quantity="one">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> video?</item>
+      <item quantity="few">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> video snimka?</item>
+      <item quantity="other">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> video snimaka?</item>
+    </plurals>
+    <plurals name="permission_delete_image" formatted="false" msgid="2303409455224710111">
+      <item quantity="one">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> sliku?</item>
+      <item quantity="few">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> slike?</item>
+      <item quantity="other">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> slika?</item>
+    </plurals>
+    <plurals name="permission_delete_generic" formatted="false" msgid="1412218850351841181">
+      <item quantity="one">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> stavku?</item>
+      <item quantity="few">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> stavke?</item>
+      <item quantity="other">Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> stavki?</item>
+    </plurals>
 </resources>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index a1d8b47..78175e3 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -20,32 +20,126 @@
     <string name="storage_description" msgid="4081716890357580107">"Лакальнае сховішча"</string>
     <string name="app_label" msgid="9035307001052716210">"Медыясховішча"</string>
     <string name="artist_label" msgid="8105600993099120273">"Выканаўца"</string>
-    <string name="ringtone_default" msgid="1744846699922263043">"Стандартны рынгтон"</string>
-    <string name="notification_sound_default" msgid="6541609166469990135">"Стандартны сігнал апавяшчэння"</string>
-    <string name="alarm_sound_default" msgid="5488847775252870314">"Стандартны сігнал будзільніка"</string>
+    <string name="unknown" msgid="2059049215682829375">"Невядома"</string>
     <string name="root_images" msgid="5861633549189045666">"Відарысы"</string>
     <string name="root_videos" msgid="8792703517064649453">"Відэа"</string>
     <string name="root_audio" msgid="3505830755201326018">"Аўдыя"</string>
+    <string name="root_documents" msgid="3829103301363849237">"Дакументы"</string>
     <string name="permission_required" msgid="1460820436132943754">"На змену ці выдаленне гэтага элемента патрабуецца дазвол."</string>
     <string name="permission_required_action" msgid="706370952366113539">"Працягнуць"</string>
-    <string name="permission_audio" msgid="344061911159388398">"Дазволіць праграме "<b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" змяніць ці выдаліць гэты музычны файл?"</string>
-    <string name="permission_video" msgid="8188037202573267740">"Дазволіць праграме "<b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" змяніць ці выдаліць гэта відэа?"</string>
-    <string name="permission_images" msgid="6711726334380945124">"Дазволіць праграме "<b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" змяніць ці выдаліць гэты відарыс?"</string>
     <string name="grant_dialog_button_allow" msgid="1644287024501033471">"Дазволіць"</string>
     <string name="grant_dialog_button_deny" msgid="6190589471415815741">"Адмовіць"</string>
-    <string name="sound_name_awaken" msgid="5266892392848526147">"Абуджэнне"</string>
-    <string name="sound_name_bounce" msgid="8771447635446665231">"Адскок"</string>
-    <string name="sound_name_drip" msgid="1744684469020662152">"Кроплі"</string>
-    <string name="sound_name_gallop" msgid="2664454314532060876">"Галоп"</string>
-    <string name="sound_name_nudge" msgid="5445751598250698244">"Штуршок"</string>
-    <string name="sound_name_orbit" msgid="4623457897813255481">"Арбіта"</string>
-    <string name="sound_name_rise" msgid="2200258555031675806">"Уздым"</string>
-    <string name="sound_name_sway" msgid="348448316663085643">"Гайданне"</string>
-    <string name="sound_name_wag" msgid="2730733083078126563">"Маханне"</string>
-    <string name="add_ringtone_text" msgid="8077717303037760418">"Дадаць рынгтон"</string>
-    <string name="add_alarm_text" msgid="2440919366091720815">"Дадаць будзільнік"</string>
-    <string name="add_notification_text" msgid="5013822215567515665">"Дадаць апавяшчэнне"</string>
-    <string name="delete_ringtone_text" msgid="2963662097583300181">"Выдаліць"</string>
-    <string name="unable_to_add_ringtone" msgid="6256097521165711269">"Немагчыма дадаць карыстальніцкі рынгтон"</string>
-    <string name="unable_to_delete_ringtone" msgid="2258180073859253495">"Немагчыма выдаліць карыстальніцкі рынгтон"</string>
+    <plurals name="permission_more_thumb" formatted="false" msgid="4392079224649478923">
+      <item quantity="one">+<xliff:g id="COUNT_1">^1</xliff:g></item>
+      <item quantity="few">+<xliff:g id="COUNT_1">^1</xliff:g></item>
+      <item quantity="many">+<xliff:g id="COUNT_1">^1</xliff:g></item>
+      <item quantity="other">+<xliff:g id="COUNT_1">^1</xliff:g></item>
+    </plurals>
+    <plurals name="permission_more_text" formatted="false" msgid="7291997297174507324">
+      <item quantity="one">Плюс <xliff:g id="COUNT_1">^1</xliff:g> дадатковы элемент</item>
+      <item quantity="few">Плюс <xliff:g id="COUNT_1">^1</xliff:g> дадатковыя элементы</item>
+      <item quantity="many">Плюс <xliff:g id="COUNT_1">^1</xliff:g> дадатковых элементаў</item>
+      <item quantity="other">Плюс <xliff:g id="COUNT_1">^1</xliff:g> дадатковага элемента</item>
+    </plurals>
+    <string name="cache_clearing_dialog_title" msgid="4672878017407595782">"Ачысціць часовыя файлы праграм?"</string>
+    <string name="cache_clearing_dialog_text" msgid="7057784635111940957">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> запытвае дазвол на выдаленне некаторых часовых файлаў. У сувязі з гэтым можа павялічыцца ўзровень выкарыстання акумулятара ці памер сотавай перадачы даных."</string>
+    <string name="cache_clearing_in_progress_title" msgid="6902220064511664209">"Выдаляюцца часовыя файлы праграм…"</string>
+    <string name="allow" msgid="8885707816848569619">"Дазволіць"</string>
+    <string name="deny" msgid="6040983710442068936">"Адмовіць"</string>
+    <plurals name="permission_write_audio" formatted="false" msgid="8914759422381305478">
+      <item quantity="one">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайл?</item>
+      <item quantity="few">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайлы?</item>
+      <item quantity="many">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайлаў?</item>
+      <item quantity="other">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайла?</item>
+    </plurals>
+    <plurals name="permission_write_video" formatted="false" msgid="1098082003326873084">
+      <item quantity="one">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> відэа?</item>
+      <item quantity="few">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> відэа?</item>
+      <item quantity="many">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> відэа?</item>
+      <item quantity="other">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> відэа?</item>
+    </plurals>
+    <plurals name="permission_write_image" formatted="false" msgid="748745548893845892">
+      <item quantity="one">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> фота?</item>
+      <item quantity="few">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> фота?</item>
+      <item quantity="many">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> фота?</item>
+      <item quantity="other">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> фота?</item>
+    </plurals>
+    <plurals name="permission_write_generic" formatted="false" msgid="3270172714743671779">
+      <item quantity="one">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> элемент?</item>
+      <item quantity="few">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> элементы?</item>
+      <item quantity="many">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> элементаў?</item>
+      <item quantity="other">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> элемента?</item>
+    </plurals>
+    <plurals name="permission_trash_audio" formatted="false" msgid="8907813869381755423">
+      <item quantity="one">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайл у сметніцу?</item>
+      <item quantity="few">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайлы ў сметніцу?</item>
+      <item quantity="many">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайлаў у сметніцу?</item>
+      <item quantity="other">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайла ў сметніцу?</item>
+    </plurals>
+    <plurals name="permission_trash_video" formatted="false" msgid="4672871911555787438">
+      <item quantity="one">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> відэа ў сметніцу?</item>
+      <item quantity="few">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> відэа ў сметніцу?</item>
+      <item quantity="many">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> відэа ў сметніцу?</item>
+      <item quantity="other">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> відэа ў сметніцу?</item>
+    </plurals>
+    <plurals name="permission_trash_image" formatted="false" msgid="6400475304599873227">
+      <item quantity="one">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> фота ў сметніцу?</item>
+      <item quantity="few">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> фота ў сметніцу?</item>
+      <item quantity="many">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> фота ў сметніцу?</item>
+      <item quantity="other">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> фота ў сметніцу?</item>
+    </plurals>
+    <plurals name="permission_trash_generic" formatted="false" msgid="3814167365075039711">
+      <item quantity="one">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> элемент у сметніцу?</item>
+      <item quantity="few">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> элементы ў сметніцу?</item>
+      <item quantity="many">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> элементаў у сметніцу?</item>
+      <item quantity="other">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> элемента ў сметніцу?</item>
+    </plurals>
+    <plurals name="permission_untrash_audio" formatted="false" msgid="7795265980168966321">
+      <item quantity="one">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайл са сметніцы?</item>
+      <item quantity="few">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайлы са сметніцы?</item>
+      <item quantity="many">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайлаў са сметніцы?</item>
+      <item quantity="other">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайла са сметніцы?</item>
+    </plurals>
+    <plurals name="permission_untrash_video" formatted="false" msgid="332894888445508879">
+      <item quantity="one">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> відэа са сметніцы?</item>
+      <item quantity="few">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> відэа са сметніцы?</item>
+      <item quantity="many">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> відэа са сметніцы?</item>
+      <item quantity="other">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> відэа са сметніцы?</item>
+    </plurals>
+    <plurals name="permission_untrash_image" formatted="false" msgid="7024071378733595056">
+      <item quantity="one">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> фота са сметніцы?</item>
+      <item quantity="few">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> фота са сметніцы?</item>
+      <item quantity="many">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> фота са сметніцы?</item>
+      <item quantity="other">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> фота са сметніцы?</item>
+    </plurals>
+    <plurals name="permission_untrash_generic" formatted="false" msgid="6872817093731198374">
+      <item quantity="one">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> элемент са сметніцы?</item>
+      <item quantity="few">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> элементы са сметніцы?</item>
+      <item quantity="many">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> элементаў са сметніцы?</item>
+      <item quantity="other">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" перамясціць <xliff:g id="COUNT">^2</xliff:g> элемента са сметніцы?</item>
+    </plurals>
+    <plurals name="permission_delete_audio" formatted="false" msgid="6848547621165184719">
+      <item quantity="one">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" выдаліць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайл?</item>
+      <item quantity="few">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" выдаліць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайлы?</item>
+      <item quantity="many">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" выдаліць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайлаў?</item>
+      <item quantity="other">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" выдаліць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайла?</item>
+    </plurals>
+    <plurals name="permission_delete_video" formatted="false" msgid="1251942606336748563">
+      <item quantity="one">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" выдаліць <xliff:g id="COUNT">^2</xliff:g> відэа?</item>
+      <item quantity="few">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" выдаліць <xliff:g id="COUNT">^2</xliff:g> відэа?</item>
+      <item quantity="many">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" выдаліць <xliff:g id="COUNT">^2</xliff:g> відэа?</item>
+      <item quantity="other">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" выдаліць <xliff:g id="COUNT">^2</xliff:g> відэа?</item>
+    </plurals>
+    <plurals name="permission_delete_image" formatted="false" msgid="2303409455224710111">
+      <item quantity="one">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" выдаліць <xliff:g id="COUNT">^2</xliff:g> фота?</item>
+      <item quantity="few">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" выдаліць <xliff:g id="COUNT">^2</xliff:g> фота?</item>
+      <item quantity="many">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" выдаліць <xliff:g id="COUNT">^2</xliff:g> фота?</item>
+      <item quantity="other">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" выдаліць <xliff:g id="COUNT">^2</xliff:g> фота?</item>
+    </plurals>
+    <plurals name="permission_delete_generic" formatted="false" msgid="1412218850351841181">
+      <item quantity="one">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" выдаліць <xliff:g id="COUNT">^2</xliff:g> элемент?</item>
+      <item quantity="few">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" выдаліць <xliff:g id="COUNT">^2</xliff:g> элементы?</item>
+      <item quantity="many">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" выдаліць <xliff:g id="COUNT">^2</xliff:g> элементаў?</item>
+      <item quantity="other">Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" выдаліць <xliff:g id="COUNT">^2</xliff:g> элемента?</item>
+    </plurals>
 </resources>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index b859bb8..c52ece9 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -20,32 +20,90 @@
     <string name="storage_description" msgid="4081716890357580107">"Локално хранилище"</string>
     <string name="app_label" msgid="9035307001052716210">"Мултимедийно хранилище"</string>
     <string name="artist_label" msgid="8105600993099120273">"Изпълнител"</string>
-    <string name="ringtone_default" msgid="1744846699922263043">"Стандартна мелодия"</string>
-    <string name="notification_sound_default" msgid="6541609166469990135">"Стандартен звук за известяване"</string>
-    <string name="alarm_sound_default" msgid="5488847775252870314">"Стандартен звук за будилника"</string>
+    <string name="unknown" msgid="2059049215682829375">"Неизвестно"</string>
     <string name="root_images" msgid="5861633549189045666">"Изображения"</string>
     <string name="root_videos" msgid="8792703517064649453">"Видеоклипове"</string>
     <string name="root_audio" msgid="3505830755201326018">"Аудио"</string>
+    <string name="root_documents" msgid="3829103301363849237">"Документи"</string>
     <string name="permission_required" msgid="1460820436132943754">"Необходимо е разрешение за промяна или изтриване на този елемент."</string>
     <string name="permission_required_action" msgid="706370952366113539">"Напред"</string>
-    <string name="permission_audio" msgid="344061911159388398">"Да се разреши ли на "<b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" да променя или изтрива тази музика?"</string>
-    <string name="permission_video" msgid="8188037202573267740">"Да се разреши ли на "<b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" да променя или изтрива този видеоклип?"</string>
-    <string name="permission_images" msgid="6711726334380945124">"Да се разреши ли на "<b>"<xliff:g id="APP_NAME">^1</xliff:g>"</b>" да променя или изтрива това изображение?"</string>
     <string name="grant_dialog_button_allow" msgid="1644287024501033471">"Разрешаване"</string>
     <string name="grant_dialog_button_deny" msgid="6190589471415815741">"Отказ"</string>
-    <string name="sound_name_awaken" msgid="5266892392848526147">"Събуждане"</string>
-    <string name="sound_name_bounce" msgid="8771447635446665231">"Енергичност"</string>
-    <string name="sound_name_drip" msgid="1744684469020662152">"Капки"</string>
-    <string name="sound_name_gallop" msgid="2664454314532060876">"Галоп"</string>
-    <string name="sound_name_nudge" msgid="5445751598250698244">"Сръчкване"</string>
-    <string name="sound_name_orbit" msgid="4623457897813255481">"Орбита"</string>
-    <string name="sound_name_rise" msgid="2200258555031675806">"Издигане"</string>
-    <string name="sound_name_sway" msgid="348448316663085643">"Люлеене"</string>
-    <string name="sound_name_wag" msgid="2730733083078126563">"Поклащане"</string>
-    <string name="add_ringtone_text" msgid="8077717303037760418">"Добавяне на мелодия"</string>
-    <string name="add_alarm_text" msgid="2440919366091720815">"Добавяне на будилник"</string>
-    <string name="add_notification_text" msgid="5013822215567515665">"Добавяне на известие"</string>
-    <string name="delete_ringtone_text" msgid="2963662097583300181">"Изтриване"</string>
-    <string name="unable_to_add_ringtone" msgid="6256097521165711269">"Персонализираната мелодия не може да се добави"</string>
-    <string name="unable_to_delete_ringtone" msgid="2258180073859253495">"Персонализираната мелодия не може да се изтрие"</string>
+    <plurals name="permission_more_thumb" formatted="false" msgid="4392079224649478923">
+      <item quantity="other">+ <xliff:g id="COUNT_1">^1</xliff:g></item>
+      <item quantity="one">+ <xliff:g id="COUNT_0">^1</xliff:g></item>
+    </plurals>
+    <plurals name="permission_more_text" formatted="false" msgid="7291997297174507324">
+      <item quantity="other">И още <xliff:g id="COUNT_1">^1</xliff:g> допълнителни елемента</item>
+      <item quantity="one">И още <xliff:g id="COUNT_0">^1</xliff:g> допълнителен елемент</item>
+    </plurals>
+    <string name="cache_clearing_dialog_title" msgid="4672878017407595782">"Да се изчистят ли временните файлове на приложенията?"</string>
+    <string name="cache_clearing_dialog_text" msgid="7057784635111940957">"<xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> иска да изчисти някои временни файлове. Това може да доведе до увеличено използване на батерията или мобилните данни."</string>
+    <string name="cache_clearing_in_progress_title" msgid="6902220064511664209">"Временните файлове на приложенията се изчистват…"</string>
+    <string name="allow" msgid="8885707816848569619">"Разрешаване"</string>
+    <string name="deny" msgid="6040983710442068936">"Отказ"</string>
+    <plurals name="permission_write_audio" formatted="false" msgid="8914759422381305478">
+      <item quantity="other">Да се разреши ли на <xliff:g id="APP_NAME_1">^1</xliff:g> да промени <xliff:g id="COUNT">^2</xliff:g> аудиофайла?</item>
+      <item quantity="one">Да се разреши ли на <xliff:g id="APP_NAME_0">^1</xliff:g> да промени този аудиофайл?</item>
+    </plurals>
+    <plurals name="permission_write_video" formatted="false" msgid="1098082003326873084">
+      <item quantity="other">Да се разреши ли на <xliff:g id="APP_NAME_1">^1</xliff:g> да промени <xliff:g id="COUNT">^2</xliff:g> видеоклипа?</item>
+      <item quantity="one">Да се разреши ли на <xliff:g id="APP_NAME_0">^1</xliff:g> да промени този видеоклип?</item>
+    </plurals>
+    <plurals name="permission_write_image" formatted="false" msgid="748745548893845892">
+      <item quantity="other">Да се разреши ли на <xliff:g id="APP_NAME_1">^1</xliff:g> да промени <xliff:g id="COUNT">^2</xliff:g> снимки?</item>
+      <item quantity="one">Да се разреши ли на <xliff:g id="APP_NAME_0">^1</xliff:g> да промени тази снимка?</item>
+    </plurals>
+    <plurals name="permission_write_generic" formatted="false" msgid="3270172714743671779">
+      <item quantity="other">Да се разреши ли на <xliff:g id="APP_NAME_1">^1</xliff:g> да промени <xliff:g id="COUNT">^2</xliff:g> елемента?</item>
+      <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="APP_NAME_1">^1</xliff:g> да премести <xliff:g id="COUNT">^2</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="APP_NAME_1">^1</xliff:g> да премести <xliff:g id="COUNT">^2</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="APP_NAME_1">^1</xliff:g> да премести <xliff:g id="COUNT">^2</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="APP_NAME_1">^1</xliff:g> да премести <xliff:g id="COUNT">^2</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="APP_NAME_1">^1</xliff:g> да премести <xliff:g id="COUNT">^2</xliff:g> аудиофайла извън кошчето?</item>
+      <item quantity="one">Да се разреши ли на <xliff:g id="APP_NAME_0">^1</xliff:g> да премести този аудиофайл извън кошчето?</item>
+    </plurals>
+    <pl