[automerger skipped] Import translations. DO NOT MERGE ANYWHERE am: 09879990b5 -s ours

am skip reason: subject contains skip directive

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

Change-Id: Ie745acd1c9f362c5af1727c050a00c872affaaae
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index 27e9f85..a542d34 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,19 +1,6 @@
-
 package {
-    default_applicable_licenses: ["packages_providers_MediaProvider_license"],
-}
-
-// Added automatically by a large-scale-change
-// See: http://go/android-license-faq
-license {
-    name: "packages_providers_MediaProvider_license",
-    visibility: [":__subpackages__"],
-    license_kinds: [
-        "SPDX-license-identifier-Apache-2.0",
-    ],
-    license_text: [
-        "NOTICE",
-    ],
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 android_app {
@@ -74,10 +61,9 @@
         "error_prone_mediaprovider",
         "glide-annotation-processor",
     ],
-
+    jarjar_rules: "jarjar-rules.txt",
     sdk_version: "module_current",
     min_sdk_version: "30",
-    target_sdk_version: "31",
 
     certificate: "media",
     privileged: true,
@@ -96,7 +82,10 @@
         ],
     },
 
-    required: ["preinstalled-packages-com.android.providers.media.module.xml"],
+    required: [
+        "preinstalled-packages-com.android.providers.media.module.xml",
+        "privapp_allowlist_com.android.providers.media.module.xml",
+    ],
 
     lint: {
         strict_updatability_linting: true,
@@ -127,6 +116,7 @@
         "src/com/android/providers/media/util/MimeUtils.java",
         "src/com/android/providers/media/util/StringUtils.java",
         "src/com/android/providers/media/playlist/*.java",
+        "src/com/android/providers/media/dao/*.java",
     ],
     sdk_version: "module_current",
     min_sdk_version: "30",
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 2f71d1c..788df5d 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -26,6 +26,7 @@
     <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" />
+    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
 
     <!-- Permissions required for reading and logging compat changes -->
     <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>
diff --git a/NOTICE b/NOTICE
deleted file mode 100644
index c5b1efa..0000000
--- a/NOTICE
+++ /dev/null
@@ -1,190 +0,0 @@
-
-   Copyright (c) 2005-2008, 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.
-
-   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.
-
-
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 5b80e36..0e2dd2f 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -6,3 +6,5 @@
 
 [Hook Scripts]
 hidden_api_txt_checksorted_hook = ${REPO_ROOT}/tools/platform-compat/hiddenapi/checksorted_sha.sh ${PREUPLOAD_COMMIT} ${REPO_ROOT}
+
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 980119c..2fc29df 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,24 +1,25 @@
 {
-    "mainline-presubmit": [
-        {
-            "name": "MediaProviderTests[com.google.android.mediaprovider.apex]"
-        },
-        {
-            "name": "CtsScopedStorageCoreHostTest[com.google.android.mediaprovider.apex]"
-        },
-        {
-            "name": "CtsScopedStorageHostTest[com.google.android.mediaprovider.apex]"
-        },
-        {
-            "name": "CtsScopedStorageDeviceOnlyTest[com.google.android.mediaprovider.apex]"
-        },
-        {
-            "name": "CtsMediaProviderTranscodeTests[com.google.android.mediaprovider.apex]"
-        },
-        {
-            "name": "CtsPhotoPickerTest[com.google.android.mediaprovider.apex]"
-        }
-    ],
+    // TODO(b/204107787): Re-enable this once MP from master can be installed on R and S devices
+    // "mainline-presubmit": [
+    //     {
+    //         "name": "MediaProviderTests[com.google.android.mediaprovider.apex]"
+    //     }
+    //     {
+    //         "name": "MediaProviderTests[com.google.android.mediaprovider.apex]"
+    //     },
+    //     {
+    //         "name": "CtsScopedStorageCoreHostTest"
+    //     },
+    //     {
+    //         "name": "CtsScopedStorageHostTest"
+    //     },
+    //     {
+    //         "name": "CtsScopedStorageDeviceOnlyTest"
+    //     },
+    //    {
+    //        "name": "CtsMediaProviderTranscodeTests[com.google.android.mediaprovider.apex]"
+    //    }
+    // ],
     "presubmit": [
         {
             "name": "MediaProviderTests"
diff --git a/apex/Android.bp b/apex/Android.bp
index 668ff1c..c735880 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -1,10 +1,6 @@
 package {
     // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    default_applicable_licenses: ["packages_providers_MediaProvider_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 apex {
@@ -18,15 +14,17 @@
 apex_defaults {
     name: "com.android.mediaprovider-defaults",
     bootclasspath_fragments: ["com.android.mediaprovider-bootclasspath-fragment"],
-    prebuilts: ["current_sdkinfo"],
+    prebuilts: [
+        "current_sdkinfo",
+        "privapp_allowlist_com.android.providers.media.module.xml"
+    ],
     key: "com.android.mediaprovider.key",
     certificate: ":com.android.mediaprovider.certificate",
     file_contexts: ":com.android.mediaprovider-file_contexts",
-    min_sdk_version: "30",
+    defaults: ["r-launched-apex-module"],
     // Indicates that pre-installed version of this apex can be compressed.
     // Whether it actually will be compressed is controlled on per-device basis.
     compressible: true,
-    updatable: true,
 }
 
 apex_key {
@@ -69,5 +67,15 @@
     // modified by the Soong or platform compat team.
     hidden_api: {
         max_target_o_low_priority: ["hiddenapi/hiddenapi-max-target-o-low-priority.txt"],
+
+        // The following packages contain classes from other modules on the
+        // bootclasspath. That means that the hidden API flags for this module
+        // has to explicitly list every single class this module provides in
+        // that package to differentiate them from the classes provided by other
+        // modules. That can include private classes that are not part of the
+        // API.
+        split_packages: [
+            "android.provider",
+        ],
     },
 }
diff --git a/apex/apex_manifest.json b/apex/apex_manifest.json
index f056b53..fe2ed11 100644
--- a/apex/apex_manifest.json
+++ b/apex/apex_manifest.json
@@ -1,4 +1,4 @@
 {
   "name": "com.android.mediaprovider",
-  "version": 339999910
+  "version": 339990000
 }
diff --git a/apex/framework/Android.bp b/apex/framework/Android.bp
index ed56b54..a48bd75 100644
--- a/apex/framework/Android.bp
+++ b/apex/framework/Android.bp
@@ -14,11 +14,7 @@
 
 package {
     // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    default_applicable_licenses: ["packages_providers_MediaProvider_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 java_sdk_library {
diff --git a/apex/framework/api/current.txt b/apex/framework/api/current.txt
index 3ea467e..8e1691a 100644
--- a/apex/framework/api/current.txt
+++ b/apex/framework/api/current.txt
@@ -1,6 +1,100 @@
 // Signature format: 2.0
 package android.provider {
 
+  public abstract class CloudMediaProvider extends android.content.ContentProvider {
+    ctor public CloudMediaProvider();
+    method public final void attachInfo(@NonNull android.content.Context, @NonNull android.content.pm.ProviderInfo);
+    method @NonNull public final android.os.Bundle call(@NonNull String, @Nullable String, @Nullable android.os.Bundle);
+    method @NonNull public final android.net.Uri canonicalize(@NonNull android.net.Uri);
+    method public final int delete(@NonNull android.net.Uri, @Nullable String, @Nullable String[]);
+    method @NonNull public final String getType(@NonNull android.net.Uri);
+    method @NonNull public final android.net.Uri insert(@NonNull android.net.Uri, @NonNull android.content.ContentValues);
+    method @Nullable public android.provider.CloudMediaProvider.CloudMediaSurfaceController onCreateCloudMediaSurfaceController(@NonNull android.os.Bundle, @NonNull android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback);
+    method @NonNull public abstract android.os.Bundle onGetMediaCollectionInfo(@NonNull android.os.Bundle);
+    method @NonNull public abstract android.os.ParcelFileDescriptor onOpenMedia(@NonNull String, @Nullable android.os.Bundle, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException;
+    method @NonNull public abstract android.content.res.AssetFileDescriptor onOpenPreview(@NonNull String, @NonNull android.graphics.Point, @Nullable android.os.Bundle, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException;
+    method @NonNull public android.database.Cursor onQueryAlbums(@NonNull android.os.Bundle);
+    method @NonNull public abstract android.database.Cursor onQueryDeletedMedia(@NonNull android.os.Bundle);
+    method @NonNull public abstract android.database.Cursor onQueryMedia(@NonNull android.os.Bundle);
+    method @NonNull public final android.os.ParcelFileDescriptor openFile(@NonNull android.net.Uri, @NonNull String) throws java.io.FileNotFoundException;
+    method @NonNull public final android.os.ParcelFileDescriptor openFile(@NonNull android.net.Uri, @NonNull String, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException;
+    method @NonNull public final android.content.res.AssetFileDescriptor openTypedAssetFile(@NonNull android.net.Uri, @NonNull String, @Nullable android.os.Bundle) throws java.io.FileNotFoundException;
+    method @NonNull public final android.content.res.AssetFileDescriptor openTypedAssetFile(@NonNull android.net.Uri, @NonNull String, @Nullable android.os.Bundle, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException;
+    method @NonNull public final android.database.Cursor query(@NonNull android.net.Uri, @Nullable String[], @Nullable android.os.Bundle, @Nullable android.os.CancellationSignal);
+    method @NonNull public final android.database.Cursor query(@NonNull android.net.Uri, @Nullable String[], @Nullable String, @Nullable String[], @Nullable String);
+    method @NonNull public final android.database.Cursor query(@NonNull android.net.Uri, @Nullable String[], @Nullable String, @Nullable String[], @Nullable String, @Nullable android.os.CancellationSignal);
+    method public final int update(@NonNull android.net.Uri, @NonNull android.content.ContentValues, @Nullable String, @Nullable String[]);
+  }
+
+  public abstract static class CloudMediaProvider.CloudMediaSurfaceController {
+    ctor public CloudMediaProvider.CloudMediaSurfaceController();
+    method public abstract void onConfigChange(@NonNull android.os.Bundle);
+    method public abstract void onDestroy();
+    method public abstract void onMediaPause(int);
+    method public abstract void onMediaPlay(int);
+    method public abstract void onMediaSeekTo(int, long);
+    method public abstract void onPlayerCreate();
+    method public abstract void onPlayerRelease();
+    method public abstract void onSurfaceChanged(int, int, int, int);
+    method public abstract void onSurfaceCreated(int, @NonNull android.view.Surface, @NonNull String);
+    method public abstract void onSurfaceDestroyed(int);
+  }
+
+  public static final class CloudMediaProvider.CloudMediaSurfaceStateChangedCallback {
+    method public void setPlaybackState(int, int, @Nullable android.os.Bundle);
+    field public static final int PLAYBACK_STATE_BUFFERING = 1; // 0x1
+    field public static final int PLAYBACK_STATE_COMPLETED = 5; // 0x5
+    field public static final int PLAYBACK_STATE_ERROR_PERMANENT_FAILURE = 7; // 0x7
+    field public static final int PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE = 6; // 0x6
+    field public static final int PLAYBACK_STATE_MEDIA_SIZE_CHANGED = 8; // 0x8
+    field public static final int PLAYBACK_STATE_PAUSED = 4; // 0x4
+    field public static final int PLAYBACK_STATE_READY = 2; // 0x2
+    field public static final int PLAYBACK_STATE_STARTED = 3; // 0x3
+  }
+
+  public final class CloudMediaProviderContract {
+    field public static final String EXTRA_ALBUM_ID = "android.provider.extra.ALBUM_ID";
+    field public static final String EXTRA_LOOPING_PLAYBACK_ENABLED = "android.provider.extra.LOOPING_PLAYBACK_ENABLED";
+    field public static final String EXTRA_MEDIA_COLLECTION_ID = "android.provider.extra.MEDIA_COLLECTION_ID";
+    field public static final String EXTRA_PAGE_TOKEN = "android.provider.extra.PAGE_TOKEN";
+    field public static final String EXTRA_PREVIEW_THUMBNAIL = "android.provider.extra.PREVIEW_THUMBNAIL";
+    field public static final String EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED = "android.provider.extra.SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED";
+    field public static final String EXTRA_SYNC_GENERATION = "android.provider.extra.SYNC_GENERATION";
+    field public static final String MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION = "com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS";
+    field public static final String PROVIDER_INTERFACE = "android.content.action.CLOUD_MEDIA_PROVIDER";
+  }
+
+  public static final class CloudMediaProviderContract.AlbumColumns {
+    field public static final String DATE_TAKEN_MILLIS = "date_taken_millis";
+    field public static final String DISPLAY_NAME = "display_name";
+    field public static final String ID = "id";
+    field public static final String MEDIA_COUNT = "album_media_count";
+    field public static final String MEDIA_COVER_ID = "album_media_cover_id";
+  }
+
+  public static final class CloudMediaProviderContract.MediaCollectionInfo {
+    field public static final String ACCOUNT_CONFIGURATION_INTENT = "account_configuration_intent";
+    field public static final String ACCOUNT_NAME = "account_name";
+    field public static final String LAST_MEDIA_SYNC_GENERATION = "last_media_sync_generation";
+    field public static final String MEDIA_COLLECTION_ID = "media_collection_id";
+  }
+
+  public static final class CloudMediaProviderContract.MediaColumns {
+    field public static final String DATE_TAKEN_MILLIS = "date_taken_millis";
+    field public static final String DURATION_MILLIS = "duration_millis";
+    field public static final String ID = "id";
+    field public static final String IS_FAVORITE = "is_favorite";
+    field public static final String MEDIA_STORE_URI = "media_store_uri";
+    field public static final String MIME_TYPE = "mime_type";
+    field public static final String SIZE_BYTES = "size_bytes";
+    field public static final String STANDARD_MIME_TYPE_EXTENSION = "standard_mime_type_extension";
+    field public static final int STANDARD_MIME_TYPE_EXTENSION_ANIMATED_WEBP = 3; // 0x3
+    field public static final int STANDARD_MIME_TYPE_EXTENSION_GIF = 1; // 0x1
+    field public static final int STANDARD_MIME_TYPE_EXTENSION_MOTION_PHOTO = 2; // 0x2
+    field public static final int STANDARD_MIME_TYPE_EXTENSION_NONE = 0; // 0x0
+    field public static final String SYNC_GENERATION = "sync_generation";
+  }
+
   public final class MediaStore {
     ctor public MediaStore();
     method public static boolean canManageMedia(@NonNull android.content.Context);
@@ -22,12 +116,16 @@
     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 public static boolean isCurrentCloudMediaProviderAuthority(@NonNull android.content.ContentResolver, @NonNull String);
     method public static boolean isCurrentSystemGallery(@NonNull android.content.ContentResolver, int, @NonNull String);
+    method public static boolean isSupportedCloudMediaProviderAuthority(@NonNull android.content.ContentResolver, @NonNull String);
+    method public static void notifyCloudMediaChangedEvent(@NonNull android.content.ContentResolver, @NonNull String, @NonNull String) throws java.lang.SecurityException;
     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_PICK_IMAGES = "android.provider.action.PICK_IMAGES";
+    field public static final String ACTION_PICK_IMAGES_SETTINGS = "android.provider.action.PICK_IMAGES_SETTINGS";
     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";
diff --git a/apex/framework/java/android/provider/CloudMediaProvider.java b/apex/framework/java/android/provider/CloudMediaProvider.java
index 2ef671a..6f3b246 100644
--- a/apex/framework/java/android/provider/CloudMediaProvider.java
+++ b/apex/framework/java/android/provider/CloudMediaProvider.java
@@ -17,9 +17,11 @@
 package android.provider;
 
 import static android.provider.CloudMediaProviderContract.EXTRA_ASYNC_CONTENT_PROVIDER;
+import static android.provider.CloudMediaProviderContract.EXTRA_AUTHORITY;
 import static android.provider.CloudMediaProviderContract.EXTRA_ERROR_MESSAGE;
 import static android.provider.CloudMediaProviderContract.EXTRA_FILE_DESCRIPTOR;
 import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED;
+import static android.provider.CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB;
 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER;
 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED;
 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_STATE_CALLBACK;
@@ -54,6 +56,7 @@
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteCallback;
+import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.Surface;
 import android.view.SurfaceHolder;
@@ -105,8 +108,6 @@
  * {@link #onGetMediaCollectionInfo}.
  *
  * @see MediaStore#ACTION_PICK_IMAGES
- *
- * @hide
  */
 public abstract class CloudMediaProvider extends ContentProvider {
     private static final String TAG = "CloudMediaProvider";
@@ -379,12 +380,14 @@
                 DEFAULT_LOOPING_PLAYBACK_ENABLED);
         final boolean muteAudio = extras.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED,
                 DEFAULT_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED);
+        final String authority = extras.getString(EXTRA_AUTHORITY);
         final CloudMediaSurfaceStateChangedCallback callback =
                 new CloudMediaSurfaceStateChangedCallback(
                         ICloudMediaSurfaceStateChangedCallback.Stub.asInterface(binder));
         final Bundle config = new Bundle();
         config.putBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, enableLoop);
         config.putBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED, muteAudio);
+        config.putString(EXTRA_AUTHORITY, authority);
         final CloudMediaSurfaceController controller =
                 onCreateCloudMediaSurfaceController(config, callback);
         if (controller == null) {
@@ -448,15 +451,29 @@
     public final AssetFileDescriptor openTypedAssetFile(
             @NonNull Uri uri, @NonNull String mimeTypeFilter, @Nullable Bundle opts,
             @Nullable CancellationSignal signal) throws FileNotFoundException {
-        String mediaId = uri.getLastPathSegment();
-        final boolean wantsThumb = (opts != null) && opts.containsKey(ContentResolver.EXTRA_SIZE)
-                && mimeTypeFilter.startsWith("image/");
-        if (wantsThumb) {
-            Point point = (Point) opts.getParcelable(ContentResolver.EXTRA_SIZE);
-            return onOpenPreview(mediaId, point, opts, signal);
+        final String mediaId = uri.getLastPathSegment();
+        final Bundle bundle = new Bundle();
+        Point previewSize = null;
+
+        final DisplayMetrics screenMetrics = getContext().getResources().getDisplayMetrics();
+        int minPreviewLength = Math.min(screenMetrics.widthPixels, screenMetrics.heightPixels);
+
+        if (opts != null) {
+            bundle.putBoolean(EXTRA_MEDIASTORE_THUMB, opts.getBoolean(EXTRA_MEDIASTORE_THUMB));
+
+            if (opts.containsKey(CloudMediaProviderContract.EXTRA_PREVIEW_THUMBNAIL)) {
+                bundle.putBoolean(CloudMediaProviderContract.EXTRA_PREVIEW_THUMBNAIL, true);
+                minPreviewLength = minPreviewLength / 2;
+            }
+
+            previewSize = opts.getParcelable(ContentResolver.EXTRA_SIZE);
         }
-        return new AssetFileDescriptor(onOpenMedia(mediaId, opts, signal), 0 /* startOffset */,
-                AssetFileDescriptor.UNKNOWN_LENGTH);
+
+        if (previewSize == null) {
+            previewSize = new Point(minPreviewLength, minPreviewLength);
+        }
+
+        return onOpenPreview(mediaId, previewSize, bundle, signal);
     }
 
     /**
diff --git a/apex/framework/java/android/provider/CloudMediaProviderContract.java b/apex/framework/java/android/provider/CloudMediaProviderContract.java
index 8dfd5c6..9e35058 100644
--- a/apex/framework/java/android/provider/CloudMediaProviderContract.java
+++ b/apex/framework/java/android/provider/CloudMediaProviderContract.java
@@ -31,8 +31,6 @@
  * provides a foundational implementation of this contract.
  *
  * @see CloudMediaProvider
- *
- * @hide
  */
 public final class CloudMediaProviderContract {
     private static final String TAG = "CloudMediaProviderContract";
@@ -458,7 +456,6 @@
      * @see CloudMediaProvider#onQueryAlbums
      * <p>
      * Type: STRING
-     * @hide
      */
     public static final String EXTRA_MEDIA_COLLECTION_ID =
             "android.provider.extra.MEDIA_COLLECTION_ID";
@@ -558,6 +555,16 @@
             "android.provider.extra.PREVIEW_THUMBNAIL";
 
     /**
+     * A boolean to indicate {@link com.android.providers.media.photopicker.PhotoPickerProvider}
+     * this request is requesting a cached thumbnail file from MediaStore.
+     *
+     * Type: BOOLEAN
+     *
+     * {@hide}
+     */
+    public static final String EXTRA_MEDIASTORE_THUMB = "android.provider.extra.MEDIASTORE_THUMB";
+
+    /**
      * Constant used to execute {@link CloudMediaProvider#onGetMediaCollectionInfo} via
      * {@link ContentProvider#call}.
      *
@@ -649,6 +656,13 @@
     public static final String EXTRA_ERROR_MESSAGE = "android.provider.extra.error_message";
 
     /**
+     * Constant used to get/set the {@link CloudMediaProvider} authority.
+     *
+     * {@hide}
+     */
+    public static final String EXTRA_AUTHORITY = "android.provider.extra.authority";
+
+    /**
      * URI path for {@link CloudMediaProvider#onQueryMedia}
      *
      * {@hide}
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 47ef4ec..ca67b8a 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -261,6 +261,11 @@
     public static final String CREATE_SURFACE_CONTROLLER = "create_surface_controller";
 
     /** {@hide} */
+    public static final String USES_FUSE_PASSTHROUGH = "uses_fuse_passthrough";
+    /** {@hide} */
+    public static final String USES_FUSE_PASSTHROUGH_RESULT = "uses_fuse_passthrough_result";
+
+    /** {@hide} */
     public static final String QUERY_ARG_LIMIT = ContentResolver.QUERY_ARG_LIMIT;
     /** {@hide} */
     public static final String QUERY_ARG_MIME_TYPE = "android:query-arg-mime_type";
@@ -706,9 +711,9 @@
      * expose a limited set of read-only operations. Specifically, picker URIs
      * can only be opened for read and queried for columns in {@link PickerMediaColumns}.
      * <p>
-     * Before this API, apps could use {@link Intent#ACTION_GET_CONTENT}. However, this
-     * new action is recommended for images and videos use-cases, since it ofers a
-     * better user experience.
+     * Before this API, apps could use {@link Intent#ACTION_GET_CONTENT}. However,
+     * {@link #ACTION_PICK_IMAGES} is now the recommended option for images and videos,
+     * since it ofers a better user experience.
      */
     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
     public static final String ACTION_PICK_IMAGES = "android.provider.action.PICK_IMAGES";
@@ -722,7 +727,6 @@
      *
      * @see #ACTION_PICK_IMAGES
      * @see #isCurrentCloudMediaProviderAuthority(ContentResolver, String)
-     * @hide
      */
     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
     public static final String ACTION_PICK_IMAGES_SETTINGS =
@@ -4682,7 +4686,6 @@
      *
      * @see android.provider.CloudMediaProvider
      * @see #isSupportedCloudMediaProviderAuthority(ContentResolver, String)
-     * @hide
      */
     public static boolean isCurrentCloudMediaProviderAuthority(@NonNull ContentResolver resolver,
             @NonNull String authority) {
@@ -4696,7 +4699,6 @@
      *
      * @see android.provider.CloudMediaProvider
      * @see #isCurrentCloudMediaProviderAuthority(ContentResolver, String)
-     * @hide
      */
     public static boolean isSupportedCloudMediaProviderAuthority(@NonNull ContentResolver resolver,
             @NonNull String authority) {
@@ -4715,8 +4717,6 @@
      * unsuccessful.
      *
      * @return {@code true} if the notification was successful, {@code false} otherwise
-     *
-     * @hide
      */
     public static void notifyCloudMediaChangedEvent(@NonNull ContentResolver resolver,
             @NonNull String authority, @NonNull String currentMediaCollectionId)
diff --git a/apex/permissions/Android.bp b/apex/permissions/Android.bp
new file mode 100644
index 0000000..e7330a8
--- /dev/null
+++ b/apex/permissions/Android.bp
@@ -0,0 +1,26 @@
+
+//
+// Copyright (C) 2022 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+    default_visibility: ["//packages/providers/MediaProvider:__subpackages__"],
+}
+
+prebuilt_etc {
+    name: "privapp_allowlist_com.android.providers.media.module.xml",
+    sub_dir: "permissions",
+    src: "com.android.providers.media.module.xml",
+}
\ No newline at end of file
diff --git a/apex/permissions/OWNERS b/apex/permissions/OWNERS
new file mode 100644
index 0000000..8b7e2e5
--- /dev/null
+++ b/apex/permissions/OWNERS
@@ -0,0 +1,2 @@
+per-file *.xml,OWNERS = set noparent
+per-file *.xml,OWNERS = file:platform/frameworks/base:/data/etc/OWNERS
diff --git a/apex/permissions/com.android.providers.media.module.xml b/apex/permissions/com.android.providers.media.module.xml
new file mode 100644
index 0000000..86da4d5
--- /dev/null
+++ b/apex/permissions/com.android.providers.media.module.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 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
+  -->
+<permissions>
+    <privapp-permissions package="com.android.providers.media.module">
+        <permission name="android.permission.INTERACT_ACROSS_USERS"/>
+        <permission name="android.permission.MANAGE_USERS"/>
+        <permission name="android.permission.USE_RESERVED_DISK"/>
+        <permission name="android.permission.WRITE_MEDIA_STORAGE"/>
+        <permission name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
+        <permission name="android.permission.WATCH_APPOPS"/>
+        <permission name="android.permission.UPDATE_APP_OPS_STATS"/>
+        <permission name="android.permission.UPDATE_DEVICE_STATS"/>
+        <!-- Permissions required for reading and logging compat changes -->
+        <permission name="android.permission.LOG_COMPAT_CHANGE" />
+        <permission name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
+        <permission name="android.permission.REGISTER_STATS_PULL_ATOM" />
+        <!-- Permissions required for reading DeviceConfig -->
+        <permission name="android.permission.READ_DEVICE_CONFIG" />
+        <permission name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND"/>
+        <permission name="android.permission.MODIFY_QUIET_MODE"/>
+        <!-- Permissions required to check if an app is in the foreground or not during IO -->
+        <permission name="android.permission.PACKAGE_USAGE_STATS"/>
+    </privapp-permissions>
+</permissions>
\ No newline at end of file
diff --git a/apex/testing/Android.bp b/apex/testing/Android.bp
index 41612ba..d42a561 100644
--- a/apex/testing/Android.bp
+++ b/apex/testing/Android.bp
@@ -1,10 +1,6 @@
 package {
     // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    default_applicable_licenses: ["packages_providers_MediaProvider_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 apex_test {
diff --git a/deploy.sh b/deploy.sh
index cc1be8e..5666be8 100755
--- a/deploy.sh
+++ b/deploy.sh
@@ -1,7 +1,7 @@
 set -e
 
 # Build both our APK and APEX combined together
-./build/soong/soong_ui.bash --make-mode -j64 MediaProviderLegacy com.google.android.mediaprovider
+MODULE_BUILD_FROM_SOURCE=true ./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
diff --git a/errorprone/Android.bp b/errorprone/Android.bp
index 3f11f91..fc3d67f 100644
--- a/errorprone/Android.bp
+++ b/errorprone/Android.bp
@@ -1,11 +1,6 @@
-
 package {
     // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    default_applicable_licenses: ["packages_providers_MediaProvider_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 java_plugin {
diff --git a/jni/Android.bp b/jni/Android.bp
index 758bee1..b94f1b9 100644
--- a/jni/Android.bp
+++ b/jni/Android.bp
@@ -16,11 +16,7 @@
 
 package {
     // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    default_applicable_licenses: ["packages_providers_MediaProvider_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 cc_library_shared {
@@ -38,6 +34,7 @@
     ],
 
     header_libs: [
+        "bpf_syscall_wrappers",
         "libnativehelper_header_only",
     ],
 
@@ -67,6 +64,7 @@
     tidy: true,
     tidy_checks: [
         "-google-runtime-int",
+        "-performance-no-int-to-ptr",
     ],
 
     sdk_version: "current",
@@ -103,6 +101,9 @@
     ],
 
     tidy: true,
+    tidy_checks: [
+        "-performance-no-int-to-ptr",
+    ],
 
     sdk_version: "current",
     stl: "c++_static",
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
old mode 100755
new mode 100644
index 34ea39c..b2b8ab3
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -17,10 +17,10 @@
 #define LIBFUSE_LOG_TAG "libfuse"
 
 #include "FuseDaemon.h"
-#include "android-base/strings.h"
 
 #include <android-base/logging.h>
 #include <android-base/properties.h>
+#include <android-base/strings.h>
 #include <android/log.h>
 #include <android/trace.h>
 #include <ctype.h>
@@ -28,11 +28,11 @@
 #include <errno.h>
 #include <fcntl.h>
 #include <fuse_i.h>
+#include <fuse_kernel.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>
@@ -51,7 +51,6 @@
 #include <unistd.h>
 
 #include <iostream>
-#include <list>
 #include <map>
 #include <mutex>
 #include <queue>
@@ -61,6 +60,8 @@
 #include <unordered_set>
 #include <vector>
 
+#define BPF_FD_JUST_USE_INT
+#include "BpfSyscallWrappers.h"
 #include "MediaProviderWrapper.h"
 #include "libfuse_jni/FuseUtils.h"
 #include "libfuse_jni/ReaddirHelper.h"
@@ -71,7 +72,6 @@
 using mediaprovider::fuse::handle;
 using mediaprovider::fuse::node;
 using mediaprovider::fuse::RedactionInfo;
-using std::list;
 using std::string;
 using std::vector;
 
@@ -116,11 +116,17 @@
 const std::regex PATTERN_OWNED_PATH(
         "^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb)/([^/]+)(/?.*)?",
         std::regex_constants::icase);
+const std::regex PATTERN_BPF_BACKING_PATH("^/storage/[^/]+/[0-9]+/Android/(data|obb)$",
+                                          std::regex_constants::icase);
 
 static constexpr char TRANSFORM_SYNTHETIC_DIR[] = "synthetic";
 static constexpr char TRANSFORM_TRANSCODE_DIR[] = "transcode";
 static constexpr char PRIMARY_VOLUME_PREFIX[] = "/storage/emulated";
 
+static constexpr char FUSE_BPF_PROG_PATH[] = "/sys/fs/bpf/prog_fuse_media_fuse_media";
+
+enum class BpfFd { REMOVE = -1 };
+
 /*
  * 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,
@@ -247,16 +253,22 @@
 
 /* Single FUSE mount */
 struct fuse {
-    explicit fuse(const std::string& _path, const ino_t _ino,
-                  const std::vector<string>& _supported_transcoding_relative_paths)
+    explicit fuse(const std::string& _path, const ino_t _ino, const bool _uncached_mode,
+                  const bool _bpf, const int _bpf_fd,
+                  const std::vector<string>& _supported_transcoding_relative_paths,
+                  const std::vector<string>& _supported_uncached_relative_paths)
         : path(_path),
           tracker(mediaprovider::fuse::NodeTracker(&lock)),
           root(node::CreateRoot(_path, &lock, _ino, &tracker)),
+          uncached_mode(_uncached_mode),
           mp(0),
           zero_addr(0),
           disable_dentry_cache(false),
           passthrough(false),
-          supported_transcoding_relative_paths(_supported_transcoding_relative_paths) {}
+          bpf(_bpf),
+          bpf_fd(_bpf_fd),
+          supported_transcoding_relative_paths(_supported_transcoding_relative_paths),
+          supported_uncached_relative_paths(_supported_uncached_relative_paths) {}
 
     inline bool IsRoot(const node* node) const { return node == root; }
 
@@ -281,6 +293,14 @@
         return node::FromInode(inode, &tracker);
     }
 
+    inline node* FromInodeNoThrow(__u64 inode) {
+        if (inode == FUSE_ROOT_ID) {
+            return root;
+        }
+
+        return node::FromInodeNoThrow(inode, &tracker);
+    }
+
     inline __u64 ToInode(node* node) const {
         if (IsRoot(node)) {
             return FUSE_ROOT_ID;
@@ -305,6 +325,41 @@
         return false;
     }
 
+    inline bool IsUncachedPath(const std::string& path) {
+        const std::string base_path = GetEffectiveRootPath() + "/";
+        for (const std::string& relative_path : supported_uncached_relative_paths) {
+            if (android::base::StartsWithIgnoreCase(path, base_path + relative_path)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    inline bool ShouldNotCache(const std::string& path) {
+        if (uncached_mode) {
+            // Cache is disabled for the entire volume.
+            return true;
+        }
+
+        if (supported_uncached_relative_paths.empty()) {
+            // By default there is no supported uncached path. Just return early in this case.
+            return false;
+        }
+
+        if (!android::base::StartsWithIgnoreCase(path, PRIMARY_VOLUME_PREFIX)) {
+            // Uncached path config applies only to primary volumes.
+            return false;
+        }
+
+        if (android::base::EndsWith(path, "/")) {
+            return IsUncachedPath(path);
+        } else {
+            // Append a slash at the end to make sure that the exact match is picked up.
+            return IsUncachedPath(path + "/");
+        }
+    }
+
     std::recursive_mutex lock;
     const string path;
     // The Inode tracker associated with this FUSE instance.
@@ -312,6 +367,8 @@
     node* const root;
     struct fuse_session* se;
 
+    const bool uncached_mode;
+
     /*
      * Used to make JNI calls to MediaProvider.
      * Responsibility of freeing this object falls on corresponding
@@ -330,9 +387,14 @@
     std::atomic_bool* active;
     std::atomic_bool disable_dentry_cache;
     std::atomic_bool passthrough;
+    std::atomic_bool bpf;
+
+    const int bpf_fd;
+
     // FUSE device id.
     std::atomic_uint dev;
     const std::vector<string> supported_transcoding_relative_paths;
+    const std::vector<string> supported_uncached_relative_paths;
 };
 
 struct OpenInfo {
@@ -413,6 +475,10 @@
     return std::regex_match(path, PATTERN_OWNED_PATH);
 }
 
+static bool is_bpf_backing_path(const string& path) {
+    return std::regex_match(path, PATTERN_BPF_BACKING_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,
@@ -429,13 +495,12 @@
     }
 }
 
-static double get_entry_timeout(const string& path, node* node, struct fuse* fuse) {
+static double get_entry_timeout(const string& path, bool should_inval, struct fuse* fuse) {
     string media_path = fuse->GetEffectiveRootPath() + "/Android/media";
-    if (fuse->disable_dentry_cache || node->ShouldInvalidate() ||
-        is_package_owned_path(path, fuse->path) ||
-        android::base::StartsWithIgnoreCase(path, media_path)) {
+    if (fuse->disable_dentry_cache || should_inval || is_package_owned_path(path, fuse->path) ||
+        android::base::StartsWithIgnoreCase(path, media_path) || fuse->ShouldNotCache(path)) {
         // We set dentry timeout to 0 for the following reasons:
-        // 1. The dentry cache was completely disabled
+        // 1. The dentry cache was completely disabled for the entire volume.
         // 2.1 Case-insensitive lookups need to invalidate other case-insensitive dentry matches
         // 2.2 Nodes supporting transforms need to be invalidated, so that subsequent lookups by a
         // uid requiring a transform is guaranteed to come to the FUSE daemon.
@@ -445,6 +510,7 @@
         // 4. 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.
+        // 5. The dentry cache was completely disabled for the given path.
         return 0;
     }
     return std::numeric_limits<double>::max();
@@ -544,32 +610,34 @@
         return nullptr;
     }
 
-    const bool should_invalidate = file_lookup_result->transforms_supported;
+    bool should_invalidate = file_lookup_result->transforms_supported;
     const bool transforms_complete = file_lookup_result->transforms_complete;
     const int transforms = file_lookup_result->transforms;
     const int transforms_reason = file_lookup_result->transforms_reason;
     const string& io_path = file_lookup_result->io_path;
+    if (transforms) {
+        // If the node requires transforms, we MUST never cache it in the VFS
+        CHECK(should_invalidate);
+    }
 
     node = parent->LookupChildByName(name, true /* acquire */, transforms);
     if (!node) {
         ino_t ino = e->attr.st_ino;
-        node = ::node::Create(parent, name, io_path, should_invalidate, transforms_complete,
-                              transforms, transforms_reason, &fuse->lock, ino, &fuse->tracker);
+        node = ::node::Create(parent, name, io_path, transforms_complete, transforms,
+                              transforms_reason, &fuse->lock, ino, &fuse->tracker);
     } else if (!mediaprovider::fuse::containsMount(path)) {
-        // Only invalidate a path if it does not contain mount.
+        // Only invalidate a path if it does not contain mount and |name| != node->GetName.
         // 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)
-        // SetShouldInvalidate 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
+        // -Set |should_invalidate| to true to invalidate lookup_name by using 0 timeout below
+        // -Explicitly invalidate node_name. Note that we invalidate async otherwise we will
+        // deadlock the kernel
         if (name != node->GetName()) {
-            // Record that we have made a case insensitive lookup, this allows us invalidate nodes
-            // correctly on subsequent lookups for the case of |node|
-            node->SetShouldInvalidate();
-
+            // Force node invalidation to fix the kernel dentry cache for case (1) above
+            should_invalidate = true;
             // Make copies of the node name and path so we're not attempting to acquire
             // any node locks from the invalidation thread. Depending on timing, we may end
             // up invalidating the wrong inode but that shouldn't result in correctness issues.
@@ -578,6 +646,9 @@
             const std::string& node_name = node->GetName();
             std::thread t([=]() { fuse_inval(fuse->se, parent_ino, child_ino, node_name, path); });
             t.detach();
+            // Update the name after |node_name| reference above has been captured in lambda
+            // This avoids invalidating the node again on subsequent accesses with |name|
+            node->SetName(name);
         }
 
         // This updated value allows us correctly decide if to keep_cache and use direct_io during
@@ -609,8 +680,19 @@
     // reuse inode numbers.
     e->generation = 0;
     e->ino = fuse->ToInode(node);
-    e->entry_timeout = get_entry_timeout(path, node, fuse);
-    e->attr_timeout = std::numeric_limits<double>::max();
+
+    // When FUSE BPF is used, the caching of node attributes and lookups is
+    // disabled to avoid possible inconsistencies between the FUSE cache and
+    // the lower file system state.
+    // With FUSE BPF the file system requests are forwarded to the lower file
+    // system bypassing the FUSE daemon, so dropping the caching does not
+    // introduce a performance regression.
+    // Currently FUSE BPF is limited to the Android/data and Android/obb
+    // directories.
+    if (!fuse->bpf || !is_bpf_backing_path(path)) {
+        e->entry_timeout = get_entry_timeout(path, should_invalidate, fuse);
+        e->attr_timeout = std::numeric_limits<double>::max();
+    }
     return node;
 }
 
@@ -679,7 +761,9 @@
 }
 
 // Return true if the path is accessible for that uid.
-static bool is_app_accessible_path(MediaProviderWrapper* mp, const string& path, uid_t uid) {
+static bool is_app_accessible_path(struct fuse* fuse, const string& path, uid_t uid) {
+    MediaProviderWrapper* mp = fuse->mp;
+
     if (uid < AID_APP_START || uid == MY_UID) {
         return true;
     }
@@ -698,7 +782,7 @@
         if (pkg == ".nomedia") {
             return true;
         }
-        if (android::base::StartsWith(path, PRIMARY_VOLUME_PREFIX)) {
+        if (!fuse->bpf && android::base::StartsWith(path, PRIMARY_VOLUME_PREFIX)) {
             // Emulated storage bind-mounts app-private data directories, and so these
             // should not be accessible through FUSE anyway.
             LOG(WARNING) << "Rejected access to app-private dir on FUSE: " << path
@@ -713,9 +797,52 @@
     return true;
 }
 
+void fuse_bpf_fill_entries(const string& path, const int bpf_fd, struct fuse_entry_param* e,
+                           int& backing_fd) {
+    /*
+     * The file descriptor `fd` must not be closed as it is closed
+     * automatically by the kernel as soon as it consumes the FUSE reply. This
+     * mechanism is necessary because userspace doesn't know when the kernel
+     * will consume the FUSE response containing `fd`, thus it may close the
+     * `fd` too soon, with the risk of assigning a backing file which is either
+     * invalid or corresponds to the wrong file in the lower file system.
+     */
+    backing_fd = open(path.c_str(), O_CLOEXEC | O_DIRECTORY | O_RDONLY);
+    if (backing_fd < 0) {
+        PLOG(ERROR) << "Failed to open: " << path;
+        return;
+    }
+
+    e->backing_action = FUSE_ACTION_REPLACE;
+    e->backing_fd = backing_fd;
+
+    if (bpf_fd >= 0) {
+        e->bpf_action = FUSE_ACTION_REPLACE;
+        e->bpf_fd = bpf_fd;
+    } else if (bpf_fd == static_cast<int>(BpfFd::REMOVE)) {
+        e->bpf_action = FUSE_ACTION_REMOVE;
+    } else {
+        e->bpf_action = FUSE_ACTION_KEEP;
+    }
+}
+
+void fuse_bpf_install(struct fuse* fuse, struct fuse_entry_param* e, const string& child_path,
+                      int& backing_fd) {
+    // TODO(b/211873756) Enable only for the primary volume. Must be
+    // extended for other media devices.
+    if (android::base::StartsWith(child_path, PRIMARY_VOLUME_PREFIX)) {
+        if (is_bpf_backing_path(child_path)) {
+            fuse_bpf_fill_entries(child_path, fuse->bpf_fd, e, backing_fd);
+        } else if (is_package_owned_path(child_path, fuse->path)) {
+            fuse_bpf_fill_entries(child_path, static_cast<int>(BpfFd::REMOVE), e, backing_fd);
+        }
+    }
+}
+
 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, const FuseOp op) {
+                       struct fuse_entry_param* e, int* error_code, const FuseOp op,
+                       int* backing_fd = NULL) {
     struct fuse* fuse = get_fuse(req);
     node* parent_node = fuse->FromInode(parent);
     if (!parent_node) {
@@ -725,7 +852,7 @@
     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)) {
+    if (!fuse->IsRoot(parent_node) && !is_app_accessible_path(fuse, parent_path, req->ctx.uid)) {
         *error_code = ENOENT;
         return nullptr;
     }
@@ -748,20 +875,27 @@
         }
     }
 
-    return make_node_entry(req, parent_node, name, child_path, e, error_code, op);
+    auto node = make_node_entry(req, parent_node, name, child_path, e, error_code, op);
+
+    if (fuse->bpf && op == FuseOp::lookup) fuse_bpf_install(fuse, e, child_path, *backing_fd);
+
+    return node;
 }
 
 static void pf_lookup(fuse_req_t req, fuse_ino_t parent, const char* name) {
     ATRACE_CALL();
     struct fuse_entry_param e;
+    int backing_fd = -1;
 
     int error_code = 0;
-    if (do_lookup(req, parent, name, &e, &error_code, FuseOp::lookup)) {
+    if (do_lookup(req, parent, name, &e, &error_code, FuseOp::lookup, &backing_fd)) {
         fuse_reply_entry(req, &e);
     } else {
         CHECK(error_code != 0);
         fuse_reply_err(req, error_code);
     }
+
+    if (backing_fd != -1) close(backing_fd);
 }
 
 static void do_forget(fuse_req_t req, struct fuse* fuse, fuse_ino_t ino, uint64_t nlookup) {
@@ -818,7 +952,7 @@
         return;
     }
     const string& path = get_path(node);
-    if (!is_app_accessible_path(fuse->mp, path, req->ctx.uid)) {
+    if (!is_app_accessible_path(fuse, path, req->ctx.uid)) {
         fuse_reply_err(req, ENOENT);
         return;
     }
@@ -846,7 +980,7 @@
         return;
     }
     const string& path = get_path(node);
-    if (!is_app_accessible_path(fuse->mp, path, req->ctx.uid)) {
+    if (!is_app_accessible_path(fuse, path, req->ctx.uid)) {
         fuse_reply_err(req, ENOENT);
         return;
     }
@@ -941,7 +1075,7 @@
     node* node = fuse->FromInode(ino);
     const string& path = node ? get_path(node) : "";
 
-    if (node && is_app_accessible_path(fuse->mp, path, req->ctx.uid)) {
+    if (node && is_app_accessible_path(fuse, 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;
@@ -962,7 +1096,7 @@
         return;
     }
     string parent_path = parent_node->BuildPath();
-    if (!is_app_accessible_path(fuse->mp, parent_path, req->ctx.uid)) {
+    if (!is_app_accessible_path(fuse, parent_path, req->ctx.uid)) {
         fuse_reply_err(req, ENOENT);
         return;
     }
@@ -1000,7 +1134,7 @@
     }
     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)) {
+    if (!is_app_accessible_path(fuse, parent_path, ctx->uid)) {
         fuse_reply_err(req, ENOENT);
         return;
     }
@@ -1041,7 +1175,7 @@
     }
     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)) {
+    if (!is_app_accessible_path(fuse, parent_path, ctx->uid)) {
         fuse_reply_err(req, ENOENT);
         return;
     }
@@ -1070,7 +1204,7 @@
         return;
     }
     const string parent_path = parent_node->BuildPath();
-    if (!is_app_accessible_path(fuse->mp, parent_path, req->ctx.uid)) {
+    if (!is_app_accessible_path(fuse, parent_path, req->ctx.uid)) {
         fuse_reply_err(req, ENOENT);
         return;
     }
@@ -1125,7 +1259,7 @@
     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)) {
+    if (!is_app_accessible_path(fuse, old_parent_path, ctx->uid)) {
         return ENOENT;
     }
 
@@ -1135,10 +1269,16 @@
         return ENOENT;
     }
 
-    node* new_parent_node = fuse->FromInode(new_parent);
-    if (!new_parent_node) return ENOENT;
+    node* new_parent_node;
+    if (fuse->bpf) {
+        new_parent_node = fuse->FromInodeNoThrow(new_parent);
+        if (!new_parent_node) return EXDEV;
+    } else {
+        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)) {
+    if (!is_app_accessible_path(fuse, new_parent_path, ctx->uid)) {
         return ENOENT;
     }
 
@@ -1236,7 +1376,7 @@
                 (redaction_needed && !has_redacted) || (!redaction_needed && has_redacted);
         bool is_cached_file_open = node->HasCachedHandle();
         bool direct_io = open_info_direct_io || (is_cached_file_open && is_redaction_change) ||
-                         is_file_locked(fd, path);
+                         is_file_locked(fd, path) || fuse->ShouldNotCache(path);
 
         if (!is_cached_file_open && is_redaction_change) {
             node->SetRedactedCache(redaction_needed);
@@ -1314,7 +1454,7 @@
     const struct fuse_ctx* ctx = fuse_req_ctx(req);
     const string& io_path = get_path(node);
     const string& build_path = node->BuildPath();
-    if (!is_app_accessible_path(fuse->mp, io_path, ctx->uid)) {
+    if (!is_app_accessible_path(fuse, io_path, ctx->uid)) {
         fuse_reply_err(req, ENOENT);
         return;
     }
@@ -1551,6 +1691,25 @@
 }
 #endif
 
+/*
+ * This function does nothing except being a placeholder to keep the FUSE
+ * driver handling flushes on close(2).
+ * In fact, kernels prior to 5.8 stop attempting flushing the cache on close(2)
+ * if the .flush operation is not implemented by the FUSE daemon.
+ * This has been fixed in the kernel by commit 614c026e8a46 ("fuse: always
+ * flush dirty data on close(2)"), merged in Linux 5.8, but until then
+ * userspace must mitigate this behavior by not leaving the .flush function
+ * pointer empty.
+ */
+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) {
@@ -1609,7 +1768,7 @@
     }
     const struct fuse_ctx* ctx = fuse_req_ctx(req);
     const string path = node->BuildPath();
-    if (!is_app_accessible_path(fuse->mp, path, ctx->uid)) {
+    if (!is_app_accessible_path(fuse, path, ctx->uid)) {
         fuse_reply_err(req, ENOENT);
         return;
     }
@@ -1659,7 +1818,7 @@
         return;
     }
     const string path = node->BuildPath();
-    if (!is_app_accessible_path(fuse->mp, path, req->ctx.uid)) {
+    if (!is_app_accessible_path(fuse, path, req->ctx.uid)) {
         fuse_reply_err(req, ENOENT);
         return;
     }
@@ -1807,7 +1966,7 @@
         return;
     }
     const string path = node->BuildPath();
-    if (path != PRIMARY_VOLUME_PREFIX && !is_app_accessible_path(fuse->mp, path, req->ctx.uid)) {
+    if (path != PRIMARY_VOLUME_PREFIX && !is_app_accessible_path(fuse, path, req->ctx.uid)) {
         fuse_reply_err(req, ENOENT);
         return;
     }
@@ -1872,7 +2031,7 @@
         return;
     }
     const string parent_path = parent_node->BuildPath();
-    if (!is_app_accessible_path(fuse->mp, parent_path, req->ctx.uid)) {
+    if (!is_app_accessible_path(fuse, parent_path, req->ctx.uid)) {
         fuse_reply_err(req, ENOENT);
         return;
     }
@@ -1996,7 +2155,7 @@
     /*.link = pf_link,*/
     .open = pf_open, .read = pf_read,
     /*.write = pf_write,*/
-    /*.flush = pf_flush,*/
+    .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,
@@ -2066,6 +2225,10 @@
     return use_fuse;
 }
 
+bool FuseDaemon::UsesFusePassthrough() const {
+    return fuse->passthrough;
+}
+
 void FuseDaemon::InvalidateFuseDentryCache(const std::string& path) {
     LOG(VERBOSE) << "Invalidating FUSE dentry cache";
     if (active.load(std::memory_order_acquire)) {
@@ -2097,8 +2260,20 @@
     return active.load(std::memory_order_acquire);
 }
 
+bool IsFuseBpfEnabled() {
+    std::string bpf_override = android::base::GetProperty("persist.sys.fuse.bpf.override", "");
+    if (bpf_override == "true") {
+        return true;
+    } else if (bpf_override == "false") {
+        return false;
+    }
+    return android::base::GetBoolProperty("ro.fuse.bpf.enabled", false);
+}
+
 void FuseDaemon::Start(android::base::unique_fd fd, const std::string& path,
-                       const std::vector<std::string>& supported_transcoding_relative_paths) {
+                       const bool uncached_mode,
+                       const std::vector<std::string>& supported_transcoding_relative_paths,
+                       const std::vector<std::string>& supported_uncached_relative_paths) {
     android::base::SetDefaultTag(LOG_TAG);
 
     struct fuse_args args;
@@ -2123,7 +2298,23 @@
         return;
     }
 
-    struct fuse fuse_default(path, stat.st_ino, supported_transcoding_relative_paths);
+    bool bpf_enabled = IsFuseBpfEnabled();
+    int bpf_fd = -1;
+    if (bpf_enabled) {
+        LOG(INFO) << "Using FUSE BPF";
+
+        bpf_fd = android::bpf::bpfFdGet(FUSE_BPF_PROG_PATH, BPF_F_RDONLY);
+        if (bpf_fd < 0) {
+            PLOG(ERROR) << "Failed to fetch BPF prog fd: " << bpf_fd;
+            bpf_enabled = false;
+        } else {
+            LOG(INFO) << "BPF prog fd fetched";
+        }
+    }
+
+    struct fuse fuse_default(path, stat.st_ino, uncached_mode, bpf_enabled, bpf_fd,
+                             supported_transcoding_relative_paths,
+                             supported_uncached_relative_paths);
     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
diff --git a/jni/FuseDaemon.h b/jni/FuseDaemon.h
index 4f90209..9db8583 100644
--- a/jni/FuseDaemon.h
+++ b/jni/FuseDaemon.h
@@ -38,8 +38,9 @@
     /**
      * Start the FUSE daemon loop that will handle filesystem calls.
      */
-    void Start(android::base::unique_fd fd, const std::string& path,
-               const std::vector<std::string>& supported_transcoding_relative_paths);
+    void Start(android::base::unique_fd fd, const std::string& path, const bool uncached_mode,
+               const std::vector<std::string>& supported_transcoding_relative_paths,
+               const std::vector<std::string>& supported_uncached_relative_paths);
 
     /**
      * Checks if the FUSE daemon is started.
@@ -52,6 +53,11 @@
     bool ShouldOpenWithFuse(int fd, bool for_read, const std::string& path);
 
     /**
+     * Check if the FUSE daemon uses FUSE passthrough
+     */
+    bool UsesFusePassthrough() const;
+
+    /**
      * Invalidate FUSE VFS dentry cache entry for path
      */
     void InvalidateFuseDentryCache(const std::string& path);
diff --git a/jni/FuseUtils.cpp b/jni/FuseUtils.cpp
index 9f30440..7b08164 100644
--- a/jni/FuseUtils.cpp
+++ b/jni/FuseUtils.cpp
@@ -35,11 +35,8 @@
         return false;
     }
 
-    // Skip over the user-id by finding the next '/'
-    size_t pos = path.find_first_of("/", prefix.length());
-    // If we can't find another '/', or the '/' immediately follows the previous,
-    // ('/storage/emulated//'), not a valid mount.
-    if (pos == std::string::npos || pos == prefix.length()) {
+    size_t pos = path.find_first_of('/', prefix.length());
+    if (pos == std::string::npos) {
         return false;
     }
 
diff --git a/jni/FuseUtilsTest.cpp b/jni/FuseUtilsTest.cpp
index dede692..d76a89c 100644
--- a/jni/FuseUtilsTest.cpp
+++ b/jni/FuseUtilsTest.cpp
@@ -20,7 +20,7 @@
 
 #include <gtest/gtest.h>
 
-using namespace mediaprovider::fuse;
+namespace mediaprovider::fuse {
 
 TEST(FuseUtilsTest, testContainsMount_isTrueForAndroidDataObb) {
     EXPECT_TRUE(containsMount("/storage/emulated/1234/Android"));
@@ -39,7 +39,6 @@
     EXPECT_FALSE(containsMount("/storage/emulated/"));
     EXPECT_FALSE(containsMount("/storage/emulated//"));
     EXPECT_FALSE(containsMount("/storage/emulated/0/"));
-    EXPECT_FALSE(containsMount("/storage/emulated/1234/"));
 }
 
 TEST(FuseUtilsTest, testContainsMount_isCaseInsensitive) {
@@ -51,8 +50,6 @@
 }
 
 TEST(FuseUtilsTest, testContainsMount_isFalseForPathWithAdditionalSlash) {
-    EXPECT_FALSE(containsMount("/storage/emulated//Android/data"));
-
     EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/"));
     EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/data/"));
     EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/obb/"));
@@ -61,3 +58,5 @@
     EXPECT_FALSE(containsMount("/storage/emulated//1234/Android/data"));
     EXPECT_FALSE(containsMount("/storage/emulated/1234//Android/data"));
 }
+
+}  // namespace mediaprovider::fuse
diff --git a/jni/MediaProviderWrapper.cpp b/jni/MediaProviderWrapper.cpp
index 01411f5..253dbbe 100644
--- a/jni/MediaProviderWrapper.cpp
+++ b/jni/MediaProviderWrapper.cpp
@@ -31,7 +31,6 @@
 
 namespace mediaprovider {
 namespace fuse {
-using android::base::GetBoolProperty;
 using std::string;
 
 namespace {
@@ -41,6 +40,14 @@
 constexpr uid_t ROOT_UID = 0;
 constexpr uid_t SHELL_UID = 2000;
 
+// These need to stay in sync with MediaProvider.java's DIRECTORY_ACCESS_FOR_* constants.
+enum DirectoryAccessRequestType {
+    kReadDirectoryRequest = 1,
+    kWriteDirectoryRequest = 2,
+    kCreateDirectoryRequest = 3,
+    kDeleteDirectoryRequest = 4,
+};
+
 /** Private helper functions **/
 
 inline bool shouldBypassMediaProvider(uid_t uid) {
@@ -91,25 +98,12 @@
     return res;
 }
 
-int isMkdirOrRmdirAllowedInternal(JNIEnv* env, jobject media_provider_object,
-                                  jmethodID mid_is_mkdir_or_rmdir_allowed, const string& path,
-                                  uid_t uid, bool forCreate) {
+int isDirAccessAllowedInternal(JNIEnv* env, jobject media_provider_object,
+                               jmethodID mid_is_diraccess_allowed, const string& path, uid_t uid,
+                               int accessType) {
     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);
+    int res = env->CallIntMethod(media_provider_object, mid_is_diraccess_allowed, j_path.get(), uid,
+                                 accessType);
 
     if (CheckForJniException(env)) {
         return EFAULT;
@@ -228,37 +222,24 @@
     media_provider_class_ = reinterpret_cast<jclass>(env->NewGlobalRef(media_provider_class_));
 
     // Cache methods - Before calling a method, make sure you cache it here
-    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_insert_file_ = CacheMethod(env, "insertFileIfNecessary", "(Ljava/lang/String;I)I");
+    mid_delete_file_ = CacheMethod(env, "deleteFile", "(Ljava/lang/String;I)I");
     mid_on_file_open_ = CacheMethod(env, "onFileOpen",
                                     "(Ljava/lang/String;Ljava/lang/String;IIIZZZ)Lcom/android/"
-                                    "providers/media/FileOpenResult;",
-                                    /*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);
+                                    "providers/media/FileOpenResult;");
+    mid_is_diraccess_allowed_ = CacheMethod(env, "isDirAccessAllowed", "(Ljava/lang/String;II)I");
     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);
+            CacheMethod(env, "getFilesInDirectory", "(Ljava/lang/String;I)[Ljava/lang/String;");
+    mid_rename_ = CacheMethod(env, "rename", "(Ljava/lang/String;Ljava/lang/String;I)I");
     mid_is_uid_allowed_access_to_data_or_obb_path_ =
-            CacheMethod(env, "isUidAllowedAccessToDataOrObbPath", "(ILjava/lang/String;)Z",
-                        /*is_static*/ false);
-    mid_on_file_created_ = CacheMethod(env, "onFileCreated", "(Ljava/lang/String;)V",
-                                       /*is_static*/ false);
-    mid_should_allow_lookup_ = CacheMethod(env, "shouldAllowLookup", "(II)Z",
-                                           /*is_static*/ false);
-    mid_is_app_clone_user_ = CacheMethod(env, "isAppCloneUser", "(I)Z",
-                                         /*is_static*/ false);
-    mid_transform_ = CacheMethod(env, "transform", "(Ljava/lang/String;Ljava/lang/String;IIIII)Z",
-                                 /*is_static*/ false);
+            CacheMethod(env, "isUidAllowedAccessToDataOrObbPath", "(ILjava/lang/String;)Z");
+    mid_on_file_created_ = CacheMethod(env, "onFileCreated", "(Ljava/lang/String;)V");
+    mid_should_allow_lookup_ = CacheMethod(env, "shouldAllowLookup", "(II)Z");
+    mid_is_app_clone_user_ = CacheMethod(env, "isAppCloneUser", "(I)Z");
+    mid_transform_ = CacheMethod(env, "transform", "(Ljava/lang/String;Ljava/lang/String;IIIII)Z");
     mid_file_lookup_ =
             CacheMethod(env, "onFileLookup",
-                        "(Ljava/lang/String;II)Lcom/android/providers/media/FileLookupResult;",
-                        /*is_static*/ false);
+                        "(Ljava/lang/String;II)Lcom/android/providers/media/FileLookupResult;");
 
     // FileLookupResult
     file_lookup_result_class_ = env->FindClass("com/android/providers/media/FileLookupResult");
@@ -375,9 +356,8 @@
     }
 
     JNIEnv* env = MaybeAttachCurrentThread();
-    return isMkdirOrRmdirAllowedInternal(env, media_provider_object_,
-                                         mid_is_mkdir_or_rmdir_allowed_, path, uid,
-                                         /*forCreate*/ true);
+    return isDirAccessAllowedInternal(env, media_provider_object_, mid_is_diraccess_allowed_, path,
+                                      uid, kCreateDirectoryRequest);
 }
 
 int MediaProviderWrapper::IsDeletingDirAllowed(const string& path, uid_t uid) {
@@ -386,9 +366,8 @@
     }
 
     JNIEnv* env = MaybeAttachCurrentThread();
-    return isMkdirOrRmdirAllowedInternal(env, media_provider_object_,
-                                         mid_is_mkdir_or_rmdir_allowed_, path, uid,
-                                         /*forCreate*/ false);
+    return isDirAccessAllowedInternal(env, media_provider_object_, mid_is_diraccess_allowed_, path,
+                                      uid, kDeleteDirectoryRequest);
 }
 
 std::vector<std::shared_ptr<DirectoryEntry>> MediaProviderWrapper::GetDirectoryEntries(
@@ -421,8 +400,9 @@
     }
 
     JNIEnv* env = MaybeAttachCurrentThread();
-    return isOpendirAllowedInternal(env, media_provider_object_, mid_is_opendir_allowed_, path, uid,
-                                    forWrite);
+    return isDirAccessAllowedInternal(env, media_provider_object_, mid_is_diraccess_allowed_, path,
+                                      uid,
+                                      forWrite ? kWriteDirectoryRequest : kReadDirectoryRequest);
 }
 
 bool MediaProviderWrapper::isUidAllowedAccessToDataOrObbPath(uid_t uid, const string& path) {
@@ -536,15 +516,12 @@
  * 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) {
+                                            const char signature[]) {
     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);
-    }
+    mid = env->GetMethodID(media_provider_class_, actual_method_name.c_str(), signature);
+
     if (!mid) {
         LOG(FATAL) << "Error caching method: " << method_name << signature;
     }
diff --git a/jni/MediaProviderWrapper.h b/jni/MediaProviderWrapper.h
index bc7c656..9fa6c4e 100644
--- a/jni/MediaProviderWrapper.h
+++ b/jni/MediaProviderWrapper.h
@@ -264,8 +264,7 @@
     jmethodID mid_delete_file_;
     jmethodID mid_on_file_open_;
     jmethodID mid_scan_file_;
-    jmethodID mid_is_mkdir_or_rmdir_allowed_;
-    jmethodID mid_is_opendir_allowed_;
+    jmethodID mid_is_diraccess_allowed_;
     jmethodID mid_get_files_in_dir_;
     jmethodID mid_rename_;
     jmethodID mid_is_uid_allowed_access_to_data_or_obb_path_;
@@ -291,8 +290,7 @@
     /**
      * Auxiliary for caching MediaProvider methods.
      */
-    jmethodID CacheMethod(JNIEnv* env, const char method_name[], const char signature[],
-                          bool is_static);
+    jmethodID CacheMethod(JNIEnv* env, const char method_name[], const char signature[]);
 
     // Attaches the current thread (if necessary) and returns the JNIEnv
     // associated with it.
diff --git a/jni/RedactionInfoTest.cpp b/jni/RedactionInfoTest.cpp
index 76eec13..3a7bd27 100644
--- a/jni/RedactionInfoTest.cpp
+++ b/jni/RedactionInfoTest.cpp
@@ -24,9 +24,8 @@
 
 #include "libfuse_jni/RedactionInfo.h"
 
-using namespace mediaprovider::fuse;
+namespace mediaprovider::fuse {
 
-using std::unique_ptr;
 using std::vector;
 
 std::ostream& operator<<(std::ostream& os, const ReadRange& rr) {
@@ -381,3 +380,5 @@
     info.getReadRanges(0, 40, &out);  // read offsets [0, 40)
     EXPECT_EQ(0, out.size());
 }
+
+}  // namespace mediaprovider::fuse
diff --git a/jni/TEST_MAPPING b/jni/TEST_MAPPING
index 5ee1bc6..aec3dd0 100644
--- a/jni/TEST_MAPPING
+++ b/jni/TEST_MAPPING
@@ -9,5 +9,16 @@
     {
       "name": "fuse_node_test"
     }
+  ],
+  "hwasan-postsubmit": [
+    {
+      "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
index 54c7a87..0215fa8 100644
--- a/jni/com_android_providers_media_FuseDaemon.cpp
+++ b/jni/com_android_providers_media_FuseDaemon.cpp
@@ -36,28 +36,38 @@
 static jclass gFdAccessResultClass;
 static jmethodID gFdAccessResultCtor;
 
-static std::vector<std::string> get_supported_transcoding_relative_paths(
-        JNIEnv* env, jobjectArray java_supported_transcoding_relative_paths) {
-    ScopedLocalRef<jobjectArray> j_transcoding_relative_paths(
-            env, java_supported_transcoding_relative_paths);
-    std::vector<std::string> transcoding_relative_paths;
+static std::vector<std::string> convert_object_array_to_string_vector(
+        JNIEnv* env, jobjectArray java_object_array, const std::string& element_description) {
+    ScopedLocalRef<jobjectArray> j_ref_object_array(env, java_object_array);
+    std::vector<std::string> utf_strings;
 
-    const int transcoding_relative_paths_count =
-            env->GetArrayLength(j_transcoding_relative_paths.get());
-    for (int i = 0; i < transcoding_relative_paths_count; i++) {
-        ScopedLocalRef<jstring> j_ref_relative_path(
-                env, (jstring)env->GetObjectArrayElement(j_transcoding_relative_paths.get(), i));
-        ScopedUtfChars j_utf_relative_path(env, j_ref_relative_path.get());
-        const char* relative_path = j_utf_relative_path.c_str();
+    const int object_array_length = env->GetArrayLength(j_ref_object_array.get());
+    for (int i = 0; i < object_array_length; i++) {
+        ScopedLocalRef<jstring> j_ref_string(
+                env, (jstring)env->GetObjectArrayElement(j_ref_object_array.get(), i));
+        ScopedUtfChars utf_chars(env, j_ref_string.get());
+        const char* utf_string = utf_chars.c_str();
 
-        if (relative_path) {
-            transcoding_relative_paths.push_back(relative_path);
+        if (utf_string) {
+            utf_strings.push_back(utf_string);
         } else {
-            LOG(ERROR) << "Error reading supported transcoding relative path at index: " << i;
+            LOG(ERROR) << "Error reading " << element_description << " at index: " << i;
         }
     }
 
-    return transcoding_relative_paths;
+    return utf_strings;
+}
+
+static std::vector<std::string> get_supported_transcoding_relative_paths(
+        JNIEnv* env, jobjectArray java_supported_transcoding_relative_paths) {
+    return convert_object_array_to_string_vector(env, java_supported_transcoding_relative_paths,
+                                                 "supported transcoding relative path");
+}
+
+static std::vector<std::string> get_supported_uncached_relative_paths(
+        JNIEnv* env, jobjectArray java_supported_uncached_relative_paths) {
+    return convert_object_array_to_string_vector(env, java_supported_uncached_relative_paths,
+                                                 "supported uncached relative path");
 }
 
 jlong com_android_providers_media_FuseDaemon_new(JNIEnv* env, jobject self,
@@ -68,7 +78,8 @@
 
 void com_android_providers_media_FuseDaemon_start(
         JNIEnv* env, jobject self, jlong java_daemon, jint fd, jstring java_path,
-        jobjectArray java_supported_transcoding_relative_paths) {
+        jboolean uncached_mode, jobjectArray java_supported_transcoding_relative_paths,
+        jobjectArray java_supported_uncached_relative_paths) {
     LOG(DEBUG) << "Starting the FUSE daemon...";
     fuse::FuseDaemon* const daemon = reinterpret_cast<fuse::FuseDaemon*>(java_daemon);
 
@@ -82,8 +93,11 @@
     const std::vector<std::string>& transcoding_relative_paths =
             get_supported_transcoding_relative_paths(env,
                     java_supported_transcoding_relative_paths);
+    const std::vector<std::string>& uncached_relative_paths =
+            get_supported_uncached_relative_paths(env, java_supported_uncached_relative_paths);
 
-    daemon->Start(std::move(ufd), utf_chars_path.c_str(), transcoding_relative_paths);
+    daemon->Start(std::move(ufd), utf_chars_path.c_str(), uncached_mode, transcoding_relative_paths,
+                  uncached_relative_paths);
 }
 
 bool com_android_providers_media_FuseDaemon_is_started(JNIEnv* env, jobject self,
@@ -117,6 +131,15 @@
     return JNI_FALSE;
 }
 
+jboolean com_android_providers_media_FuseDaemon_uses_fuse_passthrough(JNIEnv* env, jobject self,
+                                                                      jlong java_daemon) {
+    fuse::FuseDaemon* const daemon = reinterpret_cast<fuse::FuseDaemon*>(java_daemon);
+    if (daemon) {
+        return daemon->UsesFusePassthrough();
+    }
+    return JNI_FALSE;
+}
+
 void com_android_providers_media_FuseDaemon_invalidate_fuse_dentry_cache(JNIEnv* env, jobject self,
                                                                          jlong java_daemon,
                                                                          jstring java_path) {
@@ -162,12 +185,14 @@
 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;[Ljava/lang/String;)V",
+        {"native_start", "(JILjava/lang/String;Z[Ljava/lang/String;[Ljava/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_uses_fuse_passthrough", "(J)Z",
+         reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_uses_fuse_passthrough)},
         {"native_is_fuse_thread", "()Z",
          reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_is_fuse_thread)},
         {"native_is_started", "(J)Z",
diff --git a/jni/node-inl.h b/jni/node-inl.h
index af30248..15844e3 100644
--- a/jni/node-inl.h
+++ b/jni/node-inl.h
@@ -99,6 +99,14 @@
   public:
     explicit NodeTracker(std::recursive_mutex* lock) : lock_(lock) {}
 
+    bool Exists(__u64 ino) const {
+        if (kEnableInodeTracking) {
+            const node* node = reinterpret_cast<const class node*>(ino);
+            std::lock_guard<std::recursive_mutex> guard(*lock_);
+            return active_nodes_.find(node) != active_nodes_.end();
+        }
+    }
+
     void CheckTracked(__u64 ino) const {
         if (kEnableInodeTracking) {
             const node* node = reinterpret_cast<const class node*>(ino);
@@ -136,15 +144,15 @@
   public:
     // Creates a new node with the specified parent, name and lock.
     static node* Create(node* parent, const std::string& name, const std::string& io_path,
-                        bool should_invalidate, bool transforms_complete, const int transforms,
+                        const bool transforms_complete, const int transforms,
                         const int transforms_reason, std::recursive_mutex* lock, ino_t ino,
                         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, io_path, should_invalidate, transforms_complete, transforms,
-                        transforms_reason, lock, ino, tracker);
+        return new node(parent, name, io_path, transforms_complete, transforms, transforms_reason,
+                        lock, ino, tracker);
     }
 
     // Creates a new root node. Root nodes have no parents by definition
@@ -152,9 +160,8 @@
     static node* CreateRoot(const std::string& path, std::recursive_mutex* lock, ino_t ino,
                             NodeTracker* tracker) {
         std::lock_guard<std::recursive_mutex> guard(*lock);
-        node* root = new node(nullptr, path, path, false /* should_invalidate */,
-                              true /* transforms_complete */, 0 /* transforms */,
-                              0 /* transforms_reason */, lock, ino, tracker);
+        node* root = new node(nullptr, path, path, true /* transforms_complete */,
+                              0 /* transforms */, 0 /* transforms_reason */, lock, ino, tracker);
 
         // The root always has one extra reference to avoid it being
         // accidentally collected.
@@ -168,6 +175,12 @@
         return reinterpret_cast<node*>(static_cast<uintptr_t>(ino));
     }
 
+    // TODO(b/215235604)
+    static inline node* FromInodeNoThrow(__u64 ino, const NodeTracker* tracker) {
+        if (!tracker->Exists(ino)) return nullptr;
+        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));
@@ -327,7 +340,7 @@
         }
         return false;
     }
-  
+
     std::unique_ptr<FdAccessResult> CheckHandleForUid(const uid_t uid) const {
         std::lock_guard<std::recursive_mutex> guard(*lock_);
 
@@ -346,15 +359,10 @@
 
         return std::make_unique<FdAccessResult>(std::string(), false);
     }
-  
-    bool ShouldInvalidate() const {
-        std::lock_guard<std::recursive_mutex> guard(*lock_);
-        return should_invalidate_;
-    }
 
-    void SetShouldInvalidate() {
+    void SetName(std::string name) {
         std::lock_guard<std::recursive_mutex> guard(*lock_);
-        should_invalidate_ = true;
+        name_ = std::move(name);
     }
 
     bool HasRedactedCache() const {
@@ -394,8 +402,8 @@
 
   private:
     node(node* parent, const std::string& name, const std::string& io_path,
-         const bool should_invalidate, const bool transforms_complete, const int transforms,
-         const int transforms_reason, std::recursive_mutex* lock, ino_t ino, NodeTracker* tracker)
+         const bool transforms_complete, const int transforms, const int transforms_reason,
+         std::recursive_mutex* lock, ino_t ino, NodeTracker* tracker)
         : name_(name),
           io_path_(io_path),
           transforms_complete_(transforms_complete),
@@ -404,7 +412,6 @@
           refcount_(0),
           parent_(nullptr),
           has_redacted_cache_(false),
-          should_invalidate_(should_invalidate),
           deleted_(false),
           lock_(lock),
           ino_(ino),
@@ -416,10 +423,6 @@
         if (parent != nullptr) {
             AddToParent(parent);
         }
-        // If the node requires transforms, we MUST never cache it in the VFS
-        if (transforms) {
-            CHECK(should_invalidate_);
-        }
     }
 
     // Acquires a reference to a node. This maps to the "lookup count" specified
@@ -558,7 +561,6 @@
     // List of directory handles associated with this node. Guarded by |lock_|.
     std::vector<std::unique_ptr<dirhandle>> dirhandles_;
     bool has_redacted_cache_;
-    bool should_invalidate_;
     bool deleted_;
     std::recursive_mutex* lock_;
     // Inode number of the file represented by this node.
diff --git a/jni/node_test.cpp b/jni/node_test.cpp
index 7d6bfc8..f687cad 100644
--- a/jni/node_test.cpp
+++ b/jni/node_test.cpp
@@ -7,7 +7,6 @@
 #include <memory>
 #include <mutex>
 
-using mediaprovider::fuse::dirhandle;
 using mediaprovider::fuse::handle;
 using mediaprovider::fuse::node;
 using mediaprovider::fuse::NodeTracker;
@@ -33,7 +32,7 @@
 
     unique_node_ptr CreateNode(node* parent, const std::string& path, const int transforms = 0) {
         return unique_node_ptr(
-                node::Create(parent, path, "", true, true, transforms, 0, &lock_, 0, &tracker_),
+                node::Create(parent, path, "", true, transforms, 0, &lock_, 0, &tracker_),
                 &NodeTest::destroy);
     }
 
@@ -68,7 +67,7 @@
 }
 
 TEST_F(NodeTest, TestRelease) {
-    node* node = node::Create(nullptr, "/path", "", false, true, 0, 0, &lock_, 0, &tracker_);
+    node* node = node::Create(nullptr, "/path", "", true, 0, 0, &lock_, 0, &tracker_);
     acquire(node);
     acquire(node);
     ASSERT_EQ(3, GetRefCount(node));
@@ -278,10 +277,10 @@
     unique_node_ptr parent = CreateNode(nullptr, "/path");
 
     // This is the tree that we intend to delete.
-    node* child = node::Create(parent.get(), "subdir", "", false, true, 0, 0, &lock_, 0, &tracker_);
-    node::Create(child, "s1", "", false, true, 0, 0, &lock_, 0, &tracker_);
-    node* subchild2 = node::Create(child, "s2", "", false, true, 0, 0, &lock_, 0, &tracker_);
-    node::Create(subchild2, "sc2", "", false, true, 0, 0, &lock_, 0, &tracker_);
+    node* child = node::Create(parent.get(), "subdir", "", true, 0, 0, &lock_, 0, &tracker_);
+    node::Create(child, "s1", "", true, 0, 0, &lock_, 0, &tracker_);
+    node* subchild2 = node::Create(child, "s2", "", true, 0, 0, &lock_, 0, &tracker_);
+    node::Create(subchild2, "sc2", "", true, 0, 0, &lock_, 0, &tracker_);
 
     ASSERT_EQ(child, parent->LookupChildByName("subdir", false /* acquire */));
     node::DeleteTree(child);
diff --git a/legacy/Android.bp b/legacy/Android.bp
index 3b03bc8..14d7f55 100644
--- a/legacy/Android.bp
+++ b/legacy/Android.bp
@@ -1,11 +1,6 @@
-
 package {
     // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    default_applicable_licenses: ["packages_providers_MediaProvider_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 android_app {
diff --git a/legacy/src/com/android/providers/media/LegacyMediaProvider.java b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
index d51414b..9951bc5 100644
--- a/legacy/src/com/android/providers/media/LegacyMediaProvider.java
+++ b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
@@ -80,9 +80,9 @@
         Logging.initPersistent(persistentDir);
 
         mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, false, true, null,
-                null, null, null, null, null);
+                null, null, null, null, null, false);
         mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME, false, true, null,
-                null, null, null, null, null);
+                null, null, null, null, null, false);
 
         return true;
     }
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index 1f44649..9eef54e 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Ontdemp video"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Speel video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Onderbreek video"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Wolkmedia is nou deur <xliff:g id="PKG_NAME">%1$s</xliff:g> beskikbaar"</string>
     <string name="not_selected" msgid="2244008151669896758">"nie gekies nie"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie oudiolêer te wysig?}other{Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> oudiolêers te wysig?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Wysig tans oudiolêer …}other{Wysig tans <xliff:g id="COUNT">^1</xliff:g> oudiolêers …}}"</string>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 264f1dc..d99aa0b 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"የቪዲዮን ድምፀ-ከል አንሣ"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"ቪድዮ አጫውት"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"ቪዲዮን ባለበት አቁም"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"የደመና ሚዲያ አሁን ከ<xliff:g id="PKG_NAME">%1$s</xliff:g> ይገኛል"</string>
     <string name="not_selected" msgid="2244008151669896758">"አልተመረጠም"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> ይህን ኦዲዮ ፋይል እንዲቀይር ይፈቀድለት?}one{<xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ኦዲዮ ፋይልን እንዲቀይር ይፈቀድለት?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> ኦዲዮ ፋይሎችን እንዲቀይር ይፈቀድለት?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{የኦዲዮ ፋይልን በመቀየር ላይ…}one{<xliff:g id="COUNT">^1</xliff:g> የኦዲዮ ፋይልን በመቀየር ላይ…}other{<xliff:g id="COUNT">^1</xliff:g> የኦዲዮ ፋይሎችን በመቀየር ላይ…}}"</string>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 5ace0b6..cde7793 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"إعادة صوت الفيديو"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"تشغيل الفيديو"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"إيقاف الفيديو مؤقتًا"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"يتوفّر محتوى الوسائط على السحابة الإلكترونية الآن من خلال تطبيق <xliff:g id="PKG_NAME">%1$s</xliff:g>."</string>
     <string name="not_selected" msgid="2244008151669896758">"غير محدّد"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{هل تريد السماح لتطبيق <xliff:g id="APP_NAME_0">^1</xliff:g> بتعديل هذا الملف الصوتي؟}zero{هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> ملف صوتي؟}two{هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل ملفَين صوتيين (<xliff:g id="COUNT">^2</xliff:g>)؟}few{هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> ملفات صوتية؟}many{هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> ملفًا صوتيًا؟}other{هل تريد السماح لتطبيق <xliff:g id="APP_NAME_1">^1</xliff:g> بتعديل <xliff:g id="COUNT">^2</xliff:g> ملف صوتي؟}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{جارٍ تعديل ملف صوتي واحد…}zero{جارٍ تعديل <xliff:g id="COUNT">^1</xliff:g> ملف صوتي…}two{جارٍ تعديل ملفَين صوتين (<xliff:g id="COUNT">^1</xliff:g>)…}few{جارٍ تعديل <xliff:g id="COUNT">^1</xliff:g> ملفات صوتية…}many{جارٍ تعديل <xliff:g id="COUNT">^1</xliff:g> ملفًا صوتيًا…}other{جارٍ تعديل <xliff:g id="COUNT">^1</xliff:g> ملف صوتي…}}"</string>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index fd0d909..379a2ed 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"ভিডিঅ’ আনমিউট কৰক"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"ভিডিঅ’ প্লে’ কৰক"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"ভিডিঅ’ পজ কৰক"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"এতিয়া <xliff:g id="PKG_NAME">%1$s</xliff:g>ৰ পৰা ক্লাউড মিডিয়া উপলব্ধ"</string>
     <string name="not_selected" msgid="2244008151669896758">"বাছনি কৰা হোৱা নাই"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g>ক এই অডিঅ’ ফাইলটো সংশোধন কৰিবলৈ অনুমতি দিবনে?}one{<xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা অডিঅ’ ফাইল সংশোধন কৰিবলৈ অনুমতি দিবনে?}other{<xliff:g id="APP_NAME_1">^1</xliff:g>ক <xliff:g id="COUNT">^2</xliff:g> টা অডিঅ’ ফাইল সংশোধন কৰিবলৈ অনুমতি দিবনে?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{অডিঅ’ ফাইলটো সংশোধন কৰি থকা হৈছে…}one{<xliff:g id="COUNT">^1</xliff:g> টা অডিঅ’ ফাইল সংশোধন কৰি থকা হৈছে…}other{<xliff:g id="COUNT">^1</xliff:g> টা অডিঅ’ ফাইল সংশোধন কৰি থকা হৈছে…}}"</string>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index be69e07..a0d97d6 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Videonu səssiz rejimdən çıxarın"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Videonu oxudun"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Videonu durdurun"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Bulud mediası indi buradan əlçatandır: <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"seçilməyib"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu audio fayla dəyişiklik etmək icazəsi verilsin?}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?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Audio fayl dəyişdirilir…}other{<xliff:g id="COUNT">^1</xliff:g> audio fayl dəyişdirilir…}}"</string>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index 3f206e8..632006c 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Uključi zvuk videa"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Pusti video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Pauziraj video"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"<xliff:g id="PKG_NAME">%1$s</xliff:g> sada nudi medijski sadržaj u klaudu"</string>
     <string name="not_selected" msgid="2244008151669896758">"nije izabrano"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> izmeni ovaj audio fajl?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> audio fajl?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> audio fajla?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> audio fajlova?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Menja se audio fajl…}one{Menja se <xliff:g id="COUNT">^1</xliff:g> audio fajl…}few{Menjaju se <xliff:g id="COUNT">^1</xliff:g> audio fajla…}other{Menja se <xliff:g id="COUNT">^1</xliff:g> audio fajlova…}}"</string>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 60cab4a..5e535f2 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Уключыць гук відэа"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Прайграць відэа"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Прыпыніць прайграванне відэа"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"З\'явіўся доступ да воблачных мультымедыя з праграмы \"<xliff:g id="PKG_NAME">%1$s</xliff:g>\""</string>
     <string name="not_selected" msgid="2244008151669896758">"не выбраны"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Дазволіць праграме \"<xliff:g id="APP_NAME_0">^1</xliff:g>\" змяніць гэты аўдыяфайл?}one{Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайл?}few{Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайлы?}many{Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайлаў?}other{Дазволіць праграме \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" змяніць <xliff:g id="COUNT">^2</xliff:g> аўдыяфайла?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Змяняецца аўдыяфайл…}one{Змяняецца <xliff:g id="COUNT">^1</xliff:g> аўдыяфайл…}few{Змяняюцца <xliff:g id="COUNT">^1</xliff:g> аўдыяфайлы…}many{Змяняюцца <xliff:g id="COUNT">^1</xliff:g> аўдыяфайлаў…}other{Змяняюцца <xliff:g id="COUNT">^1</xliff:g> аўдыяфайла…}}"</string>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 3110c39..93dcfa1 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Включване на звука на видеоклипа отново"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Пускане на видеоклипа"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Поставяне на видеоклипа на пауза"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Вече е налице мултимедия в облака от <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"не е избрано"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Да се разреши ли на <xliff:g id="APP_NAME_0">^1</xliff:g> да промени този аудиофайл?}other{Да се разреши ли на <xliff:g id="APP_NAME_1">^1</xliff:g> да промени <xliff:g id="COUNT">^2</xliff:g> аудиофайла?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Аудиофайлът се променя…}other{<xliff:g id="COUNT">^1</xliff:g> аудиофайла се променят…}}"</string>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index 7959a45..a13b8eb 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"ভিডিও আনমিউট করুন"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"ভিডিও চালান"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"ভিডিও পজ করুন"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"ক্লাউড মিডিয়া এখন <xliff:g id="PKG_NAME">%1$s</xliff:g> থেকে উপলভ্য"</string>
     <string name="not_selected" msgid="2244008151669896758">"বেছে নেওয়া হয়নি"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g>-কে এই অডিও ফাইল পরিবর্তন করার অনুমতি দিতে চান?}one{<xliff:g id="APP_NAME_1">^1</xliff:g>-কে <xliff:g id="COUNT">^2</xliff:g>টি অডিও ফাইল পরিবর্তন করার অনুমতি দিতে চান?}other{<xliff:g id="APP_NAME_1">^1</xliff:g>-কে <xliff:g id="COUNT">^2</xliff:g>টি অডিও ফাইল পরিবর্তন করার অনুমতি দিতে চান?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{অডিও ফাইলে পরিবর্তন করা হচ্ছে…}one{<xliff:g id="COUNT">^1</xliff:g>টি অডিও ফাইলে পরিবর্তন করা হচ্ছে…}other{<xliff:g id="COUNT">^1</xliff:g>টি অডিও ফাইলে পরিবর্তন করা হচ্ছে…}}"</string>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index fe1c8dc..d2e5f61 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Uključivanje zvuka videozapisa"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Reproduciranje videozapisa"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Pauziranje videozapisa"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Medijski sadržaj u oblaku je sada dostupan od usluge <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"nije odabrano"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Dozvoliti da <xliff:g id="APP_NAME_0">^1</xliff:g> izmijeni ovaj audio fajl?}one{Dozvoliti da <xliff:g id="APP_NAME_1">^1</xliff:g> izmijeni <xliff:g id="COUNT">^2</xliff:g> audio fajl?}few{Dozvoliti da <xliff:g id="APP_NAME_1">^1</xliff:g> izmijeni <xliff:g id="COUNT">^2</xliff:g> audio fajla?}other{Dozvoliti da <xliff:g id="APP_NAME_1">^1</xliff:g> izmijeni <xliff:g id="COUNT">^2</xliff:g> audio fajlova?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Mijenjanje audio fajla…}one{Mijenjanje <xliff:g id="COUNT">^1</xliff:g> audio fajla…}few{Mijenjanje <xliff:g id="COUNT">^1</xliff:g> audio fajla…}other{Mijenjanje <xliff:g id="COUNT">^1</xliff:g> audio fajlova…}}"</string>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 8c200e8..8ff3eb3 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Deixa de silenciar el vídeo"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Reprodueix el vídeo"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Posa en pausa el vídeo"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"El contingut multimèdia al núvol ara està disponible des de <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"no seleccionat"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Vols permetre que <xliff:g id="APP_NAME_0">^1</xliff:g> modifiqui aquest fitxer d\'àudio?}other{Vols permetre que <xliff:g id="APP_NAME_1">^1</xliff:g> modifiqui <xliff:g id="COUNT">^2</xliff:g> fitxers d\'àudio?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{S\'està modificant el fitxer d\'àudio…}other{S\'estan modificant <xliff:g id="COUNT">^1</xliff:g> fitxers d\'àudio…}}"</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 95c2a04..3445ff1 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Zapnout video"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Přehrát video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Pozastavit video"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Cloudová média jsou teď k dispozici ze zdroje <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"nevybráno"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Povolit aplikaci <xliff:g id="APP_NAME_0">^1</xliff:g> upravit tento zvukový soubor?}few{Povolit aplikaci <xliff:g id="APP_NAME_1">^1</xliff:g> upravit <xliff:g id="COUNT">^2</xliff:g> zvukové soubory?}many{Povolit aplikaci <xliff:g id="APP_NAME_1">^1</xliff:g> upravit <xliff:g id="COUNT">^2</xliff:g> zvukového souboru?}other{Povolit aplikaci <xliff:g id="APP_NAME_1">^1</xliff:g> upravit <xliff:g id="COUNT">^2</xliff:g> zvukových souborů?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Úprava zvukového souboru…}few{Úprava <xliff:g id="COUNT">^1</xliff:g> zvukových souborů…}many{Úprava <xliff:g id="COUNT">^1</xliff:g> zvukového souboru…}other{Úprava <xliff:g id="COUNT">^1</xliff:g> zvukových souborů…}}"</string>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 475777d..6f15ca2 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Slå videolyden til"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Afspil video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Sæt video på pause"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Medier i skyen er nu tilgængelige fra <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"ikke valgt"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Vil du give <xliff:g id="APP_NAME_0">^1</xliff:g> tilladelse til at ændre denne lydfil?}one{Vil du give <xliff:g id="APP_NAME_1">^1</xliff:g> tilladelse til at ændre <xliff:g id="COUNT">^2</xliff:g> lydfil?}other{Vil du give <xliff:g id="APP_NAME_1">^1</xliff:g> tilladelse til at ændre <xliff:g id="COUNT">^2</xliff:g> lydfiler?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Ændrer lydfilen…}one{Ændrer <xliff:g id="COUNT">^1</xliff:g> lydfil…}other{Ændrer <xliff:g id="COUNT">^1</xliff:g> lydfiler…}}"</string>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index d7f46e7..d5b49c0 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Stummschaltung des Videos aufheben"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Video ansehen"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Video anhalten"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Cloud-Medien sind jetzt verfügbar über <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"nicht ausgewählt"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Darf <xliff:g id="APP_NAME_0">^1</xliff:g> diese Audiodatei ändern?}other{Darf <xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> Audiodateien ändern?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Audiodatei wird geändert…}other{<xliff:g id="COUNT">^1</xliff:g> Audiodateien werden geändert…}}"</string>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 841f12f..2a77cd0 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Κατάργηση σίγασης βίντεο"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Αναπαραγωγή βίντεο"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Παύση βίντεο"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Τα μέσα cloud είναι πλέον διαθέσιμα από την εφαρμογή <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"μη επιλεγμένο"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Να επιτραπεί στην εφαρμογή <xliff:g id="APP_NAME_0">^1</xliff:g> η τροποποίηση αυτού του αρχείου ήχου;}other{Να επιτραπεί στην εφαρμογή <xliff:g id="APP_NAME_1">^1</xliff:g> η τροποποίηση <xliff:g id="COUNT">^2</xliff:g> αρχείων ήχου;}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Τροποποίηση αρχείου ήχου…}other{Τροποποίηση <xliff:g id="COUNT">^1</xliff:g> αρχείων ήχου…}}"</string>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index bd12ebc..06d385c 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Unmute video"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Play video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Pause video"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Cloud media now available from <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"not selected"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Allow <xliff:g id="APP_NAME_0">^1</xliff:g> to modify this audio file?}other{Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> audio files?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modifying audio file…}other{Modifying <xliff:g id="COUNT">^1</xliff:g> audio files…}}"</string>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index bd12ebc..06d385c 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Unmute video"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Play video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Pause video"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Cloud media now available from <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"not selected"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Allow <xliff:g id="APP_NAME_0">^1</xliff:g> to modify this audio file?}other{Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> audio files?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modifying audio file…}other{Modifying <xliff:g id="COUNT">^1</xliff:g> audio files…}}"</string>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index bd12ebc..06d385c 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Unmute video"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Play video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Pause video"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Cloud media now available from <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"not selected"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Allow <xliff:g id="APP_NAME_0">^1</xliff:g> to modify this audio file?}other{Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> audio files?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modifying audio file…}other{Modifying <xliff:g id="COUNT">^1</xliff:g> audio files…}}"</string>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index bd12ebc..06d385c 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Unmute video"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Play video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Pause video"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Cloud media now available from <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"not selected"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Allow <xliff:g id="APP_NAME_0">^1</xliff:g> to modify this audio file?}other{Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> audio files?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modifying audio file…}other{Modifying <xliff:g id="COUNT">^1</xliff:g> audio files…}}"</string>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index 4250916..22092f9 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‏‏‏‏‎‎‎‎‎‏‏‎‎‏‏‏‏‏‏‎‏‎‏‏‎‎‎‏‎‏‏‏‏‏‎‏‏‏‏‏‏‏‎‎‏‏‏‎‏‎‎‎‏‏‎‎‎‎‎Unmute video‎‏‎‎‏‎"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‏‏‏‎‎‏‎‏‏‏‏‏‎‎‏‎‏‏‏‏‏‎‏‎‏‏‎‎‏‎‎‎‏‎‏‎‏‎‎‏‏‏‎‎‎‎‎‏‏‎‏‏‎‎‎‎‎‏‎Play video‎‏‎‎‏‎"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‏‎‎‎‏‏‏‎‏‏‏‏‏‎‏‏‎‎‎‎‏‏‏‎‎‎‎‎‏‎‎‏‏‏‏‎‏‏‎‎‏‏‏‎‏‏‎‏‏‏‏‏‏‏‏‏‎‏‏‎Pause video‎‏‎‎‏‎"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‎‏‏‏‎‏‎‏‏‎‏‏‏‏‎‎‏‎‏‏‎‏‏‎‎‎‎‎‎‏‏‏‎‏‏‏‏‏‎‏‎‎‎‎‎‏‎‎‏‏‎‏‏‏‏‏‏‏‎Cloud media now available from ‎‏‎‎‏‏‎<xliff:g id="PKG_NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
     <string name="not_selected" msgid="2244008151669896758">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‏‏‎‎‏‎‎‏‎‎‎‏‎‏‎‎‎‏‏‎‎‎‎‏‏‎‎‏‎‏‎‎‎‏‎‏‎‏‏‏‎‏‎‏‏‏‎‏‏‎‎‎‏‏‎‏‏‎‎not selected‎‏‎‎‏‎"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‎‎‏‎‏‏‏‎‏‏‎‎‏‎‎‏‎‏‏‏‏‏‏‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‎‎‏‎‎‏‎‏‏‏‎‎‏‎Allow ‎‏‎‎‏‏‎<xliff:g id="APP_NAME_0">^1</xliff:g>‎‏‎‎‏‏‏‎ to modify this audio file?‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‎‎‏‎‏‏‏‎‏‏‎‎‏‎‎‏‎‏‏‏‏‏‏‏‏‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‎‎‏‎‎‏‎‏‏‏‎‎‏‎Allow ‎‏‎‎‏‏‎<xliff:g id="APP_NAME_1">^1</xliff:g>‎‏‎‎‏‏‏‎ to modify ‎‏‎‎‏‏‎<xliff:g id="COUNT">^2</xliff:g>‎‏‎‎‏‏‏‎ audio files?‎‏‎‎‏‎}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‎‏‎‏‏‎‎‏‎‏‎‎‏‎‏‎‎‎‎‎‎‎‏‏‎‏‏‏‎‏‎‎‏‏‏‎‏‏‏‎‏‎‎‎‎‏‏‏‎‎‎‎‎‎‏‎Modifying audio file…‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‎‏‎‏‏‎‎‏‎‏‎‎‏‎‏‎‎‎‎‎‎‎‏‏‎‏‏‏‎‏‎‎‏‏‏‎‏‏‏‎‏‎‎‎‎‏‏‏‎‎‎‎‎‎‏‎Modifying ‎‏‎‎‏‏‎<xliff:g id="COUNT">^1</xliff:g>‎‏‎‎‏‏‏‎ audio files…‎‏‎‎‏‎}}"</string>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 78ef723..ca770b8 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Activar sonido del video"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Reproducir video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Pausar video"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Contenido multimedia en la nube ahora disponible desde <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"sin seleccionar"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{¿Deseas permitir que <xliff:g id="APP_NAME_0">^1</xliff:g> modifique este archivo de audio?}other{¿Deseas permitir que <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> archivos de audio?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modificando el archivo de audio…}other{Modificando <xliff:g id="COUNT">^1</xliff:g> archivos de audio…}}"</string>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 4a583c1..81ccee6 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Dejar de silenciar vídeo"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Reproducir vídeo"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Pausar vídeo"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Contenido multimedia en la nube ahora disponible desde <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"no seleccionado"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{¿Permitir que <xliff:g id="APP_NAME_0">^1</xliff:g> modifique este archivo de audio?}other{¿Permitir que <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> archivos de audio?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modificando archivo de audio…}other{Modificando <xliff:g id="COUNT">^1</xliff:g> archivos de audio…}}"</string>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 4b41979..fc4c00e 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Video vaigistuse tühistamine"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Esita video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Peata video"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Pilvemeedia on nüüd rakenduse <xliff:g id="PKG_NAME">%1$s</xliff:g> kaudu saadaval"</string>
     <string name="not_selected" msgid="2244008151669896758">"pole valitud"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Kas lubada rakendusel <xliff:g id="APP_NAME_0">^1</xliff:g> seda helifaili muuta?}other{Kas lubada rakendusel <xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> helifaili muuta?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Helifaili muutmine …}other{<xliff:g id="COUNT">^1</xliff:g> helifaili muutmine …}}"</string>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index 818866b..e87efd7 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Aktibatu bideoaren audioa"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Erreproduzitu bideoa"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Pausatu bideoa"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Hodeiko multimedia edukia <xliff:g id="PKG_NAME">%1$s</xliff:g> bidez atzi daiteke orain"</string>
     <string name="not_selected" msgid="2244008151669896758">"hautatu gabe"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Audio-fitxategiari aldaketak egiteko baimena eman nahi diozu <xliff:g id="APP_NAME_0">^1</xliff:g> aplikazioari?}other{<xliff:g id="COUNT">^2</xliff:g> audio-fitxategiri aldaketak egiteko baimena eman nahi diozu <xliff:g id="APP_NAME_1">^1</xliff:g> aplikazioari?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Audio-fitxategia aldatzen…}other{<xliff:g id="COUNT">^1</xliff:g> audio-fitxategi aldatzen…}}"</string>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 62d7068..7c63184 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"صدادار کردن ویدیو"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"پخش ویدیو"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"مکث ویدیو"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"رسانه ابری اکنون از <xliff:g id="PKG_NAME">%1$s</xliff:g> دردسترس است"</string>
     <string name="not_selected" msgid="2244008151669896758">"انتخاب نشده است"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{به <xliff:g id="APP_NAME_0">^1</xliff:g> اجازه می‌دهید این فایل صوتی را تغییر دهد؟}one{به <xliff:g id="APP_NAME_1">^1</xliff:g> اجازه می‌دهید <xliff:g id="COUNT">^2</xliff:g> فایل صوتی را تغییر دهد؟}other{به <xliff:g id="APP_NAME_1">^1</xliff:g> اجازه می‌دهید <xliff:g id="COUNT">^2</xliff:g> فایل صوتی را تغییر دهد؟}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{درحال اصلاح فایل صوتی…}one{درحال اصلاح <xliff:g id="COUNT">^1</xliff:g> فایل صوتی…}other{درحال اصلاح <xliff:g id="COUNT">^1</xliff:g> فایل صوتی…}}"</string>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 5512aca..734f960 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Poista videon mykistys"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Toista video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Keskeytä video"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Pilvimediaa nyt saatavilla täältä: <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"ei valittu"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Saako <xliff:g id="APP_NAME_0">^1</xliff:g> muokata tätä audiotiedostoa?}other{Saako <xliff:g id="APP_NAME_1">^1</xliff:g> muokata <xliff:g id="COUNT">^2</xliff:g> audiotiedostoa?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Muokataan audiotiedostoa…}other{Muokataan <xliff:g id="COUNT">^1</xliff:g> audiotiedostoa…}}"</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index 66f0867..f44a6f1 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Réactivez le son de la vidéo"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Faites jouer la vidéo"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Suspendez la vidéo"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Le contenu multimédia dans le nuage est maintenant offert par <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"non sélectionné"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Autoriser <xliff:g id="APP_NAME_0">^1</xliff:g> à modifier ce fichier audio?}one{Autoriser <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> fichier audio?}other{Autoriser <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> fichiers audio?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modification du fichier audio en cours…}one{Modification de <xliff:g id="COUNT">^1</xliff:g> fichier audio en cours…}other{Modification de <xliff:g id="COUNT">^1</xliff:g> fichiers audio en cours…}}"</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index e3f666e..e57c85e 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Réactiver le son de la vidéo"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Lire la vidéo"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Mettre la vidéo en pause"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Fichier multimédia cloud désormais disponible depuis <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"non sélectionné"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Autoriser <xliff:g id="APP_NAME_0">^1</xliff:g> à modifier ce fichier audio ?}one{Autoriser <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> fichier audio ?}other{Autoriser <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> fichiers audio ?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modification du fichier audio…}one{Modification de <xliff:g id="COUNT">^1</xliff:g> fichier audio…}other{Modification de <xliff:g id="COUNT">^1</xliff:g> fichiers audio…}}"</string>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index 2cb67fe..6037874 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Activar son do vídeo"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Reproducir vídeo"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Pór vídeo en pausa"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Agora podes acceder desde <xliff:g id="PKG_NAME">%1$s</xliff:g> ao contido multimedia gardado na nube"</string>
     <string name="not_selected" msgid="2244008151669896758">"elemento non seleccionado"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Queres permitir que <xliff:g id="APP_NAME_0">^1</xliff:g> modifique este ficheiro de audio?}other{Queres permitir que <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> ficheiros de audio?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modificando 1 ficheiro de audio…}other{Modificando <xliff:g id="COUNT">^1</xliff:g> ficheiros de audio…}}"</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index fd14092..0c5669d 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"વીડિયોનો અવાજ ચાલુ કરો"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"વીડિયો ચલાવો"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"વીડિયો થોભાવો"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"ક્લાઉડ મીડિયા હવે <xliff:g id="PKG_NAME">%1$s</xliff:g>માંથી પણ ઉપલબ્ધ છે"</string>
     <string name="not_selected" msgid="2244008151669896758">"પસંદ નહીં કરેલી"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g>ને આ ઑડિયો ફાઇલમાં ફેરફાર કરવાની મંજૂરી આપીએ?}one{<xliff:g id="APP_NAME_1">^1</xliff:g>ને <xliff:g id="COUNT">^2</xliff:g> ઑડિયો ફાઇલમાં ફેરફાર કરવાની મંજૂરી આપીએ?}other{<xliff:g id="APP_NAME_1">^1</xliff:g>ને <xliff:g id="COUNT">^2</xliff:g> ઑડિયો ફાઇલમાં ફેરફાર કરવાની મંજૂરી આપીએ?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ઑડિયો ફાઇલમાં ફેરફાર કરી રહ્યાં છીએ…}one{<xliff:g id="COUNT">^1</xliff:g> ઑડિયો ફાઇલમાં ફેરફાર કરી રહ્યાં છીએ…}other{<xliff:g id="COUNT">^1</xliff:g> ઑડિયો ફાઇલમાં ફેરફાર કરી રહ્યાં છીએ…}}"</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index f1a957b..337c01d 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"वीडियो अनम्यूट करें"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"वीडियो चलाएं"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"वीडियो रोकें"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"क्लाउड मीडिया अब <xliff:g id="PKG_NAME">%1$s</xliff:g> पर उपलब्ध है"</string>
     <string name="not_selected" msgid="2244008151669896758">"नहीं चुना गया"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{क्या <xliff:g id="APP_NAME_0">^1</xliff:g> को इस ऑडियो फ़ाइल में बदलाव करने की अनुमति देनी है?}one{क्या <xliff:g id="APP_NAME_1">^1</xliff:g> को <xliff:g id="COUNT">^2</xliff:g> ऑडियो फ़ाइल में बदलाव करने की अनुमति देनी है?}other{क्या <xliff:g id="APP_NAME_1">^1</xliff:g> को <xliff:g id="COUNT">^2</xliff:g> ऑडियो फ़ाइलों में बदलाव करने की अनुमति देनी है?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ऑडियो फ़ाइल में बदलाव किया जा रहा है…}one{<xliff:g id="COUNT">^1</xliff:g> ऑडियो फ़ाइल में बदलाव किया जा रहा है…}other{<xliff:g id="COUNT">^1</xliff:g> ऑडियो फ़ाइलों में बदलाव किया जा रहा है…}}"</string>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index 196406b..e259cf1 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Uključi kameru"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Reproduciraj videozapis"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Pauziraj videozapis"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Medijski sadržaj u oblaku sada je dostupan iz aplikacije <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"nije odabrano"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Želite li dopustiti aplikaciji <xliff:g id="APP_NAME_0">^1</xliff:g> da izmijeni tu audiodatoteku?}one{Želite li dopustiti aplikaciji <xliff:g id="APP_NAME_1">^1</xliff:g> da izmijeni <xliff:g id="COUNT">^2</xliff:g> audiodatoteku?}few{Želite li dopustiti aplikaciji <xliff:g id="APP_NAME_1">^1</xliff:g> da izmijeni <xliff:g id="COUNT">^2</xliff:g> audiodatoteke?}other{Želite li dopustiti aplikaciji <xliff:g id="APP_NAME_1">^1</xliff:g> da izmijeni <xliff:g id="COUNT">^2</xliff:g> audiodatoteka?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Mijenjanje audiodatoteke…}one{Mijenjanje <xliff:g id="COUNT">^1</xliff:g> audiodatoteke…}few{Mijenjanje <xliff:g id="COUNT">^1</xliff:g> audiodatoteke…}other{Mijenjanje <xliff:g id="COUNT">^1</xliff:g> audiodatoteka…}}"</string>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 24c5572..b383849 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Videó némításának feloldása"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Videó lejátszása"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Videó szüneteltetése"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"A felhőbeli médiatartalmak már hozzáférhetők a következőből: <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"nincs kiválasztva"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Engedélyezi a(z) <xliff:g id="APP_NAME_0">^1</xliff:g> számára ennek a hangfájlnak a módosítását?}other{Engedélyezi a(z) <xliff:g id="APP_NAME_1">^1</xliff:g> számára <xliff:g id="COUNT">^2</xliff:g> hangfájl módosítását?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Az audiofájl módosítása folyamatban van…}other{<xliff:g id="COUNT">^1</xliff:g> audiofájl módosítása folyamatban van…}}"</string>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 4f670b3..5c6112e 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Միացնել տեսանյութի ձայնը"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Նվագարկել տեսանյութը"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Դադարեցնել տեսանյութի նվագարկումը"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Ամպային մեդիա բովանդակությունն այժմ հասանելի է <xliff:g id="PKG_NAME">%1$s</xliff:g> հավելվածից"</string>
     <string name="not_selected" msgid="2244008151669896758">"ընտրված չէ"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Թույլատրե՞լ <xliff:g id="APP_NAME_0">^1</xliff:g> հավելվածին վերականգնել այս աուդիո ֆայլն աղբարկղից}one{Թույլատրե՞լ <xliff:g id="APP_NAME_1">^1</xliff:g> հավելվածին վերականգնել <xliff:g id="COUNT">^2</xliff:g> աուդիո ֆայլ աղբարկղից}other{Թույլատրե՞լ <xliff:g id="APP_NAME_1">^1</xliff:g> հավելվածին վերականգնել <xliff:g id="COUNT">^2</xliff:g> աուդիո ֆայլ աղբարկղից}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Աուդիո ֆայլը փոփոխվում է…}one{<xliff:g id="COUNT">^1</xliff:g> աուդիո ֆայլ փոփոխվում է…}other{<xliff:g id="COUNT">^1</xliff:g> աուդիո ֆայլ փոփոխվում է…}}"</string>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index c0a6d87..4a474c3 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Bunyikan video"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Putar video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Jeda video"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Media cloud kini tersedia dari <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"tidak dipilih"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Izinkan <xliff:g id="APP_NAME_0">^1</xliff:g> mengubah file audio ini?}other{Izinkan <xliff:g id="APP_NAME_1">^1</xliff:g> mengubah <xliff:g id="COUNT">^2</xliff:g> file audio?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Mengubah file audio …}other{Mengubah <xliff:g id="COUNT">^1</xliff:g> file audio …}}"</string>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index 5051181..b2ff858 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Hætta að þagga myndskeið"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Spila myndskeið"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Gera hlé á spilun"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Skýjaefni er nú í boði frá <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"ekki valið"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Leyfa <xliff:g id="APP_NAME_0">^1</xliff:g> að breyta þessari hljóðskrá?}one{Leyfa <xliff:g id="APP_NAME_1">^1</xliff:g> að breyta <xliff:g id="COUNT">^2</xliff:g> hljóðskrá?}other{Leyfa <xliff:g id="APP_NAME_1">^1</xliff:g> að breyta <xliff:g id="COUNT">^2</xliff:g> hljóðskrám?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Breytir hljóðskrá…}one{Breytir <xliff:g id="COUNT">^1</xliff:g> hljóðskrá…}other{Breytir <xliff:g id="COUNT">^1</xliff:g> hljóðskrám…}}"</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 6d54286..5f41776 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Riattiva audio del video"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Riproduci il video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Metti in pausa il video"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Contenuti multimediali salvati su cloud ora disponibili dall\'app <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"Elemento non selezionato"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Consentire all\'app <xliff:g id="APP_NAME_0">^1</xliff:g> di modificare questo file audio?}other{Consentire all\'app <xliff:g id="APP_NAME_1">^1</xliff:g> di modificare <xliff:g id="COUNT">^2</xliff:g> file audio?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modifica del file audio in corso…}other{Modifica di <xliff:g id="COUNT">^1</xliff:g> file audio in corso…}}"</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 949d19c..1499203 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"ביטול ההשתקה של הסרטון"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"הפעלת הסרטון"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"השהיית הסרטון"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"מדיה בענן מתוך <xliff:g id="PKG_NAME">%1$s</xliff:g> זמינה עכשיו"</string>
     <string name="not_selected" msgid="2244008151669896758">"לא נבחר"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{לאפשר לאפליקציה <xliff:g id="APP_NAME_0">^1</xliff:g> לשנות את קובץ האודיו הזה?}two{לאפשר לאפליקציה <xliff:g id="APP_NAME_1">^1</xliff:g> לשנות <xliff:g id="COUNT">^2</xliff:g> קובצי אודיו?}many{לאפשר לאפליקציה <xliff:g id="APP_NAME_1">^1</xliff:g> לשנות <xliff:g id="COUNT">^2</xliff:g> קובצי אודיו?}other{לאפשר לאפליקציה <xliff:g id="APP_NAME_1">^1</xliff:g> לשנות <xliff:g id="COUNT">^2</xliff:g> קובצי אודיו?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{מתבצע שינוי בקובץ האודיו…}two{מתבצע שינוי ב-<xliff:g id="COUNT">^1</xliff:g> קובצי אודיו…}many{מתבצע שינוי ב-<xliff:g id="COUNT">^1</xliff:g> קובצי אודיו…}other{מתבצע שינוי ב-<xliff:g id="COUNT">^1</xliff:g> קובצי אודיו…}}"</string>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 6525695..fcdd9b0 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"動画のミュートを解除します"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"動画を再生します"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"動画を一時停止します"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"<xliff:g id="PKG_NAME">%1$s</xliff:g> からクラウド メディアを利用できるようになりました"</string>
     <string name="not_selected" msgid="2244008151669896758">"未選択"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{この音声ファイルの変更を <xliff:g id="APP_NAME_0">^1</xliff:g> に許可しますか?}other{<xliff:g id="COUNT">^2</xliff:g> 件の音声ファイルの変更を <xliff:g id="APP_NAME_1">^1</xliff:g> に許可しますか?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{音声ファイルを変更しています…}other{<xliff:g id="COUNT">^1</xliff:g> 件の音声ファイルを変更しています…}}"</string>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index 06940de..b1048d9 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"ვიდეოს დადუმების მოხსნა"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"ვიდეოს დაკვრა"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"ვიდეოს დაპაუზება"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"ღრუბლოვანი მედია უკვე ხელმისაწვდომია <xliff:g id="PKG_NAME">%1$s</xliff:g>-ისგან"</string>
     <string name="not_selected" msgid="2244008151669896758">"არ არის არჩეული"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{აძლევთ უფლებას <xliff:g id="APP_NAME_0">^1</xliff:g>-ს, შეცვალოს ეს აუდიოფაილი?}other{აძლევთ უფლებას <xliff:g id="APP_NAME_1">^1</xliff:g>-ს, შეცვალოს <xliff:g id="COUNT">^2</xliff:g> აუდიოფაილი?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{მიმდინარეობს აუდიოფაილის მოდიფიკაცია…}other{მიმდინარეობს <xliff:g id="COUNT">^1</xliff:g> აუდიოფაილის მოდიფიკაცია…}}"</string>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index a9a4905..af8c7d6 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Бейненің дыбысын қосу"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Бейнені ойнату"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Бейнені кідірту"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Бұлтқа сақталған медиафайл енді <xliff:g id="PKG_NAME">%1$s</xliff:g> қолданбасында қолжетімді."</string>
     <string name="not_selected" msgid="2244008151669896758">"таңдалмаған"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> қолданбасына осы аудиофайлды өзгертуге рұқсат етілсін бе?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> қолданбасына <xliff:g id="COUNT">^2</xliff:g> аудиофайлды өзгертуге рұқсат етілсін бе?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Аудиофайл өзгертілуде…}other{<xliff:g id="COUNT">^1</xliff:g> аудиофайл өзгертілуде…}}"</string>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index 9900438..f479d9a 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"បើក​សំឡេង​វីដេអូ"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"ចាក់​វីដេអូ"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"ផ្អាក​វីដេអូ"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"ឥឡូវនេះមានមេឌៀក្នុងប្រព័ន្ធពពកពី <xliff:g id="PKG_NAME">%1$s</xliff:g> ហើយ"</string>
     <string name="not_selected" msgid="2244008151669896758">"មិនបានជ្រើសរើសទេ"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{អនុញ្ញាតឱ្យ <xliff:g id="APP_NAME_0">^1</xliff:g> កែ​ឯកសារសំឡេង​នេះ​ឬ?}other{អនុញ្ញាតឱ្យ <xliff:g id="APP_NAME_1">^1</xliff:g> កែ​ឯកសារសំឡេង <xliff:g id="COUNT">^2</xliff:g> ឬ?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{កំពុងកែ​ឯកសារសំឡេង…}other{កំពុងកែ​ឯកសារសំឡេង <xliff:g id="COUNT">^1</xliff:g>…}}"</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index c2c2935..5d23792 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"ವೀಡಿಯೊ ಅನ್‍ಮ್ಯೂಟ್ ಮಾಡಿ"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"ವೀಡಿಯೊವನ್ನು ಪ್ಲೇ ಮಾಡಿ"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"ವೀಡಿಯೊವನ್ನು ವಿರಾಮಗೊಳಿಸಿ"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"ಕ್ಲೌಡ್ ಮಾಧ್ಯಮವು ಈಗ <xliff:g id="PKG_NAME">%1$s</xliff:g> ನಿಂದ ಲಭ್ಯವಿದೆ"</string>
     <string name="not_selected" msgid="2244008151669896758">"ಆಯ್ಕೆಮಾಡಲಾಗಿಲ್ಲ"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{ಈ ಆಡಿಯೋ ಫೈಲ್ ಅನ್ನು ಮಾರ್ಪಡಿಸಲು <xliff:g id="APP_NAME_0">^1</xliff:g> ಗೆ ಅನುಮತಿ ನೀಡಬೇಕೇ?}one{ಈ <xliff:g id="COUNT">^2</xliff:g> ಆಡಿಯೋ ಫೈಲ್‌ಗಳನ್ನು ಮಾರ್ಪಡಿಸಲು <xliff:g id="APP_NAME_1">^1</xliff:g> ಗೆ ಅನುಮತಿ ನೀಡಬೇಕೇ?}other{ಈ <xliff:g id="COUNT">^2</xliff:g> ಆಡಿಯೋ ಫೈಲ್‌ಗಳನ್ನು ಮಾರ್ಪಡಿಸಲು <xliff:g id="APP_NAME_1">^1</xliff:g> ಗೆ ಅನುಮತಿ ನೀಡಬೇಕೇ?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ಆಡಿಯೋ ಫೈಲ್ ಅನ್ನು ಮಾರ್ಪಡಿಸಲಾಗುತ್ತಿದೆ…}one{<xliff:g id="COUNT">^1</xliff:g> ಆಡಿಯೋ ಫೈಲ್‌ಗಳನ್ನು ಮಾರ್ಪಡಿಸಲಾಗುತ್ತಿದೆ…}other{<xliff:g id="COUNT">^1</xliff:g> ಆಡಿಯೋ ಫೈಲ್‌ಗಳನ್ನು ಮಾರ್ಪಡಿಸಲಾಗುತ್ತಿದೆ…}}"</string>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index a32089d..c2bc09f 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"동영상 음소거 해제"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"동영상 재생"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"동영상 일시중지"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"이제 <xliff:g id="PKG_NAME">%1$s</xliff:g>에서 클라우드 미디어를 사용할 수 있습니다."</string>
     <string name="not_selected" msgid="2244008151669896758">"선택되지 않음"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g>에서 이 오디오 파일을 수정하도록 허용하시겠습니까?}other{<xliff:g id="APP_NAME_1">^1</xliff:g>에서 오디오 파일 <xliff:g id="COUNT">^2</xliff:g>개를 수정하도록 허용하시겠습니까?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{오디오 파일 수정 중…}other{오디오 파일 <xliff:g id="COUNT">^1</xliff:g>개 수정 중…}}"</string>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index a890294..40237a1 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Видеонун үнүн чыгаруу"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Видеону ойнотуу"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Видеону тындыруу"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Булуттагы медиа эми <xliff:g id="PKG_NAME">%1$s</xliff:g> кызматында жеткиликтүү"</string>
     <string name="not_selected" msgid="2244008151669896758">"тандалган жок"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> колдонмосу бул аудио файлды өзгөртсүнбү?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> колдонмосу <xliff:g id="COUNT">^2</xliff:g> аудио файлды өзгөртсүнбү?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Аудио файл өзгөртүлүүдө…}other{<xliff:g id="COUNT">^1</xliff:g> аудио файл өзгөртүлүүдө…}}"</string>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index 3920b09..5f1de6e 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"ເຊົາປິດສຽງວິດີໂອ"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"ຫຼິ້ນວິດີໂອ"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"ຢຸດວິດີໂອຊົ່ວຄາວ"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"ຕອນນີ້ສາມາດໃຊ້ມີເດຍຄລາວຈາກ <xliff:g id="PKG_NAME">%1$s</xliff:g> ໄດ້ແລ້ວ"</string>
     <string name="not_selected" msgid="2244008151669896758">"ບໍ່ໄດ້ເລືອກ"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{ອະນຸຍາດໃຫ້ <xliff:g id="APP_NAME_0">^1</xliff:g> ແກ້ໄຂໄຟລ໌ສຽງນີ້ບໍ?}other{ອະນຸຍາດໃຫ້ <xliff:g id="APP_NAME_1">^1</xliff:g> ແກ້ໄຂໄຟລ໌ສຽງ <xliff:g id="COUNT">^2</xliff:g> ໄຟລ໌ບໍ?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ກຳລັງແກ້ໄຂໄຟລ໌ສຽງ…}other{ກຳລັງແກ້ໄຂໄຟລ໌ສຽງ <xliff:g id="COUNT">^1</xliff:g> ໄຟລ໌…}}"</string>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 3cc6913..87ac100 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Įjungti vaizdo įrašo garsą"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Leisti vaizdo įrašą"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Pristabdyti vaizdo įrašą"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Debesyje esanti medija dabar pasiekiama iš „<xliff:g id="PKG_NAME">%1$s</xliff:g>“"</string>
     <string name="not_selected" msgid="2244008151669896758">"nepasirinkta"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Leisti programai „<xliff:g id="APP_NAME_0">^1</xliff:g>“ keisti šį garso failą?}one{Leisti programai „<xliff:g id="APP_NAME_1">^1</xliff:g>“ keisti <xliff:g id="COUNT">^2</xliff:g> garso failą?}few{Leisti programai „<xliff:g id="APP_NAME_1">^1</xliff:g>“ keisti <xliff:g id="COUNT">^2</xliff:g> garso failus?}many{Leisti programai „<xliff:g id="APP_NAME_1">^1</xliff:g>“ keisti <xliff:g id="COUNT">^2</xliff:g> garso failo?}other{Leisti programai „<xliff:g id="APP_NAME_1">^1</xliff:g>“ keisti <xliff:g id="COUNT">^2</xliff:g> garso failų?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Keičiamas garso failas…}one{Keičiamas <xliff:g id="COUNT">^1</xliff:g> garso failas…}few{Keičiami <xliff:g id="COUNT">^1</xliff:g> garso failai…}many{Keičiama <xliff:g id="COUNT">^1</xliff:g> garso failo…}other{Keičiama <xliff:g id="COUNT">^1</xliff:g> garso failų…}}"</string>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 314c558..da84a60 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Ieslēgt video skaņu"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Atskaņot videoklipu"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Apturēt videoklipa atskaņošanu"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Tagad mākoņa multivides saturs ir pieejams, izmantojot lietotni <xliff:g id="PKG_NAME">%1$s</xliff:g>."</string>
     <string name="not_selected" msgid="2244008151669896758">"nav atlasīts"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Vai atļaut lietotnei <xliff:g id="APP_NAME_0">^1</xliff:g> pārveidot šo audio failu?}zero{Vai atļaut lietotnei <xliff:g id="APP_NAME_1">^1</xliff:g> pārveidot <xliff:g id="COUNT">^2</xliff:g> audio failus?}one{Vai atļaut lietotnei <xliff:g id="APP_NAME_1">^1</xliff:g> pārveidot <xliff:g id="COUNT">^2</xliff:g> audio failu?}other{Vai atļaut lietotnei <xliff:g id="APP_NAME_1">^1</xliff:g> pārveidot <xliff:g id="COUNT">^2</xliff:g> audio failus?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Notiek audio faila pārveidošana…}zero{Notiek <xliff:g id="COUNT">^1</xliff:g> audio failu pārveidošana…}one{Notiek <xliff:g id="COUNT">^1</xliff:g> audio faila pārveidošana…}other{Notiek <xliff:g id="COUNT">^1</xliff:g> audio failu pārveidošana…}}"</string>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index 48cfea5..ce83f1b 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Вклучете звук на видео"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Пушти го видеото"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Паузирај го видеото"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Аудиовизуелните содржини во облак сега се достапни од <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"не е избрано"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Да се дозволи <xliff:g id="APP_NAME_0">^1</xliff:g> да ја измени аудиодатотекава?}one{Да се дозволи <xliff:g id="APP_NAME_1">^1</xliff:g> да измени <xliff:g id="COUNT">^2</xliff:g> аудиодатотека?}other{Да се дозволи <xliff:g id="APP_NAME_1">^1</xliff:g> да измени <xliff:g id="COUNT">^2</xliff:g> аудиодатотеки?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Се изменува аудиодатотеката…}one{Се изменуваат <xliff:g id="COUNT">^1</xliff:g> аудиодатотека…}other{Се изменуваат <xliff:g id="COUNT">^1</xliff:g> аудиодатотеки…}}"</string>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index 3c0566e..814ff17 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"വീഡിയോ അൺമ്യൂട്ട് ചെയ്യുന്നു"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"വീഡിയോ പ്ലേ ചെയ്യുക"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"വീഡിയോ താൽക്കാലികമായി നിർത്തുക"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"ഇപ്പോൾ <xliff:g id="PKG_NAME">%1$s</xliff:g> എന്നതിൽ നിന്ന് ക്ലൗഡ് മീഡിയ ലഭ്യമാണ്"</string>
     <string name="not_selected" msgid="2244008151669896758">"തിരഞ്ഞെടുത്തിട്ടില്ല"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{ഈ ഓഡിയോ ഫയൽ പരിഷ്ക്കരിക്കാൻ <xliff:g id="APP_NAME_0">^1</xliff:g> എന്നതിനെ അനുവദിക്കണോ?}other{<xliff:g id="COUNT">^2</xliff:g> ഓഡിയോ ഫയലുകൾ പരിഷ്ക്കരിക്കാൻ <xliff:g id="APP_NAME_1">^1</xliff:g> എന്നതിനെ അനുവദിക്കണോ?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ഓഡിയോ ഫയൽ പരിഷ്‌ക്കരിക്കുന്നു…}other{<xliff:g id="COUNT">^1</xliff:g> ഓഡിയോ ഫയലുകൾ പരിഷ്‌ക്കരിക്കുന്നു…}}"</string>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 7667b37..2b94d43 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Видеоны дууг нээх"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Видео тоглуулах"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Видеог түр зогсоох"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Үүлэн медиаг одоо <xliff:g id="PKG_NAME">%1$s</xliff:g>-с авах боломжтой боллоо"</string>
     <string name="not_selected" msgid="2244008151669896758">"сонгоогүй"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g>-д энэ аудио файлыг өөрчлөхийг зөвшөөрөх үү?}other{<xliff:g id="APP_NAME_1">^1</xliff:g>-д <xliff:g id="COUNT">^2</xliff:g> аудио файлыг өөрчлөхийг зөвшөөрөх үү?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Аудио файлыг өөрчилж байна…}other{<xliff:g id="COUNT">^1</xliff:g> аудио файлыг өөрчилж байна…}}"</string>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index a0f37d5..6dd793e 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"व्हिडिओ अनम्यूट करा"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"व्हिडिओ प्ले करा"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"व्हिडिओ थांबवा"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"आता <xliff:g id="PKG_NAME">%1$s</xliff:g> कडून क्लाउड मीडिया उपलब्ध आहे"</string>
     <string name="not_selected" msgid="2244008151669896758">"निवडला नाही"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> ला या ऑडिओ फाइलमध्ये फेरबदल करण्याची अनुमती द्यायची आहे का?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> ला <xliff:g id="COUNT">^2</xliff:g> ऑडिओ फाइलमध्ये फेरबदल करण्याची अनुमती द्यायची आहे का?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ऑडिओ फाइलमध्ये फेरबदल करत आहे…}other{<xliff:g id="COUNT">^1</xliff:g> ऑडिओ फाइलमध्ये फेरबदल करत आहे…}}"</string>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index c00e85a..a16a090 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Nyahredam video"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Mainkan video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Jeda video"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Media awan kini tersedia daripada <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"tidak dipilih"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Benarkan <xliff:g id="APP_NAME_0">^1</xliff:g> mengubah suai fail audio ini?}other{Benarkan <xliff:g id="APP_NAME_1">^1</xliff:g> mengubah suai <xliff:g id="COUNT">^2</xliff:g> fail audio?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Mengubah suai fail audio…}other{Mengubah suai <xliff:g id="COUNT">^1</xliff:g> fail audio…}}"</string>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index 7d7d1cd..62131d8 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"ဗီဒီယိုအသံပြန်ဖွင့်ရန်"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"ဗီဒီယို ဖွင့်ရန်"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"ဗီဒီယို ခဏရပ်ရန်"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"<xliff:g id="PKG_NAME">%1$s</xliff:g> တွင် Cloud မီဒီယာကို ယခု ရနိုင်ပြီ"</string>
     <string name="not_selected" msgid="2244008151669896758">"ရွေးချယ်မထားပါ"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> ကို ဤအသံဖိုင် ပြင်ဆင်ခွင့်ပြုမလား။}other{<xliff:g id="APP_NAME_1">^1</xliff:g> ကို အသံဖိုင် <xliff:g id="COUNT">^2</xliff:g> ဖိုင် ပြင်ဆင်ခွင့်ပြုမလား။}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{အသံဖိုင်ကို ပြင်ဆင်နေသည်…}other{အသံဖိုင် <xliff:g id="COUNT">^1</xliff:g> ခုကို ပြင်ဆင်နေသည်…}}"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index b163198..91d6669 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Slå på lyden i videoen"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Spill av videoen"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Sett videoen på pause"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Skymedier er nå tilgjengelige fra <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"ikke valgt"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Vil du tillate at <xliff:g id="APP_NAME_0">^1</xliff:g> endrer denne lydfilen?}other{Vil du tillate at <xliff:g id="APP_NAME_1">^1</xliff:g> endrer <xliff:g id="COUNT">^2</xliff:g> lydfiler?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Endrer lydfilen …}other{Endrer <xliff:g id="COUNT">^1</xliff:g> lydfiler …}}"</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 42c559f..b04e978 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"भिडियो अनम्युट गर्नुहोस्"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"भिडियो प्ले गर्नुहोस्"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"भिडियो पज गर्नुहोस्"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"क्लाउड मिडिया अब <xliff:g id="PKG_NAME">%1$s</xliff:g> मा उपलब्ध छ"</string>
     <string name="not_selected" msgid="2244008151669896758">"चयन गरिएको छैन"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> लाई यो अडियो फाइल परिमार्जन गर्न दिने हो?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> लाई <xliff:g id="COUNT">^2</xliff:g> वटा अडियो फाइल परिमार्जन गर्न दिने हो?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{अडियो फाइल परिमार्जन गरिँदै छ…}other{<xliff:g id="COUNT">^1</xliff:g> वटा अडियो फाइल परिमार्जन गरिँदै छन्…}}"</string>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index b812f76..7b45f1b 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Geluid van video aanzetten"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Video afspelen"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Video onderbreken"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Cloudmedia nu beschikbaar van <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"niet geselecteerd"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> toestaan dit audiobestand aan te passen?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> toestaan <xliff:g id="COUNT">^2</xliff:g> audiobestanden aan te passen?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Audiobestand aanpassen…}other{<xliff:g id="COUNT">^1</xliff:g> audiobestanden aanpassen…}}"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index 68a9347..69eb25e 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"ଭିଡିଓକୁ ଅନମ୍ୟୁଟ କରନ୍ତୁ"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"ଭିଡିଓ ଚଲାନ୍ତୁ"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"ଭିଡିଓକୁ ବିରତ କରନ୍ତୁ"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"ବର୍ତ୍ତମାନ <xliff:g id="PKG_NAME">%1$s</xliff:g>ରୁ କ୍ଲାଉଡ ମିଡିଆ ଉପଲବ୍ଧ ଅଛି"</string>
     <string name="not_selected" msgid="2244008151669896758">"ଚୟନ କରାଯାଇନାହିଁ"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{ଏହି ଅଡିଓ ଫାଇଲକୁ ପରିବର୍ତ୍ତନ କରିବା ପାଇଁ <xliff:g id="APP_NAME_0">^1</xliff:g>କୁ ଅନୁମତି ଦେବେ?}other{<xliff:g id="COUNT">^2</xliff:g>ଟି ଅଡିଓ ଫାଇଲକୁ ପରିବର୍ତ୍ତନ କରିବା ପାଇଁ <xliff:g id="APP_NAME_1">^1</xliff:g>କୁ ଅନୁମତି ଦେବେ?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ଅଡିଓ ଫାଇଲ ପରିବର୍ତ୍ତନ କରାଯାଉଛି…}other{<xliff:g id="COUNT">^1</xliff:g>ଟି ଅଡିଓ ଫାଇଲ ପରିବର୍ତ୍ତନ କରାଯାଉଛି…}}"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 429b0fa..407abb1 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"ਵੀਡੀਓ ਅਣਮਿਊਟ ਕਰੋ"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"ਵੀਡੀਓ ਚਲਾਓ"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"ਵੀਡੀਓ ਰੋਕੋ"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"ਕਲਾਊਡ ਮੀਡੀਆ ਹੁਣ <xliff:g id="PKG_NAME">%1$s</xliff:g> ਤੋਂ ਉਪਲਬਧ ਹੈ"</string>
     <string name="not_selected" msgid="2244008151669896758">"ਚੁਣਿਆ ਨਹੀਂ ਗਿਆ"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{ਕੀ <xliff:g id="APP_NAME_0">^1</xliff:g> ਨੂੰ ਇਸ ਆਡੀਓ ਫ਼ਾਈਲ ਨੂੰ ਸੋਧਣ ਦੇਣਾ ਹੈ?}one{ਕੀ <xliff:g id="APP_NAME_1">^1</xliff:g> ਨੂੰ <xliff:g id="COUNT">^2</xliff:g> ਆਡੀਓ ਫ਼ਾਈਲ ਨੂੰ ਸੋਧਣ ਦੇਣਾ ਹੈ?}other{ਕੀ <xliff:g id="APP_NAME_1">^1</xliff:g> ਨੂੰ <xliff:g id="COUNT">^2</xliff:g> ਆਡੀਓ ਫ਼ਾਈਲਾਂ ਨੂੰ ਸੋਧਣ ਦੇਣਾ ਹੈ?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ਆਡੀਓ ਫ਼ਾਈਲ ਸੋਧੀ ਜਾ ਰਹੀ ਹੈ…}one{<xliff:g id="COUNT">^1</xliff:g> ਆਡੀਓ ਫ਼ਾਈਲ ਸੋਧੀ ਜਾ ਰਹੀ ਹੈ…}other{<xliff:g id="COUNT">^1</xliff:g> ਆਡੀਓ ਫ਼ਾਈਲਾਂ ਸੋਧੀਆਂ ਜਾ ਰਹੀਆਂ ਹਨ…}}"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 6937a68..f9ad1b8 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Wyłącz wyciszenie wideo"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Odtwórz film"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Wstrzymaj film"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Multimedia w chmurze są teraz dostępne z poziomu aplikacji <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"nie wybrano"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Zezwolić aplikacji <xliff:g id="APP_NAME_0">^1</xliff:g> na zmodyfikowanie tego pliku audio?}few{Zezwolić aplikacji <xliff:g id="APP_NAME_1">^1</xliff:g> na zmodyfikowanie <xliff:g id="COUNT">^2</xliff:g> plików audio?}many{Zezwolić aplikacji <xliff:g id="APP_NAME_1">^1</xliff:g> na zmodyfikowanie <xliff:g id="COUNT">^2</xliff:g> plików audio?}other{Zezwolić aplikacji <xliff:g id="APP_NAME_1">^1</xliff:g> na zmodyfikowanie <xliff:g id="COUNT">^2</xliff:g> pliku audio?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modyfikuję plik audio…}few{Modyfikuję <xliff:g id="COUNT">^1</xliff:g> pliki audio…}many{Modyfikuję <xliff:g id="COUNT">^1</xliff:g> plików audio…}other{Modyfikuję <xliff:g id="COUNT">^1</xliff:g> pliku audio…}}"</string>
diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml
index 9e757d2..00a78ba 100644
--- a/res/values-pt-rBR/strings.xml
+++ b/res/values-pt-rBR/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Ativar o som do vídeo"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Iniciar vídeo"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Pausar vídeo"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Mídia em nuvem agora disponível no app <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"não selecionado"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Permitir que o app <xliff:g id="APP_NAME_0">^1</xliff:g> modifique esse arquivo de áudio?}one{Permitir que o app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> arquivo de áudio?}other{Permitir que o app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> arquivos de áudio?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modificando o arquivo de áudio…}one{Modificando <xliff:g id="COUNT">^1</xliff:g> arquivo de áudio…}other{Modificando <xliff:g id="COUNT">^1</xliff:g> arquivos de áudio…}}"</string>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 16f5f59..9dd0a1d 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Reative o som do vídeo"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Reproduzir vídeo"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Pausar vídeo"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Multimédia da nuvem já disponível da app <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"não selecionado"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> modifique este ficheiro de áudio?}other{Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> ficheiros de áudio?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{A modificar o ficheiro de áudio…}other{A modificar <xliff:g id="COUNT">^1</xliff:g> ficheiro(s) de áudio…}}"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index 9e757d2..00a78ba 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Ativar o som do vídeo"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Iniciar vídeo"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Pausar vídeo"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Mídia em nuvem agora disponível no app <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"não selecionado"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Permitir que o app <xliff:g id="APP_NAME_0">^1</xliff:g> modifique esse arquivo de áudio?}one{Permitir que o app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> arquivo de áudio?}other{Permitir que o app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> arquivos de áudio?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Modificando o arquivo de áudio…}one{Modificando <xliff:g id="COUNT">^1</xliff:g> arquivo de áudio…}other{Modificando <xliff:g id="COUNT">^1</xliff:g> arquivos de áudio…}}"</string>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index e99e826..9d552a9 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Activați sunetul videoclipului"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Redați videoclipul"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Întrerupeți videoclipul"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Conținutul media în cloud este acum disponibil din <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"neselectat"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Permiteți ca <xliff:g id="APP_NAME_0">^1</xliff:g> să modifice acest fișier audio?}few{Permiteți ca <xliff:g id="APP_NAME_1">^1</xliff:g> să modifice <xliff:g id="COUNT">^2</xliff:g> fișiere audio?}other{Permiteți ca <xliff:g id="APP_NAME_1">^1</xliff:g> să modifice <xliff:g id="COUNT">^2</xliff:g> de fișiere audio?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Se modifică fișierul audio…}few{Se modifică <xliff:g id="COUNT">^1</xliff:g> fișiere audio…}other{Se modifică <xliff:g id="COUNT">^1</xliff:g> de fișiere audio…}}"</string>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 43e69a0..e9c51f0 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Включить звук видео"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Воспроизвести видео"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Приостановить видео"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Медиаконтент из облака теперь доступен в приложении \"<xliff:g id="PKG_NAME">%1$s</xliff:g>\"."</string>
     <string name="not_selected" msgid="2244008151669896758">"не выбрано"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Разрешить приложению \"<xliff:g id="APP_NAME_0">^1</xliff:g>\" изменить этот аудиофайл?}one{Разрешить приложению \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" изменить <xliff:g id="COUNT">^2</xliff:g> аудиофайл?}few{Разрешить приложению \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" изменить <xliff:g id="COUNT">^2</xliff:g> аудиофайла?}many{Разрешить приложению \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" изменить <xliff:g id="COUNT">^2</xliff:g> аудиофайлов?}other{Разрешить приложению \"<xliff:g id="APP_NAME_1">^1</xliff:g>\" изменить <xliff:g id="COUNT">^2</xliff:g> аудиофайла?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Изменение аудиофайла…}one{Изменение <xliff:g id="COUNT">^1</xliff:g> аудиофайла…}few{Изменение <xliff:g id="COUNT">^1</xliff:g> аудиофайлов…}many{Изменение <xliff:g id="COUNT">^1</xliff:g> аудиофайлов…}other{Изменение <xliff:g id="COUNT">^1</xliff:g> аудиофайла…}}"</string>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index 4bfca28..351e4b3 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"වීඩියෝව නිහඬ කිරීම ඉවත් කරන්න"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"වීඩියෝව ධාවනය කරන්න"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"විඩියෝව විරාම කරන්න"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"ක්ලවුඩ් මාධ්‍ය දැන් <xliff:g id="PKG_NAME">%1$s</xliff:g> වෙතින් ලබා ගත හැකිය"</string>
     <string name="not_selected" msgid="2244008151669896758">"තෝරා නොමැත"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> හට මෙම ශ්‍රව්‍ය ගොනුව වෙනස් කිරීමට ඉඩ දෙන්නද?}one{<xliff:g id="APP_NAME_1">^1</xliff:g> හට ශ්‍රව්‍ය ගොනු <xliff:g id="COUNT">^2</xliff:g>ක් වෙනස් කිරීමට ඉඩ දෙන්නද?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> හට ශ්‍රව්‍ය ගොනු <xliff:g id="COUNT">^2</xliff:g>ක් වෙනස් කිරීමට ඉඩ දෙන්නද?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ශ්‍රව්‍ය ගොනුව වෙනස් කරමින්…}one{ශ්‍රව්‍ය ගොනු <xliff:g id="COUNT">^1</xliff:g>ක් වෙනස් කරමින්…}other{ශ්‍රව්‍ය ගොනු <xliff:g id="COUNT">^1</xliff:g>ක් වෙනස් කරමින්…}}"</string>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index c722798..938c805 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Zapnúť zvuk videa"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Prehrať video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Pozastaviť video"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Cloudové médiá sú teraz k dispozícii z aplikácie <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"nevybrané"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Chcete povoliť aplikácii <xliff:g id="APP_NAME_0">^1</xliff:g> upraviť tento zvukový súbor?}few{Chcete povoliť aplikácii <xliff:g id="APP_NAME_1">^1</xliff:g> upraviť <xliff:g id="COUNT">^2</xliff:g> zvukové súbory?}many{Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> audio files?}other{Chcete povoliť aplikácii <xliff:g id="APP_NAME_1">^1</xliff:g> upraviť <xliff:g id="COUNT">^2</xliff:g> zvukových súborov?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Upravuje sa zvukový súbor…}few{Upravujú sa <xliff:g id="COUNT">^1</xliff:g> zvukové súbory…}many{Modifying <xliff:g id="COUNT">^1</xliff:g> audio files…}other{Upravuje sa <xliff:g id="COUNT">^1</xliff:g> zvukových súborov…}}"</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index 814c9ac..dc649a9 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Vklopi zvok videa"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Predvajanje videoposnetka"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Začasna zaustavitev videoposnetka"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Predstavnost v oblaku je zdaj na voljo v aplikaciji <xliff:g id="PKG_NAME">%1$s</xliff:g>."</string>
     <string name="not_selected" msgid="2244008151669896758">"ni izbrano"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Želite dovoliti aplikaciji <xliff:g id="APP_NAME_0">^1</xliff:g>, da spremeni to zvočno datoteko?}one{Želite dovoliti aplikaciji <xliff:g id="APP_NAME_1">^1</xliff:g>, da spremeni <xliff:g id="COUNT">^2</xliff:g> zvočno datoteko?}two{Želite dovoliti aplikaciji <xliff:g id="APP_NAME_1">^1</xliff:g>, da spremeni <xliff:g id="COUNT">^2</xliff:g> zvočni datoteki?}few{Želite dovoliti aplikaciji <xliff:g id="APP_NAME_1">^1</xliff:g>, da spremeni <xliff:g id="COUNT">^2</xliff:g> zvočne datoteke?}other{Želite dovoliti aplikaciji <xliff:g id="APP_NAME_1">^1</xliff:g>, da spremeni <xliff:g id="COUNT">^2</xliff:g> zvočnih datotek?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Spreminjanje zvočne datoteke …}one{Spreminjanje <xliff:g id="COUNT">^1</xliff:g> zvočne datoteke …}two{Spreminjanje <xliff:g id="COUNT">^1</xliff:g> zvočnih datotek …}few{Spreminjanje <xliff:g id="COUNT">^1</xliff:g> zvočnih datotek …}other{Spreminjanje <xliff:g id="COUNT">^1</xliff:g> zvočnih datotek …}}"</string>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index 4793f05..2c7928e 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Aktivizo zërin e videos"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Luaj videon"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Vendos videon në pauzë"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Media në renë kompjuterike tani ofrohet nga <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"nuk është zgjedhur"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Të lejohet <xliff:g id="APP_NAME_0">^1</xliff:g> që ta modifikojë këtë skedar audio?}other{Të lejohet <xliff:g id="APP_NAME_1">^1</xliff:g> që të modifikojë <xliff:g id="COUNT">^2</xliff:g> skedarë audio?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Skedari audio po modifikohet…}other{<xliff:g id="COUNT">^1</xliff:g> skedarë audio po modifikohen…}}"</string>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 1f94934..95d9152 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Укључи звук видеа"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Пусти видео"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Паузирај видео"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"<xliff:g id="PKG_NAME">%1$s</xliff:g> сада нуди медијски садржај у клауду"</string>
     <string name="not_selected" msgid="2244008151669896758">"није изабрано"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Желите ли да дозволите да <xliff:g id="APP_NAME_0">^1</xliff:g> измени овај аудио фајл?}one{Желите ли да дозволите да <xliff:g id="APP_NAME_1">^1</xliff:g> измени <xliff:g id="COUNT">^2</xliff:g> аудио фајл?}few{Желите ли да дозволите да <xliff:g id="APP_NAME_1">^1</xliff:g> измени <xliff:g id="COUNT">^2</xliff:g> аудио фајла?}other{Желите ли да дозволите да <xliff:g id="APP_NAME_1">^1</xliff:g> измени <xliff:g id="COUNT">^2</xliff:g> аудио фајлова?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Мења се аудио фајл…}one{Мења се <xliff:g id="COUNT">^1</xliff:g> аудио фајл…}few{Мењају се <xliff:g id="COUNT">^1</xliff:g> аудио фајла…}other{Мења се <xliff:g id="COUNT">^1</xliff:g> аудио фајлова…}}"</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index f36cf58..8d7feff 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Slå på ljudet för videon"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Spela upp video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Pausa video"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Molnmedia är nu tillgänglig från <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"inte valt"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Vill du tillåta att <xliff:g id="APP_NAME_0">^1</xliff:g> ändrar den här ljudfilen?}other{Vill du tillåta att <xliff:g id="APP_NAME_1">^1</xliff:g> ändrar <xliff:g id="COUNT">^2</xliff:g> ljudfiler?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Ljudfilen ändras …}other{<xliff:g id="COUNT">^1</xliff:g> ljudfiler ändras …}}"</string>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index b3e871a..bea0fd2 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Rejesha sauti ya video"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Cheza video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Sitisha video"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Maudhui ya kwenye wingu sasa yanapatikana katika <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"haijachaguliwa"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Ungependa kuruhusu <xliff:g id="APP_NAME_0">^1</xliff:g> ibadilishe faili hii ya sauti?}other{Ungependa kuruhusu <xliff:g id="APP_NAME_1">^1</xliff:g> ibadilishe faili <xliff:g id="COUNT">^2</xliff:g> za sauti?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Inarekebisha faili ya sauti…}other{Inarekebisha faili <xliff:g id="COUNT">^1</xliff:g> za sauti…}}"</string>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index 9e24716..bd19a87 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"வீடியோவின் ஒலியை இயக்கும்"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"வீடியோவைப் பிளே செய்யும்"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"வீடியோவை இடைநிறுத்தும்"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"கிளவுட் மீடியா <xliff:g id="PKG_NAME">%1$s</xliff:g> ஆப்ஸில் தற்போது கிடைக்கிறது"</string>
     <string name="not_selected" msgid="2244008151669896758">"தேர்ந்தெடுக்கப்படவில்லை"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{இந்த ஆடியோ ஃபைலில் மாற்றங்களைச் செய்ய <xliff:g id="APP_NAME_0">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?}other{<xliff:g id="COUNT">^2</xliff:g> ஆடியோ ஃபைல்களில் மாற்றங்களைச் செய்ய <xliff:g id="APP_NAME_1">^1</xliff:g> ஆப்ஸை அனுமதிக்கவா?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ஆடியோ ஃபைலை மாற்றியமைக்கிறது…}other{<xliff:g id="COUNT">^1</xliff:g> ஆடியோ ஃபைல்களை மாற்றியமைக்கிறது…}}"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index c805f73..337b0f9 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"వీడియోను అన్‌మ్యూట్ చేయండి"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"వీడియోను ప్లే చేయండి"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"వీడియోను పాజ్ చేయండి"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"క్లౌడ్ మీడియా ఇప్పుడు <xliff:g id="PKG_NAME">%1$s</xliff:g> నుండి అందుబాటులో ఉంది"</string>
     <string name="not_selected" msgid="2244008151669896758">"ఎంచుకోబడలేదు"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{ఈ ఆడియో ఫైల్‌ను ఎడిట్ చేయడానికి <xliff:g id="APP_NAME_0">^1</xliff:g>‌ను అనుమతించాలా?}other{<xliff:g id="COUNT">^2</xliff:g> ఆడియో ఫైళ్లను ఎడిట్ చేయడానికి <xliff:g id="APP_NAME_1">^1</xliff:g>‌ను అనుమతించాలా?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{ఆడియో ఫైల్‌ను సవరిస్తోంది…}other{<xliff:g id="COUNT">^1</xliff:g> ఆడియో ఫైళ్లను సవరిస్తోంది…}}"</string>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 8a7e961..9906f64 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"เปิดเสียงวิดีโอ"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"เล่นวิดีโอ"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"หยุดวิดีโอชั่วคราว"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"ไฟล์สื่อจาก <xliff:g id="PKG_NAME">%1$s</xliff:g> ในระบบคลาวด์พร้อมให้ใช้งานแล้ว"</string>
     <string name="not_selected" msgid="2244008151669896758">"ไม่ได้เลือกไว้"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{อนุญาตให้ <xliff:g id="APP_NAME_0">^1</xliff:g> แก้ไขไฟล์เสียงนี้ไหม}other{อนุญาตให้ <xliff:g id="APP_NAME_1">^1</xliff:g> แก้ไขไฟล์เสียง <xliff:g id="COUNT">^2</xliff:g> ไฟล์ไหม}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{กำลังแก้ไขไฟล์เสียง…}other{กำลังแก้ไขไฟล์เสียง <xliff:g id="COUNT">^1</xliff:g> ไฟล์…}}"</string>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index 4cae5c5..25a169c 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"I-unmute ang video"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"I-play ang video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"I-pause ang video"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Available na ang cloud media sa <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"hindi pinili"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Payagan ang <xliff:g id="APP_NAME_0">^1</xliff:g> na baguhin ang audio file na ito?}one{Payagan ang <xliff:g id="APP_NAME_1">^1</xliff:g> na baguhin ang <xliff:g id="COUNT">^2</xliff:g> audio file?}other{Payagan ang <xliff:g id="APP_NAME_1">^1</xliff:g> na baguhin ang <xliff:g id="COUNT">^2</xliff:g> na audio file?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Binabago ang audio file…}one{Nagbabago ng <xliff:g id="COUNT">^1</xliff:g> audio file…}other{Nagbabago ng <xliff:g id="COUNT">^1</xliff:g> na audio file…}}"</string>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index d49562a..8b826d0 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Videonun sesini aç"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Videoyu oynat"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Videoyu duraklat"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Bulut üzerinde saklanan medya dosyaları artık <xliff:g id="PKG_NAME">%1$s</xliff:g> uygulamasından kullanılabilir"</string>
     <string name="not_selected" msgid="2244008151669896758">"seçili değil"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> uygulamasının bu ses dosyasını değiştirmesine izin verilsin mi?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> uygulamasının <xliff:g id="COUNT">^2</xliff:g> ses dosyasını değiştirmesine izin verilsin mi?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Ses dosyası değiştiriliyor…}other{<xliff:g id="COUNT">^1</xliff:g> ses dosyası değiştiriliyor…}}"</string>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index a296f87..8a90aa1 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Увімкнути звук у відео"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Відтворити відео"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Призупинити відео"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Тепер медіаконтент із хмари доступний у додатку <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"не вибрано"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Дозволити додатку <xliff:g id="APP_NAME_0">^1</xliff:g> змінити цей аудіофайл?}one{Дозволити додатку <xliff:g id="APP_NAME_1">^1</xliff:g> змінити <xliff:g id="COUNT">^2</xliff:g> аудіофайл?}few{Дозволити додатку <xliff:g id="APP_NAME_1">^1</xliff:g> змінити <xliff:g id="COUNT">^2</xliff:g> аудіофайли?}many{Дозволити додатку <xliff:g id="APP_NAME_1">^1</xliff:g> змінити <xliff:g id="COUNT">^2</xliff:g> аудіофайлів?}other{Дозволити додатку <xliff:g id="APP_NAME_1">^1</xliff:g> змінити <xliff:g id="COUNT">^2</xliff:g> аудіофайлу?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Змінення аудіофайлу…}one{Змінення <xliff:g id="COUNT">^1</xliff:g> аудіофайлу…}few{Змінення <xliff:g id="COUNT">^1</xliff:g> аудіофайлів…}many{Змінення <xliff:g id="COUNT">^1</xliff:g> аудіофайлів…}other{Змінення <xliff:g id="COUNT">^1</xliff:g> аудіофайлу…}}"</string>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index 4a11e46..a7b76d8 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"ویڈیو کی آواز چالو کریں"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"ویڈیو چلائیں"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"ویڈیو موقوف کریں"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"کلاؤڈ میڈیا اب <xliff:g id="PKG_NAME">%1$s</xliff:g> سے دستیاب ہے"</string>
     <string name="not_selected" msgid="2244008151669896758">"غیر منتخب کردہ"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> کو اس آڈیو فائل میں ترمیم کرنے کی اجازت دیں؟}other{<xliff:g id="APP_NAME_1">^1</xliff:g> کو <xliff:g id="COUNT">^2</xliff:g> آڈیو فائلز میں ترمیم کرنے کی اجازت دیں؟}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{آڈیو فائل میں ترمیم کی جا رہی ہے…}other{<xliff:g id="COUNT">^1</xliff:g> آڈیو فائلز میں ترمیم کی جا رہی ہے…}}"</string>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index 9a114b6..9677a11 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Video ovozini yoqish"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Videoni ochish"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Videoni pauza qilish"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Endi <xliff:g id="PKG_NAME">%1$s</xliff:g> bulutli media kontenti mavjud"</string>
     <string name="not_selected" msgid="2244008151669896758">"tanlanmagan"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> ilovasiga bu audio faylni oʻzgartirishi uchun ruxsat berilsinmi?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> ilovasiga <xliff:g id="COUNT">^2</xliff:g> ta audio faylni oʻzgartirishi uchun ruxsat berilsinmi?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Audio fayl oʻzgartirilmoqda…}other{<xliff:g id="COUNT">^1</xliff:g> ta audio fayl oʻzgartirilmoqda…}}"</string>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index b29d2b9..5566ffc 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Bật tiếng video"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Phát video"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Tạm dừng video"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Hiện đã có phương tiện đám mây từ <xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"chưa được chọn"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Cho phép <xliff:g id="APP_NAME_0">^1</xliff:g> sửa đổi tệp âm thanh này?}other{Cho phép <xliff:g id="APP_NAME_1">^1</xliff:g> sửa đổi <xliff:g id="COUNT">^2</xliff:g> tệp âm thanh?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Đang sửa đổi tệp âm thanh…}other{Đang sửa đổi <xliff:g id="COUNT">^1</xliff:g> tệp âm thanh…}}"</string>
diff --git a/res/values-watch/dimens.xml b/res/values-watch/dimens.xml
new file mode 100644
index 0000000..ed5fa00
--- /dev/null
+++ b/res/values-watch/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <dimen name="permission_dialog_width">200dp</dimen>
+</resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 9e0e3d5..8fbfa4a 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"将视频取消静音"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"播放视频"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"暂停视频"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"现在可以从“<xliff:g id="PKG_NAME">%1$s</xliff:g>”获取云端媒体"</string>
     <string name="not_selected" msgid="2244008151669896758">"未选择"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{要允许<xliff:g id="APP_NAME_0">^1</xliff:g>修改这个音频文件吗?}other{要允许<xliff:g id="APP_NAME_1">^1</xliff:g>修改这 <xliff:g id="COUNT">^2</xliff:g> 个音频文件吗?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{正在修改音频文件…}other{正在修改 <xliff:g id="COUNT">^1</xliff:g> 个音频文件…}}"</string>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index a9f5219..5c3fdd4 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"將影片取消靜音"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"播放影片"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"暫停影片"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"現可透過「<xliff:g id="PKG_NAME">%1$s</xliff:g>」使用雲端媒體"</string>
     <string name="not_selected" msgid="2244008151669896758">"未揀"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{允許 <xliff:g id="APP_NAME_0">^1</xliff:g> 修改此影片嗎?}other{允許 <xliff:g id="APP_NAME_1">^1</xliff:g> 修改 <xliff:g id="COUNT">^2</xliff:g> 部影片嗎?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{正在修改音訊檔案…}other{正在修改 <xliff:g id="COUNT">^1</xliff:g> 個音訊檔案…}}"</string>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 41c15ef..1dd87a6 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"將影片取消靜音"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"播放影片"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"暫停播放影片"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"現在可以透過「<xliff:g id="PKG_NAME">%1$s</xliff:g>」存取雲端媒體"</string>
     <string name="not_selected" msgid="2244008151669896758">"未選取"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{允許「<xliff:g id="APP_NAME_0">^1</xliff:g>」修改這個音訊檔案嗎?}other{允許「<xliff:g id="APP_NAME_1">^1</xliff:g>」修改這 <xliff:g id="COUNT">^2</xliff:g> 個音訊檔案嗎?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{正在修改音訊檔案…}other{正在修改 <xliff:g id="COUNT">^1</xliff:g> 個音訊檔案…}}"</string>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index 429ba74..9b06733 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -77,6 +77,7 @@
     <string name="picker_unmute_video" msgid="6611741290641963568">"Susa ukuthula kuvidiyo"</string>
     <string name="picker_play_video" msgid="5158816108935317185">"Dlala ividiyo"</string>
     <string name="picker_pause_video" msgid="7239492902901477371">"Misa ividiyo"</string>
+    <string name="picker_cloud_sync" msgid="997251377538536319">"Imidiya ye-cloud manje iyatholakala kusuka ku-<xliff:g id="PKG_NAME">%1$s</xliff:g>"</string>
     <string name="not_selected" msgid="2244008151669896758">"akukhethiwe"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Vumela i-<xliff:g id="APP_NAME_0">^1</xliff:g> ukuguqula leli fayela lomsindo?}one{Vumela i-<xliff:g id="APP_NAME_1">^1</xliff:g> ukuguqula amafayela omsindo angu-<xliff:g id="COUNT">^2</xliff:g>?}other{Vumela i-<xliff:g id="APP_NAME_1">^1</xliff:g> ukuguqula amafayela omsindo angu-<xliff:g id="COUNT">^2</xliff:g>?}}"</string>
     <string name="permission_progress_write_audio" msgid="6029375427984180097">"{count,plural, =1{Ilungisa ifayela lomsindo…}one{Ilungisa amafayela womsindo angu-<xliff:g id="COUNT">^1</xliff:g>…}other{Ilungisa amafayela womsindo angu-<xliff:g id="COUNT">^1</xliff:g>…}}"</string>
diff --git a/res/values/config.xml b/res/values/config.xml
index 9d3cb2c..5705b20 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -19,4 +19,6 @@
     <string-array name="config_supported_transcoding_relative_paths" translatable="false">
       <item>DCIM/Camera/</item>
     </string-array>
+    <string-array name="config_supported_uncached_relative_paths" translatable="false">
+    </string-array>
 </resources>
diff --git a/res/values/overlayable.xml b/res/values/overlayable.xml
index abddaa1..c35a43c 100644
--- a/res/values/overlayable.xml
+++ b/res/values/overlayable.xml
@@ -19,6 +19,7 @@
         <policy type="product|system|vendor">
             <item type="string" name="config_default_cloud_provider_authority"/>
             <item type="array" name="config_supported_transcoding_relative_paths"/>
+            <item type="array" name="config_supported_uncached_relative_paths"/>
         </policy>
     </overlayable>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6aeeca8..4f1570f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -196,6 +196,9 @@
     <!-- Content description for a button that pauses the current video. [CHAR LIMIT=50] -->
     <string name="picker_pause_video">Pause video</string>
 
+    <!-- Toast notifying user that cloud media content is now available from an app on their device. [CHAR LIMIT=NONE] -->
+    <string name="picker_cloud_sync">Cloud media now available from <xliff:g id="pkg_name" example="Gmail">%1$s</xliff:g></string>
+
     <!-- Default not selected text used by accessibility for an element that can be unselected. [CHAR LIMIT=NONE] -->
     <string name="not_selected">not selected</string>
 
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index fa813a1..e4700b0 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -20,6 +20,7 @@
 import static com.android.providers.media.util.Logging.LOGV;
 import static com.android.providers.media.util.Logging.TAG;
 
+import android.annotation.SuppressLint;
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentUris;
@@ -37,8 +38,10 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
+import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.provider.MediaStore;
@@ -63,6 +66,7 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.modules.utils.BackgroundThread;
+import com.android.providers.media.dao.FileRow;
 import com.android.providers.media.playlist.Playlist;
 import com.android.providers.media.util.DatabaseUtils;
 import com.android.providers.media.util.FileUtils;
@@ -79,12 +83,16 @@
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Field;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.function.Function;
 import java.util.function.UnaryOperator;
@@ -105,6 +113,45 @@
     @VisibleForTesting
     public static final String TEST_CLEAN_DB = "test_clean";
 
+    /**
+     * Key name of xattr used to set next row id for internal DB.
+     */
+    private static final String INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY = "user.intdbnextrowid".concat(
+            String.valueOf(UserHandle.myUserId()));
+
+    /**
+     * Key name of xattr used to set next row id for external DB.
+     */
+    private static final String EXTERNAL_DB_NEXT_ROW_ID_XATTR_KEY = "user.extdbnextrowid".concat(
+            String.valueOf(UserHandle.myUserId()));
+
+    /**
+     * Key name of xattr used to set session id for internal DB.
+     */
+    private static final String INTERNAL_DB_SESSION_ID_XATTR_KEY = "user.intdbsessionid".concat(
+            String.valueOf(UserHandle.myUserId()));
+
+    /**
+     * Key name of xattr used to set session id for external DB.
+     */
+    private static final String EXTERNAL_DB_SESSION_ID_XATTR_KEY = "user.extdbsessionid".concat(
+            String.valueOf(UserHandle.myUserId()));
+
+    /** Indicates a billion value used when next row id is not present in respective xattr. */
+    private static final Long NEXT_ROW_ID_DEFAULT_BILLION_VALUE = Double.valueOf(
+            Math.pow(10, 9)).longValue();
+
+    private static final Long INVALID_ROW_ID = -1L;
+
+    /**
+     * Path used for setting next row id and database session id for each user profile. Storing here
+     * because media provider does not have required permission on path /data/media/<user-id> for
+     * work profiles.
+     * For devices with adoptable storage support, opting for adoptable storage will not delete
+     * /data/media/0 directory.
+     */
+    public static final String DATA_MEDIA_XATTR_DIRECTORY_PATH = "/data/media/0";
+
     static final String INTERNAL_DATABASE_NAME = "internal.db";
     static final String EXTERNAL_DATABASE_NAME = "external.db";
 
@@ -133,6 +180,7 @@
     private final String mMigrationFileName;
     long mScanStartTime;
     long mScanStopTime;
+    private boolean mEnableNextRowIdRecovery;
 
     /**
      * Unfortunately we can have multiple instances of DatabaseHelper, causing
@@ -158,26 +206,27 @@
     private static Object sMigrationLockInternal = new Object();
     private static Object sMigrationLockExternal = new Object();
 
+    /**
+     * Object used to synchronise sequence of next row id in database.
+     */
+    private static final Object sRecoveryLock = new Object();
+
+    /** Stores cached value of next row id of the database which optimises new id inserts. */
+    private AtomicLong mNextRowIdBackup = new AtomicLong(INVALID_ROW_ID);
+
     public interface OnSchemaChangeListener {
         void onSchemaChange(@NonNull String volumeName, int versionFrom, int versionTo,
                 long itemCount, long durationMillis, String databaseUuid);
     }
 
     public interface OnFilesChangeListener {
-        void onInsert(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id,
-                int mediaType, boolean isDownload, boolean isPending);
+        void onInsert(@NonNull DatabaseHelper helper, @NonNull FileRow insertedRow);
 
-        void onUpdate(@NonNull DatabaseHelper helper, @NonNull String volumeName,
-                long oldId, int oldMediaType, boolean oldIsDownload,
-                long newId, int newMediaType, boolean newIsDownload,
-                boolean oldIsTrashed, boolean newIsTrashed,
-                boolean oldIsPending, boolean newIsPending,
-                boolean oldIsFavorite, boolean newIsFavorite,
-                int oldSpecialFormat, int newSpecialFormat,
-                String oldOwnerPackage, String newOwnerPackage, String oldPath);
+        void onUpdate(@NonNull DatabaseHelper helper, @NonNull FileRow oldRow,
+                @NonNull FileRow newRow);
 
-        void onDelete(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id,
-                int mediaType, boolean isDownload, String ownerPackage, String path);
+        /** Method invoked on database row delete. */
+        void onDelete(@NonNull DatabaseHelper helper, @NonNull FileRow deletedRow);
     }
 
     public interface OnLegacyMigrationListener {
@@ -196,10 +245,10 @@
             @Nullable OnSchemaChangeListener schemaListener,
             @Nullable OnFilesChangeListener filesListener,
             @NonNull OnLegacyMigrationListener migrationListener,
-            @Nullable UnaryOperator<String> idGenerator) {
+            @Nullable UnaryOperator<String> idGenerator, boolean enableNextRowIdRecovery) {
         this(context, name, getDatabaseVersion(context), earlyUpgrade, legacyProvider,
                 columnAnnotation, exportedSinceAnnotation, schemaListener, filesListener,
-                migrationListener, idGenerator);
+                migrationListener, idGenerator, enableNextRowIdRecovery);
     }
 
     public DatabaseHelper(Context context, String name, int version,
@@ -209,7 +258,7 @@
             @Nullable OnSchemaChangeListener schemaListener,
             @Nullable OnFilesChangeListener filesListener,
             @NonNull OnLegacyMigrationListener migrationListener,
-            @Nullable UnaryOperator<String> idGenerator) {
+            @Nullable UnaryOperator<String> idGenerator, boolean enableNextRowIdRecovery) {
         super(context, name, null, version);
         mContext = context;
         mName = name;
@@ -230,6 +279,7 @@
         mMigrationListener = migrationListener;
         mIdGenerator = idGenerator;
         mMigrationFileName = "." + mVolumeName;
+        this.mEnableNextRowIdRecovery = enableNextRowIdRecovery;
 
         // Configure default filters until we hear differently
         if (isInternal()) {
@@ -308,10 +358,15 @@
                 final boolean isDownload = Integer.parseInt(split[3]) != 0;
                 final boolean isPending = Integer.parseInt(split[4]) != 0;
 
+                FileRow insertedRow = FileRow.newBuilder(id)
+                        .setVolumeName(volumeName)
+                        .setMediaType(mediaType)
+                        .setIsDownload(isDownload)
+                        .setIsPending(isPending)
+                        .build();
                 Trace.beginSection("_INSERT");
                 try {
-                    mFilesListener.onInsert(DatabaseHelper.this, volumeName, id,
-                            mediaType, isDownload, isPending);
+                    mFilesListener.onInsert(DatabaseHelper.this, insertedRow);
                 } finally {
                     Trace.endSection();
                 }
@@ -341,13 +396,31 @@
                 final String newOwnerPackage = split[16];
                 final String oldPath = split[17];
 
+                FileRow oldRow = FileRow.newBuilder(oldId)
+                        .setVolumeName(volumeName)
+                        .setMediaType(oldMediaType)
+                        .setIsDownload(oldIsDownload)
+                        .setIsTrashed(oldIsTrashed)
+                        .setIsPending(oldIsPending)
+                        .setIsFavorite(oldIsFavorite)
+                        .setSpecialFormat(oldSpecialFormat)
+                        .setOwnerPackageName(oldOwnerPackage)
+                        .setPath(oldPath)
+                        .build();
+                FileRow newRow = FileRow.newBuilder(newId)
+                        .setVolumeName(volumeName)
+                        .setMediaType(newMediaType)
+                        .setIsDownload(newIsDownload)
+                        .setIsTrashed(newIsTrashed)
+                        .setIsPending(newIsPending)
+                        .setIsFavorite(newIsFavorite)
+                        .setSpecialFormat(newSpecialFormat)
+                        .setOwnerPackageName(newOwnerPackage)
+                        .build();
+
                 Trace.beginSection("_UPDATE");
                 try {
-                    mFilesListener.onUpdate(DatabaseHelper.this, volumeName, oldId,
-                            oldMediaType, oldIsDownload, newId, newMediaType, newIsDownload,
-                            oldIsTrashed, newIsTrashed, oldIsPending, newIsPending,
-                            oldIsFavorite, newIsFavorite, oldSpecialFormat, newSpecialFormat,
-                            oldOwnerPackage, newOwnerPackage, oldPath);
+                    mFilesListener.onUpdate(DatabaseHelper.this, oldRow, newRow);
                 } finally {
                     Trace.endSection();
                 }
@@ -365,10 +438,16 @@
                 final String ownerPackage = split[4];
                 final String path = split[5];
 
+                FileRow deletedRow = FileRow.newBuilder(id)
+                        .setVolumeName(volumeName)
+                        .setMediaType(mediaType)
+                        .setIsDownload(isDownload)
+                        .setOwnerPackageName(ownerPackage)
+                        .setPath(path)
+                        .build();
                 Trace.beginSection("_DELETE");
                 try {
-                    mFilesListener.onDelete(DatabaseHelper.this, volumeName, id,
-                            mediaType, isDownload, ownerPackage, path);
+                    mFilesListener.onDelete(DatabaseHelper.this, deletedRow);
                 } finally {
                     Trace.endSection();
                 }
@@ -421,22 +500,96 @@
 
     @Override
     public void onDowngrade(final SQLiteDatabase db, final int oldV, final int newV) {
-        Log.v(TAG, "onDowngrade() for " + mName + " from " + oldV + " to " + newV);
-        mSchemaLock.writeLock().lock();
-        try {
-            downgradeDatabase(db, oldV, newV);
-        } finally {
-            mSchemaLock.writeLock().unlock();
+        Log.w(TAG, String.format(Locale.ROOT,
+                "onDowngrade() for %s from %s to %s. Deleting database:%s in case of a "
+                        + "downgrade.", mName, oldV, newV, mName));
+        deleteDatabaseFiles();
+        throw new IllegalStateException(
+                String.format(Locale.ROOT, "Crashing MP process on database downgrade of %s.",
+                        mName));
+    }
+
+    private void deleteDatabaseFiles() {
+        File dbDir = mContext.getDatabasePath(mName).getParentFile();
+        File[] files = dbDir.listFiles();
+        if (files == null) {
+            Log.w(TAG, String.format(Locale.ROOT, "No database files found on path:%s.",
+                    dbDir.getAbsolutePath()));
+            return;
+        }
+
+        for (File file : files) {
+            if (file.getName().startsWith(mName)) {
+                file.delete();
+                Log.w(TAG, String.format(Locale.ROOT, "Database file:%s deleted.",
+                        file.getAbsolutePath()));
+            }
         }
     }
 
+
     @Override
     public void onOpen(final SQLiteDatabase db) {
         Log.v(TAG, "onOpen() for " + mName);
-
+        // Recovering before migration from legacy because recovery process will clear up data to
+        // read from xattrs once ids are persisted in xattrs.
+        tryRecoverRowIdSequence(db);
         tryMigrateFromLegacy(db);
     }
 
+    private void tryRecoverRowIdSequence(SQLiteDatabase db) {
+        if (!isNextRowIdBackupEnabled()) {
+            Log.d(TAG, "Skipping row id recovery as backup is not enabled.");
+            return;
+        }
+
+        synchronized (sRecoveryLock) {
+            boolean isLastUsedDatabaseSession = isLastUsedDatabaseSession(db);
+            Optional<Long> nextRowIdFromXattrOptional = getNextRowIdFromXattr();
+            if (isLastUsedDatabaseSession && nextRowIdFromXattrOptional.isPresent()) {
+                Log.i(TAG, String.format(Locale.ROOT,
+                        "No database change across sequential open calls for %s.", mName));
+                mNextRowIdBackup.set(nextRowIdFromXattrOptional.get());
+                updateSessionIdInDatabaseAndExternalStorage(db);
+                return;
+            }
+
+            Log.w(TAG, String.format(Locale.ROOT,
+                    "%s database inconsistent: isLastUsedDatabaseSession:%b, "
+                            + "nextRowIdOptionalPresent:%b", mName, isLastUsedDatabaseSession,
+                    nextRowIdFromXattrOptional.isPresent()));
+            // TODO(b/222313219): Add an assert to ensure that next row id xattr is always
+            // present when DB session id matches across sequential open calls.
+            updateNextRowIdInDatabaseAndExternalStorage(db);
+            updateSessionIdInDatabaseAndExternalStorage(db);
+        }
+    }
+
+    @GuardedBy("sRecoveryLock")
+    private boolean isLastUsedDatabaseSession(SQLiteDatabase db) {
+        Optional<String> lastUsedSessionIdFromDatabasePathXattr = getXattr(db.getPath(),
+                getSessionIdXattrKeyForDatabase());
+        Optional<String> lastUsedSessionIdFromExternalStoragePathXattr = getXattr(
+                DATA_MEDIA_XATTR_DIRECTORY_PATH, getSessionIdXattrKeyForDatabase());
+
+        return lastUsedSessionIdFromDatabasePathXattr.isPresent()
+                && lastUsedSessionIdFromExternalStoragePathXattr.isPresent()
+                && lastUsedSessionIdFromDatabasePathXattr.get().equals(
+                lastUsedSessionIdFromExternalStoragePathXattr.get());
+    }
+
+    @GuardedBy("sRecoveryLock")
+    private void updateSessionIdInDatabaseAndExternalStorage(SQLiteDatabase db) {
+        final String uuid = UUID.randomUUID().toString();
+        boolean setOnDatabase = setXattr(db.getPath(), getSessionIdXattrKeyForDatabase(), uuid);
+        boolean setOnExternalStorage = setXattr(DATA_MEDIA_XATTR_DIRECTORY_PATH,
+                getSessionIdXattrKeyForDatabase(), uuid);
+        if (setOnDatabase && setOnExternalStorage) {
+            Log.i(TAG, String.format(Locale.ROOT, "SessionId set to %s on paths %s and %s.", uuid,
+                    db.getPath(), DATA_MEDIA_XATTR_DIRECTORY_PATH));
+        }
+    }
+
     private void tryMigrateFromLegacy(SQLiteDatabase db) {
         final Object migrationLock;
         if (isInternal()) {
@@ -1656,7 +1809,8 @@
     }
 
     private void updateUserId(SQLiteDatabase db) {
-        db.execSQL(String.format("ALTER TABLE files ADD COLUMN _user_id INTEGER DEFAULT %d;",
+        db.execSQL(String.format(Locale.ROOT,
+                "ALTER TABLE files ADD COLUMN _user_id INTEGER DEFAULT %d;",
                 UserHandle.myUserId()));
     }
 
@@ -2108,4 +2262,137 @@
                 return false;
         }
     }
+
+    @SuppressLint("DefaultLocale")
+    @GuardedBy("sRecoveryLock")
+    private void updateNextRowIdInDatabaseAndExternalStorage(SQLiteDatabase db) {
+        Optional<Long> nextRowIdOptional = getNextRowIdFromXattr();
+        // Use a billion as the next row id if not found on external storage.
+        long nextRowId = nextRowIdOptional.orElse(NEXT_ROW_ID_DEFAULT_BILLION_VALUE);
+
+        backupNextRowId(nextRowId);
+        // Insert and delete a row to update sqlite_sequence counter
+        db.execSQL(String.format(Locale.ROOT, "INSERT INTO files(_ID) VALUES (%d)", nextRowId));
+        db.execSQL(String.format(Locale.ROOT, "DELETE FROM files WHERE _ID=%d", nextRowId));
+        Log.i(TAG, String.format(Locale.ROOT, "Updated sqlite counter of Files table of %s to %d.",
+                mName, nextRowId));
+    }
+
+    /**
+     * Backs up next row id value in xattr to {@code nextRowId} + BackupFrequency. Also updates
+     * respective in-memory next row id cached value.
+     */
+    protected void backupNextRowId(long nextRowId) {
+        long backupId = nextRowId + getNextRowIdBackupFrequency();
+        boolean setOnExternalStorage = setXattr(DATA_MEDIA_XATTR_DIRECTORY_PATH,
+                getNextRowIdXattrKeyForDatabase(),
+                String.valueOf(backupId));
+        if (setOnExternalStorage) {
+            mNextRowIdBackup.set(backupId);
+            Log.i(TAG, String.format(Locale.ROOT, "Backed up next row id as:%d on path:%s for %s.",
+                    backupId, DATA_MEDIA_XATTR_DIRECTORY_PATH, mName));
+        }
+    }
+
+    protected Optional<Long> getNextRowIdFromXattr() {
+        try {
+            return Optional.of(Long.parseLong(new String(
+                    Os.getxattr(DATA_MEDIA_XATTR_DIRECTORY_PATH,
+                            getNextRowIdXattrKeyForDatabase()))));
+        } catch (Exception e) {
+            Log.e(TAG, String.format(Locale.ROOT, "Xattr:%s not found on external storage.",
+                    getNextRowIdXattrKeyForDatabase()), e);
+            return Optional.empty();
+        }
+    }
+
+    protected String getNextRowIdXattrKeyForDatabase() {
+        if (isInternal()) {
+            return INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY;
+        } else if (isExternal()) {
+            return EXTERNAL_DB_NEXT_ROW_ID_XATTR_KEY;
+        }
+        throw new RuntimeException(
+                String.format(Locale.ROOT, "Next row id xattr key not defined for database:%s.",
+                        mName));
+    }
+
+    protected String getSessionIdXattrKeyForDatabase() {
+        if (isInternal()) {
+            return INTERNAL_DB_SESSION_ID_XATTR_KEY;
+        } else if (isExternal()) {
+            return EXTERNAL_DB_SESSION_ID_XATTR_KEY;
+        }
+        throw new RuntimeException(
+                String.format(Locale.ROOT, "Session id xattr key not defined for database:%s.",
+                        mName));
+    }
+
+    protected static boolean setXattr(String path, String key, String value) {
+        try (ParcelFileDescriptor pfd = ParcelFileDescriptor.open(new File(path),
+                ParcelFileDescriptor.MODE_READ_ONLY)) {
+            // Map id value to xattr key
+            Os.setxattr(path, key, value.getBytes(), 0);
+            Os.fsync(pfd.getFileDescriptor());
+            Log.d(TAG, String.format("xattr set to %s for key:%s on path: %s.", value, key, path));
+            return true;
+        } catch (Exception e) {
+            Log.e(TAG, String.format(Locale.ROOT, "Failed to set xattr:%s to %s for path: %s.", key,
+                    value, path), e);
+            return false;
+        }
+    }
+
+    protected static Optional<String> getXattr(String path, String key) {
+        try {
+            return Optional.of(Arrays.toString(Os.getxattr(path, key)));
+        } catch (Exception e) {
+            Log.w(TAG, String.format(Locale.ROOT,
+                    "Exception encountered while reading xattr:%s from path:%s.", key, path));
+            return Optional.empty();
+        }
+    }
+
+    protected Optional<Long> getNextRowId() {
+        if (mNextRowIdBackup.get() == INVALID_ROW_ID) {
+            return getNextRowIdFromXattr();
+        }
+
+        return Optional.of(mNextRowIdBackup.get());
+    }
+
+    boolean isNextRowIdBackupEnabled() {
+        if (!mEnableNextRowIdRecovery) {
+            return false;
+        }
+
+        if (mVersion < VERSION_R) {
+            // Do not back up next row id if DB version is less than R. This is unlikely to hit
+            // as we will backport row id backup changes till Android R.
+            Log.v(TAG, "Skipping next row id backup for android versions less than R.");
+            return false;
+        }
+
+        if (isInternal()) {
+            // Skip id reuse fix for internal db as it can lead to ids starting from a billion
+            // and can cause aberrant behaviour in Ringtones Manager. Reference: b/229153534.
+            Log.v(TAG, "Skipping next row id backup for internal database.");
+            return false;
+        }
+
+        if (!(new File(DATA_MEDIA_XATTR_DIRECTORY_PATH)).exists()) {
+            Log.w(TAG, String.format(Locale.ROOT,
+                    "Skipping row id recovery as path:%s does not exist.",
+                    DATA_MEDIA_XATTR_DIRECTORY_PATH));
+            return false;
+        }
+
+        return SystemProperties.getBoolean("persist.sys.fuse.backup.nextrowid_enabled",
+                true);
+    }
+
+    public static int getNextRowIdBackupFrequency() {
+        return SystemProperties.getInt("persist.sys.fuse.backup.nextrowid_backup_frequency",
+                1000);
+    }
 }
diff --git a/src/com/android/providers/media/FileAccessAttributes.java b/src/com/android/providers/media/FileAccessAttributes.java
new file mode 100644
index 0000000..e9dc556
--- /dev/null
+++ b/src/com/android/providers/media/FileAccessAttributes.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media;
+
+import android.database.Cursor;
+
+/**
+ * Class to represent the file metadata stored in the database (SQLite/xAttr)
+ */
+public final class FileAccessAttributes {
+    private final long mId;
+    private final int mMediaType;
+    private final boolean mIsPending;
+    private final boolean mIsTrashed;
+    // TODO(b/227348809): Remove ownerId field when we add the logic to check ownerId from xattr
+    private final int mOwnerId;
+    private final String mOwnerPackageName;
+
+    public FileAccessAttributes(long id, int mediaType, boolean isPending,
+            boolean isTrashed, int ownerId, String ownerPackageName) {
+        this.mId = id;
+        this.mMediaType = mediaType;
+        this.mIsPending = isPending;
+        this.mIsTrashed = isTrashed;
+        this.mOwnerId = ownerId;
+        this.mOwnerPackageName = ownerPackageName;
+    }
+
+    public static FileAccessAttributes fromCursor(Cursor c) {
+        final long id  = c.getLong(0);
+        String ownerPackageName = c.getString(1);
+        final boolean isPending = c.getInt(2) != 0;
+        final int mediaType = c.getInt(3);
+        final boolean isTrashed = c.getInt(4) != 0;
+        return new FileAccessAttributes(id, mediaType, isPending, isTrashed, -1,
+                ownerPackageName);
+    }
+
+    public String toString() {
+        return String.format("Id: %s, Mediatype: %s, isPending: %s, "
+                        + "isTrashed: %s, ownerpackageName: %s", this.mId, this.mMediaType,
+                mIsPending, mIsTrashed, mOwnerId);
+    }
+
+    public long getId() {
+        return this.mId;
+    }
+
+    public int getMediaType() {
+        return this.mMediaType;
+    }
+
+    public int getOwnerId() {
+        return this.mOwnerId;
+    }
+
+    public boolean isTrashed() {
+        return this.mIsTrashed;
+    }
+
+    public boolean isPending() {
+        return this.mIsPending;
+    }
+
+    public String getOwnerPackageName() {
+        return this.mOwnerPackageName;
+    }
+}
diff --git a/src/com/android/providers/media/LocalCallingIdentity.java b/src/com/android/providers/media/LocalCallingIdentity.java
index 6191457..7387b44 100644
--- a/src/com/android/providers/media/LocalCallingIdentity.java
+++ b/src/com/android/providers/media/LocalCallingIdentity.java
@@ -17,7 +17,6 @@
 package com.android.providers.media;
 
 import static android.Manifest.permission.ACCESS_MEDIA_LOCATION;
-import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
 import static android.app.AppOpsManager.MODE_ALLOWED;
 import static android.app.AppOpsManager.permissionToOp;
 import static android.content.pm.PackageManager.PERMISSION_DENIED;
@@ -306,6 +305,7 @@
     }
 
     private boolean hasPermissionInternal(int permission) {
+        boolean targetSdkIsAtLeastT = getTargetSdkVersion() > Build.VERSION_CODES.S_V2;
         // While we're here, enforce any broad user-level restrictions
         if ((uid == Process.SHELL_UID) && context.getSystemService(UserManager.class)
                 .hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER)) {
@@ -338,13 +338,13 @@
 
             case PERMISSION_READ_AUDIO:
                 return checkPermissionReadAudio(
-                        context, pid, uid, getPackageName(), attributionTag);
+                        context, pid, uid, getPackageName(), attributionTag, targetSdkIsAtLeastT);
             case PERMISSION_READ_VIDEO:
                 return checkPermissionReadVideo(
-                        context, pid, uid, getPackageName(), attributionTag);
+                        context, pid, uid, getPackageName(), attributionTag, targetSdkIsAtLeastT);
             case PERMISSION_READ_IMAGES:
                 return checkPermissionReadImages(
-                        context, pid, uid, getPackageName(), attributionTag);
+                        context, pid, uid, getPackageName(), attributionTag, targetSdkIsAtLeastT);
             case PERMISSION_WRITE_AUDIO:
                 return checkPermissionWriteAudio(
                         context, pid, uid, getPackageName(), attributionTag);
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 1093f52..680cde0 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -106,6 +106,7 @@
 import static com.android.providers.media.util.SyntheticPathUtils.isRedactedPath;
 import static com.android.providers.media.util.SyntheticPathUtils.isSyntheticPath;
 
+import android.annotation.IntDef;
 import android.app.AppOpsManager;
 import android.app.AppOpsManager.OnOpActiveChangedListener;
 import android.app.AppOpsManager.OnOpChangedListener;
@@ -220,6 +221,7 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.providers.media.DatabaseHelper.OnFilesChangeListener;
 import com.android.providers.media.DatabaseHelper.OnLegacyMigrationListener;
+import com.android.providers.media.dao.FileRow;
 import com.android.providers.media.fuse.ExternalStorageServiceImpl;
 import com.android.providers.media.fuse.FuseDaemon;
 import com.android.providers.media.metrics.PulledMetrics;
@@ -227,7 +229,6 @@
 import com.android.providers.media.photopicker.PickerSyncController;
 import com.android.providers.media.photopicker.data.ExternalDbFacade;
 import com.android.providers.media.photopicker.data.PickerDbFacade;
-import com.android.providers.media.photopicker.data.model.Category;
 import com.android.providers.media.playlist.Playlist;
 import com.android.providers.media.scan.MediaScanner;
 import com.android.providers.media.scan.ModernMediaScanner;
@@ -241,10 +242,12 @@
 import com.android.providers.media.util.Metrics;
 import com.android.providers.media.util.MimeUtils;
 import com.android.providers.media.util.PermissionUtils;
+import com.android.providers.media.util.Preconditions;
 import com.android.providers.media.util.SQLiteQueryBuilder;
 import com.android.providers.media.util.SpecialFormatDetector;
 import com.android.providers.media.util.StringUtils;
 import com.android.providers.media.util.UserCache;
+import com.android.providers.media.util.XAttrUtils;
 import com.android.providers.media.util.XmpInterface;
 
 import com.google.common.hash.Hashing;
@@ -257,6 +260,8 @@
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.nio.charset.StandardCharsets;
@@ -616,6 +621,7 @@
                     if (pkg != null) {
                         invalidateLocalCallingIdentityCache(pkg, "package " + intent.getAction());
                         if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
+                            mUserCache.invalidateWorkProfileOwnerApps(pkg);
                             mPickerSyncController.notifyPackageRemoval(pkg);
                         }
                     } else {
@@ -724,58 +730,62 @@
      */
     private final OnFilesChangeListener mFilesListener = new OnFilesChangeListener() {
         @Override
-        public void onInsert(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id,
-                int mediaType, boolean isDownload, boolean isPending) {
-            handleInsertedRowForFuse(id);
-            acceptWithExpansion(helper::notifyInsert, volumeName, id, mediaType, isDownload);
-
+        public void onInsert(@NonNull DatabaseHelper helper, @NonNull FileRow insertedRow) {
+            handleInsertedRowForFuse(insertedRow.getId());
+            acceptWithExpansion(helper::notifyInsert, insertedRow.getVolumeName(),
+                    insertedRow.getId(), insertedRow.getMediaType(), insertedRow.isDownload());
+            updateNextRowIdXattr(helper, insertedRow.getId());
             helper.postBackground(() -> {
                 if (helper.isExternal()) {
                     // Update the quota type on the filesystem
-                    Uri fileUri = MediaStore.Files.getContentUri(volumeName, id);
-                    updateQuotaTypeForUri(fileUri, mediaType);
+                    Uri fileUri = MediaStore.Files.getContentUri(insertedRow.getVolumeName(),
+                            insertedRow.getId());
+                    updateQuotaTypeForUri(fileUri, insertedRow.getMediaType());
                 }
 
                 // Tell our SAF provider so it knows when views are no longer empty
-                MediaDocumentsProvider.onMediaStoreInsert(getContext(), volumeName, mediaType, id);
+                MediaDocumentsProvider.onMediaStoreInsert(getContext(), insertedRow.getVolumeName(),
+                        insertedRow.getMediaType(), insertedRow.getId());
 
-                if (mExternalDbFacade.onFileInserted(mediaType, isPending)) {
+                if (mExternalDbFacade.onFileInserted(insertedRow.getMediaType(),
+                        insertedRow.isPending())) {
                     mPickerSyncController.notifyMediaEvent();
                 }
             });
         }
 
         @Override
-        public void onUpdate(@NonNull DatabaseHelper helper, @NonNull String volumeName,
-                long oldId, int oldMediaType, boolean oldIsDownload,
-                long newId, int newMediaType, boolean newIsDownload,
-                boolean oldIsTrashed, boolean newIsTrashed,
-                boolean oldIsPending, boolean newIsPending,
-                boolean oldIsFavorite, boolean newIsFavorite,
-                int oldSpecialFormat, int newSpecialFormat,
-                String oldOwnerPackage, String newOwnerPackage, String oldPath) {
-            final boolean isDownload = oldIsDownload || newIsDownload;
-            final Uri fileUri = MediaStore.Files.getContentUri(volumeName, oldId);
-            handleUpdatedRowForFuse(oldPath, oldOwnerPackage, oldId, newId);
-            handleOwnerPackageNameChange(oldPath, oldOwnerPackage, newOwnerPackage);
-            acceptWithExpansion(helper::notifyUpdate, volumeName, oldId, oldMediaType, isDownload);
-
+        public void onUpdate(@NonNull DatabaseHelper helper, @NonNull FileRow oldRow,
+                @NonNull FileRow newRow) {
+            final boolean isDownload = oldRow.isDownload() || newRow.isDownload();
+            final Uri fileUri = MediaStore.Files.getContentUri(oldRow.getVolumeName(),
+                    oldRow.getId());
+            handleUpdatedRowForFuse(oldRow.getPath(), oldRow.getOwnerPackageName(), oldRow.getId(),
+                    newRow.getId());
+            handleOwnerPackageNameChange(oldRow.getPath(), oldRow.getOwnerPackageName(),
+                    newRow.getOwnerPackageName());
+            acceptWithExpansion(helper::notifyUpdate, oldRow.getVolumeName(), oldRow.getId(),
+                    oldRow.getMediaType(), isDownload);
+            updateNextRowIdXattr(helper, newRow.getId());
             helper.postBackground(() -> {
                 if (helper.isExternal()) {
                     // Update the quota type on the filesystem
-                    updateQuotaTypeForUri(fileUri, newMediaType);
+                    updateQuotaTypeForUri(fileUri, newRow.getMediaType());
                 }
 
-                if (mExternalDbFacade.onFileUpdated(oldId, oldMediaType, newMediaType, oldIsTrashed,
-                                newIsTrashed, oldIsPending, newIsPending, oldIsFavorite,
-                                newIsFavorite, oldSpecialFormat, newSpecialFormat)) {
+                if (mExternalDbFacade.onFileUpdated(oldRow.getId(),
+                        oldRow.getMediaType(), newRow.getMediaType(),
+                        oldRow.isTrashed(), newRow.isTrashed(),
+                        oldRow.isPending(), newRow.isPending(),
+                        oldRow.isFavorite(), newRow.isFavorite(),
+                        oldRow.getSpecialFormat(), newRow.getSpecialFormat())) {
                     mPickerSyncController.notifyMediaEvent();
                 }
             });
 
-            if (newMediaType != oldMediaType) {
-                acceptWithExpansion(helper::notifyUpdate, volumeName, oldId, newMediaType,
-                        isDownload);
+            if (newRow.getMediaType() != oldRow.getMediaType()) {
+                acceptWithExpansion(helper::notifyUpdate, oldRow.getVolumeName(), oldRow.getId(),
+                        newRow.getMediaType(), isDownload);
 
                 helper.postBackground(() -> {
                     // Invalidate any thumbnails when the media type changes
@@ -785,12 +795,13 @@
         }
 
         @Override
-        public void onDelete(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id,
-                int mediaType, boolean isDownload, String ownerPackageName, String path) {
-            handleDeletedRowForFuse(path, ownerPackageName, id);
-            acceptWithExpansion(helper::notifyDelete, volumeName, id, mediaType, isDownload);
+        public void onDelete(@NonNull DatabaseHelper helper, @NonNull FileRow deletedRow) {
+            handleDeletedRowForFuse(deletedRow.getPath(), deletedRow.getOwnerPackageName(),
+                    deletedRow.getId());
+            acceptWithExpansion(helper::notifyDelete, deletedRow.getVolumeName(),
+                    deletedRow.getId(), deletedRow.getMediaType(), deletedRow.isDownload());
             // Remove cached transcoded file if any
-            mTranscodeHelper.deleteCachedTranscodeFile(id);
+            mTranscodeHelper.deleteCachedTranscodeFile(deletedRow.getId());
 
             helper.postBackground(() -> {
                 // Item no longer exists, so revoke all access to it
@@ -798,32 +809,58 @@
                 try {
                     acceptWithExpansion((uri) -> {
                         getContext().revokeUriPermission(uri, ~0);
-                    }, volumeName, id, mediaType, isDownload);
+                    },
+                            deletedRow.getVolumeName(), deletedRow.getId(),
+                            deletedRow.getMediaType(), deletedRow.isDownload());
                 } finally {
                     Trace.endSection();
                 }
 
-                switch (mediaType) {
+                switch (deletedRow.getMediaType()) {
                     case FileColumns.MEDIA_TYPE_PLAYLIST:
                     case FileColumns.MEDIA_TYPE_AUDIO:
                         if (helper.isExternal()) {
-                            removePlaylistMembers(mediaType, id);
+                            removePlaylistMembers(deletedRow.getMediaType(), deletedRow.getId());
                         }
                 }
 
                 // Invalidate any thumbnails now that media is gone
-                invalidateThumbnails(MediaStore.Files.getContentUri(volumeName, id));
+                invalidateThumbnails(MediaStore.Files.getContentUri(deletedRow.getVolumeName(),
+                        deletedRow.getId()));
 
                 // Tell our SAF provider so it can revoke too
-                MediaDocumentsProvider.onMediaStoreDelete(getContext(), volumeName, mediaType, id);
+                MediaDocumentsProvider.onMediaStoreDelete(getContext(), deletedRow.getVolumeName(),
+                        deletedRow.getMediaType(), deletedRow.getId());
 
-                if (mExternalDbFacade.onFileDeleted(id, mediaType)) {
+                if (mExternalDbFacade.onFileDeleted(deletedRow.getId(),
+                        deletedRow.getMediaType())) {
                     mPickerSyncController.notifyMediaEvent();
                 }
             });
         }
     };
 
+    protected void updateNextRowIdXattr(DatabaseHelper helper, long id) {
+        if (!helper.isNextRowIdBackupEnabled()) {
+            Log.v(TAG, "Skipping next row id backup.");
+            return;
+        }
+
+        Optional<Long> nextRowIdBackupOptional = helper.getNextRowId();
+        if (!nextRowIdBackupOptional.isPresent()) {
+            throw new RuntimeException(
+                    String.format(Locale.ROOT, "Cannot find next row id xattr for %s.",
+                            helper.getDatabaseName()));
+        }
+
+        if (id >= nextRowIdBackupOptional.get()) {
+            helper.backupNextRowId(id);
+        } else {
+            Log.v(TAG, String.format(Locale.ROOT, "Inserted id:%d less than next row id backup:%d.",
+                    id, nextRowIdBackupOptional.get()));
+        }
+    }
+
     private final UnaryOperator<String> mIdGenerator = path -> {
         final long rowId = mCallingIdentity.get().getDeletedRowId(path);
         if (rowId != -1 && isFuseThread()) {
@@ -907,11 +944,16 @@
     }
 
     /**
-     * Ensure that default folders are created on mounted primary storage
-     * devices. We only do this once per volume so we don't annoy the user if
-     * deleted manually.
+     * Ensure that default folders are created on mounted storage devices.
+     * We only do this once per volume so we don't annoy the user if deleted
+     * manually.
      */
     private void ensureDefaultFolders(@NonNull MediaVolume volume, @NonNull SQLiteDatabase db) {
+        if (volume.isExternallyManaged()) {
+            // Default folders should not be automatically created inside volumes managed from
+            // outside Android.
+            return;
+        }
         final String volumeName = volume.getName();
         String key;
         if (volumeName.equals(MediaStore.VOLUME_EXTERNAL_PRIMARY)) {
@@ -947,6 +989,12 @@
      * disk, then all thumbnails will be considered stable and will be deleted.
      */
     private void ensureThumbnailsValid(@NonNull MediaVolume volume, @NonNull SQLiteDatabase db) {
+        if (volume.isExternallyManaged()) {
+            // Default folders and thumbnail directories should not be automatically created inside
+            // volumes managed from outside Android, and there is no need to ensure the validity of
+            // their thumbnails here.
+            return;
+        }
         final String uuidFromDatabase = DatabaseHelper.getOrCreateUuid(db);
         try {
             for (File dir : getThumbnailDirectories(volume)) {
@@ -1015,13 +1063,22 @@
 
         mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, false, false,
                 Column.class, ExportedSince.class, Metrics::logSchemaChange, mFilesListener,
-                MIGRATION_LISTENER, mIdGenerator);
+                MIGRATION_LISTENER, mIdGenerator, true);
         mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME, false, false,
                 Column.class, ExportedSince.class, Metrics::logSchemaChange, mFilesListener,
-                MIGRATION_LISTENER, mIdGenerator);
+                MIGRATION_LISTENER, mIdGenerator, true);
         mExternalDbFacade = new ExternalDbFacade(getContext(), mExternalDatabase, mVolumeCache);
         mPickerDbFacade = new PickerDbFacade(context);
-        mPickerSyncController = new PickerSyncController(context, mPickerDbFacade, this);
+
+        final String localPickerProvider = PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
+        final String allowedCloudProviders =
+                getStringDeviceConfig(PickerSyncController.ALLOWED_CLOUD_PROVIDERS_KEY,
+                        /* default */ "");
+        final int pickerSyncDelayMs = getIntDeviceConfig(PickerSyncController.SYNC_DELAY_MS,
+                /* default */ 5000);
+
+        mPickerSyncController = new PickerSyncController(context, mPickerDbFacade,
+                localPickerProvider, allowedCloudProviders, pickerSyncDelayMs);
         mPickerDataLayer = new PickerDataLayer(context, mPickerDbFacade, mPickerSyncController);
         mPickerUriResolver = new PickerUriResolver(context, mPickerDbFacade);
 
@@ -1053,9 +1110,18 @@
                     @Override
                     public void onStateChanged(@NonNull StorageVolume volume) {
                         updateVolumes();
-                   }
+                    }
                 });
 
+        if (SdkLevel.isAtLeastT()) {
+            try {
+                mStorageManager.setCloudMediaProvider(mPickerSyncController.getCloudProvider());
+            } catch (SecurityException e) {
+                // This can happen in unit tests
+                Log.w(TAG, "Failed to update the system_server with the latest cloud provider", e);
+            }
+        }
+
         updateVolumes();
         attachVolume(MediaVolume.fromInternal(), /* validate */ false);
         for (MediaVolume volume : mVolumeCache.getExternalVolumes()) {
@@ -1069,6 +1135,12 @@
 
         mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE,
                 null /* all packages */, mModeListener);
+        mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_AUDIO,
+                null /* all packages */, mModeListener);
+        mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_IMAGES,
+                null /* all packages */, mModeListener);
+        mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_VIDEO,
+                null /* all packages */, mModeListener);
         mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_EXTERNAL_STORAGE,
                 null /* all packages */, mModeListener);
         mAppOpsManager.startWatchingMode(permissionToOp(ACCESS_MEDIA_LOCATION),
@@ -1113,6 +1185,16 @@
         return true;
     }
 
+    Optional<DatabaseHelper> getDatabaseHelper(String dbName) {
+        if (dbName.equalsIgnoreCase(INTERNAL_DATABASE_NAME)) {
+            return Optional.of(mInternalDatabase);
+        } else if (dbName.equalsIgnoreCase(EXTERNAL_DATABASE_NAME)) {
+            return Optional.of(mExternalDatabase);
+        }
+
+        return Optional.empty();
+    }
+
     @Override
     public void onCallingPackageChanged() {
         // Identity of the current thread has changed, so invalidate caches
@@ -2874,9 +2956,8 @@
             final String newMimeType = MimeUtils.resolveMimeType(new File(newPath));
             final String oldMimeType = MimeUtils.resolveMimeType(new File(oldPath));
             final boolean isSameMimeType = newMimeType.equalsIgnoreCase(oldMimeType);
-            final ContentValues contentValues = getContentValuesForFuseRename(newPath, newMimeType,
+            ContentValues contentValues = getContentValuesForFuseRename(newPath, newMimeType,
                     wasHidden, isHidden, isSameMimeType);
-
             if (!updateDatabaseForFuseRename(helper, oldPath, newPath, contentValues)) {
                 if (!bypassRestrictions) {
                     // Check for other URI format grants for oldPath only. Check right before
@@ -6262,9 +6343,9 @@
             case MediaStore.SET_CLOUD_PROVIDER_CALL: {
                 // TODO(b/190713331): Remove after initial development
                 final String cloudProvider = extras.getString(MediaStore.EXTRA_CLOUD_PROVIDER);
-                Log.i(TAG, "Developer initiated cloud provider switch: " + cloudProvider);
-                mPickerSyncController.setCloudProvider(cloudProvider);
-                // fall through
+                Log.i(TAG, "Test initiated cloud provider switch: " + cloudProvider);
+                mPickerSyncController.forceSetCloudProvider(cloudProvider);
+                // fall-through
             }
             case MediaStore.SYNC_PROVIDERS_CALL: {
                 syncAllMedia();
@@ -6300,6 +6381,20 @@
                         notifyCloudEventResult);
                 return bundle;
             }
+            case MediaStore.USES_FUSE_PASSTHROUGH: {
+                boolean isEnabled = false;
+                try {
+                    FuseDaemon daemon = getFuseDaemonForFile(new File(arg));
+                    if (daemon != null) {
+                        isEnabled = daemon.usesFusePassthrough();
+                    }
+                } catch (FileNotFoundException e) {
+                }
+
+                Bundle bundle = new Bundle();
+                bundle.putBoolean(MediaStore.USES_FUSE_PASSTHROUGH_RESULT, isEnabled);
+                return bundle;
+            }
             default:
                 throw new UnsupportedOperationException("Unsupported call: " + method);
         }
@@ -6310,8 +6405,7 @@
         // local_provider while running as MediaProvider
         final long t = Binder.clearCallingIdentity();
         try {
-            // TODO(b/190713331): Remove after initial development
-            Log.v(TAG, "Developer initiated provider sync");
+            Log.v(TAG, "Test initiated cloud provider sync");
             mPickerSyncController.syncAllMedia();
         } finally {
             Binder.restoreCallingIdentity(t);
@@ -7819,7 +7913,15 @@
             // level from #3
             // 5. Return the fd from #4 to the app or throw an exception if any of the conditions
             // are not met
-            return getOriginalMediaFormatFileDescriptor(opts);
+            try {
+                return getOriginalMediaFormatFileDescriptor(opts);
+            } finally {
+                // Clearing the Bundle closes the underlying Parcel, ensuring that the input fd
+                // owned by the Parcel is closed immediately and not at the next GC.
+                // This works around a change in behavior introduced by:
+                // aosp/Icfe8880cad00c3cd2afcbe4b92400ad4579e680e
+                opts.clear();
+            }
         }
 
         // This is needed for thumbnail resolution as it doesn't go through openFileCommon
@@ -8020,8 +8122,14 @@
      */
     Cursor queryForSingleItem(Uri uri, String[] projection, String selection,
             String[] selectionArgs, CancellationSignal signal) throws FileNotFoundException {
-        final Cursor c = query(uri, projection,
-                DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null), signal, true);
+        Cursor c = null;
+        try {
+            c = query(uri, projection,
+                    DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null),
+                    signal, true);
+        } catch (IllegalArgumentException  e) {
+            throw new FileNotFoundException("Volume not found for " + uri);
+        }
         if (c == null) {
             throw new FileNotFoundException("Missing cursor for " + uri);
         } else if (c.getCount() < 1) {
@@ -8741,6 +8849,69 @@
         return !matcher.matches();
     }
 
+    private FileAccessAttributes queryForFileAttributes(final String path)
+            throws FileNotFoundException {
+        Trace.beginSection("queryFileAttr");
+        final Uri contentUri = FileUtils.getContentUriForPath(path);
+        final String[] projection = new String[]{
+                MediaColumns._ID,
+                MediaColumns.OWNER_PACKAGE_NAME,
+                MediaColumns.IS_PENDING,
+                FileColumns.MEDIA_TYPE,
+                MediaColumns.IS_TRASHED
+        };
+        final String selection = MediaColumns.DATA + "=?";
+        final String[] selectionArgs = new String[]{path};
+        FileAccessAttributes fileAccessAttributes;
+        try (final Cursor c = queryForSingleItemAsMediaProvider(contentUri, projection,
+                selection,
+                selectionArgs, null)) {
+            fileAccessAttributes = FileAccessAttributes.fromCursor(c);
+        }
+        Trace.endSection();
+        return fileAccessAttributes;
+    }
+
+    private void checkIfFileOpenIsPermitted(String path,
+            FileAccessAttributes fileAccessAttributes, String redactedUriId,
+            boolean forWrite) throws FileNotFoundException {
+        final File file = new File(path);
+        Uri fileUri = MediaStore.Files.getContentUri(extractVolumeName(path),
+                fileAccessAttributes.getId());
+        // We don't check ownership for files with IS_PENDING set by FUSE
+        // Please note that even if ownerPackageName is null, the check below will throw an
+        // IllegalStateException
+        if (fileAccessAttributes.isTrashed() || (fileAccessAttributes.isPending()
+                && !isPendingFromFuse(new File(path)))) {
+            requireOwnershipForItem(fileAccessAttributes.getOwnerPackageName(), fileUri);
+        }
+
+        // Check that path looks consistent before uri checks
+        if (!FileUtils.contains(Environment.getStorageDirectory(), file)) {
+            checkWorldReadAccess(file.getAbsolutePath());
+        }
+
+        try {
+            // checkAccess throws FileNotFoundException only from checkWorldReadAccess(),
+            // which we already check above. Hence, handling only SecurityException.
+            if (redactedUriId != null) {
+                fileUri = ContentUris.removeId(fileUri).buildUpon().appendPath(
+                        redactedUriId).build();
+            }
+            checkAccess(fileUri, Bundle.EMPTY, file, forWrite);
+        } catch (SecurityException e) {
+            // Check for other Uri formats only when the single uri check flow fails.
+            // Throw the previous exception if the multi-uri checks failed.
+            final String uriId = redactedUriId == null
+                    ? Long.toString(fileAccessAttributes.getId()) : redactedUriId;
+            if (getOtherUriGrantsForPath(path, fileAccessAttributes.getMediaType(),
+                    uriId, forWrite) == null) {
+                throw e;
+            }
+        }
+    }
+
+
     /**
      * Checks if the app identified by the given UID is allowed to open the given file for the given
      * access mode.
@@ -8824,59 +8995,23 @@
                 return new FileOpenResult(OsConstants.EACCES /* status */, originalUid,
                         mediaCapabilitiesUid, new long[0]);
             }
-
-            final Uri contentUri = FileUtils.getContentUriForPath(path);
-            final String[] projection = new String[]{
-                    MediaColumns._ID,
-                    MediaColumns.OWNER_PACKAGE_NAME,
-                    MediaColumns.IS_PENDING,
-                    FileColumns.MEDIA_TYPE,
-                    MediaColumns.IS_TRASHED
-            };
-            final String selection = MediaColumns.DATA + "=?";
-            final String[] selectionArgs = new String[]{path};
-            final long id;
-            final int mediaType;
-            final boolean isPending;
-            final boolean isTrashed;
-            String ownerPackageName = null;
-            try (final Cursor c = queryForSingleItemAsMediaProvider(contentUri, projection,
-                    selection,
-                    selectionArgs, null)) {
-                id = c.getLong(0);
-                ownerPackageName = c.getString(1);
-                isPending = c.getInt(2) != 0;
-                mediaType = c.getInt(3);
-                isTrashed = c.getInt(4) != 0;
-            }
-            final File file = new File(path);
-            Uri fileUri = MediaStore.Files.getContentUri(extractVolumeName(path), id);
-            // We don't check ownership for files with IS_PENDING set by FUSE
-            if (isTrashed || (isPending && !isPendingFromFuse(new File(path)))) {
-                requireOwnershipForItem(ownerPackageName, fileUri);
-            }
-
-            // Check that path looks consistent before uri checks
-            if (!FileUtils.contains(Environment.getStorageDirectory(), file)) {
-                checkWorldReadAccess(file.getAbsolutePath());
-            }
-
-            try {
-                // checkAccess throws FileNotFoundException only from checkWorldReadAccess(),
-                // which we already check above. Hence, handling only SecurityException.
-                if (redactedUriId != null) {
-                    fileUri = ContentUris.removeId(fileUri).buildUpon().appendPath(
-                            redactedUriId).build();
-                }
-                checkAccess(fileUri, Bundle.EMPTY, file, forWrite);
-            } catch (SecurityException e) {
-                // Check for other Uri formats only when the single uri check flow fails.
-                // Throw the previous exception if the multi-uri checks failed.
-                final String uriId = redactedUriId == null ? Long.toString(id) : redactedUriId;
-                if (getOtherUriGrantsForPath(path, mediaType, uriId, forWrite) == null) {
-                    throw e;
+            // TODO: Fetch owner id from Android/media directory and check if caller is owner
+            FileAccessAttributes fileAttributes = null;
+            if (XAttrUtils.ENABLE_XATTR_METADATA_FOR_FUSE) {
+                Optional<FileAccessAttributes> fileAttributesThroughXattr =
+                        XAttrUtils.getFileAttributesFromXAttr(path,
+                                XAttrUtils.FILE_ACCESS_XATTR_KEY);
+                if (fileAttributesThroughXattr.isPresent()) {
+                    fileAttributes = fileAttributesThroughXattr.get();
                 }
             }
+
+            // FileAttributes will be null if the xattr call failed or the flag to enable xattr
+            // metadata support is not set
+            if (fileAttributes == null)  {
+                fileAttributes = queryForFileAttributes(path);
+            }
+            checkIfFileOpenIsPermitted(path, fileAttributes, redactedUriId, forWrite);
             isSuccess = true;
             return new FileOpenResult(0 /* status */, originalUid, mediaCapabilitiesUid,
                     redact ? getRedactionRangesForFuse(path, ioPath, originalUid, uid, tid,
@@ -9268,80 +9403,52 @@
         }
     }
 
-    /**
-     * Checks if the app with the given UID is allowed to create or delete the directory with the
-     * given path.
-     *
-     * @param path File path of the directory that the app wants to create/delete
-     * @param uid UID of the app that wants to create/delete the directory
-     * @param forCreate denotes whether the operation is directory creation or deletion
-     * @return 0 if the operation is allowed, or the following {@code errno} values:
-     * <ul>
-     * <li>{@link OsConstants#EACCES} if the app tries to create/delete a dir in another app's
-     * external directory, or if the calling package is a legacy app that doesn't have
-     * WRITE_EXTERNAL_STORAGE permission.
-     * <li>{@link OsConstants#EPERM} if the app tries to create/delete a top-level directory.
-     * </ul>
-     *
-     * Called from JNI in jni/MediaProviderWrapper.cpp
-     */
-    @Keep
-    public int isDirectoryCreationOrDeletionAllowedForFuse(
-            @NonNull String path, int uid, boolean forCreate) {
-        final LocalCallingIdentity token =
-                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
-        PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
+    // These need to stay in sync with MediaProviderWrapper.cpp's DirectoryAccessRequestType enum
+    @IntDef(flag = true, prefix = { "DIRECTORY_ACCESS_FOR_" }, value = {
+            DIRECTORY_ACCESS_FOR_READ,
+            DIRECTORY_ACCESS_FOR_WRITE,
+            DIRECTORY_ACCESS_FOR_CREATE,
+            DIRECTORY_ACCESS_FOR_DELETE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @VisibleForTesting
+    @interface DirectoryAccessType {}
 
-        try {
-            // App dirs are not indexed, so we don't create an entry for the file.
-            if (isPrivatePackagePathNotAccessibleByCaller(path)) {
-                Log.e(TAG, "Can't modify another app's external directory!");
-                return OsConstants.EACCES;
-            }
+    @VisibleForTesting
+    static final int DIRECTORY_ACCESS_FOR_READ = 1;
 
-            if (shouldBypassFuseRestrictions(/*forWrite*/ true, path)) {
-                return 0;
-            }
-            // Legacy apps that made is this far don't have the right storage permission and hence
-            // are not allowed to access anything other than their external app directory
-            if (isCallingPackageRequestingLegacy()) {
-                return OsConstants.EACCES;
-            }
+    @VisibleForTesting
+    static final int DIRECTORY_ACCESS_FOR_WRITE = 2;
 
-            final String[] relativePath = sanitizePath(extractRelativePath(path));
-            final boolean isTopLevelDir =
-                    relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]);
-            if (isTopLevelDir) {
-                // We allow creating the default top level directories only, all other operations on
-                // top level directories are not allowed.
-                if (forCreate && FileUtils.isDefaultDirectoryName(extractDisplayName(path))) {
-                    return 0;
-                }
-                Log.e(TAG,
-                        "Creating a non-default top level directory or deleting an existing"
-                                + " one is not allowed!");
-                return OsConstants.EPERM;
-            }
-            return 0;
-        } finally {
-            restoreLocalCallingIdentity(token);
-        }
-    }
+    @VisibleForTesting
+    static final int DIRECTORY_ACCESS_FOR_CREATE = 3;
+
+    @VisibleForTesting
+    static final int DIRECTORY_ACCESS_FOR_DELETE = 4;
 
     /**
-     * Checks whether the app with the given UID is allowed to open the directory denoted by the
+     * Checks whether the app with the given UID is allowed to access the directory denoted by the
      * given path.
      *
      * @param path directory's path
      * @param uid UID of the requesting app
-     * @return 0 if it's allowed to open the diretory, {@link OsConstants#EACCES} if the calling
-     * package is a legacy app that doesn't have READ_EXTERNAL_STORAGE permission,
-     * {@link OsConstants#ENOENT}  otherwise.
+     * @param accessType type of access being requested - eg {@link
+     * MediaProvider#DIRECTORY_ACCESS_FOR_READ}
+     * @return 0 if it's allowed to access the directory, {@link OsConstants#ENOENT} for attempts
+     * to access a private package path in Android/data or Android/obb the caller doesn't have
+     * access to, and otherwise {@link OsConstants#EACCES} if the calling package is a legacy app
+     * that doesn't have READ_EXTERNAL_STORAGE permission or for other invalid attempts to access
+     * Android/data or Android/obb dirs.
      *
      * Called from JNI in jni/MediaProviderWrapper.cpp
      */
     @Keep
-    public int isOpendirAllowedForFuse(@NonNull String path, int uid, boolean forWrite) {
+    public int isDirAccessAllowedForFuse(@NonNull String path, int uid,
+            @DirectoryAccessType int accessType) {
+        Preconditions.checkArgumentInRange(accessType, 1, DIRECTORY_ACCESS_FOR_DELETE,
+                "accessType");
+
+        final boolean forRead = accessType == DIRECTORY_ACCESS_FOR_READ;
         final LocalCallingIdentity token =
                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
         PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
@@ -9354,14 +9461,16 @@
                 return OsConstants.ENOENT;
             }
 
-            if (shouldBypassFuseRestrictions(forWrite, path)) {
+            if (shouldBypassFuseRestrictions(/* forWrite= */ !forRead, path)) {
                 return 0;
             }
 
-            // Do not allow apps to open Android/data or Android/obb dirs.
-            // On primary volumes, apps that get special access to these directories get it via
-            // mount views of lowerfs. On secondary volumes, such apps would return early from
-            // shouldBypassFuseRestrictions above.
+            // Do not allow apps that reach this point to access Android/data or Android/obb dirs.
+            // Creation should be via getContext().getExternalFilesDir() etc methods.
+            // Reads and writes on primary volumes should be via mount views of lowerfs for apps
+            // that get special access to these directories.
+            // Reads and writes on secondary volumes would be provided via an early return from
+            // shouldBypassFuseRestrictions above (again just for apps with special access).
             if (isDataOrObbPath(path)) {
                 return OsConstants.EACCES;
             }
@@ -9373,22 +9482,34 @@
             }
             // This is a non-legacy app. Rest of the directories are generally writable
             // except for non-default top-level directories.
-            if (forWrite) {
+            if (!forRead) {
                 final String[] relativePath = sanitizePath(extractRelativePath(path));
                 if (relativePath.length == 0) {
-                    Log.e(TAG, "Directoy write not allowed on invalid relative path for " + path);
+                    Log.e(TAG,
+                            "Directory update not allowed on invalid relative path for " + path);
                     return OsConstants.EPERM;
                 }
                 final boolean isTopLevelDir =
                         relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]);
                 if (isTopLevelDir) {
-                    if (FileUtils.isDefaultDirectoryName(extractDisplayName(path))) {
-                        return 0;
-                    } else {
-                        Log.e(TAG,
-                                "Writing to a non-default top level directory is not allowed!");
+                    // We don't allow deletion of any top-level folders
+                    if (accessType == DIRECTORY_ACCESS_FOR_DELETE) {
+                        Log.e(TAG, "Deleting top level directories are not allowed!");
                         return OsConstants.EACCES;
                     }
+
+                    // We allow creating or writing to default top-level folders, but we don't
+                    // allow creation or writing to non-default top-level folders.
+                    if ((accessType == DIRECTORY_ACCESS_FOR_CREATE
+                            || accessType == DIRECTORY_ACCESS_FOR_WRITE)
+                            && FileUtils.isDefaultDirectoryName(extractDisplayName(path))) {
+                        return 0;
+                    }
+
+                    Log.e(TAG,
+                            "Creating or writing to a non-default top level directory is not "
+                                    + "allowed!");
+                    return OsConstants.EACCES;
                 }
             }
 
@@ -9999,6 +10120,12 @@
         return mTranscodeHelper.getSupportedRelativePaths();
     }
 
+    public List<String> getSupportedUncachedRelativePaths() {
+        return StringUtils.verifySupportedUncachedRelativePaths(
+                       StringUtils.getStringArrayConfig(getContext(),
+                               R.array.config_supported_uncached_relative_paths));
+    }
+
     /**
      * Creating a new method for Transcoding to avoid any merge conflicts.
      * TODO(b/170465810): Remove this when the code is refactored.
diff --git a/src/com/android/providers/media/MediaService.java b/src/com/android/providers/media/MediaService.java
index f406a59..cf154ab 100644
--- a/src/com/android/providers/media/MediaService.java
+++ b/src/com/android/providers/media/MediaService.java
@@ -27,7 +27,6 @@
 import android.content.Intent;
 import android.media.RingtoneManager;
 import android.net.Uri;
-import android.os.Bundle;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.storage.StorageVolume;
@@ -151,6 +150,16 @@
     public static void onScanVolume(Context context, MediaVolume volume, int reason)
             throws IOException {
         final String volumeName = volume.getName();
+        if (!MediaStore.VOLUME_INTERNAL.equals(volumeName) && volume.getPath() == null) {
+            /* This is a very unexpected state and can only ever happen with app-cloned users.
+              In general, MediaVolumes should always be mounted and have a path, however, if the
+              user failed to unlock properly, MediaProvider still gets the volume from the
+              StorageManagerService because MediaProvider is special cased there. See
+              StorageManagerService#getVolumeList. Reference bug: b/207723670. */
+            Log.w(TAG, String.format("Skipping volume scan for %s when volume path is null.",
+                    volumeName));
+            return;
+        }
         UserHandle owner = volume.getUser();
         if (owner == null) {
             // Can happen for the internal volume
diff --git a/src/com/android/providers/media/MediaUpgradeReceiver.java b/src/com/android/providers/media/MediaUpgradeReceiver.java
index 983892a..38cdc75 100644
--- a/src/com/android/providers/media/MediaUpgradeReceiver.java
+++ b/src/com/android/providers/media/MediaUpgradeReceiver.java
@@ -17,17 +17,17 @@
 package com.android.providers.media;
 
 import android.content.BroadcastReceiver;
+import android.content.ContentProviderClient;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
-import android.provider.Column;
-import android.provider.ExportedSince;
+import android.provider.MediaStore;
 import android.util.Log;
 
 import com.android.providers.media.util.ForegroundThread;
-import com.android.providers.media.util.Metrics;
 
 import java.io.File;
+import java.util.Optional;
 
 /**
  * This will be launched during system boot, after the core system has
@@ -68,26 +68,25 @@
             File dbDir = context.getDatabasePath("foo").getParentFile();
             String[] files = dbDir.list();
             if (files == null) return;
-            for (int i=0; i<files.length; i++) {
+
+            MediaProvider mediaProvider = getMediaProvider(context);
+            for (int i = 0; i < files.length; i++) {
                 String file = files[i];
-                if (MediaProvider.isMediaDatabaseName(file)) {
+                Optional<DatabaseHelper> helper = mediaProvider.getDatabaseHelper(file);
+                if (helper.isPresent()) {
                     long startTime = System.currentTimeMillis();
                     Log.i(TAG, "---> Start upgrade of media database " + file);
                     try {
-                        DatabaseHelper helper = new DatabaseHelper(context, file, false, false,
-                                Column.class, ExportedSince.class, Metrics::logSchemaChange, null,
-                                MediaProvider.MIGRATION_LISTENER, null);
-                        helper.runWithTransaction((db) -> {
+                        helper.get().runWithTransaction((db) -> {
                             // Perform just enough to force database upgrade
                             return db.getVersion();
                         });
-                        helper.close();
                     } catch (Throwable t) {
                         Log.wtf(TAG, "Error during upgrade of media db " + file, t);
-                    } finally {
                     }
+
                     Log.i(TAG, "<--- Finished upgrade of media database " + file
-                            + " in " + (System.currentTimeMillis()-startTime) + "ms");
+                            + " in " + (System.currentTimeMillis() - startTime) + "ms");
                 }
             }
         } catch (Throwable t) {
@@ -95,4 +94,14 @@
             Log.wtf(TAG, "Error during upgrade attempt.", t);
         }
     }
+
+    private MediaProvider getMediaProvider(Context context) {
+        try (ContentProviderClient cpc =
+                     context.getContentResolver().acquireContentProviderClient(
+                             MediaStore.AUTHORITY)) {
+            return (MediaProvider) cpc.getLocalContentProvider();
+        } catch (Exception e) {
+            throw new IllegalStateException("Failed to acquire MediaProvider", e);
+        }
+    }
 }
diff --git a/src/com/android/providers/media/MediaVolume.java b/src/com/android/providers/media/MediaVolume.java
index 3a2d792..4934365 100644
--- a/src/com/android/providers/media/MediaVolume.java
+++ b/src/com/android/providers/media/MediaVolume.java
@@ -153,7 +153,8 @@
         UserHandle user = storageVolume.getOwner();
         File path = storageVolume.getDirectory();
         String id = storageVolume.getId();
-        boolean externallyManaged = false;
+        boolean externallyManaged =
+                SdkLevel.isAtLeastT() ? storageVolume.isExternallyManaged() : false;
         boolean publicVolume = !externallyManaged && !storageVolume.isPrimary();
         return new MediaVolume(name, user, path, id, externallyManaged, publicVolume);
     }
diff --git a/src/com/android/providers/media/PermissionActivity.java b/src/com/android/providers/media/PermissionActivity.java
index d50c574..a79890a 100644
--- a/src/com/android/providers/media/PermissionActivity.java
+++ b/src/com/android/providers/media/PermissionActivity.java
@@ -17,6 +17,8 @@
 package com.android.providers.media;
 
 import static com.android.providers.media.MediaProvider.AUDIO_MEDIA_ID;
+import static com.android.providers.media.MediaProvider.AUDIO_PLAYLISTS_ID;
+import static com.android.providers.media.MediaProvider.FILES_ID;
 import static com.android.providers.media.MediaProvider.IMAGES_MEDIA_ID;
 import static com.android.providers.media.MediaProvider.VIDEO_MEDIA_ID;
 import static com.android.providers.media.MediaProvider.collectUris;
@@ -25,7 +27,10 @@
 import static com.android.providers.media.util.PermissionUtils.checkPermissionAccessMediaLocation;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionManageMedia;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionManager;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadAudio;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadImages;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionReadStorage;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadVideo;
 
 import android.app.Activity;
 import android.app.AlertDialog;
@@ -47,9 +52,9 @@
 import android.graphics.ImageDecoder.Source;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Icon;
-import android.icu.text.MessageFormat;
 import android.net.Uri;
 import android.os.AsyncTask;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.provider.MediaStore;
@@ -71,6 +76,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.modules.utils.build.SdkLevel;
 import com.android.providers.media.MediaProvider.LocalUriMatcher;
 import com.android.providers.media.util.Metrics;
 import com.android.providers.media.util.StringUtils;
@@ -78,10 +84,7 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Comparator;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Locale;
-import java.util.Map;
 import java.util.Objects;
 import java.util.function.Predicate;
 import java.util.function.ToIntFunction;
@@ -122,6 +125,13 @@
         progressDialog.show();
     };
 
+    private boolean mShouldCheckReadAudio;
+    private boolean mShouldCheckReadAudioOrReadVideo;
+    private boolean mShouldCheckReadImages;
+    private boolean mShouldCheckReadVideo;
+    private boolean mShouldCheckMediaPermissions;
+    private boolean mShouldForceShowingDialog;
+
     private static final Long LEAST_SHOW_PROGRESS_TIME_MS = 300L;
     private static final Long BEFORE_SHOW_PROGRESS_TIME_MS = 300L;
 
@@ -164,7 +174,7 @@
         getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
         getWindow().setDimAmount(0.0f);
 
-
+        boolean isTargetSdkAtLeastT = false;
         // All untrusted input values here were validated when generating the
         // original PendingIntent
         try {
@@ -174,6 +184,8 @@
             appInfo = resolveCallingAppInfo();
             label = resolveAppLabel(appInfo);
             verb = resolveVerb();
+            isTargetSdkAtLeastT = appInfo.targetSdkVersion > Build.VERSION_CODES.S_V2;
+            mShouldCheckMediaPermissions = isTargetSdkAtLeastT && SdkLevel.isAtLeastT();
             data = resolveData();
             volumeName = MediaStore.getVolumeName(uris.get(0));
         } catch (Exception e) {
@@ -186,8 +198,23 @@
         // Create Progress dialog
         createProgressDialog();
 
-        if (!shouldShowActionDialog(this, -1 /* pid */, appInfo.uid, getCallingPackage(),
-                null /* attributionTag */, verb)) {
+        final boolean shouldShowActionDialog;
+        if (mShouldCheckMediaPermissions) {
+            if (mShouldForceShowingDialog) {
+                shouldShowActionDialog = true;
+            } else {
+                shouldShowActionDialog = shouldShowActionDialog(this, -1 /* pid */, appInfo.uid,
+                        getCallingPackage(), null /* attributionTag */, verb,
+                        mShouldCheckMediaPermissions, mShouldCheckReadAudio, mShouldCheckReadImages,
+                        mShouldCheckReadVideo, mShouldCheckReadAudioOrReadVideo,
+                        isTargetSdkAtLeastT);
+            }
+        } else {
+            shouldShowActionDialog = shouldShowActionDialog(this, -1 /* pid */, appInfo.uid,
+                    getCallingPackage(), null /* attributionTag */, verb);
+        }
+
+        if (!shouldShowActionDialog) {
             onPositiveAction(null, 0);
             return;
         }
@@ -382,6 +409,18 @@
     @VisibleForTesting
     static boolean shouldShowActionDialog(@NonNull Context context, int pid, int uid,
             @NonNull String packageName, @Nullable String attributionTag, @NonNull String verb) {
+        return shouldShowActionDialog(context, pid, uid, packageName, attributionTag,
+                verb, /* shouldCheckMediaPermissions */ false, /* shouldCheckReadAudio */ false,
+                /* shouldCheckReadImages */ false, /* shouldCheckReadVideo */ false,
+                /* mShouldCheckReadAudioOrReadVideo */ false, /* isTargetSdkAtLeastT */ false);
+    }
+
+    @VisibleForTesting
+    static boolean shouldShowActionDialog(@NonNull Context context, int pid, int uid,
+            @NonNull String packageName, @Nullable String attributionTag, @NonNull String verb,
+            boolean shouldCheckMediaPermissions, boolean shouldCheckReadAudio,
+            boolean shouldCheckReadImages, boolean shouldCheckReadVideo,
+            boolean mShouldCheckReadAudioOrReadVideo, boolean isTargetSdkAtLeastT) {
         // Favorite-related requests are automatically granted for now; we still
         // make developers go through this no-op dialog flow to preserve our
         // ability to start prompting in the future
@@ -389,12 +428,49 @@
             return false;
         }
 
-        // check READ_EXTERNAL_STORAGE and MANAGE_EXTERNAL_STORAGE permissions
-        if (!checkPermissionReadStorage(context, pid, uid, packageName, attributionTag)
-                && !checkPermissionManager(context, pid, uid, packageName, attributionTag)) {
-            Log.d(TAG, "No permission READ_EXTERNAL_STORAGE or MANAGE_EXTERNAL_STORAGE");
-            return true;
+        // no MANAGE_EXTERNAL_STORAGE permission
+        if (!checkPermissionManager(context, pid, uid, packageName, attributionTag)) {
+            if (shouldCheckMediaPermissions) {
+                // check READ_MEDIA_AUDIO
+                if (shouldCheckReadAudio && !checkPermissionReadAudio(context, pid, uid,
+                        packageName, attributionTag, isTargetSdkAtLeastT)) {
+                    Log.d(TAG, "No permission READ_MEDIA_AUDIO or MANAGE_EXTERNAL_STORAGE");
+                    return true;
+                }
+
+                // check READ_MEDIA_IMAGES
+                if (shouldCheckReadImages && !checkPermissionReadImages(context, pid, uid,
+                        packageName, attributionTag, isTargetSdkAtLeastT)) {
+                    Log.d(TAG, "No permission READ_MEDIA_IMAGES or MANAGE_EXTERNAL_STORAGE");
+                    return true;
+                }
+
+                // check READ_MEDIA_VIDEO
+                if (shouldCheckReadVideo && !checkPermissionReadVideo(context, pid, uid,
+                        packageName, attributionTag, isTargetSdkAtLeastT)) {
+                    Log.d(TAG, "No permission READ_MEDIA_VIDEO or MANAGE_EXTERNAL_STORAGE");
+                    return true;
+                }
+
+                // For subtitle case, check READ_MEDIA_AUDIO or READ_MEDIA_VIDEO
+                if (mShouldCheckReadAudioOrReadVideo
+                        && !checkPermissionReadAudio(context, pid, uid, packageName, attributionTag,
+                        isTargetSdkAtLeastT)
+                        && !checkPermissionReadVideo(context, pid, uid, packageName, attributionTag,
+                        isTargetSdkAtLeastT)) {
+                    Log.d(TAG, "No permission READ_MEDIA_AUDIO, READ_MEDIA_VIDEO or "
+                            + "MANAGE_EXTERNAL_STORAGE");
+                    return true;
+                }
+            } else {
+                // check READ_EXTERNAL_STORAGE
+                if (!checkPermissionReadStorage(context, pid, uid, packageName, attributionTag)) {
+                    Log.d(TAG, "No permission READ_EXTERNAL_STORAGE or MANAGE_EXTERNAL_STORAGE");
+                    return true;
+                }
+            }
         }
+
         // check MANAGE_MEDIA permission
         if (!checkPermissionManageMedia(context, pid, uid, packageName, attributionTag)) {
             Log.d(TAG, "No permission MANAGE_MEDIA");
@@ -490,13 +566,41 @@
     private @NonNull String resolveData() {
         final LocalUriMatcher matcher = new LocalUriMatcher(MediaStore.AUTHORITY);
         final int firstMatch = matcher.matchUri(uris.get(0), false);
+        parseDataToCheckPermissions(firstMatch);
+        boolean isMixedTypes = false;
+
         for (int i = 1; i < uris.size(); i++) {
             final int match = matcher.matchUri(uris.get(i), false);
             if (match != firstMatch) {
+                // If we don't need to check new permission, we can return DATA_GENERIC here. We
+                // don't need to resolve the other uris.
+                if (!mShouldCheckMediaPermissions) {
+                    return DATA_GENERIC;
+                }
                 // Any mismatch means we need to use generic strings
-                return DATA_GENERIC;
+                isMixedTypes = true;
+            }
+
+            parseDataToCheckPermissions(match);
+
+            if (isMixedTypes && mShouldForceShowingDialog) {
+                // Already know the data is mixed types and should force showing dialog. Don't need
+                // to resolve the other uris.
+                break;
+            }
+
+            if (mShouldCheckReadAudio && mShouldCheckReadImages && mShouldCheckReadVideo
+                    && mShouldCheckReadAudioOrReadVideo) {
+                // Already need to check all permissions for the mixed types. Don't need to resolve
+                // the other uris.
+                break;
             }
         }
+
+        if (isMixedTypes) {
+            return DATA_GENERIC;
+        }
+
         switch (firstMatch) {
             case AUDIO_MEDIA_ID: return DATA_AUDIO;
             case VIDEO_MEDIA_ID: return DATA_VIDEO;
@@ -505,6 +609,31 @@
         }
     }
 
+    private void parseDataToCheckPermissions(int match) {
+        switch (match) {
+            case AUDIO_MEDIA_ID:
+            case AUDIO_PLAYLISTS_ID:
+                mShouldCheckReadAudio = true;
+                break;
+            case VIDEO_MEDIA_ID:
+                mShouldCheckReadVideo = true;
+                break;
+            case IMAGES_MEDIA_ID:
+                mShouldCheckReadImages = true;
+                break;
+            case FILES_ID:
+                //  PermissionActivity is not exported. And we have a check in
+                //  MediaProvider#createRequest method. If it matches FILES_ID, it is subtitle case.
+                //  Check audio or video for it.
+                mShouldCheckReadAudioOrReadVideo = true;
+                break;
+            default:
+                // It is not the expected case. Force showing the dialog
+                mShouldForceShowingDialog = true;
+
+        }
+    }
+
     /**
      * Resolve the dialog title string to be displayed to the user. All
      * arguments have been bound and this string is ready to be displayed.
diff --git a/src/com/android/providers/media/TranscodeHelperImpl.java b/src/com/android/providers/media/TranscodeHelperImpl.java
index b1f0502..09e132f 100644
--- a/src/com/android/providers/media/TranscodeHelperImpl.java
+++ b/src/com/android/providers/media/TranscodeHelperImpl.java
@@ -50,8 +50,6 @@
 import android.content.pm.InstallSourceInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.Property;
-import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
 import android.content.res.XmlResourceParser;
 import android.database.Cursor;
 import android.media.ApplicationMediaCapabilities;
@@ -99,6 +97,7 @@
 import com.android.providers.media.util.FileUtils;
 import com.android.providers.media.util.ForegroundThread;
 import com.android.providers.media.util.SQLiteQueryBuilder;
+import com.android.providers.media.util.StringUtils;
 
 import java.io.BufferedReader;
 import java.io.File;
@@ -113,7 +112,6 @@
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
@@ -286,8 +284,8 @@
                         MAX_TRANSCODE_DURATION_MS);
         mTranscodeDenialController = new TranscodeDenialController(mActivityManager,
                 mTranscodingUiNotifier, maxTranscodeDurationMs);
-        mSupportedRelativePaths = verifySupportedRelativePaths(getStringArrayConfig(
-                        R.array.config_supported_transcoding_relative_paths));
+        mSupportedRelativePaths = verifySupportedRelativePaths(StringUtils.getStringArrayConfig(
+                        mContext, R.array.config_supported_transcoding_relative_paths));
         mHasHdrPlugin = hasHDRPlugin();
 
         parseTranscodeCompatManifest();
@@ -889,16 +887,6 @@
         return verifiedPaths;
     }
 
-    private List<String> getStringArrayConfig(int resId) {
-        final Resources res = mContext.getResources();
-        try {
-            final String[] configValue = res.getStringArray(resId);
-            return Arrays.asList(configValue);
-        } catch (NotFoundException e) {
-            return new ArrayList<String>();
-        }
-    }
-
     private Optional<Boolean> checkAppCompatSupport(int uid, int fileFlags) {
         int supportedFlags = 0;
         int unsupportedFlags = 0;
diff --git a/src/com/android/providers/media/dao/FileRow.java b/src/com/android/providers/media/dao/FileRow.java
new file mode 100644
index 0000000..3410c0c
--- /dev/null
+++ b/src/com/android/providers/media/dao/FileRow.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2022 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.dao;
+
+/** DAO object representing database row of Files table of a MediaProvider database. */
+public class FileRow {
+
+    private final long mId;
+    private String mPath;
+    private String mOwnerPackageName;
+    private String mVolumeName;
+    private int mMediaType;
+    private boolean mIsDownload;
+    private boolean mIsPending;
+    private boolean mIsTrashed;
+    private boolean mIsFavorite;
+    private int mSpecialFormat;
+
+    public static class Builder {
+        private final long mId;
+        private String mPath;
+        private String mOwnerPackageName;
+        private String mVolumeName;
+        private int mMediaType;
+        private boolean mIsDownload;
+        private boolean mIsPending;
+        private boolean mIsTrashed;
+        private boolean mIsFavorite;
+        private int mSpecialFormat;
+
+        Builder(long id) {
+            this.mId = id;
+        }
+
+        public Builder setPath(String path) {
+            this.mPath = path;
+            return this;
+        }
+
+        public Builder setOwnerPackageName(String ownerPackageName) {
+            this.mOwnerPackageName = ownerPackageName;
+            return this;
+        }
+
+        public Builder setVolumeName(String volumeName) {
+            this.mVolumeName = volumeName;
+            return this;
+        }
+
+        public Builder setMediaType(int mediaType) {
+            this.mMediaType = mediaType;
+            return this;
+        }
+
+        public Builder setIsDownload(boolean download) {
+            mIsDownload = download;
+            return this;
+        }
+
+        public Builder setIsPending(boolean pending) {
+            mIsPending = pending;
+            return this;
+        }
+
+        public Builder setIsTrashed(boolean trashed) {
+            mIsTrashed = trashed;
+            return this;
+        }
+
+        public Builder setIsFavorite(boolean favorite) {
+            mIsFavorite = favorite;
+            return this;
+        }
+
+        public Builder setSpecialFormat(int specialFormat) {
+            this.mSpecialFormat = specialFormat;
+            return this;
+        }
+
+        public FileRow build() {
+            FileRow fileRow = new FileRow(this.mId);
+            fileRow.mPath = this.mPath;
+            fileRow.mOwnerPackageName = this.mOwnerPackageName;
+            fileRow.mVolumeName = this.mVolumeName;
+            fileRow.mMediaType = this.mMediaType;
+            fileRow.mIsDownload = this.mIsDownload;
+            fileRow.mIsPending = this.mIsPending;
+            fileRow.mIsTrashed = this.mIsTrashed;
+            fileRow.mIsFavorite = this.mIsFavorite;
+            fileRow.mSpecialFormat = this.mSpecialFormat;
+
+            return fileRow;
+        }
+    }
+
+    public static Builder newBuilder(long id) {
+        return new Builder(id);
+    }
+
+    private FileRow(long id) {
+        this.mId = id;
+    }
+
+    public long getId() {
+        return mId;
+    }
+
+    public String getPath() {
+        return mPath;
+    }
+
+    public String getOwnerPackageName() {
+        return mOwnerPackageName;
+    }
+
+    public String getVolumeName() {
+        return mVolumeName;
+    }
+
+    public int getMediaType() {
+        return mMediaType;
+    }
+
+    public boolean isDownload() {
+        return mIsDownload;
+    }
+
+    public boolean isPending() {
+        return mIsPending;
+    }
+
+    public boolean isTrashed() {
+        return mIsTrashed;
+    }
+
+    public boolean isFavorite() {
+        return mIsFavorite;
+    }
+
+    public int getSpecialFormat() {
+        return mSpecialFormat;
+    }
+}
diff --git a/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java b/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java
index c82cfc0..84f4205 100644
--- a/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java
+++ b/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java
@@ -23,6 +23,7 @@
 import android.os.Environment;
 import android.os.OperationCanceledException;
 import android.os.ParcelFileDescriptor;
+import android.os.storage.StorageManager;
 import android.os.storage.StorageVolume;
 import android.provider.MediaStore;
 import android.service.storage.ExternalStorageService;
@@ -31,6 +32,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.modules.utils.build.SdkLevel;
 import com.android.providers.media.MediaProvider;
 import com.android.providers.media.MediaService;
 import com.android.providers.media.MediaVolume;
@@ -64,6 +66,17 @@
 
         MediaProvider mediaProvider = getMediaProvider();
 
+        boolean uncachedMode = false;
+        if (SdkLevel.isAtLeastT()) {
+            StorageVolume vol =
+                    getSystemService(StorageManager.class).getStorageVolume(upperFileSystemPath);
+            if (vol != null && vol.isExternallyManaged()) {
+                // Cache should be disabled when the volume is externally managed.
+                Log.i(TAG, "Disabling cache for externally managed volume " + upperFileSystemPath);
+                uncachedMode = true;
+            }
+        }
+
         synchronized (sLock) {
             if (sFuseDaemons.containsKey(sessionId)) {
                 Log.w(TAG, "Session already started with id: " + sessionId);
@@ -74,8 +87,11 @@
                 // mounts of the lower filesystem.
                 final String[] supportedTranscodingRelativePaths =
                         mediaProvider.getSupportedTranscodingRelativePaths().toArray(new String[0]);
+                final String[] supportedUncachedRelativePaths =
+                        mediaProvider.getSupportedUncachedRelativePaths().toArray(new String[0]);
                 FuseDaemon daemon = new FuseDaemon(mediaProvider, this, deviceFd, sessionId,
-                        upperFileSystemPath.getPath(), supportedTranscodingRelativePaths);
+                        upperFileSystemPath.getPath(), uncachedMode,
+                        supportedTranscodingRelativePaths, supportedUncachedRelativePaths);
                 daemon.start();
                 sFuseDaemons.put(sessionId, daemon);
             }
diff --git a/src/com/android/providers/media/fuse/FuseDaemon.java b/src/com/android/providers/media/fuse/FuseDaemon.java
index 9595843..97e14fe 100644
--- a/src/com/android/providers/media/fuse/FuseDaemon.java
+++ b/src/com/android/providers/media/fuse/FuseDaemon.java
@@ -42,22 +42,27 @@
     private final MediaProvider mMediaProvider;
     private final int mFuseDeviceFd;
     private final String mPath;
+    private final boolean mUncachedMode;
     private final String[] mSupportedTranscodingRelativePaths;
+    private final String[] mSupportedUncachedRelativePaths;
     private final ExternalStorageServiceImpl mService;
     @GuardedBy("mLock")
     private long mPtr;
 
     public FuseDaemon(@NonNull MediaProvider mediaProvider,
             @NonNull ExternalStorageServiceImpl service, @NonNull ParcelFileDescriptor fd,
-            @NonNull String sessionId, @NonNull String path,
-            String[] supportedTranscodingRelativePaths) {
+            @NonNull String sessionId, @NonNull String path, boolean uncachedMode,
+            String[] supportedTranscodingRelativePaths, String[] supportedUncachedRelativePaths) {
         mMediaProvider = Objects.requireNonNull(mediaProvider);
         mService = Objects.requireNonNull(service);
         setName(Objects.requireNonNull(sessionId));
         mFuseDeviceFd = Objects.requireNonNull(fd).detachFd();
         mPath = Objects.requireNonNull(path);
+        mUncachedMode = uncachedMode;
         mSupportedTranscodingRelativePaths
                 = Objects.requireNonNull(supportedTranscodingRelativePaths);
+        mSupportedUncachedRelativePaths
+                = Objects.requireNonNull(supportedUncachedRelativePaths);
     }
 
     /** Starts a FUSE session. Does not return until the lower filesystem is unmounted. */
@@ -73,7 +78,9 @@
         }
 
         Log.i(TAG, "Starting thread for " + getName() + " ...");
-        native_start(ptr, mFuseDeviceFd, mPath, mSupportedTranscodingRelativePaths); // Blocks
+        native_start(ptr, mFuseDeviceFd, mPath, mUncachedMode,
+                mSupportedTranscodingRelativePaths,
+                mSupportedUncachedRelativePaths); // Blocks
         Log.i(TAG, "Exiting thread for " + getName() + " ...");
 
         synchronized (mLock) {
@@ -161,6 +168,21 @@
     }
 
     /**
+     * Checks if the FuseDaemon uses the FUSE passthrough feature.
+     *
+     * @return {@code true} if the FuseDaemon uses FUSE passthrough, {@code false} otherwise
+     */
+    public boolean usesFusePassthrough() {
+        synchronized (mLock) {
+            if (mPtr == 0) {
+                Log.i(TAG, "usesFusePassthrough failed, FUSE daemon unavailable");
+                return false;
+            }
+            return native_uses_fuse_passthrough(mPtr);
+        }
+    }
+
+    /**
      * Invalidates FUSE VFS dentry cache for {@code path}
      */
     public void invalidateFuseDentryCache(String path) {
@@ -187,11 +209,13 @@
 
     // Takes ownership of the passed in file descriptor!
     private native void native_start(long daemon, int deviceFd, String path,
-            String[] supportedTranscodingRelativePaths);
+            boolean uncachedMode, String[] supportedTranscodingRelativePaths,
+            String[] supportedUncachedRelativePaths);
 
     private native void native_delete(long daemon);
     private native boolean native_should_open_with_fuse(long daemon, String path, boolean readLock,
             int fd);
+    private native boolean native_uses_fuse_passthrough(long daemon);
     private native void native_invalidate_fuse_dentry_cache(long daemon, String path);
     private native boolean native_is_started(long daemon);
     private native FdAccessResult native_check_fd_access(long daemon, int fd, int uid);
diff --git a/src/com/android/providers/media/metrics/MPUiEventLoggerImpl.java b/src/com/android/providers/media/metrics/MPUiEventLoggerImpl.java
index 68f951e..fadb70c 100644
--- a/src/com/android/providers/media/metrics/MPUiEventLoggerImpl.java
+++ b/src/com/android/providers/media/metrics/MPUiEventLoggerImpl.java
@@ -31,6 +31,11 @@
     }
 
     @Override
+    public void log(@NonNull UiEventEnum event, @Nullable InstanceId instance) {
+        logWithInstanceId(event, 0, null, instance);
+    }
+
+    @Override
     public void log(@NonNull UiEventEnum event, int uid, @Nullable String packageName) {
         final int eventID = event.getId();
         if (eventID > 0) {
@@ -66,7 +71,8 @@
                     /* event_id = 1 */ eventID,
                     /* package_name = 2 */ packageName,
                     /* instance_id = 3 */ 0,
-                    /* position_picked = 4 */ position);
+                    /* position_picked = 4 */ position,
+                    /* is_pinned = 5 */ false);
         }
     }
 
@@ -79,7 +85,8 @@
                     /* event_id = 1 */ eventID,
                     /* package_name = 2 */ packageName,
                     /* instance_id = 3 */ instance.getId(),
-                    /* position_picked = 4 */ position);
+                    /* position_picked = 4 */ position,
+                    /* is_pinned = 5 */ false);
         } else {
             logWithPosition(event, uid, packageName, position);
         }
diff --git a/src/com/android/providers/media/metrics/PulledMetrics.java b/src/com/android/providers/media/metrics/PulledMetrics.java
index 599eee5..b1986f2 100644
--- a/src/com/android/providers/media/metrics/PulledMetrics.java
+++ b/src/com/android/providers/media/metrics/PulledMetrics.java
@@ -80,21 +80,27 @@
         if (!isInitialized) {
             return;
         }
-
-        storageAccessMetrics.logMimeType(uid, mimeType);
+        BackgroundThread.getExecutor().execute(() -> {
+            storageAccessMetrics.logMimeType(uid, mimeType);
+        });
     }
 
     /**
      * Logs the storage access and attributes it to the given {@code uid}.
      *
-     * <p>Should only be called from a FUSE thread.
+     * <p>This is a no-op if it's called from a non-FUSE thread.
      */
     public static void logFileAccessViaFuse(int uid, @NonNull String file) {
         if (!isInitialized) {
             return;
         }
-
-        storageAccessMetrics.logAccessViaFuse(uid, file);
+        // Log only if it's a FUSE thread
+        if (!FuseDaemon.native_is_fuse_thread()) {
+            return;
+        }
+        BackgroundThread.getExecutor().execute(() -> {
+            storageAccessMetrics.logAccessViaFuse(uid, file);
+        });
     }
 
     /**
@@ -111,7 +117,9 @@
         if (FuseDaemon.native_is_fuse_thread()) {
             return;
         }
-        storageAccessMetrics.logAccessViaMediaProvider(uid, volumeName);
+        BackgroundThread.getExecutor().execute(() -> {
+            storageAccessMetrics.logAccessViaMediaProvider(uid, volumeName);
+        });
     }
 
     private static class StatsPullCallbackHandler implements StatsManager.StatsPullAtomCallback {
diff --git a/src/com/android/providers/media/photopicker/PhotoPickerProvider.java b/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
index ee69ecf..34d6040 100644
--- a/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
+++ b/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
@@ -16,62 +16,34 @@
 
 package com.android.providers.media.photopicker;
 
-import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_PAUSED;
-import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_STARTED;
+import static android.provider.CloudMediaProviderContract.EXTRA_AUTHORITY;
 import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED;
-import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_BUFFERING;
-import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_COMPLETED;
-import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_MEDIA_SIZE_CHANGED;
-import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_READY;
+import static android.provider.CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB;
 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED;
-import static android.provider.CloudMediaProviderContract.MediaCollectionInfo;
 
-import android.annotation.DurationMillisLong;
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
-import android.content.Context;
 import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.graphics.Point;
-import android.media.AudioManager;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.CancellationSignal;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.OperationCanceledException;
 import android.os.ParcelFileDescriptor;
 import android.provider.CloudMediaProvider;
 import android.provider.MediaStore;
-import android.util.Log;
-import android.view.Surface;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.providers.media.LocalCallingIdentity;
 import com.android.providers.media.MediaProvider;
 import com.android.providers.media.photopicker.data.CloudProviderQueryExtras;
 import com.android.providers.media.photopicker.data.ExternalDbFacade;
 import com.android.providers.media.photopicker.ui.remotepreview.RemotePreviewHandler;
+import com.android.providers.media.photopicker.ui.remotepreview.RemoteSurfaceController;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.google.android.exoplayer2.DefaultLoadControl;
-import com.google.android.exoplayer2.DefaultRenderersFactory;
-import com.google.android.exoplayer2.ExoPlayer;
-import com.google.android.exoplayer2.LoadControl;
-import com.google.android.exoplayer2.MediaItem;
-import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.Player.State;
-import com.google.android.exoplayer2.analytics.AnalyticsCollector;
-import com.google.android.exoplayer2.source.MediaParserExtractorAdapter;
-import com.google.android.exoplayer2.source.ProgressiveMediaSource;
-import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
-import com.google.android.exoplayer2.upstream.ContentDataSource;
-import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
-import com.google.android.exoplayer2.util.Clock;
-import com.google.android.exoplayer2.video.VideoSize;
-
-import java.io.File;
 import java.io.FileNotFoundException;
 
 /**
@@ -124,9 +96,16 @@
         final Bundle opts = new Bundle();
         opts.putParcelable(ContentResolver.EXTRA_SIZE, size);
 
+        String mimeTypeFilter = null;
+        if (extras.getBoolean(EXTRA_MEDIASTORE_THUMB)) {
+            // This is a request for thumbnail, set "image/*" to get cached thumbnails from
+            // MediaProvider.
+            mimeTypeFilter = "image/*";
+        }
+
         final LocalCallingIdentity token = mMediaProvider.clearLocalCallingIdentity();
         try {
-            return mMediaProvider.openTypedAssetFile(fromMediaId(mediaId), "image/*", opts);
+            return mMediaProvider.openTypedAssetFile(fromMediaId(mediaId), mimeTypeFilter, opts);
         } finally {
             mMediaProvider.restoreLocalCallingIdentity(token);
         }
@@ -157,10 +136,12 @@
     public CloudMediaSurfaceController onCreateCloudMediaSurfaceController(@NonNull Bundle config,
             CloudMediaSurfaceStateChangedCallback callback) {
         if (RemotePreviewHandler.isRemotePreviewEnabled()) {
-            boolean enableLoop = config.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, false);
-            boolean muteAudio = config.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED,
+            final String authority = config.getString(EXTRA_AUTHORITY,
+                    PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY);
+            final boolean enableLoop = config.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, false);
+            final boolean muteAudio = config.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED,
                     false);
-            return new CloudMediaSurfaceControllerImpl(getContext(), enableLoop, muteAudio,
+            return new RemoteSurfaceController(getContext(), authority, enableLoop, muteAudio,
                     callback);
         }
         return null;
@@ -179,247 +160,4 @@
         return MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL,
                 Long.parseLong(mediaId));
     }
-
-    private static final class CloudMediaSurfaceControllerImpl extends CloudMediaSurfaceController {
-
-        // The minimum duration of media that the player will attempt to ensure is buffered at all
-        // times.
-        private static final int MIN_BUFFER_MS = 1000;
-        // The maximum duration of media that the player will attempt to buffer.
-        private static final int MAX_BUFFER_MS = 2000;
-        // The duration of media that must be buffered for playback to start or resume following a
-        // user action such as a seek.
-        private static final int BUFFER_FOR_PLAYBACK_MS = 1000;
-        // The default duration of media that must be buffered for playback to resume after a
-        // rebuffer.
-        private static final int BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 1000;
-        private static final LoadControl sLoadControl = new DefaultLoadControl.Builder()
-                .setBufferDurationsMs(
-                        MIN_BUFFER_MS,
-                        MAX_BUFFER_MS,
-                        BUFFER_FOR_PLAYBACK_MS,
-                        BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS).build();
-
-        private final Context mContext;
-        private final CloudMediaSurfaceStateChangedCallback mCallback;
-        private final Handler mHandler = new Handler(Looper.getMainLooper());
-        private final Player.Listener mEventListener = new Player.Listener() {
-            @Override
-            public void onPlaybackStateChanged(@State int state) {
-                Log.d(TAG, "Received player event " + state);
-
-                switch (state) {
-                    case Player.STATE_READY:
-                        mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_READY,
-                                null);
-                        return;
-                    case Player.STATE_BUFFERING:
-                        mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_BUFFERING,
-                                null);
-                        return;
-                    case Player.STATE_ENDED:
-                        mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_COMPLETED,
-                                null);
-                        return;
-                    default:
-                }
-            }
-
-            @Override
-            public void onIsPlayingChanged(boolean isPlaying) {
-                mCallback.setPlaybackState(mCurrentSurfaceId, isPlaying ? PLAYBACK_STATE_STARTED :
-                                PLAYBACK_STATE_PAUSED, null);
-            }
-
-            @Override
-            public void onVideoSizeChanged(VideoSize videoSize) {
-                Point size = new Point(videoSize.width, videoSize.height);
-                Bundle bundle = new Bundle();
-                bundle.putParcelable(ContentResolver.EXTRA_SIZE, size);
-                mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_MEDIA_SIZE_CHANGED,
-                        bundle);
-            }
-        };
-
-        private boolean mEnableLoop;
-        private boolean mMuteAudio;
-        private ExoPlayer mPlayer;
-        private int mCurrentSurfaceId = -1;
-
-        CloudMediaSurfaceControllerImpl(Context context, boolean enableLoop, boolean muteAudio,
-                CloudMediaSurfaceStateChangedCallback callback) {
-            mCallback = callback;
-            mContext = context;
-            mEnableLoop = enableLoop;
-            mMuteAudio = muteAudio;
-            Log.d(TAG, "Surface controller created.");
-        }
-
-        @Override
-        public void onPlayerCreate() {
-            mHandler.post(() -> {
-                mPlayer = createExoPlayer();
-                mPlayer.addListener(mEventListener);
-                updateLoopingPlaybackStatus();
-                updateAudioMuteStatus();
-                Log.d(TAG, "Player created.");
-            });
-        }
-
-        @Override
-        public void onPlayerRelease() {
-            mHandler.post(() -> {
-                mPlayer.removeListener(mEventListener);
-                mPlayer.release();
-                mPlayer = null;
-                Log.d(TAG, "Player released.");
-            });
-        }
-
-        @Override
-        public void onSurfaceCreated(int surfaceId, @NonNull Surface surface,
-                @NonNull String mediaId) {
-            mHandler.post(() -> {
-                try {
-                    // onSurfaceCreated may get called while the player is already rendering on a
-                    // different surface. In that case, pause the player before preparing it for
-                    // rendering on the new surface.
-                    // Unfortunately, Exoplayer#stop doesn't seem to work here. If we call stop(),
-                    // as soon as the player becomes ready again, it automatically starts to play
-                    // the new media. The reason is that Exoplayer treats play/pause as calls to
-                    // the method Exoplayer#setPlayWhenReady(boolean) with true and false
-                    // respectively. So, if we don't pause(), then since the previous play() call
-                    // had set setPlayWhenReady to true, the player would start the playback as soon
-                    // as it gets ready with the new media item.
-                    if (mPlayer.isPlaying()) {
-                        mPlayer.pause();
-                    }
-
-                    mCurrentSurfaceId = surfaceId;
-
-                    final Uri mediaUri =
-                            Uri.parse(
-                                    MediaStore.Files.getContentUri(
-                                            MediaStore.VOLUME_EXTERNAL)
-                                    + File.separator + mediaId);
-                    mPlayer.setMediaItem(MediaItem.fromUri(mediaUri));
-                    mPlayer.setVideoSurface(surface);
-                    mPlayer.prepare();
-
-                    Log.d(TAG, "Surface prepared: " + surfaceId + ". Surface: " + surface
-                            + ". MediaId: " + mediaId);
-                } catch (RuntimeException e) {
-                    Log.e(TAG, "Error preparing player with surface.", e);
-                }
-            });
-        }
-
-        @Override
-        public void onSurfaceChanged(int surfaceId, int format, int width, int height) {
-            Log.d(TAG, "Surface changed: " + surfaceId + ". Format: " + format + ". Width: "
-                    + width + ". Height: " + height);
-        }
-
-        @Override
-        public void onSurfaceDestroyed(int surfaceId) {
-            mHandler.post(() -> {
-                if (mCurrentSurfaceId != surfaceId) {
-                    // This means that the player is already using some other surface, hence
-                    // nothing to do.
-                    return;
-                }
-                if (mPlayer.isPlaying()) {
-                    mPlayer.stop();
-                }
-                mPlayer.clearVideoSurface();
-                mCurrentSurfaceId = -1;
-
-                Log.d(TAG, "Surface released: " + surfaceId);
-            });
-        }
-
-        @Override
-        public void onMediaPlay(int surfaceId) {
-            mHandler.post(() -> {
-                mPlayer.play();
-                Log.d(TAG, "Media played: " + surfaceId);
-            });
-        }
-
-        @Override
-        public void onMediaPause(int surfaceId) {
-            mHandler.post(() -> {
-                if (mPlayer.isPlaying()) {
-                    mPlayer.pause();
-                    Log.d(TAG, "Media paused: " + surfaceId);
-                }
-            });
-        }
-
-        @Override
-        public void onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis) {
-            mHandler.post(() -> {
-                mPlayer.seekTo((int) timestampMillis);
-                Log.d(TAG, "Media seeked: " + surfaceId + ". Timestamp: " + timestampMillis);
-            });
-        }
-
-        @Override
-        public void onConfigChange(@NonNull Bundle config) {
-            final boolean enableLoop = config.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED,
-                    mEnableLoop);
-            final boolean muteAudio = config.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED,
-                    mMuteAudio);
-            mHandler.post(() -> {
-                if (mEnableLoop != enableLoop) {
-                    mEnableLoop = enableLoop;
-                    updateLoopingPlaybackStatus();
-                }
-
-                if (mMuteAudio != muteAudio) {
-                    mMuteAudio = muteAudio;
-                    updateAudioMuteStatus();
-                }
-            });
-            Log.d(TAG, "Config changed. Updated config params: " + config);
-        }
-
-        @Override
-        public void onDestroy() {
-            Log.d(TAG, "Surface controller destroyed.");
-        }
-
-        private ExoPlayer createExoPlayer() {
-            // ProgressiveMediaFactory will be enough for video playback of videos on the device.
-            // This also reduces apk size.
-            ProgressiveMediaSource.Factory mediaSourceFactory = new ProgressiveMediaSource.Factory(
-                    () -> new ContentDataSource(mContext), MediaParserExtractorAdapter.FACTORY);
-
-            return new ExoPlayer.Builder(mContext,
-                    new DefaultRenderersFactory(mContext),
-                    new DefaultTrackSelector(mContext),
-                    mediaSourceFactory,
-                    sLoadControl,
-                    DefaultBandwidthMeter.getSingletonInstance(mContext),
-                    new AnalyticsCollector(Clock.DEFAULT)).buildExoPlayer();
-        }
-
-        private void updateLoopingPlaybackStatus() {
-            mPlayer.setRepeatMode(mEnableLoop ? Player.REPEAT_MODE_ONE : Player.REPEAT_MODE_OFF);
-        }
-
-        private void updateAudioMuteStatus() {
-            if (mMuteAudio) {
-                mPlayer.setVolume(0f);
-            } else {
-                AudioManager audioManager = mContext.getSystemService(AudioManager.class);
-                if (audioManager == null) {
-                    Log.e(TAG, "Couldn't find AudioManager while trying to set volume,"
-                            + " unable to set volume");
-                    return;
-                }
-                mPlayer.setVolume(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC));
-            }
-        }
-    }
 }
diff --git a/src/com/android/providers/media/photopicker/PickerDataLayer.java b/src/com/android/providers/media/photopicker/PickerDataLayer.java
index 23a8d73..a62ba39 100644
--- a/src/com/android/providers/media/photopicker/PickerDataLayer.java
+++ b/src/com/android/providers/media/photopicker/PickerDataLayer.java
@@ -69,6 +69,11 @@
             // Refresh the 'media' table
             mSyncController.syncAllMedia();
 
+            if (TextUtils.isEmpty(albumId)) {
+                // Notify that the picker is launched in case there's any pending UI notification
+                mSyncController.notifyPickerLaunch();
+            }
+
             // Fetch all merged and deduped cloud and local media from 'media' table
             // This also matches 'merged' albums like Favorites because |authority| will
             // be null, hence we have to fetch the data from the picker db
diff --git a/src/com/android/providers/media/photopicker/PickerSyncController.java b/src/com/android/providers/media/photopicker/PickerSyncController.java
index 5632228..d0fb856 100644
--- a/src/com/android/providers/media/photopicker/PickerSyncController.java
+++ b/src/com/android/providers/media/photopicker/PickerSyncController.java
@@ -16,44 +16,46 @@
 
 package com.android.providers.media.photopicker;
 
+import static android.content.ContentResolver.EXTRA_HONORED_ARGS;
 import static android.provider.CloudMediaProviderContract.EXTRA_ALBUM_ID;
-import static android.provider.CloudMediaProviderContract.EXTRA_SYNC_GENERATION;
 import static android.provider.CloudMediaProviderContract.EXTRA_MEDIA_COLLECTION_ID;
 import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_TOKEN;
 import static android.provider.CloudMediaProviderContract.EXTRA_SYNC_GENERATION;
 import static android.provider.CloudMediaProviderContract.MediaCollectionInfo;
-import static android.content.ContentResolver.EXTRA_HONORED_ARGS;
-import static com.android.providers.media.PickerUriResolver.getMediaUri;
+
 import static com.android.providers.media.PickerUriResolver.getDeletedMediaUri;
 import static com.android.providers.media.PickerUriResolver.getMediaCollectionInfoUri;
+import static com.android.providers.media.PickerUriResolver.getMediaUri;
 
 import android.annotation.IntDef;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ProviderInfo;
 import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Process;
-import android.os.SystemProperties;
+import android.os.storage.StorageManager;
 import android.provider.CloudMediaProvider;
 import android.provider.CloudMediaProviderContract;
-import android.provider.DeviceConfig;
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
+import android.widget.Toast;
+
 import androidx.annotation.GuardedBy;
 import androidx.annotation.VisibleForTesting;
+
 import com.android.modules.utils.BackgroundThread;
-import com.android.providers.media.MediaProvider;
-import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.providers.media.R;
-import com.android.providers.media.util.DeviceConfigUtils;
+import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.util.ForegroundThread;
 import com.android.providers.media.util.StringUtils;
 
 import java.lang.annotation.Retention;
@@ -71,11 +73,12 @@
 public class PickerSyncController {
     private static final String TAG = "PickerSyncController";
 
-    public static final String PROP_DEFAULT_SYNC_DELAY_MS = "pickerdb.default_sync_delay_ms";
+    public static final String SYNC_DELAY_MS = "default_sync_delay_ms";
+    public static final String ALLOWED_CLOUD_PROVIDERS_KEY = "allowed_cloud_providers";
 
     private static final String PREFS_KEY_CLOUD_PROVIDER_AUTHORITY = "cloud_provider_authority";
-    private static final String PREFS_KEY_CLOUD_PROVIDER_PKGNAME = "cloud_provider_pkg_name";
-    private static final String PREFS_KEY_CLOUD_PROVIDER_UID = "cloud_provider_uid";
+    private static final String PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION =
+            "cloud_provider_pending_notification";
     private static final String PREFS_KEY_CLOUD_PREFIX = "cloud_provider:";
     private static final String PREFS_KEY_LOCAL_PREFIX = "local_provider:";
 
@@ -84,14 +87,6 @@
     public static final String LOCAL_PICKER_PROVIDER_AUTHORITY =
             "com.android.providers.media.photopicker";
 
-    public static final String PROP_USE_ALLOWED_CLOUD_PROVIDERS =
-            "persist.sys.photopicker.use_allowed_cloud_providers";
-    public static final String ALLOWED_CLOUD_PROVIDERS_KEY = "allowed_cloud_providers";
-
-    private static final String DEFAULT_CLOUD_PROVIDER_AUTHORITY = null;
-    private static final String DEFAULT_CLOUD_PROVIDER_PKGNAME = null;
-    private static final int DEFAULT_CLOUD_PROVIDER_UID = -1;
-
     private static final int SYNC_TYPE_NONE = 0;
     private static final int SYNC_TYPE_MEDIA_INCREMENTAL = 1;
     private static final int SYNC_TYPE_MEDIA_FULL = 2;
@@ -120,14 +115,7 @@
     private CloudProviderInfo mCloudProviderInfo;
 
     public PickerSyncController(Context context, PickerDbFacade dbFacade,
-            MediaProvider mediaProvider) {
-        this(context, dbFacade, LOCAL_PICKER_PROVIDER_AUTHORITY, mediaProvider.getIntDeviceConfig(
-                        DeviceConfig.NAMESPACE_STORAGE, PROP_DEFAULT_SYNC_DELAY_MS, 5000));
-    }
-
-    @VisibleForTesting
-    PickerSyncController(Context context, PickerDbFacade dbFacade,
-            String localProvider, long syncDelayMs) {
+            String localProvider, String allowedCloudProviders, long syncDelayMs) {
         mContext = context;
         mSyncPrefs = mContext.getSharedPreferences(PICKER_SYNC_PREFS_FILE_NAME,
                 Context.MODE_PRIVATE);
@@ -138,30 +126,23 @@
         mSyncDelayMs = syncDelayMs;
         mSyncAllMediaCallback = this::syncAllMedia;
 
-        final String cloudProviderAuthority = mUserPrefs.getString(
-                PREFS_KEY_CLOUD_PROVIDER_AUTHORITY,
-                DEFAULT_CLOUD_PROVIDER_AUTHORITY);
-        final String cloudProviderPackageName = mUserPrefs.getString(
-                PREFS_KEY_CLOUD_PROVIDER_PKGNAME,
-                DEFAULT_CLOUD_PROVIDER_PKGNAME);
-        final int cloudProviderUid = mUserPrefs.getInt(PREFS_KEY_CLOUD_PROVIDER_UID,
-                DEFAULT_CLOUD_PROVIDER_UID);
+        final String cachedAuthority = mUserPrefs.getString(
+                PREFS_KEY_CLOUD_PROVIDER_AUTHORITY, null);
 
-        if (SystemProperties.getBoolean(PROP_USE_ALLOWED_CLOUD_PROVIDERS, false)) {
-            mAllowedCloudProviders = getAllowedCloudProviders();
+        mAllowedCloudProviders = parseAllowedCloudProviders(allowedCloudProviders);
+
+        final CloudProviderInfo defaultInfo = getDefaultCloudProviderInfo(cachedAuthority);
+
+        if (Objects.equals(defaultInfo.authority, cachedAuthority)) {
+            // Just set it without persisting since it's not changing and persisting would
+            // notify the user that cloud media is now available
+            mCloudProviderInfo = defaultInfo;
         } else {
-            mAllowedCloudProviders = new ArraySet<>();
+            // Persist it so that we notify the user that cloud media is now available
+            persistCloudProviderInfo(defaultInfo);
         }
 
-        if (cloudProviderAuthority == null) {
-            // TODO: Only get default if it wasn't set by the user
-            final CloudProviderInfo defaultCloudProviderInfo = getDefaultCloudProviderInfo();
-            Log.i(TAG, "Cloud provider is set to Default " + defaultCloudProviderInfo.authority);
-            setCloudProviderInfo(defaultCloudProviderInfo);
-        } else {
-            mCloudProviderInfo = new CloudProviderInfo(cloudProviderAuthority,
-                cloudProviderPackageName, cloudProviderUid);
-        }
+        Log.d(TAG, "Initialized cloud provider to: " + mCloudProviderInfo.authority);
     }
 
     /**
@@ -215,7 +196,7 @@
      * Returns the supported cloud {@link CloudMediaProvider} infos.
      */
     public CloudProviderInfo getCloudProviderInfo(String authority) {
-        for (CloudProviderInfo info : getSupportedCloudProviders()) {
+        for (CloudProviderInfo info : getSupportedCloudProviders(/* ignoreAllowList */ false)) {
             if (info.authority.equals(authority)) {
                 return info;
             }
@@ -229,15 +210,12 @@
      */
     @VisibleForTesting
     List<CloudProviderInfo> getSupportedCloudProviders() {
+        return getSupportedCloudProviders(/* ignoreAllowList */ false);
+    }
+
+    private List<CloudProviderInfo> getSupportedCloudProviders(boolean ignoreAllowList) {
         final List<CloudProviderInfo> result = new ArrayList<>();
 
-        final boolean useAllowedCloudProviders =
-            SystemProperties.getBoolean(PROP_USE_ALLOWED_CLOUD_PROVIDERS, false);
-
-        if (useAllowedCloudProviders && mAllowedCloudProviders.isEmpty()) {
-            return result;
-        }
-
         final PackageManager pm = mContext.getPackageManager();
         final Intent intent = new Intent(CloudMediaProviderContract.PROVIDER_INTERFACE);
         final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, /* flags */ 0);
@@ -247,8 +225,8 @@
             if (providerInfo.authority != null
                     && CloudMediaProviderContract.MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION.equals(
                             providerInfo.readPermission)
-                    && (!useAllowedCloudProviders
-                        || mAllowedCloudProviders.contains(providerInfo.authority))) {
+                    && (ignoreAllowList
+                            || mAllowedCloudProviders.contains(providerInfo.authority))) {
                 result.add(new CloudProviderInfo(providerInfo.authority,
                                 providerInfo.applicationInfo.packageName,
                                 providerInfo.applicationInfo.uid));
@@ -281,7 +259,7 @@
         if (authority == null || !newProviderInfo.isEmpty()) {
             synchronized (mLock) {
                 final String oldAuthority = mCloudProviderInfo.authority;
-                setCloudProviderInfo(newProviderInfo);
+                persistCloudProviderInfo(newProviderInfo);
                 resetCachedMediaCollectionInfo(newProviderInfo.authority);
 
                 // Disable cloud provider queries on the db until next sync
@@ -301,6 +279,20 @@
         return false;
     }
 
+    /**
+     * Set cloud provider and update allowed cloud providers
+     */
+    @VisibleForTesting
+    public void forceSetCloudProvider(String authority) {
+        if (authority == null) {
+            mAllowedCloudProviders.clear();
+        } else {
+            mAllowedCloudProviders.add(authority);
+        }
+
+        setCloudProvider(authority);
+    }
+
     public String getCloudProvider() {
         synchronized (mLock) {
             return mCloudProviderInfo.authority;
@@ -345,7 +337,11 @@
             return true;
         }
 
-        final List<CloudProviderInfo> infos = getSupportedCloudProviders();
+        // TODO(b/232738117): Enforce allow list here. This works around some CTS failure late in
+        // Android T. The current implementation is fine since cloud providers is only supported
+        // for app developers testing.
+        final List<CloudProviderInfo> infos = getSupportedCloudProviders(
+                /* ignoreAllowList */ true);
         for (CloudProviderInfo info : infos) {
             if (info.uid == uid && info.authority.equals(authority)) {
                 return true;
@@ -380,6 +376,48 @@
         }
     }
 
+    /**
+     * Notifies about picker UI launched
+     */
+    public void notifyPickerLaunch() {
+        final String packageName;
+        synchronized (mLock) {
+            packageName = mCloudProviderInfo.packageName;
+        }
+
+        final boolean hasPendingNotification = mUserPrefs.getBoolean(
+                PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION, false);
+
+        if (!hasPendingNotification || (packageName == null)) {
+            Log.d(TAG, "No pending UI notification");
+            return;
+        }
+
+        // Offload showing the UI on a fg thread to avoid the expensive binder request
+        // to fetch the app name blocking the picker launch
+        ForegroundThread.getHandler().post(() -> {
+            Log.i(TAG, "Cloud media now available in the picker");
+
+            final PackageManager pm = mContext.getPackageManager();
+            String appName = packageName;
+            try {
+                ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0);
+                appName = (String) pm.getApplicationLabel(appInfo);
+            } catch (final NameNotFoundException e) {
+                Log.i(TAG, "Failed to get appName for package: " + packageName);
+            }
+
+            final String message = mContext.getResources().getString(R.string.picker_cloud_sync,
+                    appName);
+            Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
+        });
+
+        // Clear the notification
+        final SharedPreferences.Editor editor = mUserPrefs.edit();
+        editor.putBoolean(PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION, false);
+        editor.apply();
+    }
+
     private void syncAlbumMediaFromProvider(String authority, String albumId) {
         final Bundle queryArgs = new Bundle();
         queryArgs.putString(EXTRA_ALBUM_ID, albumId);
@@ -515,24 +553,36 @@
         }
     }
 
-    private void setCloudProviderInfo(CloudProviderInfo info) {
+    private void persistCloudProviderInfo(CloudProviderInfo info) {
         synchronized (mLock) {
             mCloudProviderInfo = info;
         }
 
+        final String authority = info.authority;
         final SharedPreferences.Editor editor = mUserPrefs.edit();
 
         if (info.isEmpty()) {
             editor.remove(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY);
-            editor.remove(PREFS_KEY_CLOUD_PROVIDER_PKGNAME);
-            editor.remove(PREFS_KEY_CLOUD_PROVIDER_UID);
+            editor.putBoolean(PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION, false);
         } else {
-            editor.putString(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY, info.authority);
-            editor.putString(PREFS_KEY_CLOUD_PROVIDER_PKGNAME, info.packageName);
-            editor.putInt(PREFS_KEY_CLOUD_PROVIDER_UID, info.uid);
+            editor.putString(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY, authority);
+            editor.putBoolean(PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATTION, true);
         }
 
         editor.apply();
+
+        if (SdkLevel.isAtLeastT()) {
+            try {
+                StorageManager sm = mContext.getSystemService(StorageManager.class);
+                sm.setCloudMediaProvider(authority);
+            } catch (SecurityException e) {
+                // When run as part of the unit tests, the notification fails because only the
+                // MediaProvider uid can notify
+                Log.w(TAG, "Failed to notify the system of cloud provider update to: " + authority);
+            }
+        }
+
+        Log.d(TAG, "Updated cloud provider to: " + authority);
     }
 
     private void cacheMediaCollectionInfo(String authority, Bundle bundle) {
@@ -674,8 +724,9 @@
                 + totalRowcount + ". Cursor count: " + cursorCount);
     }
 
-    private CloudProviderInfo getDefaultCloudProviderInfo() {
-        final List<CloudProviderInfo> infos = getSupportedCloudProviders();
+    private CloudProviderInfo getDefaultCloudProviderInfo(String cachedProvider) {
+        final List<CloudProviderInfo> infos =
+                getSupportedCloudProviders(/* ignoreAllowList */ false);
 
         if (infos.size() == 1) {
             Log.i(TAG, "Only 1 cloud provider found, hence "
@@ -684,7 +735,16 @@
         } else {
             final String defaultCloudProviderAuthority = StringUtils.getStringConfig(
                 mContext, R.string.config_default_cloud_provider_authority);
-            Log.i(TAG, "Default cloud provider to be used is " + defaultCloudProviderAuthority);
+            Log.i(TAG, "Found multiple cloud providers but OEM default is: "
+                    + defaultCloudProviderAuthority);
+
+            if (cachedProvider != null) {
+                for (CloudProviderInfo info : infos) {
+                    if (info.authority.equals(defaultCloudProviderAuthority)) {
+                        return info;
+                    }
+                }
+            }
 
             if (defaultCloudProviderAuthority != null) {
                 for (CloudProviderInfo info : infos) {
@@ -699,10 +759,9 @@
         return CloudProviderInfo.EMPTY;
     }
 
-    private Set<String> getAllowedCloudProviders() {
+    private Set<String> parseAllowedCloudProviders(String config) {
         Set<String> allowedProviders = new ArraySet<>();
-        final String[] allowedProvidersConfig = DeviceConfigUtils.getStringDeviceConfig(
-                ALLOWED_CLOUD_PROVIDERS_KEY, "").split(",");
+        final String[] allowedProvidersConfig = config.split(",");
 
         if (allowedProvidersConfig.length == 0 || allowedProvidersConfig[0].isEmpty()) {
             Log.i(TAG, "Empty allowed cloud providers");
@@ -762,9 +821,9 @@
         private final int uid;
 
         private CloudProviderInfo() {
-            this.authority = DEFAULT_CLOUD_PROVIDER_AUTHORITY;
-            this.packageName = DEFAULT_CLOUD_PROVIDER_PKGNAME;
-            this.uid = DEFAULT_CLOUD_PROVIDER_UID;
+            this.authority = null;
+            this.packageName = null;
+            this.uid = -1;
         }
 
         CloudProviderInfo(String authority, String packageName, int uid) {
diff --git a/src/com/android/providers/media/photopicker/data/glide/PickerModelLoader.java b/src/com/android/providers/media/photopicker/data/glide/PickerModelLoader.java
index 5034760..2477caf 100644
--- a/src/com/android/providers/media/photopicker/data/glide/PickerModelLoader.java
+++ b/src/com/android/providers/media/photopicker/data/glide/PickerModelLoader.java
@@ -16,16 +16,17 @@
 
 package com.android.providers.media.photopicker.data.glide;
 
+import static com.android.providers.media.photopicker.ui.ImageLoader.THUMBNAIL_REQUEST;
+
 import android.content.Context;
+import android.content.UriMatcher;
 import android.net.Uri;
 import android.os.ParcelFileDescriptor;
-import android.provider.MediaStore;
-
-import com.android.providers.media.photopicker.PickerSyncController;
+import android.provider.CloudMediaProviderContract;
 
 import com.bumptech.glide.load.Options;
-import com.bumptech.glide.signature.ObjectKey;
 import com.bumptech.glide.load.model.ModelLoader;
+import com.bumptech.glide.signature.ObjectKey;
 
 /**
  * Custom {@link ModelLoader} to load thumbnails from cloud media provider.
@@ -40,22 +41,19 @@
     @Override
     public LoadData<ParcelFileDescriptor> buildLoadData(Uri model, int width, int height,
             Options options) {
+        final boolean isThumbRequest = Boolean.TRUE.equals(options.get(THUMBNAIL_REQUEST));
         return new LoadData<>(new ObjectKey(model),
-                new PickerThumbnailFetcher(mContext, model, width, height));
+                new PickerThumbnailFetcher(mContext, model, width, height, isThumbRequest));
     }
 
     @Override
     public boolean handles(Uri model) {
-        if (model == null) return false;
+        final int pickerId = 1;
+        final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
+        matcher.addURI(model.getAuthority(),
+                CloudMediaProviderContract.URI_PATH_MEDIA + "/*", pickerId);
 
-        String authority = model.getAuthority();
-        // TODO(b/210190677): Handle all local picker provider uris irrespective of cloud or local.
-        // PickerModuleLoader fetches thumbnail data by forwarding the request to corresponding
-        // ContentProvider. For local provider uris, this request goes to MediaProvider where video
-        // thumbnail is obtained from the mid-point of the video. For PhotoPicker, we need the
-        // thumbnail from the first frame. Hence, as a temporary fix, local provider uris will be
-        // handled by default Glide module.
-        return !PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY.equals(authority)
-                && !MediaStore.AUTHORITY.equals(authority);
+        // Matches picker URIs of the form content://<authority>/media
+        return matcher.match(model) == pickerId;
     }
 }
diff --git a/src/com/android/providers/media/photopicker/data/glide/PickerThumbnailFetcher.java b/src/com/android/providers/media/photopicker/data/glide/PickerThumbnailFetcher.java
index 15827ff..9dcb211 100644
--- a/src/com/android/providers/media/photopicker/data/glide/PickerThumbnailFetcher.java
+++ b/src/com/android/providers/media/photopicker/data/glide/PickerThumbnailFetcher.java
@@ -23,6 +23,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
+import android.provider.CloudMediaProviderContract;
 
 import com.bumptech.glide.Priority;
 import com.bumptech.glide.load.DataSource;
@@ -37,27 +38,36 @@
  */
 public class PickerThumbnailFetcher implements DataFetcher<ParcelFileDescriptor> {
 
-    private final Context context;
-    private final Uri model;
-    private final int width;
-    private final int height;
+    private final Context mContext;
+    private final Uri mModel;
+    private final int mWidth;
+    private final int mHeight;
+    private final boolean mIsThumbRequest;
 
-    PickerThumbnailFetcher(Context context, Uri model, int width, int height) {
-        this.context = context;
-        this.model = model;
-        this.width = width;
-        this.height = height;
+    PickerThumbnailFetcher(Context context, Uri model, int width, int height,
+            boolean isThumbRequest) {
+        mContext = context;
+        mModel = model;
+        mWidth = width;
+        mHeight = height;
+        mIsThumbRequest = isThumbRequest;
     }
 
     @Override
     public void loadData(Priority priority, DataCallback<? super ParcelFileDescriptor> callback) {
-        ContentResolver contentResolver = context.getContentResolver();
+        ContentResolver contentResolver = mContext.getContentResolver();
         final Bundle opts = new Bundle();
-        opts.putParcelable(ContentResolver.EXTRA_SIZE, new Point(width, height));
-        try (AssetFileDescriptor afd = contentResolver.openTypedAssetFileDescriptor(model,
+        opts.putParcelable(ContentResolver.EXTRA_SIZE, new Point(mWidth, mHeight));
+        opts.putBoolean(CloudMediaProviderContract.EXTRA_PREVIEW_THUMBNAIL, true);
+
+        if (mIsThumbRequest) {
+            opts.putBoolean(CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB, true);
+        }
+
+        try (AssetFileDescriptor afd = contentResolver.openTypedAssetFileDescriptor(mModel,
                 /* mimeType */ "image/*", opts, /* cancellationSignal */ null)) {
             if (afd == null) {
-                final String err = "Failed to load data for " + model;
+                final String err = "Failed to load data for " + mModel;
                 callback.onLoadFailed(new FileNotFoundException(err));
                 return;
             }
diff --git a/src/com/android/providers/media/photopicker/ui/DevicePolicyResources.java b/src/com/android/providers/media/photopicker/ui/DevicePolicyResources.java
new file mode 100644
index 0000000..383a260
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/ui/DevicePolicyResources.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2022 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.photopicker.ui;
+
+import android.app.admin.DevicePolicyManager;
+
+/**
+ * Class containing the required identifiers to update device management resources.
+ *
+ * <p>See {@link DevicePolicyManager#getDrawable} and {@link DevicePolicyManager#getString}.
+ */
+public class DevicePolicyResources {
+
+    /**
+     * Class containing the identifiers used to update device management-related system strings.
+     */
+    public static final class Strings {
+        private static final String PREFIX = "MediaProvider.";
+
+        /**
+         * The text shown to switch to the work profile in PhotoPicker.
+         */
+        public static final String SWITCH_TO_WORK_MESSAGE =
+                PREFIX + "SWITCH_TO_WORK_MESSAGE";
+
+        /**
+         * The text shown to switch to the personal profile in PhotoPicker.
+         */
+        public static final String SWITCH_TO_PERSONAL_MESSAGE =
+                PREFIX + "SWITCH_TO_PERSONAL_MESSAGE";
+
+        /**
+         * The title for error dialog in PhotoPicker when the admin blocks cross user
+         * interaction for the intent.
+         */
+        public static final String BLOCKED_BY_ADMIN_TITLE =
+                PREFIX + "BLOCKED_BY_ADMIN_TITLE";
+
+        /**
+         * The message for error dialog in PhotoPicker when the admin blocks cross user
+         * interaction from the personal profile.
+         */
+        public static final String BLOCKED_FROM_PERSONAL_MESSAGE =
+                PREFIX + "BLOCKED_FROM_PERSONAL_MESSAGE";
+
+        /**
+         * The message for error dialog in PhotoPicker when the admin blocks cross user
+         * interaction from the work profile.
+         */
+        public static final String BLOCKED_FROM_WORK_MESSAGE =
+                PREFIX + "BLOCKED_FROM_WORK_MESSAGE";
+
+        /**
+         * The title of the error dialog in PhotoPicker when the user tries to switch to work
+         * content, but work profile is off.
+         */
+        public static final String WORK_PROFILE_PAUSED_TITLE =
+                PREFIX + "WORK_PROFILE_PAUSED_TITLE";
+
+        /**
+         * The message of the error dialog in PhotoPicker when the user tries to switch to work
+         * content, but work profile is off.
+         */
+        public static final String WORK_PROFILE_PAUSED_MESSAGE =
+                PREFIX + "WORK_PROFILE_PAUSED_MESSAGE";
+    }
+
+    /**
+     * Class containing the identifiers used to update device management-related system drawable.
+     */
+    public static final class Drawables {
+        /**
+         * General purpose work profile icon (i.e. generic icon badging).
+         */
+        public static final String WORK_PROFILE_ICON = "WORK_PROFILE_ICON";
+
+        /**
+         * Class containing the style identifiers used to update device management-related system
+         * drawable.
+         */
+        public static final class Style {
+            /**
+             * A style identifier indicating that the updatable drawable is an outline.
+             */
+            public static final String OUTLINE = "OUTLINE";
+        }
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/ui/ExoPlayerWrapper.java b/src/com/android/providers/media/photopicker/ui/ExoPlayerWrapper.java
index fe36d35..fd891a4 100644
--- a/src/com/android/providers/media/photopicker/ui/ExoPlayerWrapper.java
+++ b/src/com/android/providers/media/photopicker/ui/ExoPlayerWrapper.java
@@ -34,7 +34,7 @@
 import com.google.android.exoplayer2.LoadControl;
 import com.google.android.exoplayer2.MediaItem;
 import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.analytics.AnalyticsCollector;
+import com.google.android.exoplayer2.analytics.DefaultAnalyticsCollector;
 import com.google.android.exoplayer2.source.MediaParserExtractorAdapter;
 import com.google.android.exoplayer2.source.ProgressiveMediaSource;
 import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
@@ -126,11 +126,11 @@
 
         return new ExoPlayer.Builder(mContext,
                 new DefaultRenderersFactory(mContext),
-                new DefaultTrackSelector(mContext),
                 mediaSourceFactory,
+                new DefaultTrackSelector(mContext),
                 sLoadControl,
                 DefaultBandwidthMeter.getSingletonInstance(mContext),
-                new AnalyticsCollector(Clock.DEFAULT)).buildExoPlayer();
+                new DefaultAnalyticsCollector(Clock.DEFAULT)).build();
     }
 
     private void setupPlayerLayout(StyledPlayerView styledPlayerView, ImageView imageView) {
diff --git a/src/com/android/providers/media/photopicker/ui/ImageLoader.java b/src/com/android/providers/media/photopicker/ui/ImageLoader.java
index 9c6fc94..d629471 100644
--- a/src/com/android/providers/media/photopicker/ui/ImageLoader.java
+++ b/src/com/android/providers/media/photopicker/ui/ImageLoader.java
@@ -20,25 +20,29 @@
 import android.graphics.ImageDecoder;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
+import android.provider.CloudMediaProviderContract;
 import android.provider.MediaStore;
 import android.util.Log;
 import android.widget.ImageView;
 
 import androidx.annotation.NonNull;
 
-import com.bumptech.glide.Glide;
-import com.bumptech.glide.request.RequestOptions;
-import com.bumptech.glide.signature.ObjectKey;
-
 import com.android.providers.media.photopicker.data.model.Category;
 import com.android.providers.media.photopicker.data.model.Item;
 
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.Option;
+import com.bumptech.glide.request.RequestOptions;
+import com.bumptech.glide.signature.ObjectKey;
+
 /**
  * A class to assist with loading and managing the Images (i.e. thumbnails and preview) associated
  * with item.
  */
 public class ImageLoader {
 
+    public static final Option<Boolean> THUMBNAIL_REQUEST =
+            Option.memory(CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB, false);
     private static final String TAG = "ImageLoader";
     private final Context mContext;
 
@@ -59,7 +63,7 @@
         Glide.with(mContext)
                 .asBitmap()
                 .load(category.getCoverUri())
-                .thumbnail()
+                .apply(RequestOptions.option(THUMBNAIL_REQUEST, true))
                 .into(imageView);
     }
 
@@ -78,7 +82,7 @@
                 .asBitmap()
                 .load(uri)
                 .signature(getGlideSignature(item, /* prefix */ ""))
-                .thumbnail()
+                .apply(RequestOptions.option(THUMBNAIL_REQUEST, true))
                 .into(imageView);
     }
 
@@ -91,6 +95,7 @@
     public void loadImagePreview(@NonNull Item item, @NonNull ImageView imageView)  {
         if (item.isGif()) {
             Glide.with(mContext)
+                    .asGif()
                     .load(item.getContentUri())
                     .signature(getGlideSignature(item, /* prefix */ ""))
                     .into(imageView);
diff --git a/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java b/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
index e28ef87..8ce7311 100644
--- a/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
+++ b/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
@@ -42,8 +42,7 @@
     private final ImageLoader mImageLoader;
     private final RemotePreviewHandler mRemotePreviewHandler;
     private final PlaybackHandler mPlaybackHandler;
-    private final boolean mIsRemotePreviewEnabled =
-            RemotePreviewHandler.isRemotePreviewEnabled();
+    private final boolean mIsRemotePreviewEnabled = RemotePreviewHandler.isRemotePreviewEnabled();
 
     PreviewAdapter(Context context, MuteStatus muteStatus) {
         mImageLoader = new ImageLoader(context);
diff --git a/src/com/android/providers/media/photopicker/ui/ProfileDialogFragment.java b/src/com/android/providers/media/photopicker/ui/ProfileDialogFragment.java
index 630029c..8ae8e1a 100644
--- a/src/com/android/providers/media/photopicker/ui/ProfileDialogFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/ProfileDialogFragment.java
@@ -16,16 +16,29 @@
 
 package com.android.providers.media.photopicker.ui;
 
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Drawables.Style.OUTLINE;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Drawables.WORK_PROFILE_ICON;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Strings.BLOCKED_BY_ADMIN_TITLE;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Strings.BLOCKED_FROM_PERSONAL_MESSAGE;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Strings.BLOCKED_FROM_WORK_MESSAGE;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Strings.WORK_PROFILE_PAUSED_MESSAGE;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Strings.WORK_PROFILE_PAUSED_TITLE;
+
 import android.app.Dialog;
+import android.app.admin.DevicePolicyManager;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
 import android.os.Bundle;
 import android.util.Log;
 
+import androidx.annotation.RequiresApi;
 import androidx.fragment.app.DialogFragment;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentTransaction;
 import androidx.lifecycle.ViewModelProvider;
 
+import com.android.modules.utils.build.SdkLevel;
 import com.android.providers.media.R;
 import com.android.providers.media.photopicker.data.UserIdManager;
 import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
@@ -41,24 +54,11 @@
         final PickerViewModel pickerViewModel = new ViewModelProvider(requireActivity()).get(
                 PickerViewModel.class);
         final UserIdManager userIdManager = pickerViewModel.getUserIdManager();
-
         final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity());
         if (userIdManager.isBlockedByAdmin()) {
-            builder.setIcon(R.drawable.ic_lock);
-            builder.setTitle(getString(R.string.picker_profile_admin_title));
-            final String message = userIdManager.isManagedUserSelected() ?
-                    getString(R.string.picker_profile_admin_msg_from_work) :
-                    getString(R.string.picker_profile_admin_msg_from_personal);
-            builder.setMessage(message);
-            builder.setPositiveButton(android.R.string.ok, null);
+            setBlockedByAdminParams(userIdManager.isManagedUserSelected(), builder);
         } else if (userIdManager.isWorkProfileOff()) {
-            builder.setIcon(R.drawable.ic_work_outline);
-            builder.setTitle(getString(R.string.picker_profile_work_paused_title));
-            builder.setMessage(getString(R.string.picker_profile_work_paused_msg));
-            // TODO(b/197199728): Add listener to turn on apps. This maybe a bit tricky because
-            // after turning on Work profile, work profile MediaProvider may not be available
-            // immediately.
-            builder.setPositiveButton(android.R.string.ok, null);
+            setWorkProfileOffParams(builder);
         } else {
             Log.e(TAG, "Unknown error for profile dialog");
             return null;
@@ -66,6 +66,68 @@
         return builder.create();
     }
 
+    private void setBlockedByAdminParams(
+            boolean isManagedUserSelected, MaterialAlertDialogBuilder builder) {
+        String title;
+        String message;
+        if (SdkLevel.isAtLeastT()) {
+            title = getUpdatedEnterpriseString(
+                    BLOCKED_BY_ADMIN_TITLE, R.string.picker_profile_admin_title);
+            message = isManagedUserSelected
+                    ? getUpdatedEnterpriseString(
+                            BLOCKED_FROM_WORK_MESSAGE, R.string.picker_profile_admin_msg_from_work)
+                    : getUpdatedEnterpriseString(
+                            BLOCKED_FROM_PERSONAL_MESSAGE,
+                            R.string.picker_profile_admin_msg_from_personal);
+        } else {
+            title = getString(R.string.picker_profile_admin_title);
+            message = isManagedUserSelected
+                    ? getString(R.string.picker_profile_admin_msg_from_work)
+                    : getString(R.string.picker_profile_admin_msg_from_personal);
+        }
+        builder.setIcon(R.drawable.ic_lock);
+        builder.setTitle(title);
+        builder.setMessage(message);
+        builder.setPositiveButton(android.R.string.ok, null);
+    }
+
+    private void setWorkProfileOffParams(MaterialAlertDialogBuilder builder) {
+        Drawable icon;
+        String title;
+        String message;
+        if (SdkLevel.isAtLeastT()) {
+            icon = getUpdatedWorkProfileIcon();
+            title = getUpdatedEnterpriseString(
+                    WORK_PROFILE_PAUSED_TITLE, R.string.picker_profile_work_paused_title);
+            message = getUpdatedEnterpriseString(
+                    WORK_PROFILE_PAUSED_MESSAGE, R.string.picker_profile_work_paused_msg);
+        } else {
+            icon = getContext().getDrawable(R.drawable.ic_work_outline);
+            title = getContext().getString(R.string.picker_profile_work_paused_title);
+            message = getContext().getString(R.string.picker_profile_work_paused_msg);
+        }
+        builder.setIcon(icon);
+        builder.setTitle(title);
+        builder.setMessage(message);
+        // TODO(b/197199728): Add listener to turn on apps. This maybe a bit tricky because
+        // after turning on Work profile, work profile MediaProvider may not be available
+        // immediately.
+        builder.setPositiveButton(android.R.string.ok, null);
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    private String getUpdatedEnterpriseString(String updatableStringId, int defaultStringId) {
+        final DevicePolicyManager dpm = getContext().getSystemService(DevicePolicyManager.class);
+        return dpm.getResources().getString(updatableStringId, () -> getString(defaultStringId));
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    private Drawable getUpdatedWorkProfileIcon() {
+        final DevicePolicyManager dpm = getContext().getSystemService(DevicePolicyManager.class);
+        return dpm.getResources().getDrawable(WORK_PROFILE_ICON, OUTLINE, () ->
+                getContext().getDrawable(R.drawable.ic_work_outline));
+    }
+
     public static void show(FragmentManager fm) {
         FragmentTransaction ft = fm.beginTransaction();
         Fragment f = new ProfileDialogFragment();
diff --git a/src/com/android/providers/media/photopicker/ui/TabFragment.java b/src/com/android/providers/media/photopicker/ui/TabFragment.java
index 19d2ee6..d037ba7 100644
--- a/src/com/android/providers/media/photopicker/ui/TabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/TabFragment.java
@@ -15,9 +15,17 @@
  */
 package com.android.providers.media.photopicker.ui;
 
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Drawables.Style.OUTLINE;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Drawables.WORK_PROFILE_ICON;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Strings.SWITCH_TO_PERSONAL_MESSAGE;
+import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Strings.SWITCH_TO_WORK_MESSAGE;
+
+import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
 import android.os.Bundle;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
@@ -32,12 +40,14 @@
 import androidx.annotation.ColorInt;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 import androidx.fragment.app.Fragment;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 import androidx.lifecycle.ViewModelProvider;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.android.modules.utils.build.SdkLevel;
 import com.android.providers.media.R;
 import com.android.providers.media.photopicker.PhotoPickerActivity;
 import com.android.providers.media.photopicker.data.Selection;
@@ -297,17 +307,56 @@
     }
 
     private void updateProfileButtonContent(boolean isManagedUserSelected) {
-        final int iconResId;
-        final int textResId;
+        final Drawable icon;
+        final String text;
         if (isManagedUserSelected) {
-            iconResId = R.drawable.ic_personal_mode;
-            textResId = R.string.picker_personal_profile;
+            icon = getContext().getDrawable(R.drawable.ic_personal_mode);
+            text = getSwitchToPersonalMessage();
         } else {
-            iconResId = R.drawable.ic_work_outline;
-            textResId = R.string.picker_work_profile;
+            icon = getWorkProfileIcon();
+            text = getSwitchToWorkMessage();
         }
-        mProfileButton.setIconResource(iconResId);
-        mProfileButton.setText(textResId);
+        mProfileButton.setIcon(icon);
+        mProfileButton.setText(text);
+    }
+
+    private String getSwitchToPersonalMessage() {
+        if (SdkLevel.isAtLeastT()) {
+            return getUpdatedEnterpriseString(
+                    SWITCH_TO_PERSONAL_MESSAGE, R.string.picker_personal_profile);
+        } else {
+            return getContext().getString(R.string.picker_personal_profile);
+        }
+    }
+
+    private String getSwitchToWorkMessage() {
+        if (SdkLevel.isAtLeastT()) {
+            return getUpdatedEnterpriseString(
+                    SWITCH_TO_WORK_MESSAGE, R.string.picker_work_profile);
+        } else {
+            return getContext().getString(R.string.picker_work_profile);
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    private String getUpdatedEnterpriseString(String updatableStringId, int defaultStringId) {
+        final DevicePolicyManager dpm = getContext().getSystemService(DevicePolicyManager.class);
+        return dpm.getResources().getString(updatableStringId, () -> getString(defaultStringId));
+    }
+
+    private Drawable getWorkProfileIcon() {
+        if (SdkLevel.isAtLeastT()) {
+            return getUpdatedWorkProfileIcon();
+        } else {
+            return getContext().getDrawable(R.drawable.ic_work_outline);
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    private Drawable getUpdatedWorkProfileIcon() {
+        DevicePolicyManager dpm = getContext().getSystemService(DevicePolicyManager.class);
+        return dpm.getResources().getDrawable(WORK_PROFILE_ICON, OUTLINE, () ->
+                getContext().getDrawable(R.drawable.ic_work_outline));
     }
 
     private void updateProfileButtonColor(boolean isDisabled) {
diff --git a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java
index 7b299ba..ccf2885 100644
--- a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java
+++ b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java
@@ -16,6 +16,7 @@
 
 package com.android.providers.media.photopicker.ui.remotepreview;
 
+import static android.provider.CloudMediaProviderContract.EXTRA_AUTHORITY;
 import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED;
 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER;
 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED;
@@ -41,6 +42,7 @@
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 
+import com.android.providers.media.photopicker.PickerSyncController;
 import com.android.providers.media.photopicker.data.MuteStatus;
 import com.android.providers.media.photopicker.data.model.Item;
 import com.android.providers.media.photopicker.ui.PreviewVideoHolder;
@@ -75,8 +77,11 @@
     private boolean mIsInBackground = false;
     private int mSurfaceCounter = 0;
 
+    /**
+     * Returns {@code true} if remote preview is enabled.
+     */
     public static boolean isRemotePreviewEnabled() {
-        return SystemProperties.getBoolean("sys.photopicker.remote_preview", false);
+        return SystemProperties.getBoolean("sys.photopicker.remote_preview", true);
     }
 
     public RemotePreviewHandler(Context context, MuteStatus muteStatus) {
@@ -89,26 +94,16 @@
      *
      * @param viewHolder {@link PreviewVideoHolder} for the media item under preview
      * @param item       {@link Item} to be previewed
-     * @return true if the given {@link Item} can be previewed remotely, else false
      */
-    public boolean onViewAttachedToWindow(PreviewVideoHolder viewHolder, Item item) {
-        RemotePreviewSession session = createRemotePreviewSession(item, viewHolder);
-        if (session == null) {
-            Log.w(TAG, "Failed to create RemotePreviewSession.");
-            return false;
-        }
+    public void onViewAttachedToWindow(PreviewVideoHolder viewHolder, Item item) {
+        final RemotePreviewSession session = createRemotePreviewSession(item, viewHolder);
+        final SurfaceHolder holder = viewHolder.getSurfaceHolder();
 
-        SurfaceHolder holder = viewHolder.getSurfaceHolder();
         mSessionMap.put(holder, session);
         // Ensure that we don't add the same callback twice, since we don't remove callbacks
         // anywhere else.
         holder.removeCallback(mSurfaceHolderCallback);
         holder.addCallback(mSurfaceHolderCallback);
-
-        mCurrentPreviewState.item = item;
-        mCurrentPreviewState.viewHolder = viewHolder;
-
-        return true;
     }
 
     /**
@@ -134,6 +129,9 @@
             return false;
         }
 
+        mCurrentPreviewState.item = item;
+        mCurrentPreviewState.viewHolder = session.getPreviewVideoHolder();
+
         session.requestPlayMedia();
         return true;
     }
@@ -158,9 +156,11 @@
     private RemotePreviewSession createRemotePreviewSession(Item item,
             PreviewVideoHolder previewVideoHolder) {
         String authority = item.getContentUri().getAuthority();
-        SurfaceControllerProxy controller = getSurfaceController(authority);
+        SurfaceControllerProxy controller = getSurfaceController(authority, false);
         if (controller == null) {
-            return null;
+            Log.w(TAG, "Failed to create RemotePreviewSession for " + authority
+                    + ". Fallback to openPreview");
+            controller = getSurfaceController(authority, true);
         }
 
         return new RemotePreviewSession(mSurfaceCounter++, item.getId(), authority, controller,
@@ -175,7 +175,7 @@
         }
 
         mSessionMap.put(holder, session);
-        session.surfaceCreated(holder.getSurface());
+        session.surfaceCreated();
         session.requestPlayMedia();
     }
 
@@ -200,14 +200,15 @@
     }
 
     @Nullable
-    private SurfaceControllerProxy getSurfaceController(String authority) {
+    private SurfaceControllerProxy getSurfaceController(String authority,
+            boolean localControllerFallback) {
         if (mControllers.containsKey(authority)) {
             return mControllers.get(authority);
         }
 
         SurfaceControllerProxy controller = null;
         try {
-            controller = createController(authority);
+            controller = createController(authority, localControllerFallback);
             if (controller != null) {
                 mControllers.put(authority, controller);
             }
@@ -228,12 +229,20 @@
         mControllers.clear();
     }
 
-    private SurfaceControllerProxy createController(String authority) {
-        Log.i(TAG, "Creating new SurfaceController for authority: " + authority);
+    private SurfaceControllerProxy createController(String authority,
+            boolean localControllerFallback) {
+        Log.i(TAG, "Creating new SurfaceController for authority: " + authority
+                + ". localControllerFallback: " + localControllerFallback);
         Bundle extras = new Bundle();
         extras.putBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, true);
         extras.putBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED, mMuteStatus.isVolumeMuted());
         extras.putBinder(EXTRA_SURFACE_STATE_CALLBACK, mSurfaceStateChangedCallbackWrapper);
+
+        if (localControllerFallback) {
+            extras.putString(EXTRA_AUTHORITY, authority);
+            authority = PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
+        }
+
         final Bundle surfaceControllerBundle = mContext.getContentResolver().call(
                 createSurfaceControllerUri(authority),
                 METHOD_CREATE_SURFACE_CONTROLLER, /* arg */ null, extras);
@@ -284,7 +293,7 @@
 
             Surface surface = holder.getSurface();
             RemotePreviewSession session = mSessionMap.get(holder);
-            session.surfaceCreated(surface);
+            session.surfaceCreated();
         }
 
         @Override
diff --git a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java
index b3bc2b6..4cd7e8e 100644
--- a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java
+++ b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java
@@ -33,7 +33,6 @@
 import android.os.RemoteException;
 import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PlaybackState;
 import android.util.Log;
-import android.view.Surface;
 import android.view.View;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
@@ -89,7 +88,9 @@
     private final AccessibilityStateChangeListener mAccessibilityStateChangeListener =
             this::updateAccessibilityState;
 
+    private SurfaceChangeData mSurfaceChangeData;
     private boolean mIsSurfaceCreated = false;
+    private boolean mIsSurfaceCreationNotified = false;
     private boolean mIsPlaybackRequested = false;
     @PlaybackState
     private int mCurrentPlaybackState = PLAYBACK_STATE_BUFFERING;
@@ -126,20 +127,36 @@
         return mAuthority;
     }
 
-    void surfaceCreated(@NonNull Surface surface) {
+    @NonNull
+    PreviewVideoHolder getPreviewVideoHolder() {
+        return mPreviewVideoHolder;
+    }
+
+    void surfaceCreated() {
         if (mIsSurfaceCreated) {
             throw new IllegalStateException("Surface is already created.");
         }
-
-        if (surface == null) {
-            throw new IllegalStateException("surfaceCreated() called with null surface.");
+        if (mIsSurfaceCreationNotified) {
+            throw new IllegalStateException(
+                    "Surface creation has been already notified to SurfaceController.");
         }
 
-        try {
-            mSurfaceController.onSurfaceCreated(mSurfaceId, surface, mMediaId);
-            mIsSurfaceCreated = true;
-        } catch (RemoteException e) {
-            Log.e(TAG, "Failure in onSurfaceCreated().", e);
+        mIsSurfaceCreated = true;
+
+        // Notify surface creation only if playback has been already requested, else this will be
+        // done in requestPlayMedia() when playback is explicitly requested.
+        if (mIsPlaybackRequested) {
+            notifySurfaceCreated();
+        }
+    }
+
+    void surfaceChanged(int format, int width, int height) {
+        mSurfaceChangeData = new SurfaceChangeData(format, width, height);
+
+        // Notify surface change only if playback has been already requested, else this will be
+        // done in requestPlayMedia() when playback is explicitly requested.
+        if (mIsPlaybackRequested) {
+            notifySurfaceChanged();
         }
     }
 
@@ -148,8 +165,16 @@
             throw new IllegalStateException("Surface is not created.");
         }
 
+        mSurfaceChangeData = null;
+
         tearDownUI();
 
+        if (!mIsSurfaceCreationNotified) {
+            // If we haven't notified surface creation yet, then no need to notify surface
+            // destruction either.
+            return;
+        }
+
         try {
             mSurfaceController.onSurfaceDestroyed(mSurfaceId);
         } catch (RemoteException e) {
@@ -157,18 +182,6 @@
         }
     }
 
-    void surfaceChanged(int format, int width, int height) {
-        if (!mIsSurfaceCreated) {
-            throw new IllegalStateException("Surface is not created.");
-        }
-
-        try {
-            mSurfaceController.onSurfaceChanged(mSurfaceId, format, width, height);
-        } catch (RemoteException e) {
-            Log.e(TAG, "Failure in onSurfaceChanged().", e);
-        }
-    }
-
     void requestPlayMedia() {
         // When the user is at the first item in ViewPager, swiping further right would trigger the
         // callback {@link ViewPager2.PageTransformer#transforPage(View, int)}, which would call
@@ -186,6 +199,15 @@
             return;
         }
 
+        // Now that playback has been requested, try to notify surface creation and surface change
+        // so that player can be prepared with the surface.
+        if (mIsSurfaceCreated) {
+            notifySurfaceCreated();
+        }
+        if (mSurfaceChangeData != null) {
+            notifySurfaceChanged();
+        }
+
         mIsPlaybackRequested = true;
     }
 
@@ -219,10 +241,54 @@
         }
     }
 
+    private void notifySurfaceCreated() {
+        if (!mIsSurfaceCreated) {
+            throw new IllegalStateException("Surface is not created.");
+        }
+        if (mIsSurfaceCreationNotified) {
+            throw new IllegalStateException(
+                    "Surface creation has already been notified to SurfaceController.");
+        }
+
+        try {
+            mSurfaceController.onSurfaceCreated(mSurfaceId,
+                    mPreviewVideoHolder.getSurfaceHolder().getSurface(), mMediaId);
+            mIsSurfaceCreationNotified = true;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failure in notifySurfaceCreated().", e);
+        }
+    }
+
+    private void notifySurfaceChanged() {
+        if (!mIsSurfaceCreated) {
+            throw new IllegalStateException("Surface is not created.");
+        }
+        if (!mIsSurfaceCreationNotified) {
+            throw new IllegalStateException(
+                    "Surface creation has not been notified to SurfaceController.");
+        }
+
+        if (mSurfaceChangeData == null) {
+            throw new IllegalStateException("No surface change data present.");
+        }
+
+        try {
+            mSurfaceController.onSurfaceChanged(mSurfaceId, mSurfaceChangeData.getFormat(),
+                    mSurfaceChangeData.getWidth(), mSurfaceChangeData.getHeight());
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failure in notifySurfaceChanged().", e);
+        }
+    }
+
     private void playMedia() {
         if (!mIsSurfaceCreated) {
             throw new IllegalStateException("Surface is not created.");
         }
+        if (!mIsSurfaceCreationNotified) {
+            throw new IllegalStateException(
+                    "Surface creation has not been notified to SurfaceController.");
+        }
+
         if (mCurrentPlaybackState == PLAYBACK_STATE_STARTED) {
             throw new IllegalStateException("Player is already playing.");
         }
@@ -238,6 +304,11 @@
         if (!mIsSurfaceCreated) {
             throw new IllegalStateException("Surface is not created.");
         }
+        if (!mIsSurfaceCreationNotified) {
+            throw new IllegalStateException(
+                    "Surface creation has not been notified to SurfaceController.");
+        }
+
         if (mCurrentPlaybackState != PLAYBACK_STATE_STARTED) {
             throw new IllegalStateException("Player is not playing.");
         }
@@ -269,9 +340,8 @@
     }
 
     private void initUI() {
-        // We hide the player view and show the thumbnail till the player is ready and we know the
-        // media size. However, since we want the surface to be created, we cannot use View.GONE
-        // here.
+        // We show the thumbnail view till the player is ready and when we know the
+        // media size, then we hide the thumbnail view.
         mPreviewVideoHolder.getPlayerContainer().setVisibility(View.INVISIBLE);
         mPreviewVideoHolder.getThumbnailView().setVisibility(View.VISIBLE);
         mPreviewVideoHolder.getPlayerControlsRoot().setVisibility(View.GONE);
@@ -333,4 +403,29 @@
                 visible ? View.VISIBLE : View.GONE);
         mPlayerControlsVisibilityStatus.setShouldShowPlayerControlsForNextItem(visible);
     }
+
+    private static final class SurfaceChangeData {
+
+        private int mFormat;
+        private int mWidth;
+        private int mHeight;
+
+        SurfaceChangeData(int format, int width, int height) {
+            mFormat = format;
+            mWidth = width;
+            mHeight = height;
+        }
+
+        int getFormat() {
+            return mFormat;
+        }
+
+        int getWidth() {
+            return mWidth;
+        }
+
+        int getHeight() {
+            return mHeight;
+        }
+    }
 }
diff --git a/src/com/android/providers/media/photopicker/ui/remotepreview/RemoteSurfaceController.java b/src/com/android/providers/media/photopicker/ui/remotepreview/RemoteSurfaceController.java
new file mode 100644
index 0000000..37a5921
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/ui/remotepreview/RemoteSurfaceController.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2022 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.photopicker.ui.remotepreview;
+
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceController;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_BUFFERING;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_COMPLETED;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_MEDIA_SIZE_CHANGED;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_PAUSED;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_READY;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_STARTED;
+import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED;
+import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED;
+
+import android.annotation.DurationMillisLong;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Point;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.Surface;
+
+import androidx.annotation.NonNull;
+
+import com.android.providers.media.PickerUriResolver;
+
+import com.google.android.exoplayer2.DefaultLoadControl;
+import com.google.android.exoplayer2.DefaultRenderersFactory;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.LoadControl;
+import com.google.android.exoplayer2.MediaItem;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Player.State;
+import com.google.android.exoplayer2.analytics.DefaultAnalyticsCollector;
+import com.google.android.exoplayer2.source.MediaParserExtractorAdapter;
+import com.google.android.exoplayer2.source.ProgressiveMediaSource;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.upstream.ContentDataSource;
+import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
+import com.google.android.exoplayer2.util.Clock;
+import com.google.android.exoplayer2.video.VideoSize;
+
+/**
+ * Implements a {@link CloudMediaSurfaceController} for a cloud provider authority and initializes
+ * an ExoPlayer instance to render cloud media to {@link Surface} instances.
+ */
+public class RemoteSurfaceController extends CloudMediaSurfaceController {
+    private static final String TAG = "RemoteSurfaceController";
+
+    // The minimum duration of media that the player will attempt to ensure is buffered at all
+    // times.
+    private static final int MIN_BUFFER_MS = 1000;
+    // The maximum duration of media that the player will attempt to buffer.
+    private static final int MAX_BUFFER_MS = 2000;
+    // The duration of media that must be buffered for playback to start or resume following a
+    // user action such as a seek.
+    private static final int BUFFER_FOR_PLAYBACK_MS = 1000;
+    // The default duration of media that must be buffered for playback to resume after a
+    // rebuffer.
+    private static final int BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 1000;
+    private static final LoadControl sLoadControl = new DefaultLoadControl.Builder()
+            .setBufferDurationsMs(
+                    MIN_BUFFER_MS,
+                    MAX_BUFFER_MS,
+                    BUFFER_FOR_PLAYBACK_MS,
+                    BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS).build();
+
+    private final String mAuthority;
+    private final Context mContext;
+    private final CloudMediaSurfaceStateChangedCallback mCallback;
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private final Player.Listener mEventListener = new Player.Listener() {
+            @Override
+            public void onPlaybackStateChanged(@State int state) {
+                Log.d(TAG, "Received player event " + state);
+
+                switch (state) {
+                    case Player.STATE_READY:
+                        mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_READY,
+                                null);
+                        return;
+                    case Player.STATE_BUFFERING:
+                        mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_BUFFERING,
+                                null);
+                        return;
+                    case Player.STATE_ENDED:
+                        mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_COMPLETED,
+                                null);
+                        return;
+                    default:
+                }
+            }
+
+            @Override
+            public void onIsPlayingChanged(boolean isPlaying) {
+                mCallback.setPlaybackState(mCurrentSurfaceId, isPlaying ? PLAYBACK_STATE_STARTED :
+                        PLAYBACK_STATE_PAUSED, null);
+            }
+
+            @Override
+            public void onVideoSizeChanged(VideoSize videoSize) {
+                Point size = new Point(videoSize.width, videoSize.height);
+                Bundle bundle = new Bundle();
+                bundle.putParcelable(ContentResolver.EXTRA_SIZE, size);
+                mCallback.setPlaybackState(mCurrentSurfaceId, PLAYBACK_STATE_MEDIA_SIZE_CHANGED,
+                        bundle);
+            }
+        };
+
+    private boolean mEnableLoop;
+    private boolean mMuteAudio;
+    private ExoPlayer mPlayer;
+    private int mCurrentSurfaceId = -1;
+
+    public RemoteSurfaceController(Context context, String authority, boolean enableLoop,
+            boolean muteAudio, CloudMediaSurfaceStateChangedCallback callback) {
+        mAuthority = authority;
+        mCallback = callback;
+        mContext = context;
+        mEnableLoop = enableLoop;
+        mMuteAudio = muteAudio;
+        Log.d(TAG, "Surface controller created.");
+    }
+
+    @Override
+    public void onPlayerCreate() {
+        mHandler.post(() -> {
+            mPlayer = createExoPlayer();
+            mPlayer.addListener(mEventListener);
+            updateLoopingPlaybackStatus();
+            updateAudioMuteStatus();
+            Log.d(TAG, "Player created.");
+        });
+    }
+
+    @Override
+    public void onPlayerRelease() {
+        mHandler.post(() -> {
+            mPlayer.removeListener(mEventListener);
+            mPlayer.release();
+            mPlayer = null;
+            Log.d(TAG, "Player released.");
+        });
+    }
+
+    @Override
+    public void onSurfaceCreated(int surfaceId, @NonNull Surface surface,
+            @NonNull String mediaId) {
+        mHandler.post(() -> {
+            try {
+                // onSurfaceCreated may get called while the player is already rendering on a
+                // different surface. In that case, pause the player before preparing it for
+                // rendering on the new surface.
+                // Unfortunately, Exoplayer#stop doesn't seem to work here. If we call stop(),
+                // as soon as the player becomes ready again, it automatically starts to play
+                // the new media. The reason is that Exoplayer treats play/pause as calls to
+                // the method Exoplayer#setPlayWhenReady(boolean) with true and false
+                // respectively. So, if we don't pause(), then since the previous play() call
+                // had set setPlayWhenReady to true, the player would start the playback as soon
+                // as it gets ready with the new media item.
+                if (mPlayer.isPlaying()) {
+                    mPlayer.pause();
+                }
+
+                mCurrentSurfaceId = surfaceId;
+
+                final Uri mediaUri = PickerUriResolver.getMediaUri(mAuthority).buildUpon()
+                        .appendPath(mediaId).build();
+                mPlayer.setMediaItem(MediaItem.fromUri(mediaUri));
+                mPlayer.setVideoSurface(surface);
+                mPlayer.prepare();
+
+                Log.d(TAG, "Surface prepared: " + surfaceId + ". Surface: " + surface
+                        + ". MediaId: " + mediaId);
+            } catch (RuntimeException e) {
+                Log.e(TAG, "Error preparing player with surface.", e);
+            }
+        });
+    }
+
+    @Override
+    public void onSurfaceChanged(int surfaceId, int format, int width, int height) {
+        Log.d(TAG, "Surface changed: " + surfaceId + ". Format: " + format + ". Width: "
+                + width + ". Height: " + height);
+    }
+
+    @Override
+    public void onSurfaceDestroyed(int surfaceId) {
+        mHandler.post(() -> {
+            if (mCurrentSurfaceId != surfaceId) {
+                // This means that the player is already using some other surface, hence
+                // nothing to do.
+                return;
+            }
+            if (mPlayer.isPlaying()) {
+                mPlayer.stop();
+            }
+            mPlayer.clearVideoSurface();
+            mCurrentSurfaceId = -1;
+
+            Log.d(TAG, "Surface released: " + surfaceId);
+        });
+    }
+
+    @Override
+    public void onMediaPlay(int surfaceId) {
+        mHandler.post(() -> {
+            mPlayer.play();
+            Log.d(TAG, "Media played: " + surfaceId);
+        });
+    }
+
+    @Override
+    public void onMediaPause(int surfaceId) {
+        mHandler.post(() -> {
+            if (mPlayer.isPlaying()) {
+                mPlayer.pause();
+                Log.d(TAG, "Media paused: " + surfaceId);
+            }
+        });
+    }
+
+    @Override
+    public void onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis) {
+        mHandler.post(() -> {
+            mPlayer.seekTo((int) timestampMillis);
+            Log.d(TAG, "Media seeked: " + surfaceId + ". Timestamp: " + timestampMillis);
+        });
+    }
+
+    @Override
+    public void onConfigChange(@NonNull Bundle config) {
+        final boolean enableLoop = config.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED,
+                mEnableLoop);
+        final boolean muteAudio = config.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED,
+                mMuteAudio);
+        mHandler.post(() -> {
+            if (mEnableLoop != enableLoop) {
+                mEnableLoop = enableLoop;
+                updateLoopingPlaybackStatus();
+            }
+
+            if (mMuteAudio != muteAudio) {
+                mMuteAudio = muteAudio;
+                updateAudioMuteStatus();
+            }
+        });
+        Log.d(TAG, "Config changed. Updated config params: " + config);
+    }
+
+    @Override
+    public void onDestroy() {
+        Log.d(TAG, "Surface controller destroyed.");
+    }
+
+    private ExoPlayer createExoPlayer() {
+        // ProgressiveMediaFactory will be enough for video playback of videos on the device.
+        // This also reduces apk size.
+        ProgressiveMediaSource.Factory mediaSourceFactory = new ProgressiveMediaSource.Factory(
+                () -> new ContentDataSource(mContext), MediaParserExtractorAdapter.FACTORY);
+
+        return new ExoPlayer.Builder(mContext,
+                new DefaultRenderersFactory(mContext),
+                mediaSourceFactory,
+                new DefaultTrackSelector(mContext),
+                sLoadControl,
+                DefaultBandwidthMeter.getSingletonInstance(mContext),
+                new DefaultAnalyticsCollector(Clock.DEFAULT)).build();
+    }
+
+    private void updateLoopingPlaybackStatus() {
+        mPlayer.setRepeatMode(mEnableLoop ? Player.REPEAT_MODE_ONE : Player.REPEAT_MODE_OFF);
+    }
+
+    private void updateAudioMuteStatus() {
+        if (mMuteAudio) {
+            mPlayer.setVolume(0f);
+        } else {
+            AudioManager audioManager = mContext.getSystemService(AudioManager.class);
+            if (audioManager == null) {
+                Log.e(TAG, "Couldn't find AudioManager while trying to set volume,"
+                        + " unable to set volume");
+                return;
+            }
+            mPlayer.setVolume(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC));
+        }
+    }
+}
diff --git a/src/com/android/providers/media/util/DeviceConfigUtils.java b/src/com/android/providers/media/util/DeviceConfigUtils.java
deleted file mode 100644
index 7740d1d..0000000
--- a/src/com/android/providers/media/util/DeviceConfigUtils.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.providers.media.util;
-
-import static com.android.providers.media.util.Logging.TAG;
-
-import android.os.Binder;
-import android.provider.DeviceConfig;
-import android.util.Log;
-import com.android.modules.utils.build.SdkLevel;
-
-public class DeviceConfigUtils {
-
-    public static String getStringDeviceConfig(String key, String defaultValue) {
-        if (!canReadDeviceConfig(key, defaultValue)) {
-            return defaultValue;
-        }
-
-        final long token = Binder.clearCallingIdentity();
-        try {
-            return DeviceConfig.getString(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT, key,
-                    defaultValue);
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    private static <T> boolean canReadDeviceConfig(String key, T defaultValue) {
-        if (SdkLevel.isAtLeastS()) {
-            return true;
-        }
-
-        Log.w(TAG, "Cannot read device config before Android S. Returning defaultValue: "
-                + defaultValue + " for key: " + key);
-        return false;
-    }
-}
diff --git a/src/com/android/providers/media/util/LongArray.java b/src/com/android/providers/media/util/LongArray.java
index 630b41f..7ea750e 100644
--- a/src/com/android/providers/media/util/LongArray.java
+++ b/src/com/android/providers/media/util/LongArray.java
@@ -31,7 +31,7 @@
 
     private  LongArray(long[] array, int size) {
         mValues = array;
-        mSize = checkArgumentInRange(size, 0, array.length, "size");
+        mSize = Preconditions.checkArgumentInRange(size, 0, array.length, "size");
     }
 
     /**
@@ -73,7 +73,7 @@
      * created from the current content of this LongArray padded with 0s.
      */
     public void resize(int newSize) {
-        checkArgumentNonnegative(newSize);
+        Preconditions.checkArgumentNonnegative(newSize);
         if (newSize <= mValues.length) {
             Arrays.fill(mValues, newSize, mValues.length, 0);
         } else {
@@ -222,29 +222,6 @@
         return true;
     }
 
-    public static int checkArgumentNonnegative(final int value) {
-        if (value < 0) {
-            throw new IllegalArgumentException();
-        }
-
-        return value;
-    }
-
-    public static int checkArgumentInRange(int value, int lower, int upper,
-            String valueName) {
-        if (value < lower) {
-            throw new IllegalArgumentException(
-                    String.format(
-                            "%s is out of range of [%d, %d] (too low)", valueName, lower, upper));
-        } else if (value > upper) {
-            throw new IllegalArgumentException(
-                    String.format(
-                            "%s is out of range of [%d, %d] (too high)", valueName, lower, upper));
-        }
-
-        return value;
-    }
-
     public static void checkBounds(int len, int index) {
         if (index < 0 || len <= index) {
             throw new ArrayIndexOutOfBoundsException("length=" + len + "; index=" + index);
diff --git a/src/com/android/providers/media/util/Memory.java b/src/com/android/providers/media/util/Memory.java
index 355cabe..f2306e3 100644
--- a/src/com/android/providers/media/util/Memory.java
+++ b/src/com/android/providers/media/util/Memory.java
@@ -46,4 +46,54 @@
             dst[offset  ] = (byte) ((value >> 24) & 0xff);
         }
     }
+
+    public static long peekLong(byte[] src, int offset, ByteOrder order) {
+        if (order == ByteOrder.BIG_ENDIAN) {
+            int h = ((src[offset++] & 0xff) << 24) |
+                    ((src[offset++] & 0xff) << 16) |
+                    ((src[offset++] & 0xff) <<  8) |
+                    ((src[offset++] & 0xff) <<  0);
+            int l = ((src[offset++] & 0xff) << 24) |
+                    ((src[offset++] & 0xff) << 16) |
+                    ((src[offset++] & 0xff) <<  8) |
+                    ((src[offset  ] & 0xff) <<  0);
+            return (((long) h) << 32L) | ((long) l) & 0xffffffffL;
+        } else {
+            int l = ((src[offset++] & 0xff) <<  0) |
+                    ((src[offset++] & 0xff) <<  8) |
+                    ((src[offset++] & 0xff) << 16) |
+                    ((src[offset++] & 0xff) << 24);
+            int h = ((src[offset++] & 0xff) <<  0) |
+                    ((src[offset++] & 0xff) <<  8) |
+                    ((src[offset++] & 0xff) << 16) |
+                    ((src[offset  ] & 0xff) << 24);
+            return (((long) h) << 32L) | ((long) l) & 0xffffffffL;
+        }
+    }
+
+    public static void pokeLong(byte[] dst, int offset, long value, ByteOrder order) {
+        if (order == ByteOrder.BIG_ENDIAN) {
+            int i = (int) (value >> 32);
+            dst[offset++] = (byte) ((i >> 24) & 0xff);
+            dst[offset++] = (byte) ((i >> 16) & 0xff);
+            dst[offset++] = (byte) ((i >>  8) & 0xff);
+            dst[offset++] = (byte) ((i >>  0) & 0xff);
+            i = (int) value;
+            dst[offset++] = (byte) ((i >> 24) & 0xff);
+            dst[offset++] = (byte) ((i >> 16) & 0xff);
+            dst[offset++] = (byte) ((i >>  8) & 0xff);
+            dst[offset  ] = (byte) ((i >>  0) & 0xff);
+        } else {
+            int i = (int) value;
+            dst[offset++] = (byte) ((i >>  0) & 0xff);
+            dst[offset++] = (byte) ((i >>  8) & 0xff);
+            dst[offset++] = (byte) ((i >> 16) & 0xff);
+            dst[offset++] = (byte) ((i >> 24) & 0xff);
+            i = (int) (value >> 32);
+            dst[offset++] = (byte) ((i >>  0) & 0xff);
+            dst[offset++] = (byte) ((i >>  8) & 0xff);
+            dst[offset++] = (byte) ((i >> 16) & 0xff);
+            dst[offset  ] = (byte) ((i >> 24) & 0xff);
+        }
+    }
 }
diff --git a/src/com/android/providers/media/util/PermissionUtils.java b/src/com/android/providers/media/util/PermissionUtils.java
index 28dc14b..b1d9836 100644
--- a/src/com/android/providers/media/util/PermissionUtils.java
+++ b/src/com/android/providers/media/util/PermissionUtils.java
@@ -23,15 +23,18 @@
 import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
 import static android.Manifest.permission.MANAGE_MEDIA;
 import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
+import static android.Manifest.permission.READ_MEDIA_AUDIO;
+import static android.Manifest.permission.READ_MEDIA_IMAGES;
+import static android.Manifest.permission.READ_MEDIA_VIDEO;
 import static android.Manifest.permission.UPDATE_DEVICE_STATS;
 import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
 import static android.app.AppOpsManager.MODE_ALLOWED;
-import static android.app.AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES;
 import static android.app.AppOpsManager.OPSTR_LEGACY_STORAGE;
 import static android.app.AppOpsManager.OPSTR_NO_ISOLATED_STORAGE;
 import static android.app.AppOpsManager.OPSTR_READ_MEDIA_AUDIO;
 import static android.app.AppOpsManager.OPSTR_READ_MEDIA_IMAGES;
 import static android.app.AppOpsManager.OPSTR_READ_MEDIA_VIDEO;
+import static android.app.AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES;
 import static android.app.AppOpsManager.OPSTR_WRITE_MEDIA_AUDIO;
 import static android.app.AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES;
 import static android.app.AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO;
@@ -47,6 +50,8 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.modules.utils.build.SdkLevel;
+
 public class PermissionUtils {
 
     // Callers must hold both the old and new permissions, so that we can
@@ -147,9 +152,18 @@
         return checkNoIsolatedStorageGranted(context, uid, packageName, attributionTag);
     }
 
-    public static boolean checkPermissionReadAudio(@NonNull Context context, int pid, int uid,
-            @NonNull String packageName, @Nullable String attributionTag) {
-        if (!checkPermissionForPreflight(context, READ_EXTERNAL_STORAGE, pid, uid, packageName)) {
+    public static boolean checkPermissionReadAudio(
+            @NonNull Context context,
+            int pid,
+            int uid,
+            @NonNull String packageName,
+            @Nullable String attributionTag,
+            boolean targetSdkIsAtLeastT) {
+
+        String permission = targetSdkIsAtLeastT && SdkLevel.isAtLeastT()
+                ? READ_MEDIA_AUDIO : READ_EXTERNAL_STORAGE;
+
+        if (!checkPermissionForPreflight(context, permission, pid, uid, packageName)) {
             return false;
         }
         return checkAppOpAllowingLegacy(context, OPSTR_READ_MEDIA_AUDIO, pid,
@@ -168,11 +182,20 @@
                 generateAppOpMessage(packageName, sOpDescription.get()));
     }
 
-    public static boolean checkPermissionReadVideo(@NonNull Context context, int pid, int uid,
-            @NonNull String packageName, @Nullable String attributionTag) {
-        if (!checkPermissionForPreflight(context, READ_EXTERNAL_STORAGE, pid, uid, packageName)) {
+    public static boolean checkPermissionReadVideo(
+            @NonNull Context context,
+            int pid,
+            int uid,
+            @NonNull String packageName,
+            @Nullable String attributionTag,
+            boolean targetSdkIsAtLeastT) {
+        String permission = targetSdkIsAtLeastT && SdkLevel.isAtLeastT()
+                ? READ_MEDIA_VIDEO : READ_EXTERNAL_STORAGE;
+
+        if (!checkPermissionForPreflight(context, permission, pid, uid, packageName)) {
             return false;
         }
+
         return checkAppOpAllowingLegacy(context, OPSTR_READ_MEDIA_VIDEO, pid,
                 uid, packageName, attributionTag,
                 generateAppOpMessage(packageName, sOpDescription.get()));
@@ -189,11 +212,20 @@
                 generateAppOpMessage(packageName, sOpDescription.get()));
     }
 
-    public static boolean checkPermissionReadImages(@NonNull Context context, int pid, int uid,
-            @NonNull String packageName, @Nullable String attributionTag) {
-        if (!checkPermissionForPreflight(context, READ_EXTERNAL_STORAGE, pid, uid, packageName)) {
+    public static boolean checkPermissionReadImages(
+            @NonNull Context context,
+            int pid,
+            int uid,
+            @NonNull String packageName,
+            @Nullable String attributionTag,
+            boolean targetSdkIsAtLeastT) {
+        String permission = targetSdkIsAtLeastT && SdkLevel.isAtLeastT()
+                ? READ_MEDIA_IMAGES : READ_EXTERNAL_STORAGE;
+
+        if (!checkPermissionForPreflight(context, permission, pid, uid, packageName)) {
             return false;
         }
+
         return checkAppOpAllowingLegacy(context, OPSTR_READ_MEDIA_IMAGES, pid,
                 uid, packageName, attributionTag,
                 generateAppOpMessage(packageName, sOpDescription.get()));
@@ -424,6 +456,7 @@
             return checkRuntimePermission(context, permission, pid, uid, packageName,
                     attributionTag, message, forDataDelivery);
         }
+
         return context.checkPermission(permission, pid, uid) == PERMISSION_GRANTED;
     }
 
@@ -441,6 +474,9 @@
             case ACCESS_MEDIA_LOCATION:
             case READ_EXTERNAL_STORAGE:
             case WRITE_EXTERNAL_STORAGE:
+            case READ_MEDIA_AUDIO:
+            case READ_MEDIA_VIDEO:
+            case READ_MEDIA_IMAGES:
                 return true;
         }
         return false;
diff --git a/src/com/android/providers/media/util/Preconditions.java b/src/com/android/providers/media/util/Preconditions.java
new file mode 100644
index 0000000..fb87130
--- /dev/null
+++ b/src/com/android/providers/media/util/Preconditions.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.util;
+
+public final class Preconditions {
+
+    /**
+     * Ensures that that the argument numeric value is non-negative (greater than or equal to 0).
+     *
+     * @param value a numeric int value
+     * @return the validated numeric value
+     * @throws IllegalArgumentException if {@code value} was negative
+     */
+    public static int checkArgumentNonnegative(final int value) {
+        if (value < 0) {
+            throw new IllegalArgumentException();
+        }
+
+        return value;
+    }
+
+    /**
+     * Ensures that the argument int value is within the inclusive range.
+     *
+     * @param value a int value
+     * @param lower the lower endpoint of the inclusive range
+     * @param upper the upper endpoint of the inclusive range
+     * @param valueName the name of the argument to use if the check fails
+     *
+     * @return the validated int value
+     *
+     * @throws IllegalArgumentException if {@code value} was not within the range
+     */
+    public static int checkArgumentInRange(int value, int lower, int upper,
+            String valueName) {
+        if (value < lower) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "%s is out of range of [%d, %d] (too low)", valueName, lower, upper));
+        } else if (value > upper) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "%s is out of range of [%d, %d] (too high)", valueName, lower, upper));
+        }
+
+        return value;
+    }
+}
diff --git a/src/com/android/providers/media/util/StringUtils.java b/src/com/android/providers/media/util/StringUtils.java
index 0d9d0e3..49bf935 100644
--- a/src/com/android/providers/media/util/StringUtils.java
+++ b/src/com/android/providers/media/util/StringUtils.java
@@ -16,17 +16,24 @@
 
 package com.android.providers.media.util;
 
+import static com.android.providers.media.util.Logging.TAG;
+
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.Resources.NotFoundException;
 import android.icu.text.MessageFormat;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 
-import androidx.annotation.Nullable;
-
 public class StringUtils {
 
   /**
@@ -68,4 +75,41 @@
   public static boolean equalIgnoreCase(@Nullable String a, @Nullable String b) {
       return (a != null) && a.equalsIgnoreCase(b);
   }
+
+  /**
+   * Returns a string array config as a {@code List<String>}.
+   */
+  public static List<String> getStringArrayConfig(Context context, int resId) {
+      final Resources res = context.getResources();
+      try {
+          final String[] configValue = res.getStringArray(resId);
+          return Arrays.asList(configValue);
+      } catch (NotFoundException e) {
+          return new ArrayList<String>();
+      }
+  }
+
+  /**
+   * Returns the list of uncached relative paths after removing invalid ones.
+   */
+  public static List<String> verifySupportedUncachedRelativePaths(List<String> unverifiedPaths) {
+      final List<String> verifiedPaths = new ArrayList<>();
+      for (final String path : unverifiedPaths) {
+          if (path == null) {
+              continue;
+          }
+          if (path.startsWith("/")) {
+              Log.w(TAG, "Relative path config must not start with '/'. Ignoring: " + path);
+              continue;
+          }
+          if (!path.endsWith("/")) {
+              Log.w(TAG, "Relative path config must end with '/'. Ignoring: " + path);
+              continue;
+          }
+
+          verifiedPaths.add(path);
+      }
+
+      return verifiedPaths;
+  }
 }
diff --git a/src/com/android/providers/media/util/UserCache.java b/src/com/android/providers/media/util/UserCache.java
index cc58b67..fa46779 100644
--- a/src/com/android/providers/media/util/UserCache.java
+++ b/src/com/android/providers/media/util/UserCache.java
@@ -18,7 +18,9 @@
 
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
 import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+import static com.android.providers.media.util.Logging.TAG;
 
+import android.annotation.SuppressLint;
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
@@ -26,7 +28,9 @@
 import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.util.Log;
 import android.util.LongSparseArray;
+import android.util.SparseArray;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
@@ -49,14 +53,21 @@
  * aren't guaranteed to be received before the volume events for a user.
  */
 public class UserCache {
+    // This is being used for non work profile users. It is introduced to remove the necessity of
+    // second cache i.e. mUserIsWorkProfile
+    private static final String NO_WORK_PROFILE_OWNER_APP = "No Work Profile Owner App";
+
     final Object mLock = new Object();
     final Context mContext;
     final UserManager mUserManager;
 
     @GuardedBy("mLock")
     final LongSparseArray<Context> mUserContexts = new LongSparseArray<>();
+
+    // This contains a mapping from userId to packageName of the Profile Owner App
+    // or NO_WORK_PROFILE_OWNER_APP
     @GuardedBy("mLock")
-    final LongSparseArray<Boolean> mUserIsWorkProfile = new LongSparseArray<>();
+    final SparseArray<String> mWorkProfileOwnerApps = new SparseArray<>();
 
     @GuardedBy("mLock")
     final ArrayList<UserHandle> mUsers = new ArrayList<>();
@@ -68,6 +79,7 @@
         update();
     }
 
+    @SuppressLint("NewApi")
     private void update() {
         List<UserHandle> profiles = mUserManager.getEnabledProfiles();
         synchronized (mLock) {
@@ -122,10 +134,11 @@
             // Owner user can not have a work profile
             return false;
         }
+
         synchronized (mLock) {
-            int index = mUserIsWorkProfile.indexOfKey(userId);
+            int index = mWorkProfileOwnerApps.indexOfKey(userId);
             if (index >= 0) {
-                return mUserIsWorkProfile.valueAt(index);
+                return !NO_WORK_PROFILE_OWNER_APP.equals(mWorkProfileOwnerApps.valueAt(index));
             }
         }
 
@@ -137,12 +150,19 @@
         for (ApplicationInfo ai : packageManager.getInstalledApplications(
                 MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE)) {
             if (policyManager.isProfileOwnerApp(ai.packageName)) {
+                synchronized (mLock) {
+                    mWorkProfileOwnerApps.put(userId, ai.packageName);
+                }
                 isWorkProfile = true;
+                break;
             }
         }
 
-        synchronized (mLock) {
-            mUserIsWorkProfile.put(userId, isWorkProfile);
+        if(!isWorkProfile) {
+            synchronized (mLock) {
+                // NO_WORK_PROFILE_OWNER_APP is being used for all the non work profile users
+                mWorkProfileOwnerApps.put(userId, NO_WORK_PROFILE_OWNER_APP);
+            }
         }
 
         return isWorkProfile;
@@ -210,4 +230,28 @@
             }
         }
     }
+
+    public void invalidateWorkProfileOwnerApps(@NonNull String packageName) {
+        synchronized (mLock) {
+            if (mWorkProfileOwnerApps.size() == 0) {
+                Log.w(TAG, "WorkProfileOwnerApps cache is empty");
+                return;
+            }
+
+            boolean cacheMissForGivenPackage = true;
+            for (int i = 0; i < mWorkProfileOwnerApps.size(); i++) {
+                final int userId = mWorkProfileOwnerApps.keyAt(i);
+                if (packageName.equals(mWorkProfileOwnerApps.get(userId))) {
+                    Log.i(TAG, "Invalidating WorkProfileOwnerApps cache for package " + packageName
+                            + ". UserId: " + userId);
+                    mWorkProfileOwnerApps.remove(userId);
+                    cacheMissForGivenPackage = false;
+                }
+            }
+
+            if(cacheMissForGivenPackage) {
+                Log.w(TAG, "WorkProfileOwnerApps cache miss for package " + packageName);
+            }
+        }
+    }
 }
diff --git a/src/com/android/providers/media/util/XAttrUtils.java b/src/com/android/providers/media/util/XAttrUtils.java
new file mode 100644
index 0000000..236f7ed
--- /dev/null
+++ b/src/com/android/providers/media/util/XAttrUtils.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.util;
+
+import static com.android.providers.media.util.FileUtils.extractDisplayName;
+import static com.android.providers.media.util.FileUtils.extractRelativePath;
+import static com.android.providers.media.util.Logging.TAG;
+
+import android.os.SystemProperties;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import com.android.providers.media.FileAccessAttributes;
+
+import java.nio.ByteOrder;
+import java.util.Optional;
+
+public class XAttrUtils {
+
+    /**
+     * Path on which {@link XAttrUtils#DATA_MEDIA_XATTR_DIRECTORY_PATH} is set.
+     * /storage/emulated/.. can point to /data/media/.. on ext4/f2fs on modern devices. However, for
+     * legacy devices with sdcardfs, it points to /mnt/runtime/.. which then points to
+     * /data/media/.. sdcardfs does not support xattrs, hence xattrs are set on /data/media/.. path.
+     *
+     * TODO(b/220895679): Add logic to handle external sd cards with primary volume with paths
+     * /mnt/expand/<volume>/media/<user-id>.
+     */
+    static final String DATA_MEDIA_XATTR_DIRECTORY_PATH = String.format(
+            "/data/media/%s", UserHandle.myUserId());
+
+    static final int SIZE_OF_FILE_ATTRIBUTES = 18;
+
+    /**
+     * Flag to turn on reading file metadata through xattr in FUSE file open calls
+     */
+    public static final boolean ENABLE_XATTR_METADATA_FOR_FUSE =
+            SystemProperties.getBoolean("persist.sys.fuse.perf.xattr_metadata_enabled",
+                    false);
+
+    /**
+     * XAttribute key against which the file metadata is stored
+     */
+    public static final String FILE_ACCESS_XATTR_KEY = "user.fattr";
+
+    public static Optional<FileAccessAttributes> getFileAttributesFromXAttr(String path,
+            String key) {
+        Trace.beginSection("getFileAttributesFromXAttr");
+        String relativePathWithDisplayName = DATA_MEDIA_XATTR_DIRECTORY_PATH + "/"
+                + extractRelativePath(path) + extractDisplayName(path);
+        try {
+            return Optional.of(deserializeFileAccessAttributes(
+                    Os.getxattr(relativePathWithDisplayName, key)));
+        } catch (ErrnoException e) {
+            Log.w(TAG,
+                    String.format("Exception encountered while reading xattr:%s from path:%s.", key,
+                            path));
+            return Optional.empty();
+        } finally {
+            Trace.endSection();
+        }
+    }
+
+    /**
+     * Serializes file access attributes into byte array that will be stored in the xattr.
+     * This method serializes only the id, mediaType, isPending, isTrashed and ownerId fields.
+     * @param fileAccessAttributes File attributes to be stored as byte[] in the file inode
+     * @return byte[]
+     */
+    public static byte[] serializeFileAccessAttributes(
+            FileAccessAttributes fileAccessAttributes) {
+        byte[] bytes = new byte[SIZE_OF_FILE_ATTRIBUTES];
+        int offset = 0;
+        ByteOrder byteOrder = ByteOrder.nativeOrder();
+
+        Memory.pokeLong(bytes, offset, fileAccessAttributes.getId(), byteOrder);
+        offset += Long.BYTES;
+
+        // TODO(b/227753174): Merge mediaType and the booleans in a single byte
+        Memory.pokeInt(bytes, offset, fileAccessAttributes.getMediaType(), byteOrder);
+        offset += Integer.BYTES;
+
+        bytes[offset++] = (byte) (fileAccessAttributes.isPending() ? 1 : 0);
+        bytes[offset++] = (byte) (fileAccessAttributes.isTrashed() ? 1 : 0);
+
+        Memory.pokeInt(bytes, offset, fileAccessAttributes.getOwnerId(), byteOrder);
+        offset += Integer.BYTES;
+        if (offset != SIZE_OF_FILE_ATTRIBUTES) {
+            Log.wtf(TAG, "Error: Serialized byte[] is of unexpected size");
+        }
+        return bytes;
+    }
+
+    /**
+     * Deserialize the byte[] data into the corresponding fields - id, mediaType, isPending,
+     * isTrashed and ownerId in that order, and returns an instance of FileAccessAttributes
+     * containing this deserialized data.
+     * @param data Data that is read from the file inode as a result of the xattr call
+     * @return FileAccessAttributes
+     */
+    public static FileAccessAttributes deserializeFileAccessAttributes(byte[] data) {
+        ByteOrder byteOrder = ByteOrder.nativeOrder();
+        int offset = 0;
+
+        long id = Memory.peekLong(data, offset, byteOrder);
+        offset += Long.BYTES;
+
+        int mediaType = Memory.peekInt(data, offset, byteOrder);
+        offset += Integer.BYTES;
+
+        boolean isPending = data[offset++] != 0;
+        boolean isTrashed = data[offset++] != 0;
+
+        int ownerId = Memory.peekInt(data, offset, byteOrder);
+        offset += Integer.BYTES;
+        if (offset != SIZE_OF_FILE_ATTRIBUTES) {
+            Log.wtf(TAG, " Error: Deserialized attributes are of unexpected size");
+        }
+        return new FileAccessAttributes(id, mediaType, isPending, isTrashed,
+                ownerId, null);
+    }
+}
diff --git a/tests/Android.bp b/tests/Android.bp
index 9626125..05aa556 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -1,3 +1,8 @@
+package {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 android_test_helper_app {
     name: "MediaProviderTestAppForPermissionActivity",
     manifest: "test_app/TestAppForPermissionActivity.xml",
@@ -18,6 +23,25 @@
 }
 
 android_test_helper_app {
+    name: "MediaProviderTestAppForPermissionActivity33",
+    manifest: "test_app/TestAppForPermissionActivity33.xml",
+    srcs: [
+        "test_app/src/**/*.java",
+        "src/com/android/providers/media/util/TestUtils.java",
+    ],
+    static_libs: [
+        "cts-install-lib",
+    ],
+    sdk_version: "test_current",
+    target_sdk_version: "33",
+    min_sdk_version: "30",
+    test_suites: [
+        "general-tests",
+        "mts-mediaprovider",
+    ],
+}
+
+android_test_helper_app {
     name: "MediaProviderTestAppWithStoragePerms",
     manifest: "test_app/TestAppWithStoragePerms.xml",
     srcs: [
@@ -37,6 +61,25 @@
 }
 
 android_test_helper_app {
+    name: "MediaProviderTestAppWithMediaPerms",
+    manifest: "test_app/TestAppWithMediaPerms.xml",
+    srcs: [
+        "test_app/src/**/*.java",
+        "src/com/android/providers/media/util/TestUtils.java",
+    ],
+    static_libs: [
+        "cts-install-lib",
+    ],
+    sdk_version: "test_current",
+    target_sdk_version: "30",
+    min_sdk_version: "30",
+    test_suites: [
+        "general-tests",
+        "mts-mediaprovider",
+    ],
+}
+
+android_test_helper_app {
     name: "MediaProviderTestAppWithoutPerms",
     manifest: "test_app/TestAppWithoutPerms.xml",
     srcs: [
@@ -79,15 +122,6 @@
 // on the device being tested, so we can't sign our tests with a key that
 // will allow instrumentation.  Thus we pull all the sources we need to
 // run tests against into the test itself.
-package {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    default_applicable_licenses: ["packages_providers_MediaProvider_license"],
-}
-
 android_test {
     name: "MediaProviderTests",
     test_suites: [
@@ -162,8 +196,10 @@
 
     java_resources: [
         ":MediaProviderTestAppWithStoragePerms",
+        ":MediaProviderTestAppWithMediaPerms",
         ":MediaProviderTestAppWithoutPerms",
         ":MediaProviderTestAppForPermissionActivity",
+        ":MediaProviderTestAppForPermissionActivity33",
         ":LegacyMediaProviderTestApp",
     ],
 
diff --git a/tests/AndroidTest.xml b/tests/AndroidTest.xml
index fc561af..7be4b76 100644
--- a/tests/AndroidTest.xml
+++ b/tests/AndroidTest.xml
@@ -17,7 +17,9 @@
     <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
         <option name="test-file-name" value="MediaProviderTests.apk" />
         <option name="test-file-name" value="MediaProviderTestAppForPermissionActivity.apk" />
+        <option name="test-file-name" value="MediaProviderTestAppForPermissionActivity33.apk" />
         <option name="test-file-name" value="MediaProviderTestAppWithStoragePerms.apk" />
+        <option name="test-file-name" value="MediaProviderTestAppWithMediaPerms.apk" />
         <option name="test-file-name" value="MediaProviderTestAppWithoutPerms.apk" />
         <option name="test-file-name" value="LegacyMediaProviderTestApp.apk" />
         <option name="install-arg" value="-g" />
diff --git a/tests/client/Android.bp b/tests/client/Android.bp
index 1541da6..7e51828 100644
--- a/tests/client/Android.bp
+++ b/tests/client/Android.bp
@@ -1,10 +1,6 @@
 package {
     // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    default_applicable_licenses: ["packages_providers_MediaProvider_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 android_test {
diff --git a/tests/src/com/android/providers/media/DatabaseHelperTest.java b/tests/src/com/android/providers/media/DatabaseHelperTest.java
index 36ffd64..f1ce350 100644
--- a/tests/src/com/android/providers/media/DatabaseHelperTest.java
+++ b/tests/src/com/android/providers/media/DatabaseHelperTest.java
@@ -18,20 +18,18 @@
 
 import static android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY;
 
-import static com.android.providers.media.DatabaseHelper.VERSION_LATEST;
-import static com.android.providers.media.DatabaseHelper.VERSION_S;
-import static com.android.providers.media.DatabaseHelper.makePristineSchema;
+import static com.android.providers.media.DatabaseHelper.TEST_CLEAN_DB;
+import static com.android.providers.media.DatabaseHelper.TEST_DOWNGRADE_DB;
 import static com.android.providers.media.DatabaseHelper.TEST_RECOMPUTE_DB;
 import static com.android.providers.media.DatabaseHelper.TEST_UPGRADE_DB;
-import static com.android.providers.media.DatabaseHelper.TEST_DOWNGRADE_DB;
-import static com.android.providers.media.DatabaseHelper.TEST_CLEAN_DB;
+import static com.android.providers.media.DatabaseHelper.makePristineSchema;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
 import android.Manifest;
@@ -43,8 +41,8 @@
 import android.os.UserHandle;
 import android.provider.Column;
 import android.provider.ExportedSince;
-import android.provider.MediaStore.Audio.AudioColumns;
 import android.provider.MediaStore.Audio;
+import android.provider.MediaStore.Audio.AudioColumns;
 import android.provider.MediaStore.Files.FileColumns;
 import android.util.Log;
 
@@ -270,6 +268,7 @@
         try (DatabaseHelper helper = before.getConstructor(Context.class, String.class)
                 .newInstance(sIsolatedContext, TEST_DOWNGRADE_DB)) {
             SQLiteDatabase db = helper.getWritableDatabaseForTest();
+            assertThat(sIsolatedContext.getDatabasePath(TEST_DOWNGRADE_DB).exists()).isTrue();
             {
                 final ContentValues values = new ContentValues();
                 values.put(FileColumns.DATA,
@@ -285,13 +284,11 @@
             }
         }
 
-        // Downgrade will wipe data, but at least we don't crash
+        // Downgrade will delete the database file and crash the process
         try (DatabaseHelper helper = after.getConstructor(Context.class, String.class)
                 .newInstance(sIsolatedContext, TEST_DOWNGRADE_DB)) {
-            SQLiteDatabase db = helper.getWritableDatabaseForTest();
-            try (Cursor c = db.query("files", null, null, null, null, null, null, null)) {
-                assertEquals(0, c.getCount());
-            }
+            assertThrows(RuntimeException.class, helper::getWritableDatabaseForTest);
+            assertThat(sIsolatedContext.getDatabasePath(TEST_DOWNGRADE_DB).exists()).isFalse();
         }
     }
 
@@ -569,52 +566,6 @@
         }
     }
 
-    /**
-     * Test that database downgrade changed the UUID saved in database file.
-     */
-    @Test
-    public void testDowngradeChangesUUID() throws Exception {
-        Class<? extends DatabaseHelper> dbVersionHigher = DatabaseHelperT.class;
-        Class<? extends DatabaseHelper> dbVersionLower = DatabaseHelperS.class;
-        String originalUUID;
-        int originalVersion;
-        // Create the database with database version = dbVersionLower
-        try (DatabaseHelper helper = dbVersionLower.getConstructor(Context.class, String.class)
-                .newInstance(sIsolatedContext, TEST_DOWNGRADE_DB)) {
-            SQLiteDatabase db = helper.getWritableDatabaseForTest();
-            originalUUID = DatabaseHelper.getOrCreateUuid(db);
-            originalVersion = db.getVersion();
-            // Verify that original version of the database is dbVersionLower.
-            assertWithMessage("Current database version")
-                    .that(db.getVersion()).isEqualTo(VERSION_S);
-        }
-        // Upgrade the database by changing the version to dbVersionHigher
-        try (DatabaseHelper helper = dbVersionHigher.getConstructor(Context.class, String.class)
-                .newInstance(sIsolatedContext, TEST_DOWNGRADE_DB)) {
-            SQLiteDatabase db = helper.getWritableDatabaseForTest();
-            // Verify that upgrade resulted in database version change.
-            assertWithMessage("Current database version after upgrade")
-                    .that(db.getVersion()).isNotEqualTo(originalVersion);
-            // Verify that upgrade resulted in database version same as latest version.
-            assertWithMessage("Current database version after upgrade")
-                    .that(db.getVersion()).isEqualTo(DatabaseHelper.VERSION_T);
-            // Verify that upgrade didn't change UUID
-            assertWithMessage("Current database UUID after upgrade")
-                    .that(DatabaseHelper.getOrCreateUuid(db)).isEqualTo(originalUUID);
-        }
-        // Downgrade the database by changing the version to dbVersionLower
-        try (DatabaseHelper helper = dbVersionLower.getConstructor(Context.class, String.class)
-                .newInstance(sIsolatedContext, TEST_DOWNGRADE_DB)) {
-            SQLiteDatabase db = helper.getWritableDatabaseForTest();
-            // Verify that downgraded version is same as original database version before upgrade
-            assertWithMessage("Current database version after downgrade")
-                    .that(db.getVersion()).isEqualTo(originalVersion);
-            // Verify that downgrade changed UUID
-            assertWithMessage("Current database UUID after downgrade")
-                    .that(DatabaseHelper.getOrCreateUuid(db)).isNotEqualTo(originalUUID);
-        }
-    }
-
     private static String normalize(String sql) {
         return sql != null ? sql.replace(", ", ",") : null;
     }
@@ -634,7 +585,7 @@
     private static class DatabaseHelperO extends DatabaseHelper {
         public DatabaseHelperO(Context context, String name) {
             super(context, name, DatabaseHelper.VERSION_O, false, false, Column.class,
-                    ExportedSince.class, null, null, null, null);
+                    ExportedSince.class, null, null, null, null, false);
         }
 
         @Override
@@ -646,7 +597,7 @@
     private static class DatabaseHelperP extends DatabaseHelper {
         public DatabaseHelperP(Context context, String name) {
             super(context, name, DatabaseHelper.VERSION_P, false, false, Column.class,
-                    ExportedSince.class, null, null, null, null);
+                    ExportedSince.class, null, null, null, null, false);
         }
 
         @Override
@@ -658,7 +609,7 @@
     private static class DatabaseHelperQ extends DatabaseHelper {
         public DatabaseHelperQ(Context context, String name) {
             super(context, name, DatabaseHelper.VERSION_Q, false, false, Column.class,
-                    ExportedSince.class, null, null, null, null);
+                    ExportedSince.class, null, null, null, null, false);
         }
 
         @Override
@@ -670,7 +621,7 @@
     private static class DatabaseHelperR extends DatabaseHelper {
         public DatabaseHelperR(Context context, String name) {
             super(context, name, DatabaseHelper.VERSION_R, false, false, Column.class,
-                    ExportedSince.class, null, null, MediaProvider.MIGRATION_LISTENER, null);
+                    ExportedSince.class, null, null, MediaProvider.MIGRATION_LISTENER, null, false);
         }
 
         @Override
@@ -682,7 +633,7 @@
     private static class DatabaseHelperS extends DatabaseHelper {
         public DatabaseHelperS(Context context, String name) {
             super(context, name, VERSION_S, false, false, Column.class, ExportedSince.class, null,
-                    null, MediaProvider.MIGRATION_LISTENER, null);
+                    null, MediaProvider.MIGRATION_LISTENER, null, false);
         }
 
 
@@ -695,7 +646,7 @@
     private static class DatabaseHelperT extends DatabaseHelper {
         public DatabaseHelperT(Context context, String name) {
             super(context, name, DatabaseHelper.VERSION_T, false, false, Column.class,
-                    ExportedSince.class, null, null, MediaProvider.MIGRATION_LISTENER, null);
+                    ExportedSince.class, null, null, MediaProvider.MIGRATION_LISTENER, null, false);
         }
     }
 
diff --git a/tests/src/com/android/providers/media/MediaProviderForFuseTest.java b/tests/src/com/android/providers/media/MediaProviderForFuseTest.java
index 2a0aa48..df9f575 100644
--- a/tests/src/com/android/providers/media/MediaProviderForFuseTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderForFuseTest.java
@@ -16,6 +16,11 @@
 
 package com.android.providers.media;
 
+import static com.android.providers.media.MediaProvider.DIRECTORY_ACCESS_FOR_READ;
+import static com.android.providers.media.MediaProvider.DIRECTORY_ACCESS_FOR_WRITE;
+import static com.android.providers.media.MediaProvider.DIRECTORY_ACCESS_FOR_CREATE;
+import static com.android.providers.media.MediaProvider.DIRECTORY_ACCESS_FOR_DELETE;
+
 import android.Manifest;
 import android.app.UiAutomation;
 import android.content.ContentResolver;
@@ -25,6 +30,7 @@
 import android.os.Bundle;
 import android.os.Environment;
 import android.provider.MediaStore;
+import android.system.OsConstants;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -107,7 +113,10 @@
 
         // We can write our file
         FileOpenResult result = sMediaProvider.onFileOpenForFuse(
-                file.getPath(), file.getPath(), sTestUid, 0 /* tid */, 0 /* transforms_reason */,
+                file.getPath(),
+                file.getPath(),
+                sTestUid,
+                0 /* tid */, 0 /* transforms_reason */,
                 true /* forWrite */, false /* redact */, false /* transcode_metrics */);
         Truth.assertThat(result.status).isEqualTo(0);
         Truth.assertThat(result.redactionRanges).isEqualTo(new long[0]);
@@ -228,14 +237,76 @@
     }
 
     @Test
-    public void test_isOpendirAllowedForFuse() throws Exception {
-        Truth.assertThat(sMediaProvider.isOpendirAllowedForFuse(
-                sTestDir.getPath(), sTestUid, /* forWrite */ false)).isEqualTo(0);
-    }
+    public void test_isDirAccessAllowedForFuse() throws Exception {
+        //verify can create and write but not delete top-level default folder
+        final File topLevelDefaultDir = Environment.buildExternalStoragePublicDirs(
+                Environment.DIRECTORY_PICTURES)[0];
+        final String topLevelDefaultDirPath = topLevelDefaultDir.getPath();
+        Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+                topLevelDefaultDirPath, sTestUid,
+                DIRECTORY_ACCESS_FOR_READ)).isEqualTo(0);
+        Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+                topLevelDefaultDirPath, sTestUid,
+                DIRECTORY_ACCESS_FOR_CREATE)).isEqualTo(0);
+        Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+                topLevelDefaultDirPath, sTestUid,
+                DIRECTORY_ACCESS_FOR_WRITE)).isEqualTo(0);
+        Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+                topLevelDefaultDirPath, sTestUid,
+                DIRECTORY_ACCESS_FOR_DELETE)).isEqualTo(
+                OsConstants.EACCES);
 
-    @Test
-    public void test_isDirectoryCreationOrDeletionAllowedForFuse() throws Exception {
-        Truth.assertThat(sMediaProvider.isDirectoryCreationOrDeletionAllowedForFuse(
-                sTestDir.getPath(), sTestUid, true)).isEqualTo(0);
+        //verify cannot create or write top-level non-default folder, but can read it
+        final File topLevelNonDefaultDir = Environment.buildExternalStoragePublicDirs(
+                "non-default-dir")[0];
+        final String topLevelNonDefaultDirPath = topLevelNonDefaultDir.getPath();
+        Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+                topLevelNonDefaultDirPath, sTestUid,
+                DIRECTORY_ACCESS_FOR_READ)).isEqualTo(0);
+        Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+                topLevelNonDefaultDirPath, sTestUid,
+                DIRECTORY_ACCESS_FOR_CREATE)).isEqualTo(
+                OsConstants.EACCES);
+        Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+                topLevelNonDefaultDirPath, sTestUid,
+                DIRECTORY_ACCESS_FOR_WRITE)).isEqualTo(OsConstants.EACCES);
+        Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+                topLevelNonDefaultDirPath, sTestUid,
+                DIRECTORY_ACCESS_FOR_DELETE)).isEqualTo(OsConstants.EACCES);
+
+        //verify can read, create, write and delete random non-top-level folder
+        final File lowerLevelNonDefaultDir = new File(topLevelDefaultDir,
+                "subdir" + System.nanoTime());
+        lowerLevelNonDefaultDir.mkdirs();
+        final String lowerLevelNonDefaultDirPath = lowerLevelNonDefaultDir.getPath();
+        Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+                lowerLevelNonDefaultDirPath, sTestUid,
+                DIRECTORY_ACCESS_FOR_READ)).isEqualTo(0);
+        Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+                lowerLevelNonDefaultDirPath, sTestUid,
+                DIRECTORY_ACCESS_FOR_CREATE)).isEqualTo(0);
+        Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+                lowerLevelNonDefaultDirPath, sTestUid,
+                DIRECTORY_ACCESS_FOR_WRITE)).isEqualTo(0);
+        Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+                lowerLevelNonDefaultDirPath, sTestUid,
+                DIRECTORY_ACCESS_FOR_DELETE)).isEqualTo(0);
+
+        //verify cannot update outside /storage folder
+        final File rootDir = new File("/myfolder");
+        final String rootDirPath = rootDir.getPath();
+        Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+                rootDirPath, sTestUid,
+                DIRECTORY_ACCESS_FOR_READ)).isEqualTo(0);
+        Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+                rootDirPath, sTestUid,
+                DIRECTORY_ACCESS_FOR_CREATE)).isEqualTo(OsConstants.EPERM);
+        Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+                rootDirPath, sTestUid,
+                DIRECTORY_ACCESS_FOR_WRITE)).isEqualTo(OsConstants.EPERM);
+        Truth.assertThat(sMediaProvider.isDirAccessAllowedForFuse(
+                rootDirPath, sTestUid,
+                DIRECTORY_ACCESS_FOR_DELETE)).isEqualTo(OsConstants.EPERM);
+
     }
 }
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index 6df8ab3..a04f57b 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -1576,9 +1576,8 @@
         Bundle opts = new Bundle();
         opts.putString(MediaStore.EXTRA_MODE, "w");
 
-        try {
-            AssetFileDescriptor afd = sContext.getContentResolver().openTypedAssetFile(mediaUri,
-                    "*/*", opts, null);
+        try (AssetFileDescriptor afd = sContext.getContentResolver().openTypedAssetFile(mediaUri,
+                    "*/*", opts, null)) {
             String rawText = "Hello";
             Os.write(afd.getFileDescriptor(), rawText.getBytes(StandardCharsets.UTF_8),
                     0, rawText.length());
diff --git a/tests/src/com/android/providers/media/PermissionActivityTest.java b/tests/src/com/android/providers/media/PermissionActivityTest.java
index a7fe5b4..281e93a 100644
--- a/tests/src/com/android/providers/media/PermissionActivityTest.java
+++ b/tests/src/com/android/providers/media/PermissionActivityTest.java
@@ -21,6 +21,9 @@
 import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
 import static android.Manifest.permission.MANAGE_MEDIA;
 import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
+import static android.Manifest.permission.READ_MEDIA_AUDIO;
+import static android.Manifest.permission.READ_MEDIA_IMAGES;
+import static android.Manifest.permission.READ_MEDIA_VIDEO;
 import static android.Manifest.permission.UPDATE_APP_OPS_STATS;
 
 import static androidx.test.InstrumentationRegistry.getContext;
@@ -33,7 +36,10 @@
 import static com.android.providers.media.util.PermissionUtils.checkPermissionAccessMediaLocation;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionManageMedia;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionManager;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadAudio;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadImages;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionReadStorage;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadVideo;
 import static com.android.providers.media.util.TestUtils.adoptShellPermission;
 import static com.android.providers.media.util.TestUtils.dropShellPermission;
 
@@ -75,6 +81,8 @@
 public class PermissionActivityTest {
     private static final String TEST_APP_PACKAGE_NAME =
             "com.android.providers.media.testapp.permission";
+    private static final String TEST_APP_33_PACKAGE_NAME =
+            "com.android.providers.media.testapp.permissionmedia";
 
     private static final String OP_ACCESS_MEDIA_LOCATION =
             AppOpsManager.permissionToOp(ACCESS_MEDIA_LOCATION);
@@ -84,6 +92,12 @@
             AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE);
     private static final String OP_READ_EXTERNAL_STORAGE =
             AppOpsManager.permissionToOp(READ_EXTERNAL_STORAGE);
+    private static final String OP_READ_MEDIA_IMAGES =
+            AppOpsManager.permissionToOp(READ_MEDIA_IMAGES);
+    private static final String OP_READ_MEDIA_AUDIO =
+            AppOpsManager.permissionToOp(READ_MEDIA_AUDIO);
+    private static final String OP_READ_MEDIA_VIDEO =
+            AppOpsManager.permissionToOp(READ_MEDIA_VIDEO);
 
     // The list is used to restore the permissions after the test is finished.
     // The default value for these app ops is {@link AppOpsManager#MODE_DEFAULT}
@@ -104,10 +118,12 @@
 
     private static final int TEST_APP_PID = -1;
     private int mTestAppUid = -1;
+    private int mTestAppUid33 = -1;
 
     @Before
     public void setUp() throws Exception {
         mTestAppUid = getContext().getPackageManager().getPackageUid(TEST_APP_PACKAGE_NAME, 0);
+        mTestAppUid33 = getContext().getPackageManager().getPackageUid(TEST_APP_33_PACKAGE_NAME, 0);
     }
 
     @Test
@@ -147,7 +163,8 @@
         adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
 
         try {
-            setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList);
+            setupPermissions(
+                    mTestAppUid, enableAppOpsList, disableAppOpsList, TEST_APP_PACKAGE_NAME);
 
             assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
                     TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isTrue();
@@ -158,6 +175,103 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void testShouldShowActionDialog_noRMAAndMES_true_33() throws Exception {
+        final String[] enableAppOpsList =
+                {OP_MANAGE_MEDIA, OP_READ_MEDIA_IMAGES, OP_READ_MEDIA_VIDEO};
+        final String[] disableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE, OP_READ_MEDIA_AUDIO};
+        adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+        try {
+            setupPermissions(
+                    mTestAppUid33, enableAppOpsList, disableAppOpsList, TEST_APP_33_PACKAGE_NAME);
+
+            assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid33,
+                    TEST_APP_33_PACKAGE_NAME, null, VERB_TRASH,
+                    /* shouldCheckMediaPermissions */ true, /* shouldCheckReadAudio */ true,
+                    /* shouldCheckReadImages */ false, /* shouldCheckReadVideo */ false,
+                    /* mShouldCheckReadAudioOrReadVideo */ false,
+                    /* isTargetSdkAtLeastT */ true)).isTrue();
+        } finally {
+            restoreDefaultAppOpPermissions(mTestAppUid33);
+            dropShellPermission();
+        }
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void testShouldShowActionDialog_noRMIAndMES_true_33() throws Exception {
+        final String[] enableAppOpsList =
+                {OP_MANAGE_MEDIA, OP_READ_MEDIA_AUDIO, OP_READ_MEDIA_VIDEO};
+        final String[] disableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE, OP_READ_MEDIA_IMAGES};
+        adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+        try {
+            setupPermissions(
+                    mTestAppUid33, enableAppOpsList, disableAppOpsList, TEST_APP_33_PACKAGE_NAME);
+
+            assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid33,
+                    TEST_APP_33_PACKAGE_NAME, null, VERB_TRASH,
+                    /* shouldCheckMediaPermissions */ true, /* shouldCheckReadAudio */ false,
+                    /* shouldCheckReadImages */ true, /* shouldCheckReadVideo */ false,
+                    /* mShouldCheckReadAudioOrReadVideo */ false,
+                    /* isTargetSdkAtLeastT */ true)).isTrue();
+        } finally {
+            restoreDefaultAppOpPermissions(mTestAppUid33);
+            dropShellPermission();
+        }
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void testShouldShowActionDialog_noRMVAndMES_true_33() throws Exception {
+        final String[] enableAppOpsList =
+                {OP_MANAGE_MEDIA, OP_READ_MEDIA_AUDIO, OP_READ_MEDIA_IMAGES};
+        final String[] disableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE, OP_READ_MEDIA_VIDEO};
+        adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+        try {
+            setupPermissions(
+                    mTestAppUid33, enableAppOpsList, disableAppOpsList, TEST_APP_33_PACKAGE_NAME);
+
+            assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid33,
+                    TEST_APP_33_PACKAGE_NAME, null, VERB_TRASH,
+                    /* shouldCheckMediaPermissions */ true, /* shouldCheckReadAudio */ false,
+                    /* shouldCheckReadImages */ false, /* shouldCheckReadVideo */ true,
+                    /* mShouldCheckReadAudioOrReadVideo */ false,
+                    /* isTargetSdkAtLeastT */ true)).isTrue();
+        } finally {
+            restoreDefaultAppOpPermissions(mTestAppUid33);
+            dropShellPermission();
+        }
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void testShouldShowActionDialogForSubtitle_noRMARMVAndMES_true_33() throws Exception {
+        final String[] enableAppOpsList =
+                {OP_MANAGE_MEDIA, OP_READ_MEDIA_IMAGES};
+        final String[] disableAppOpsList =
+                {OP_MANAGE_EXTERNAL_STORAGE, OP_READ_MEDIA_AUDIO, OP_READ_MEDIA_VIDEO};
+        adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+        try {
+            setupPermissions(
+                    mTestAppUid33, enableAppOpsList, disableAppOpsList, TEST_APP_33_PACKAGE_NAME);
+
+            assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid33,
+                    TEST_APP_33_PACKAGE_NAME, null, VERB_TRASH,
+                    /* shouldCheckMediaPermissions */ true, /* shouldCheckReadAudio */ false,
+                    /* shouldCheckReadImages */ false, /* shouldCheckReadVideo */ false,
+                    /* mShouldCheckReadAudioOrReadVideo */ true,
+                    /* isTargetSdkAtLeastT */ true)).isTrue();
+        } finally {
+            restoreDefaultAppOpPermissions(mTestAppUid33);
+            dropShellPermission();
+        }
+    }
+
+    @Test
     @SdkSuppress(minSdkVersion = 31, codeName = "S")
     public void testShouldShowActionDialog_noMANAGE_MEDIA_true() throws Exception {
         final String[] enableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE, OP_READ_EXTERNAL_STORAGE};
@@ -165,7 +279,8 @@
         adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
 
         try {
-            setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList);
+            setupPermissions(
+                    mTestAppUid, enableAppOpsList, disableAppOpsList, TEST_APP_PACKAGE_NAME);
 
             assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
                     TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isTrue();
@@ -176,14 +291,43 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void testShouldShowActionDialog_noMANAGE_MEDIA_true_33() throws Exception {
+        final String[] enableAppOpsList = {
+            OP_MANAGE_EXTERNAL_STORAGE,
+            OP_READ_MEDIA_AUDIO,
+            OP_READ_MEDIA_VIDEO,
+            OP_READ_MEDIA_IMAGES
+        };
+        final String[] disableAppOpsList = {OP_MANAGE_MEDIA};
+        adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+        try {
+            setupPermissions(
+                    mTestAppUid33, enableAppOpsList, disableAppOpsList, TEST_APP_33_PACKAGE_NAME);
+
+            assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid33,
+                    TEST_APP_33_PACKAGE_NAME, null, VERB_TRASH,
+                    /* shouldCheckMediaPermissions */ true, /* shouldCheckReadAudio */ true,
+                    /* shouldCheckReadImages */ true, /* shouldCheckReadVideo */ true,
+                    /* mShouldCheckReadAudioOrReadVideo */ true,
+                    /* isTargetSdkAtLeastT */ true)).isTrue();
+        } finally {
+            restoreDefaultAppOpPermissions(mTestAppUid33);
+            dropShellPermission();
+        }
+    }
+
+    @Test
     @SdkSuppress(minSdkVersion = 31, codeName = "S")
-    public void testShouldShowActionDialog_hasPermissionWithRES_false() throws Exception {
+    public void testShouldShowActionDialog_hasMMWithRES_false() throws Exception {
         final String[] enableAppOpsList = {OP_MANAGE_MEDIA, OP_READ_EXTERNAL_STORAGE};
         final String[] disableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE};
         adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
 
         try {
-            setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList);
+            setupPermissions(
+                    mTestAppUid, enableAppOpsList, disableAppOpsList, TEST_APP_PACKAGE_NAME);
 
             assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
                     TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isFalse();
@@ -194,14 +338,40 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void testShouldShowActionDialog_hasMMWithRM_false_33() throws Exception {
+        final String[] enableAppOpsList = {
+            OP_MANAGE_MEDIA, OP_READ_MEDIA_AUDIO, OP_READ_MEDIA_VIDEO, OP_READ_MEDIA_IMAGES
+        };
+        final String[] disableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE};
+        adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+        try {
+            setupPermissions(
+                    mTestAppUid33, enableAppOpsList, disableAppOpsList, TEST_APP_33_PACKAGE_NAME);
+
+            assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid33,
+                    TEST_APP_33_PACKAGE_NAME, null, VERB_TRASH,
+                    /* shouldCheckMediaPermissions */ true, /* shouldCheckReadAudio */ true,
+                    /* shouldCheckReadImages */ true, /* shouldCheckReadVideo */ true,
+                    /* mShouldCheckReadAudioOrReadVideo */ true,
+                    /* isTargetSdkAtLeastT */ true)).isFalse();
+        } finally {
+            restoreDefaultAppOpPermissions(mTestAppUid33);
+            dropShellPermission();
+        }
+    }
+
+    @Test
     @SdkSuppress(minSdkVersion = 31, codeName = "S")
-    public void testShouldShowActionDialog_hasPermissionWithMES_false() throws Exception {
+    public void testShouldShowActionDialog_hasMMWithMES_false() throws Exception {
         final String[] enableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE, OP_MANAGE_MEDIA};
         final String[] disableAppOpsList = {OP_READ_EXTERNAL_STORAGE};
         adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
 
         try {
-            setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList);
+            setupPermissions(
+                    mTestAppUid, enableAppOpsList, disableAppOpsList, TEST_APP_PACKAGE_NAME);
 
             assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
                     TEST_APP_PACKAGE_NAME, null, VERB_TRASH)).isFalse();
@@ -212,6 +382,32 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void testShouldShowActionDialog_hasMMWithMES_false_33() throws Exception {
+        final String[] enableAppOpsList = {OP_MANAGE_EXTERNAL_STORAGE, OP_MANAGE_MEDIA};
+        final String[] disableAppOpsList = {
+            OP_READ_MEDIA_AUDIO, OP_READ_MEDIA_VIDEO, OP_READ_MEDIA_IMAGES
+        };
+
+        adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+        try {
+            setupPermissions(
+                    mTestAppUid33, enableAppOpsList, disableAppOpsList, TEST_APP_33_PACKAGE_NAME);
+
+            assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid33,
+                    TEST_APP_33_PACKAGE_NAME, null, VERB_TRASH,
+                    /* shouldCheckMediaPermissions */ true, /* shouldCheckReadAudio */ true,
+                    /* shouldCheckReadImages */ true, /* shouldCheckReadVideo */ true,
+                    /* mShouldCheckReadAudioOrReadVideo */ true,
+                    /* isTargetSdkAtLeastT */ true)).isFalse();
+        } finally {
+            restoreDefaultAppOpPermissions(mTestAppUid33);
+            dropShellPermission();
+        }
+    }
+
+    @Test
     @SdkSuppress(minSdkVersion = 31, codeName = "S")
     public void testShouldShowActionDialog_writeNoACCESS_MEDIA_LOCATION_true() throws Exception {
         final String[] enableAppOpsList =
@@ -220,7 +416,8 @@
         adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
 
         try {
-            setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList);
+            setupPermissions(
+                    mTestAppUid, enableAppOpsList, disableAppOpsList, TEST_APP_PACKAGE_NAME);
 
             assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
                     TEST_APP_PACKAGE_NAME, null, VERB_WRITE)).isTrue();
@@ -242,7 +439,8 @@
         adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
 
         try {
-            setupPermissions(mTestAppUid, enableAppOpsList, disableAppOpsList);
+            setupPermissions(
+                    mTestAppUid, enableAppOpsList, disableAppOpsList, TEST_APP_PACKAGE_NAME);
 
             assertThat(shouldShowActionDialog(getContext(), TEST_APP_PID, mTestAppUid,
                     TEST_APP_PACKAGE_NAME, null, VERB_WRITE)).isFalse();
@@ -253,7 +451,7 @@
     }
 
     private static void setupPermissions(int uid, @NonNull String[] enableAppOpsList,
-            @NonNull String[] disableAppOpsList) throws Exception {
+            @NonNull String[] disableAppOpsList, @NonNull String packageName) throws Exception {
         for (String op : enableAppOpsList) {
             modifyAppOp(uid, op, AppOpsManager.MODE_ALLOWED);
         }
@@ -262,8 +460,10 @@
             modifyAppOp(uid, op, AppOpsManager.MODE_ERRORED);
         }
 
-        pollForAppOpPermissions(TEST_APP_PID, uid, enableAppOpsList, /* hasPermission= */ true);
-        pollForAppOpPermissions(TEST_APP_PID, uid, disableAppOpsList, /* hasPermission= */ false);
+        pollForAppOpPermissions(
+                TEST_APP_PID, packageName, uid, enableAppOpsList, /* hasPermission= */ true);
+        pollForAppOpPermissions(
+                TEST_APP_PID, packageName, uid, disableAppOpsList, /* hasPermission= */ false);
     }
 
     private static void restoreDefaultAppOpPermissions(int uid) {
@@ -296,16 +496,16 @@
         getContext().getSystemService(AppOpsManager.class).setUidMode(op, uid, mode);
     }
 
-    private static void pollForAppOpPermissions(int pid, int uid, String[] opList,
-            boolean hasPermission) throws Exception {
+    private static void pollForAppOpPermissions(int pid, @NonNull String packageName, int uid,
+            String[] opList, boolean hasPermission) throws Exception {
         long current = System.currentTimeMillis();
         final long timeout = current + TIMEOUT_MILLIS;
         final HashSet<String> checkedOpSet = new HashSet<>();
 
         while (current < timeout && checkedOpSet.size() < opList.length) {
             for (String op : opList) {
-                if (!checkedOpSet.contains(op) && checkPermission(op, pid, uid,
-                        TEST_APP_PACKAGE_NAME, hasPermission)) {
+                if (!checkedOpSet.contains(op)
+                        && checkPermission(op, pid, uid, packageName, hasPermission)) {
                     checkedOpSet.add(op);
                     continue;
                 }
@@ -326,6 +526,15 @@
         if (TextUtils.equals(op, OP_READ_EXTERNAL_STORAGE)) {
             return expected == checkPermissionReadStorage(context, pid, uid, packageName,
                     /* attributionTag= */ null);
+        } else if (TextUtils.equals(op, OP_READ_MEDIA_IMAGES)) {
+            return expected == checkPermissionReadImages(
+                context, pid, uid, packageName, /* attributionTag= */ null, /* isAtleastT */ true);
+        } else if (TextUtils.equals(op, OP_READ_MEDIA_AUDIO)) {
+            return expected == checkPermissionReadAudio(
+                context, pid, uid, packageName, /* attributionTag= */ null, /* isAtleastT */ true);
+        } else if (TextUtils.equals(op, OP_READ_MEDIA_VIDEO)) {
+            return expected == checkPermissionReadVideo(
+                context, pid, uid, packageName, /* attributionTag= */ null, /* isAtleastT */ true);
         } else if (TextUtils.equals(op, OP_MANAGE_EXTERNAL_STORAGE)) {
             return expected == checkPermissionManager(context, pid, uid, packageName,
                     /* attributionTag= */ null);
diff --git a/tests/src/com/android/providers/media/PickerUriResolverTest.java b/tests/src/com/android/providers/media/PickerUriResolverTest.java
index b691155..3de26ec 100644
--- a/tests/src/com/android/providers/media/PickerUriResolverTest.java
+++ b/tests/src/com/android/providers/media/PickerUriResolverTest.java
@@ -24,10 +24,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import android.Manifest;
 import android.content.ContentUris;
@@ -57,8 +57,8 @@
 import org.junit.runner.RunWith;
 
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 
 @RunWith(AndroidJUnit4.class)
 public class PickerUriResolverTest {
@@ -350,17 +350,17 @@
     }
 
     private void testOpenFile(Uri uri) throws Exception {
-        ParcelFileDescriptor pfd = sTestPickerUriResolver.openFile(uri, "r", /* signal */ null,
-                /* callingPid */ -1, /* callingUid */ -1);
-
-        assertThat(pfd).isNotNull();
+        try (ParcelFileDescriptor pfd = sTestPickerUriResolver.openFile(uri, "r", /* signal */ null,
+                /* callingPid */ -1, /* callingUid */ -1)) {
+            assertThat(pfd).isNotNull();
+        }
     }
 
     private void testOpenTypedAssetFile(Uri uri) throws Exception {
-        AssetFileDescriptor afd =  sTestPickerUriResolver.openTypedAssetFile(uri, "image/*",
-                /* opts */ null, /* signal */ null, /* callingPid */ -1, /* callingUid */ -1);
-
-        assertThat(afd).isNotNull();
+        try (AssetFileDescriptor afd = sTestPickerUriResolver.openTypedAssetFile(uri, "image/*",
+                /* opts */ null, /* signal */ null, /* callingPid */ -1, /* callingUid */ -1)) {
+            assertThat(afd).isNotNull();
+        }
     }
 
     private void testQuery(Uri uri) throws Exception {
diff --git a/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java b/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
index 0ba045f..7608c92 100644
--- a/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
+++ b/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
@@ -16,12 +16,12 @@
 
 package com.android.providers.media.photopicker;
 
-import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES;
-import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS;
-import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS;
+import static android.provider.CloudMediaProviderContract.AlbumColumns;
 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA;
 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS;
-import static android.provider.CloudMediaProviderContract.AlbumColumns;
+import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES;
+import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS;
+import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS;
 import static android.provider.CloudMediaProviderContract.MediaColumns;
 import static android.provider.MediaStore.VOLUME_EXTERNAL;
 
@@ -41,16 +41,12 @@
 import android.net.Uri;
 import android.os.Environment;
 import android.os.ParcelFileDescriptor;
-import android.provider.DeviceConfig;
 import android.provider.MediaStore;
 
 import androidx.test.InstrumentationRegistry;
 
-import com.android.providers.media.photopicker.data.ExternalDbFacade;
 import com.android.providers.media.photopicker.data.ItemsProvider;
-import com.android.providers.media.photopicker.data.PickerDbFacade;
 import com.android.providers.media.photopicker.data.model.Category;
-import com.android.providers.media.photopicker.data.model.Item;
 import com.android.providers.media.photopicker.data.model.UserId;
 import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
 
@@ -634,11 +630,13 @@
     }
 
     private void assertCategoryUriIsValid(Uri uri) throws Exception {
-        final AssetFileDescriptor fd1 = mIsolatedResolver.openTypedAssetFile(uri, "image/*", null,
-                null);
-        assertThat(fd1).isNotNull();
-        final ParcelFileDescriptor fd2 = mIsolatedResolver.openFileDescriptor(uri, "r");
-        assertThat(fd2).isNotNull();
+        try (AssetFileDescriptor fd1 = mIsolatedResolver.openTypedAssetFile(uri, "image/*",
+                null, null)) {
+            assertThat(fd1).isNotNull();
+        }
+        try (ParcelFileDescriptor fd2 = mIsolatedResolver.openFileDescriptor(uri, "r")) {
+            assertThat(fd2).isNotNull();
+        }
     }
 
     private void assertCategoriesNoMatch(String expectedCategoryName) {
diff --git a/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java b/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
index 47033cc..6931cd6 100644
--- a/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
@@ -119,8 +119,10 @@
 
         mDbHelper = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_1);
         mFacade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY, mDbHelper);
+        final String allowedCloudProviders = CLOUD_PRIMARY_PROVIDER_AUTHORITY + ","
+                + CLOUD_SECONDARY_PROVIDER_AUTHORITY;
         mController = new PickerSyncController(mContext, mFacade, LOCAL_PROVIDER_AUTHORITY,
-                /* syncDelay */ 0);
+                allowedCloudProviders, /* syncDelay */ 0);
         mDataLayer = new PickerDataLayer(mContext, mFacade, mController);
 
         // Set cloud provider to null to discard
diff --git a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
index 224e252..53d6837 100644
--- a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
@@ -18,22 +18,21 @@
 
 import static com.android.providers.media.PickerProviderMediaGenerator.MediaGenerator;
 import static com.android.providers.media.photopicker.PickerSyncController.CloudProviderInfo;
+
 import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
-import android.content.SharedPreferences;
 import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
 import android.database.Cursor;
 import android.os.Bundle;
 import android.os.Process;
 import android.os.SystemClock;
-import android.os.SystemProperties;
-import android.provider.CloudMediaProviderContract;
+import android.os.storage.StorageManager;
 import android.provider.CloudMediaProviderContract.MediaColumns;
 import android.provider.MediaStore;
 import android.util.Pair;
@@ -41,24 +40,20 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.modules.utils.BackgroundThread;
 import com.android.providers.media.PickerProviderMediaGenerator;
+import com.android.providers.media.R;
 import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
 import com.android.providers.media.photopicker.data.PickerDbFacade;
-import com.android.providers.media.R;
-import com.android.providers.media.util.DeviceConfigUtils;
-
-import java.io.File;
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.MockitoSession;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 @RunWith(AndroidJUnit4.class)
 public class PickerSyncControllerTest {
@@ -129,8 +124,11 @@
 
         mDbHelper = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_1);
         mFacade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY, mDbHelper);
+
+        final String allowedCloudProviders = CLOUD_PRIMARY_PROVIDER_AUTHORITY + ","
+                + CLOUD_SECONDARY_PROVIDER_AUTHORITY;
         mController = new PickerSyncController(mContext, mFacade, LOCAL_PROVIDER_AUTHORITY,
-                /* syncDelay */ 0);
+                allowedCloudProviders, /* syncDelay */ 0);
 
         // Set cloud provider to null to avoid trying to sync it during other tests
         // that might be using an IsolatedContext
@@ -609,57 +607,34 @@
 
     @Test
     public void testGetSupportedCloudProviders_useAllowList() {
-        MockitoSession mockSession = ExtendedMockito.mockitoSession()
-                .mockStatic(DeviceConfigUtils.class)
-                .mockStatic(SystemProperties.class)
-                .startMocking();
+        CloudProviderInfo primaryInfo = new CloudProviderInfo(CLOUD_PRIMARY_PROVIDER_AUTHORITY,
+                PACKAGE_NAME,
+                Process.myUid());
+        CloudProviderInfo secondaryInfo = new CloudProviderInfo(
+                CLOUD_SECONDARY_PROVIDER_AUTHORITY,
+                PACKAGE_NAME,
+                Process.myUid());
 
-        try {
-            when(SystemProperties.getBoolean(
-                PickerSyncController.PROP_USE_ALLOWED_CLOUD_PROVIDERS, false))
-                .thenReturn(true);
+        // 1. Allow list is subset of existing providers list
+        PickerSyncController controller = new PickerSyncController(mContext, mFacade,
+                LOCAL_PROVIDER_AUTHORITY, CLOUD_PRIMARY_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
+        List<CloudProviderInfo> providers = controller.getSupportedCloudProviders();
+        assertThat(providers).containsExactly(primaryInfo);
 
-            CloudProviderInfo primaryInfo = new CloudProviderInfo(CLOUD_PRIMARY_PROVIDER_AUTHORITY,
-                    PACKAGE_NAME,
-                    Process.myUid());
-            CloudProviderInfo secondaryInfo = new CloudProviderInfo(
-                    CLOUD_SECONDARY_PROVIDER_AUTHORITY,
-                    PACKAGE_NAME,
-                    Process.myUid());
+        String allowedCloudProviders = CLOUD_PRIMARY_PROVIDER_AUTHORITY + ","
+                + CLOUD_SECONDARY_PROVIDER_AUTHORITY;
+        controller = new PickerSyncController(mContext, mFacade,
+                LOCAL_PROVIDER_AUTHORITY, allowedCloudProviders, SYNC_DELAY_MS);
+        providers = controller.getSupportedCloudProviders();
+        assertThat(providers).containsExactly(primaryInfo, secondaryInfo);
 
-            // 1. Allow list is subset of existing providers list
-            when(DeviceConfigUtils.getStringDeviceConfig(
-                    PickerSyncController.ALLOWED_CLOUD_PROVIDERS_KEY, ""))
-                .thenReturn(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
-            PickerSyncController controller = new PickerSyncController(mContext, mFacade,
-                    LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
-            List<CloudProviderInfo> providers = controller.getSupportedCloudProviders();
-            assertThat(providers).containsExactly(primaryInfo);
-
-            // 2. Allow list is exactly the same as existing providers list
-            when(DeviceConfigUtils.getStringDeviceConfig(
-                    PickerSyncController.ALLOWED_CLOUD_PROVIDERS_KEY, ""))
-                .thenReturn(CLOUD_PRIMARY_PROVIDER_AUTHORITY
-                            + "," + CLOUD_SECONDARY_PROVIDER_AUTHORITY);
-            controller = new PickerSyncController(mContext, mFacade,
-                    LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
-            providers = controller.getSupportedCloudProviders();
-            assertThat(providers).containsExactly(primaryInfo, secondaryInfo);
-
-            // 3. Allow list containing existing providers list + others
-            when(DeviceConfigUtils.getStringDeviceConfig(
-                    PickerSyncController.ALLOWED_CLOUD_PROVIDERS_KEY, ""))
-                .thenReturn(CLOUD_PRIMARY_PROVIDER_AUTHORITY
-                            + "," + CLOUD_SECONDARY_PROVIDER_AUTHORITY
-                            + "," + CLOUD_PRIMARY_PROVIDER_AUTHORITY + "invalid");
-            controller = new PickerSyncController(mContext, mFacade,
-                    LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
-            providers = controller.getSupportedCloudProviders();
-            assertThat(providers).containsExactly(primaryInfo, secondaryInfo);
-        }
-        finally {
-            mockSession.finishMocking();
-        }
+        allowedCloudProviders = CLOUD_PRIMARY_PROVIDER_AUTHORITY
+                + "," + CLOUD_SECONDARY_PROVIDER_AUTHORITY
+                + "," + CLOUD_PRIMARY_PROVIDER_AUTHORITY + "invalid";
+        controller = new PickerSyncController(mContext, mFacade,
+                LOCAL_PROVIDER_AUTHORITY, allowedCloudProviders, SYNC_DELAY_MS);
+        providers = controller.getSupportedCloudProviders();
+        assertThat(providers).containsExactly(primaryInfo, secondaryInfo);
     }
 
     @Test
@@ -712,7 +687,7 @@
     @Test
     public void testNotifyMediaEvent() {
         PickerSyncController controller = new PickerSyncController(mContext, mFacade,
-                LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
+                LOCAL_PROVIDER_AUTHORITY, "", SYNC_DELAY_MS);
 
         // 1. Add media and notify
         addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
@@ -737,7 +712,7 @@
         PickerDbFacade facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
                 dbHelper);
         PickerSyncController controller = new PickerSyncController(mContext, facade,
-                LOCAL_PROVIDER_AUTHORITY, /* syncDelay */ 0);
+                LOCAL_PROVIDER_AUTHORITY, "", /* syncDelay */ 0);
 
         addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
         controller.syncAllMedia();
@@ -756,7 +731,7 @@
 
         facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY, dbHelper);
         controller = new PickerSyncController(mContext, facade,
-                LOCAL_PROVIDER_AUTHORITY, /* syncDelay */ 0);
+                LOCAL_PROVIDER_AUTHORITY, "", /* syncDelay */ 0);
 
         // Initially empty db
         try (Cursor cr = queryMedia(facade)) {
@@ -779,7 +754,7 @@
         PickerDbFacade facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
                 dbHelperV1);
         PickerSyncController controller = new PickerSyncController(mContext, facade,
-                LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
+                LOCAL_PROVIDER_AUTHORITY, "", SYNC_DELAY_MS);
 
         addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
         controller.syncAllMedia();
@@ -795,7 +770,7 @@
         PickerDatabaseHelper dbHelperV2 = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_2);
         facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY, dbHelperV2);
         controller = new PickerSyncController(mContext, facade,
-                LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
+                LOCAL_PROVIDER_AUTHORITY, "", SYNC_DELAY_MS);
 
         // Initially empty db
         try (Cursor cr = queryMedia(facade)) {
@@ -818,7 +793,7 @@
         PickerDbFacade facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
                 dbHelperV2);
         PickerSyncController controller = new PickerSyncController(mContext, facade,
-                LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
+                LOCAL_PROVIDER_AUTHORITY, "", SYNC_DELAY_MS);
 
         addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
         controller.syncAllMedia();
@@ -835,7 +810,7 @@
         facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
                 dbHelperV1);
         controller = new PickerSyncController(mContext, facade,
-                LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
+                LOCAL_PROVIDER_AUTHORITY, "", SYNC_DELAY_MS);
 
         // Initially empty db
         try (Cursor cr = queryMedia(facade)) {
@@ -967,7 +942,7 @@
         PickerDbFacade facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
                 dbHelperV1);
         PickerSyncController controller = new PickerSyncController(mContext, facade,
-                LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
+                LOCAL_PROVIDER_AUTHORITY, CLOUD_PRIMARY_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
 
         controller.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         assertThat(controller.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -978,7 +953,7 @@
         facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
                 dbHelperV2);
         controller = new PickerSyncController(mContext, facade,
-                LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
+                LOCAL_PROVIDER_AUTHORITY, CLOUD_PRIMARY_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
 
         assertThat(controller.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
     }
@@ -1077,14 +1052,19 @@
 
         when(mockContext.getResources()).thenReturn(mockResources);
         when(mockContext.getPackageManager()).thenReturn(mContext.getPackageManager());
+        when(mockContext.getSystemService(StorageManager.class))
+                .thenReturn(mContext.getSystemService(StorageManager.class));
         when(mockContext.getSharedPreferences(anyString(), anyInt())).thenAnswer(i -> {
             return mContext.getSharedPreferences((String)i.getArgument(0), (int)i.getArgument(1));
         });
         when(mockResources.getString(R.string.config_default_cloud_provider_authority))
                 .thenReturn(defaultProvider);
 
+        final String allowedCloudProviders = CLOUD_PRIMARY_PROVIDER_AUTHORITY + ","
+                + CLOUD_SECONDARY_PROVIDER_AUTHORITY;
+
         return new PickerSyncController(mockContext, mFacade,
-                LOCAL_PROVIDER_AUTHORITY, SYNC_DELAY_MS);
+                LOCAL_PROVIDER_AUTHORITY, allowedCloudProviders, SYNC_DELAY_MS);
     }
 
     private static void assertCursor(Cursor cursor, String id, String expectedAuthority) {
diff --git a/tests/src/com/android/providers/media/photopicker/data/ExternalDbFacadeTest.java b/tests/src/com/android/providers/media/photopicker/data/ExternalDbFacadeTest.java
index d253bf9..3a37efa 100644
--- a/tests/src/com/android/providers/media/photopicker/data/ExternalDbFacadeTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/ExternalDbFacadeTest.java
@@ -620,7 +620,6 @@
                 assertThat(cursor.getCount()).isEqualTo(0);
             }
 
-
             try (Cursor cursor = facade.queryMedia(/* generation */ 0,
                             ALBUM_ID_CAMERA, VIDEO_MIME_TYPE)) {
                 assertThat(cursor.getCount()).isEqualTo(0);
@@ -739,27 +738,14 @@
                 // We verify the order of the albums:
                 // Camera, Screenshots and Downloads
                 cursor.moveToNext();
-                assertAlbumColumns(facade,
-                        cursor,
-                        ALBUM_ID_CAMERA,
-                        /* mediaCoverId */ "1",
-                        DATE_TAKEN_MS1,
+                assertAlbumColumns(facade, cursor, ALBUM_ID_CAMERA, DATE_TAKEN_MS1, /* count */ 1);
+
+                cursor.moveToNext();
+                assertAlbumColumns(facade, cursor, ALBUM_ID_SCREENSHOTS, DATE_TAKEN_MS2,
                         /* count */ 1);
 
                 cursor.moveToNext();
-                assertAlbumColumns(facade,
-                        cursor,
-                        ALBUM_ID_SCREENSHOTS,
-                        /* mediaCoverId */ "2",
-                        DATE_TAKEN_MS2,
-                        /* count */ 1);
-
-                cursor.moveToNext();
-                assertAlbumColumns(facade,
-                        cursor,
-                        ALBUM_ID_DOWNLOADS,
-                        /* mediaCoverId */ "3",
-                        DATE_TAKEN_MS3,
+                assertAlbumColumns(facade, cursor, ALBUM_ID_DOWNLOADS, DATE_TAKEN_MS3,
                         /* count */ 1);
             }
         }
@@ -791,12 +777,7 @@
 
                 // We verify the order of the albums only the image in camera is shown
                 cursor.moveToNext();
-                assertAlbumColumns(facade,
-                        cursor,
-                        ALBUM_ID_CAMERA,
-                        /* mediaCoverId */ "1",
-                        DATE_TAKEN_MS1,
-                        /* count */ 1);
+                assertAlbumColumns(facade, cursor, ALBUM_ID_CAMERA, DATE_TAKEN_MS1, /* count */ 1);
             }
         }
     }
@@ -888,7 +869,7 @@
     }
 
     private static void assertAlbumColumns(ExternalDbFacade facade, Cursor cursor,
-            String displayName, String mediaCoverId, long dateTakenMs, long count) {
+            String displayName, long dateTakenMs, long count) {
         int displayNameIndex = cursor.getColumnIndex(
                 CloudMediaProviderContract.AlbumColumns.DISPLAY_NAME);
         int idIndex = cursor.getColumnIndex(CloudMediaProviderContract.AlbumColumns.MEDIA_COVER_ID);
@@ -897,7 +878,7 @@
         int countIndex = cursor.getColumnIndex(CloudMediaProviderContract.AlbumColumns.MEDIA_COUNT);
 
         assertThat(cursor.getString(displayNameIndex)).isEqualTo(displayName);
-        assertThat(cursor.getString(idIndex)).isEqualTo(mediaCoverId);
+        assertThat(cursor.getString(idIndex)).isNotNull();
         assertThat(cursor.getLong(dateTakenIndex)).isEqualTo(dateTakenMs);
         assertThat(cursor.getLong(countIndex)).isEqualTo(count);
     }
@@ -930,7 +911,8 @@
 
     private static class TestDatabaseHelper extends DatabaseHelper {
         public TestDatabaseHelper(Context context) {
-            super(context, TEST_CLEAN_DB, 1, false, false, null, null, null, null, null, null);
+            super(context, TEST_CLEAN_DB, 1, false, false, null, null, null, null, null, null,
+                    false);
         }
     }
 }
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/AlbumsTabTest.java b/tests/src/com/android/providers/media/photopicker/espresso/AlbumsTabTest.java
index a3c3526..0ab97db 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/AlbumsTabTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/AlbumsTabTest.java
@@ -37,6 +37,7 @@
 
 import com.android.providers.media.R;
 
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -50,6 +51,7 @@
     public ActivityScenarioRule<PhotoPickerTestActivity> mRule =
             new ActivityScenarioRule<>(PhotoPickerBaseTest.getMultiSelectionIntent());
 
+    @Ignore("b/227478958 Odd failure to verify Downloads album")
     @Test
     public void testAlbumGrid() {
         // Goto Albums page
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectLongPressTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectLongPressTest.java
index 2fbba6c..de6ad97 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectLongPressTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectLongPressTest.java
@@ -101,9 +101,11 @@
 
         try (ViewPager2IdlingResource idlingResource
                      = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
-            // Verify video player is displayed
             assertMultiSelectLongPressCommonLayoutMatches();
-            onView(withId(R.id.preview_player_view)).check(matches(isDisplayed()));
+            // Verify thumbnail view is displayed
+            onView(withId(R.id.preview_video_image)).check(matches(isDisplayed()));
+            // TODO (b/232792753): Assert video player visibility using custom IdlingResource
+
             // Verify no special format icon is previewed
             onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
             onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
@@ -250,4 +252,4 @@
         onView(withId(R.id.preview_selected_check_button)).check(matches(not(isDisplayed())));
         onView(withId(R.id.preview_add_button)).check(matches(not(isDisplayed())));
     }
-}
\ No newline at end of file
+}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectTest.java
index fd1908f..3920059 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectTest.java
@@ -65,7 +65,7 @@
 
 @RunWith(AndroidJUnit4ClassRunner.class)
 public class PreviewMultiSelectTest extends PhotoPickerBaseTest {
-    private static final int PLAYER_VIEW_ID = R.id.preview_player_view;
+    private static final int VIDEO_PREVIEW_THUMBNAIL_ID = R.id.preview_video_image;
 
     @Rule
     public ActivityScenarioRule<PhotoPickerTestActivity> mRule
@@ -207,7 +207,7 @@
             // TODO(b/197083539): We don't check the video image to be visible or not because its
             // visibility is time sensitive. Try waiting till player is ready and assert that video
             // image is no more visible.
-            onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PLAYER_VIEW_ID))
+            onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, VIDEO_PREVIEW_THUMBNAIL_ID))
                     .check(matches(isDisplayed()));
             // Verify no special format icon is previewed
             assertSpecialFormatBadgeDoesNotExist();
@@ -313,16 +313,17 @@
 
             // Verify that "View Selected" shows the video item, not the image item that was
             // previewed earlier with preview on long press
-            onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PLAYER_VIEW_ID))
+            onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, VIDEO_PREVIEW_THUMBNAIL_ID))
                     .check(matches(isDisplayed()));
 
             // Swipe and verify we don't preview the image item
             swipeLeftAndWait(PREVIEW_VIEW_PAGER_ID);
-            onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PLAYER_VIEW_ID))
+            onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, VIDEO_PREVIEW_THUMBNAIL_ID))
                     .check(matches(isDisplayed()));
             swipeRightAndWait(PREVIEW_VIEW_PAGER_ID);
-            onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PLAYER_VIEW_ID))
+            onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, VIDEO_PREVIEW_THUMBNAIL_ID))
                     .check(matches(isDisplayed()));
+            // TODO (b/232792753): Assert video player visibility using custom IdlingResource
         }
     }
 
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java
index edeccd8..df11c29 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java
@@ -134,9 +134,11 @@
 
         try (ViewPager2IdlingResource idlingResource
                      = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
-            // Verify video player is displayed
             assertSingleSelectCommonLayoutMatches();
-            onView(withId(R.id.preview_player_view)).check(matches(isDisplayed()));
+            // Verify thumbnail view is displayed
+            onView(withId(R.id.preview_video_image)).check(matches(isDisplayed()));
+            // TODO (b/232792753): Assert video player visibility using custom IdlingResource
+
             // Verify no special format icon is previewed
             onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
             onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
diff --git a/tests/src/com/android/providers/media/scan/MediaScannerTest.java b/tests/src/com/android/providers/media/scan/MediaScannerTest.java
index afb7597..f8c27e6 100644
--- a/tests/src/com/android/providers/media/scan/MediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/MediaScannerTest.java
@@ -46,13 +46,14 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.providers.media.PickerUriResolver;
+import com.android.providers.media.DatabaseHelper;
 import com.android.providers.media.MediaDocumentsProvider;
 import com.android.providers.media.MediaProvider;
+import com.android.providers.media.PickerUriResolver;
 import com.android.providers.media.R;
-import com.android.providers.media.util.FileUtils;
 import com.android.providers.media.photopicker.PhotoPickerProvider;
 import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.util.FileUtils;
 
 import org.junit.Before;
 import org.junit.Ignore;
@@ -124,6 +125,11 @@
                 public void addOnPropertiesChangedListener(OnPropertiesChangedListener listener) {
                     // Ignore
                 }
+
+                @Override
+                protected void updateNextRowIdXattr(DatabaseHelper helper, long id) {
+                    // Ignoring this as test app would not have access to update xattr.
+                }
             };
             mProvider.attachInfo(this, info);
             mResolver.addProvider(MediaStore.AUTHORITY, mProvider);
diff --git a/tests/src/com/android/providers/media/util/FileUtilsTest.java b/tests/src/com/android/providers/media/util/FileUtilsTest.java
index 574a2db..266c144 100644
--- a/tests/src/com/android/providers/media/util/FileUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/FileUtilsTest.java
@@ -971,16 +971,24 @@
     public void testExtractPathOwnerPackageName() {
         assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/data/foo"))
                 .isEqualTo("foo");
+        assertThat(extractPathOwnerPackageName("/storage/emulated/0/android/data/foo"))
+                .isEqualTo("foo");
         assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/obb/foo"))
                 .isEqualTo("foo");
+        assertThat(extractPathOwnerPackageName("/storage/emulated/0/android/obb/foo"))
+                .isEqualTo("foo");
         assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/media/foo"))
                 .isEqualTo("foo");
+        assertThat(extractPathOwnerPackageName("/storage/emulated/0/android/media/foo"))
+                .isEqualTo("foo");
         assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/Android/data/foo"))
                 .isEqualTo("foo");
         assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/Android/obb/foo"))
                 .isEqualTo("foo");
         assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/Android/media/foo"))
                 .isEqualTo("foo");
+        assertThat(extractPathOwnerPackageName("/storage/ABCD-1234/android/media/foo"))
+                .isEqualTo("foo");
 
         assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/data")).isNull();
         assertThat(extractPathOwnerPackageName("/storage/emulated/0/Android/obb")).isNull();
diff --git a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
index c434bd2..45e9316 100644
--- a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
@@ -51,11 +51,8 @@
 import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteStorage;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteVideo;
 import static com.android.providers.media.util.PermissionUtils.checkWriteImagesOrVideoAppOps;
-import static com.android.providers.media.util.TestUtils.QUERY_TYPE;
-import static com.android.providers.media.util.TestUtils.RUN_INFINITE_ACTIVITY;
 import static com.android.providers.media.util.TestUtils.adoptShellPermission;
 import static com.android.providers.media.util.TestUtils.dropShellPermission;
-import static com.android.providers.media.util.TestUtils.getPid;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -63,26 +60,29 @@
 
 import android.app.AppOpsManager;
 import android.content.Context;
-import android.content.Intent;
 
 import androidx.test.filters.SdkSuppress;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.cts.install.lib.TestApp;
+import com.android.modules.utils.build.SdkLevel;
 
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.HashMap;
-import java.util.Map;
-
 @RunWith(AndroidJUnit4.class)
 public class PermissionUtilsTest {
     private static final TestApp TEST_APP_WITH_STORAGE_PERMS = new TestApp(
             "TestAppWithStoragePerms",
             "com.android.providers.media.testapp.withstorageperms", 1, false,
             "MediaProviderTestAppWithStoragePerms.apk");
+    private static final TestApp TEST_APP_WITH_MEDIA_PERMS =
+            new TestApp(
+                    "TestAppWithMediaPerms",
+                    "com.android.providers.media.testapp.withmediaperms",
+                    1,
+                    false,
+                    "MediaProviderTestAppWithMediaPerms.apk");
     private static final TestApp TEST_APP_WITHOUT_PERMS = new TestApp("TestAppWithoutPerms",
             "com.android.providers.media.testapp.withoutperms", 1, false,
             "MediaProviderTestAppWithoutPerms.apk");
@@ -120,11 +120,11 @@
         assertThat(checkPermissionReadStorage(context, pid, uid, packageName, null)).isTrue();
         assertThat(checkPermissionWriteStorage(context, pid, uid, packageName, null)).isTrue();
 
-        assertThat(checkPermissionReadAudio(context, pid, uid, packageName, null)).isTrue();
+        assertThat(checkPermissionReadAudio(context, pid, uid, packageName, null, false)).isTrue();
         assertThat(checkPermissionWriteAudio(context, pid, uid, packageName, null)).isFalse();
-        assertThat(checkPermissionReadVideo(context, pid, uid, packageName, null)).isTrue();
+        assertThat(checkPermissionReadVideo(context, pid, uid, packageName, null, false)).isTrue();
         assertThat(checkPermissionWriteVideo(context, pid, uid, packageName, null)).isFalse();
-        assertThat(checkPermissionReadImages(context, pid, uid, packageName, null)).isTrue();
+        assertThat(checkPermissionReadImages(context, pid, uid, packageName, null, false)).isTrue();
         assertThat(checkPermissionWriteImages(context, pid, uid, packageName, null)).isFalse();
         assertThat(checkPermissionInstallPackages(context, pid, uid, packageName, null)).isFalse();
     }
@@ -142,6 +142,9 @@
 
     @Test
     public void testDefaultPermissionsOnTestAppWithStoragePerms() throws Exception {
+        if (!SdkLevel.isAtLeastT()) {
+            return;
+        }
         String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
         int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
         adoptShellPermission(UPDATE_APP_OPS_STATS);
@@ -159,9 +162,49 @@
                     checkPermissionAccessMtp(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isFalse();
             assertThat(
-                    checkPermissionWriteStorage(getContext(), TEST_APP_PID, testAppUid, packageName,
+                    checkPermissionWriteStorage(getContext(), TEST_APP_PID, testAppUid,
+                        packageName, null)).isTrue();
+            assertThat(
+                    checkPermissionReadStorage(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isTrue();
-            checkReadPermissions(TEST_APP_PID, testAppUid, packageName, true);
+            assertMediaReadPermissions(TEST_APP_PID, testAppUid, packageName,
+                false /* targetSdkIsAtLeastT */, true /* expected */);
+            // APPs with W_E_S can also read media.
+            assertMediaReadPermissions(TEST_APP_PID, testAppUid, packageName,
+                true /* targetSdkIsAtLeastT */, true /* expected */);
+
+        } finally {
+            dropShellPermission();
+        }
+    }
+
+    @Test
+    public void testDefaultPermissionsOnTestAppWithMediaPerms() throws Exception {
+        if (!SdkLevel.isAtLeastT()) {
+            return;
+        }
+        String packageName = TEST_APP_WITH_MEDIA_PERMS.getPackageName();
+        int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
+        adoptShellPermission(UPDATE_APP_OPS_STATS);
+
+        try {
+            assertThat(checkPermissionSelf(getContext(), TEST_APP_PID, testAppUid)).isFalse();
+            assertThat(checkPermissionShell(getContext(), TEST_APP_PID, testAppUid)).isFalse();
+            assertThat(checkIsLegacyStorageGranted(getContext(), testAppUid, packageName, null))
+                    .isFalse();
+            assertThat(checkPermissionInstallPackages(
+                        getContext(), TEST_APP_PID, testAppUid, packageName, null)).isFalse();
+            assertThat(checkPermissionAccessMtp(
+                        getContext(), TEST_APP_PID, testAppUid, packageName, null)).isFalse();
+            assertThat(checkPermissionWriteStorage(
+                        getContext(), TEST_APP_PID, testAppUid, packageName, null)).isFalse();
+            assertThat(checkPermissionReadStorage(
+                        getContext(), TEST_APP_PID, testAppUid, packageName, null)).isFalse();
+            assertMediaReadPermissions(TEST_APP_PID, testAppUid, packageName,
+                true /* targetSdkIsAtLeastT */, true /* expected */);
+            assertMediaReadPermissions(TEST_APP_PID, testAppUid, packageName,
+                false /* targetSdkIsAtLeastT */, false /* expected */);
+
         } finally {
             dropShellPermission();
         }
@@ -193,15 +236,20 @@
                             null)).isFalse();
             assertThat(
                     checkPermissionInstallPackages(getContext(), TEST_APP_PID, testAppUid,
-                            packageName,
-                            null)).isFalse();
+                        packageName, null)).isFalse();
             assertThat(
                     checkPermissionAccessMtp(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isFalse();
             assertThat(
                     checkPermissionWriteStorage(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isFalse();
-            checkReadPermissions(TEST_APP_PID, testAppUid, packageName, false);
+            assertThat(
+                    checkPermissionReadStorage( getContext(), TEST_APP_PID, testAppUid, packageName,
+                            null)).isFalse();
+            assertMediaReadPermissions(TEST_APP_PID, testAppUid, packageName,
+                false /* targetSdkIsAtLeastT */, false /* expected */);
+            assertMediaReadPermissions(TEST_APP_PID, testAppUid, packageName,
+                true /* targetSdkIsAtLeastT */, false /* expected */);
         } finally {
             dropShellPermission();
         }
@@ -238,9 +286,12 @@
                     checkPermissionAccessMtp(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isFalse();
             assertThat(
-                    checkPermissionWriteStorage(getContext(), TEST_APP_PID, testAppUid, packageName,
-                            null)).isFalse();
-            checkReadPermissions(TEST_APP_PID, testAppUid, packageName, true);
+                    checkPermissionWriteStorage(getContext(), TEST_APP_PID, testAppUid,
+                        packageName, null)).isFalse();
+            assertThat(checkPermissionReadStorage(getContext(), TEST_APP_PID, testAppUid,
+                        packageName, null)).isTrue();
+            assertMediaReadPermissions(TEST_APP_PID, testAppUid, packageName,
+                false /* targetSdkIsAtLeastT */, true /* expected */);
         } finally {
             dropShellPermission();
         }
@@ -346,26 +397,32 @@
     }
 
     @Test
-    public void testReadVideoOnTestApp() throws Exception {
-        final String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
-        int testAppUid = getContext().getPackageManager().getPackageUid(
-                packageName, 0);
+    public void testReadVideoOnTestAppWithStoragePerms() throws Exception {
+        assertReadVideoOnTestApp(TEST_APP_WITH_STORAGE_PERMS);
+    }
+
+    @Test
+    public void testReadVideoOnTestAppWithMediaPerms() throws Exception {
+        if (!SdkLevel.isAtLeastT()) {
+            return;
+        }
+        assertReadVideoOnTestApp(TEST_APP_WITH_MEDIA_PERMS);
+    }
+
+    private static void assertReadVideoOnTestApp(TestApp app) throws Exception {
+        boolean isAtLeastT = (app == TEST_APP_WITH_MEDIA_PERMS) ? true : false;
+        final String packageName = app.getPackageName();
+        int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
         adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
-
         try {
-            assertThat(
-                    checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid, packageName,
-                            null)).isTrue();
-
+            assertThat(checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid,
+                    packageName, null, isAtLeastT)).isTrue();
             modifyAppOp(testAppUid, OPSTR_READ_MEDIA_VIDEO, AppOpsManager.MODE_ERRORED);
-            assertThat(
-                    checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid, packageName,
-                            null)).isFalse();
-
+            assertThat(checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid,
+                    packageName, null, isAtLeastT)).isFalse();
             modifyAppOp(testAppUid, OPSTR_READ_MEDIA_VIDEO, AppOpsManager.MODE_ALLOWED);
-            assertThat(
-                    checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid, packageName,
-                            null)).isTrue();
+            assertThat(checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid,
+                packageName, null, isAtLeastT)).isTrue();
         } finally {
             dropShellPermission();
         }
@@ -398,51 +455,68 @@
     }
 
     @Test
-    public void testReadAudioOnTestApp() throws Exception {
-        final String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
-        int testAppUid = getContext().getPackageManager().getPackageUid(
-                packageName, 0);
-        adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+    public void testReadAudioOnTestAppWithStoragePerms() throws Exception {
+        assertReadAudioOnTestApp(TEST_APP_WITH_STORAGE_PERMS);
+    }
 
+    @Test
+    public void testReadAudioOnTestAppWithMediaPerms() throws Exception {
+        if (!SdkLevel.isAtLeastT()) {
+            return;
+        }
+        assertReadAudioOnTestApp(TEST_APP_WITH_MEDIA_PERMS);
+    }
+
+    private static void assertReadAudioOnTestApp(TestApp app) throws Exception {
+        boolean isAtLeastT = (app == TEST_APP_WITH_MEDIA_PERMS) ? true : false;
+        final String packageName = app.getPackageName();
+        int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
+        adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
         try {
-            assertThat(
-                    checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid, packageName,
-                            null)).isTrue();
+            assertThat(checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid,
+                        packageName, null, isAtLeastT)).isTrue();
 
             modifyAppOp(testAppUid, OPSTR_READ_MEDIA_AUDIO, AppOpsManager.MODE_ERRORED);
-            assertThat(
-                    checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid, packageName,
-                            null)).isFalse();
+            assertThat(checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid,
+                        packageName, null, isAtLeastT)).isFalse();
 
             modifyAppOp(testAppUid, OPSTR_READ_MEDIA_AUDIO, AppOpsManager.MODE_ALLOWED);
-            assertThat(
-                    checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid, packageName,
-                            null)).isTrue();
+            assertThat(checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid,
+                        packageName, null, isAtLeastT)).isTrue();
         } finally {
             dropShellPermission();
         }
     }
 
     @Test
-    public void testReadImagesOnTestApp() throws Exception {
-        final String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
+    public void testReadImagesOnTestAppWithStoragePerms() throws Exception {
+        assertReadImagesOnTestApp(TEST_APP_WITH_STORAGE_PERMS);
+    }
+
+    @Test
+    public void testReadImagesOnTestAppWithMediaPerms() throws Exception {
+        if (!SdkLevel.isAtLeastT()) {
+            return;
+        }
+        assertReadImagesOnTestApp(TEST_APP_WITH_MEDIA_PERMS);
+    }
+
+    private static void assertReadImagesOnTestApp(TestApp app) throws Exception {
+        boolean isAtLeastT = (app == TEST_APP_WITH_MEDIA_PERMS) ? true : false;
+        final String packageName = app.getPackageName();
         int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
         adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
-
         try {
-            assertThat(
-                    checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid, packageName,
-                            null)).isTrue();
+            assertThat(checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid,
+                            packageName, null, isAtLeastT)).isTrue();
 
             modifyAppOp(testAppUid, OPSTR_READ_MEDIA_IMAGES, AppOpsManager.MODE_ERRORED);
-            assertThat(
-                    checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid, packageName,
-                            null)).isFalse();
+            assertThat(checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid,
+                            packageName, null, isAtLeastT)).isFalse();
 
             modifyAppOp(testAppUid, OPSTR_READ_MEDIA_IMAGES, AppOpsManager.MODE_ALLOWED);
-            assertThat(
-                    checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid, packageName,
-                            null)).isTrue();
+            assertThat(checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid,
+                            packageName, null, isAtLeastT)).isTrue();
         } finally {
             dropShellPermission();
         }
@@ -491,15 +565,19 @@
                 .isFalse();
     }
 
-    static private void checkReadPermissions(int pid, int uid, String packageName,
-            boolean expected) {
-        assertEquals(expected,
-                checkPermissionReadStorage(getContext(), pid, uid, packageName, null));
-        assertEquals(expected,
-                checkPermissionReadAudio(getContext(), pid, uid, packageName, null));
-        assertEquals(expected,
-                checkPermissionReadImages(getContext(), pid, uid, packageName, null));
-        assertEquals(expected,
-                checkPermissionReadVideo(getContext(), pid, uid, packageName, null));
+    private static void assertMediaReadPermissions(
+            int pid, int uid, String packageName, boolean targetSdkIsAtLeastT, boolean expected) {
+        assertEquals(
+                expected,
+                checkPermissionReadAudio(
+                        getContext(), pid, uid, packageName, null, targetSdkIsAtLeastT));
+        assertEquals(
+                expected,
+                checkPermissionReadImages(
+                        getContext(), pid, uid, packageName, null, targetSdkIsAtLeastT));
+        assertEquals(
+                expected,
+                checkPermissionReadVideo(
+                        getContext(), pid, uid, packageName, null, targetSdkIsAtLeastT));
     }
 }
diff --git a/tests/src/com/android/providers/media/util/StringUtilsTest.java b/tests/src/com/android/providers/media/util/StringUtilsTest.java
index 51a571e..c4556c4 100644
--- a/tests/src/com/android/providers/media/util/StringUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/StringUtilsTest.java
@@ -18,7 +18,9 @@
 
 import static com.android.providers.media.util.StringUtils.equalIgnoreCase;
 import static com.android.providers.media.util.StringUtils.startsWithIgnoreCase;
+import static com.android.providers.media.util.StringUtils.verifySupportedUncachedRelativePaths;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
@@ -27,6 +29,10 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 @RunWith(AndroidJUnit4.class)
 public class StringUtilsTest {
     @Test
@@ -52,4 +58,14 @@
         assertFalse(startsWithIgnoreCase(null, "audio/"));
         assertFalse(startsWithIgnoreCase(null, null));
     }
+
+    @Test public void testVerifySupportedUncachedRelativePaths() throws Exception {
+        assertEquals(
+                new ArrayList<String>(Arrays.asList("path/", "path/path/")),
+                verifySupportedUncachedRelativePaths(
+                        new ArrayList<String>(
+                                Arrays.asList(null, "", "/",
+                                              "path", "/path", "path/", "/path/",
+                                              "path/path", "/path/path", "path/path/"))));
+    }
 }
diff --git a/tests/test_app/TestAppForPermissionActivity33.xml b/tests/test_app/TestAppForPermissionActivity33.xml
new file mode 100644
index 0000000..4e8e74b
--- /dev/null
+++ b/tests/test_app/TestAppForPermissionActivity33.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.providers.media.testapp.permissionmedia"
+          android:versionCode="1"
+          android:versionName="1.0">
+
+    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="33" />
+
+    <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.MANAGE_MEDIA"/>
+    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
+    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
+    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
+
+    <application android:label="TestAppPerms33">
+        <activity android:name="com.android.providers.media.util.TestAppActivity"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
\ No newline at end of file
diff --git a/tests/test_app/TestAppWithMediaPerms.xml b/tests/test_app/TestAppWithMediaPerms.xml
new file mode 100644
index 0000000..5671919
--- /dev/null
+++ b/tests/test_app/TestAppWithMediaPerms.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.providers.media.testapp.withmediaperms"
+          android:versionCode="1"
+          android:versionName="1.0">
+
+    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="32" />
+    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
+    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
+    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
+
+    <application android:label="TestAppWithMediaPerms">
+        <activity android:name="com.android.providers.media.util.TestAppActivity"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/tools/dialogs/Android.bp b/tools/dialogs/Android.bp
index d0ccff2..98267be 100644
--- a/tools/dialogs/Android.bp
+++ b/tools/dialogs/Android.bp
@@ -1,10 +1,6 @@
 package {
     // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    default_applicable_licenses: ["packages_providers_MediaProvider_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 android_app {
diff --git a/tools/dialogs/AndroidManifest.xml b/tools/dialogs/AndroidManifest.xml
index 960cb13..73527cc 100644
--- a/tools/dialogs/AndroidManifest.xml
+++ b/tools/dialogs/AndroidManifest.xml
@@ -4,6 +4,9 @@
      package="com.android.providers.media.tools.dialogs">
 
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
+    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
+    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
     <uses-permission android:name="android.permission.MANAGE_MEDIA"/>
 
diff --git a/tools/dialogs/src/com/android/providers/media/tools/dialogs/DialogsActivity.java b/tools/dialogs/src/com/android/providers/media/tools/dialogs/DialogsActivity.java
index 867bfaf..f52354d 100644
--- a/tools/dialogs/src/com/android/providers/media/tools/dialogs/DialogsActivity.java
+++ b/tools/dialogs/src/com/android/providers/media/tools/dialogs/DialogsActivity.java
@@ -17,6 +17,9 @@
 package com.android.providers.media.tools.dialogs;
 
 import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
+import static android.Manifest.permission.READ_MEDIA_AUDIO;
+import static android.Manifest.permission.READ_MEDIA_IMAGES;
+import static android.Manifest.permission.READ_MEDIA_VIDEO;
 import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 
@@ -25,6 +28,7 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.AsyncTask;
+import android.os.Build;
 import android.os.Bundle;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Files.FileColumns;
@@ -62,11 +66,23 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        if (checkSelfPermission(READ_EXTERNAL_STORAGE) != PERMISSION_GRANTED
-                || checkSelfPermission(WRITE_EXTERNAL_STORAGE) != PERMISSION_GRANTED) {
-            requestPermissions(new String[] { READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE }, 42);
-            finish();
-            return;
+        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
+            if (checkSelfPermission(READ_EXTERNAL_STORAGE) != PERMISSION_GRANTED
+                    || checkSelfPermission(WRITE_EXTERNAL_STORAGE) != PERMISSION_GRANTED) {
+                requestPermissions(new String[]{READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE}, 42);
+                android.util.Log.d("doc", "finish");
+                finish();
+                return;
+            }
+        } else {
+            if (checkSelfPermission(READ_MEDIA_AUDIO) != PERMISSION_GRANTED
+                    || checkSelfPermission(READ_MEDIA_IMAGES) != PERMISSION_GRANTED
+                    || checkSelfPermission(READ_MEDIA_VIDEO) != PERMISSION_GRANTED) {
+                requestPermissions(
+                        new String[]{READ_MEDIA_AUDIO, READ_MEDIA_IMAGES, READ_MEDIA_VIDEO}, 42);
+                finish();
+                return;
+            }
         }
 
         mBody = new LinearLayout(this);
diff --git a/tools/photopicker/Android.bp b/tools/photopicker/Android.bp
index f606c22..d05c935 100644
--- a/tools/photopicker/Android.bp
+++ b/tools/photopicker/Android.bp
@@ -1,10 +1,6 @@
 package {
     // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "packages_providers_MediaProvider_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    default_applicable_licenses: ["packages_providers_MediaProvider_license"],
+    default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
 android_app {