Merge "Merge Android 14 QPR2 to AOSP main" into main
diff --git a/Android.bp b/Android.bp
index 044dbd7..6402ce2 100644
--- a/Android.bp
+++ b/Android.bp
@@ -20,16 +20,20 @@
         "modules-utils-build",
         "modules-utils-uieventlogger-interface",
         "glide-prebuilt",
+        "glide-integration-recyclerview-prebuilt",
+        "glide-integration-webpdecoder-prebuilt",
         "glide-gifdecoder-prebuilt",
         "glide-disklrucache-prebuilt",
         "glide-annotation-and-compiler-prebuilt",
         "androidx.fragment_fragment",
         "androidx.vectordrawable_vectordrawable-animated",
         "androidx.exifinterface_exifinterface",
+        "androidx.work_work-runtime",
         "exoplayer-mediaprovider-ui",
         "modules-utils-shell-command-handler",
         "SettingsLibProfileSelector",
         "SettingsLibSelectorWithWidgetPreference",
+        "mediaprovider_flags_java_lib",
     ],
 
     libs: [
@@ -117,7 +121,6 @@
 java_library {
     name: "mediaprovider-database",
     srcs: [
-        "src/com/android/providers/media/LegacyDatabaseHelper.java",
         "src/com/android/providers/media/util/DatabaseUtils.java",
         "src/com/android/providers/media/util/FileUtils.java",
         "src/com/android/providers/media/util/ForegroundThread.java",
@@ -168,3 +171,18 @@
     name: "media_provider",
     src: "cli/media_provider_cli_wrapper.sh",
 }
+
+aconfig_declarations {
+    name: "mediaprovider_flags",
+    package: "com.android.providers.media.flags",
+    srcs: ["mediaprovider_flags.aconfig"],
+}
+
+java_aconfig_library {
+    name: "mediaprovider_flags_java_lib",
+    aconfig_declarations: "mediaprovider_flags",
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.mediaprovider",
+    ],
+}
\ No newline at end of file
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 43cc969..8884bd9 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1,4 +1,5 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
         package="com.android.providers.media.module">
 
     <meta-data
@@ -91,6 +92,18 @@
             android:authorities="com.android.providers.media.remote_video_preview"
             android:exported="false" />
 
+        <!-- Don't initialise WorkManager by default at startup -->
+        <provider
+            android:name="androidx.startup.InitializationProvider"
+            android:authorities="${applicationId}.androidx-startup"
+            android:exported="false"
+            tools:node="merge">
+            <meta-data
+                android:name="androidx.work.WorkManagerInitializer"
+                android:value="androidx.startup"
+                tools:node="remove" />
+        </provider>
+
         <!-- Handles database upgrades after OTAs, then disables itself -->
         <receiver android:name="com.android.providers.media.MediaUpgradeReceiver"
             android:exported="true">
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 08df77c..5d49e62 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,7 +1,13 @@
 {
     "mainline-presubmit": [
         {
-            "name": "MediaProviderTests[com.google.android.mediaprovider.apex]"
+            "name": "MediaProviderTests[com.google.android.mediaprovider.apex]",
+            "options": [
+                {
+                    // Ignore the tests with @RunOnlyOnPostsubmit annotation
+                    "exclude-annotation": "com.android.providers.media.library.RunOnlyOnPostsubmit"
+                }
+            ]
         },
         {
             "name": "CtsScopedStorageCoreHostTest[com.google.android.mediaprovider.apex]"
@@ -13,20 +19,27 @@
             "name": "CtsScopedStorageDeviceOnlyTest[com.google.android.mediaprovider.apex]"
         },
         {
-            "name": "CtsMediaProviderTranscodeTests[com.google.android.mediaprovider.apex]"
+            "name": "CtsScopedStorageBypassDatabaseOperationsTest[com.google.android.mediaprovider.apex]"
         },
         {
-            "name": "CtsPhotoPickerTest[com.google.android.mediaprovider.apex]",
-            "options": [
-              {
-                "exclude-annotation": "androidx.test.filters.LargeTest"
-              }
-            ]
+            "name": "CtsScopedStorageGeneralTest[com.google.android.mediaprovider.apex]"
+        },
+        {
+            "name": "CtsScopedStorageRedactUriTest[com.google.android.mediaprovider.apex]"
+        },
+        {
+            "name": "CtsMediaProviderTranscodeTests[com.google.android.mediaprovider.apex]"
         }
     ],
     "presubmit": [
         {
-            "name": "MediaProviderTests"
+            "name": "MediaProviderTests",
+            "options": [
+                {
+                    // Ignore the tests with @RunOnlyOnPostsubmit annotation
+                    "exclude-annotation": "com.android.providers.media.library.RunOnlyOnPostsubmit"
+                }
+            ]
         },
         {
             "name": "MediaProviderClientTests",
@@ -62,15 +75,16 @@
             "name": "CtsScopedStorageDeviceOnlyTest"
         },
         {
-            "name": "fuse_node_test"
+            "name": "CtsScopedStorageBypassDatabaseOperationsTest"
         },
         {
-            "name": "CtsPhotoPickerTest",
-            "options": [
-                {
-                    "exclude-annotation": "androidx.test.filters.LargeTest"
-                }
-            ]
+            "name": "CtsScopedStorageGeneralTest"
+        },
+        {
+            "name": "CtsScopedStorageRedactUriTest"
+        },
+        {
+            "name": "fuse_node_test"
         }
     ],
     "postsubmit": [
@@ -82,7 +96,7 @@
             "name": "CtsMediaProviderTranscodeTests"
         },
         {
-            "name": "CtsAppSecurityHostTestCases",
+            "name": "CtsStorageHostTestCases",
             "options": [
                 {
                     "include-filter": "android.appsecurity.cts.ExternalStorageHostTest"
@@ -91,6 +105,34 @@
         },
         {
             "name": "CtsPhotoPickerTest"
+        },
+        {
+            "name": "MediaProviderTests",
+            "options": [
+                {
+                    // Only execute the tests with @RunOnlyOnPostsubmit annotation
+                    "include-annotation": "com.android.providers.media.library.RunOnlyOnPostsubmit"
+                }
+            ]
+        }
+    ],
+    "mainline-postsubmit": [
+        {
+            "name": "MediaProviderTests[com.google.android.mediaprovider.apex]",
+            "options": [
+                {
+                    // Only execute the tests with @RunOnlyOnPostsubmit annotation
+                    "include-annotation": "com.android.providers.media.library.RunOnlyOnPostsubmit"
+                }
+            ]
+        },
+        {
+            "name": "CtsPhotoPickerTest[com.google.android.mediaprovider.apex]",
+            "options": [
+                {
+                    "exclude-annotation": "androidx.test.filters.LargeTest"
+                }
+            ]
         }
     ]
 }
diff --git a/apex/Android.bp b/apex/Android.bp
index ebedce5..2d497d2 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -7,7 +7,7 @@
     name: "com.android.mediaprovider",
     defaults: ["com.android.mediaprovider-defaults"],
     manifest: "apex_manifest.json",
-    apps: ["MediaProvider"],
+    apps: ["MediaProvider", "PdfViewer"],
     compat_configs: ["media-provider-platform-compat-config"],
 }
 
diff --git a/apex/framework/api/current.txt b/apex/framework/api/current.txt
index b0d655d..cddfcfa 100644
--- a/apex/framework/api/current.txt
+++ b/apex/framework/api/current.txt
@@ -56,6 +56,7 @@
     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_SIZE = "android.provider.extra.PAGE_SIZE";
     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";
@@ -149,6 +150,7 @@
     field public static final String EXTRA_MEDIA_RADIO_CHANNEL = "android.intent.extra.radio_channel";
     field public static final String EXTRA_MEDIA_TITLE = "android.intent.extra.title";
     field public static final String EXTRA_OUTPUT = "output";
+    field @FlaggedApi("com.android.providers.media.flags.pick_ordered_images") public static final String EXTRA_PICK_IMAGES_IN_ORDER = "android.provider.extra.PICK_IMAGES_IN_ORDER";
     field public static final String EXTRA_PICK_IMAGES_MAX = "android.provider.extra.PICK_IMAGES_MAX";
     field public static final String EXTRA_SCREEN_ORIENTATION = "android.intent.extra.screenOrientation";
     field public static final String EXTRA_SHOW_ACTION_ICONS = "android.intent.extra.showActionIcons";
diff --git a/apex/framework/api/lint-baseline.txt b/apex/framework/api/lint-baseline.txt
new file mode 100644
index 0000000..1ed25ad
--- /dev/null
+++ b/apex/framework/api/lint-baseline.txt
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+RequiresPermission: android.provider.MediaStore#canManageMedia(android.content.Context):
+    Method 'canManageMedia' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.provider.MediaStore#setRequireOriginal(android.net.Uri):
+    Method 'setRequireOriginal' documentation mentions permissions without declaring @RequiresPermission
diff --git a/apex/framework/api/module-lib-lint-baseline.txt b/apex/framework/api/module-lib-lint-baseline.txt
new file mode 100644
index 0000000..44629d8
--- /dev/null
+++ b/apex/framework/api/module-lib-lint-baseline.txt
@@ -0,0 +1,9 @@
+// Baseline format: 1.0
+RequiresPermission: android.provider.MediaStore#canManageMedia(android.content.Context):
+    Method 'canManageMedia' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.provider.MediaStore#setRequireOriginal(android.net.Uri):
+    Method 'setRequireOriginal' documentation mentions permissions without declaring @RequiresPermission
+
+
+SdkConstant: android.provider.MediaStore#ACTION_USER_SELECT_IMAGES_FOR_APP:
+    Field 'ACTION_USER_SELECT_IMAGES_FOR_APP' is missing @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
diff --git a/apex/framework/api/system-lint-baseline.txt b/apex/framework/api/system-lint-baseline.txt
new file mode 100644
index 0000000..44629d8
--- /dev/null
+++ b/apex/framework/api/system-lint-baseline.txt
@@ -0,0 +1,9 @@
+// Baseline format: 1.0
+RequiresPermission: android.provider.MediaStore#canManageMedia(android.content.Context):
+    Method 'canManageMedia' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.provider.MediaStore#setRequireOriginal(android.net.Uri):
+    Method 'setRequireOriginal' documentation mentions permissions without declaring @RequiresPermission
+
+
+SdkConstant: android.provider.MediaStore#ACTION_USER_SELECT_IMAGES_FOR_APP:
+    Field 'ACTION_USER_SELECT_IMAGES_FOR_APP' is missing @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
diff --git a/apex/framework/java/android/provider/CloudMediaProvider.java b/apex/framework/java/android/provider/CloudMediaProvider.java
index 924ec02..665739e 100644
--- a/apex/framework/java/android/provider/CloudMediaProvider.java
+++ b/apex/framework/java/android/provider/CloudMediaProvider.java
@@ -206,6 +206,7 @@
      * <li> {@link CloudMediaProviderContract#EXTRA_SYNC_GENERATION}
      * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN}
      * <li> {@link CloudMediaProviderContract#EXTRA_ALBUM_ID}
+     * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE}
      * </ul>
      * @return cursor representing media items containing all
      * {@link CloudMediaProviderContract.MediaColumns} columns
@@ -259,6 +260,7 @@
      * <ul>
      * <li> {@link CloudMediaProviderContract#EXTRA_SYNC_GENERATION}
      * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_TOKEN}
+     * <li> {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE}
      * </ul>
      * @return cursor representing album items containing all
      * {@link CloudMediaProviderContract.AlbumColumns} columns
@@ -270,7 +272,9 @@
     }
 
     /**
-     * Returns a thumbnail of {@code size} for a media item identified by {@code mediaId}.
+     * Returns a thumbnail of {@code size} for a media item identified by {@code mediaId}
+     * <p>The cloud media provider should strictly return thumbnail in the original
+     * {@link CloudMediaProviderContract.MediaColumns#MIME_TYPE} of the item.
      * <p>
      * This is expected to be a much lower resolution version than the item returned by
      * {@link #onOpenMedia}.
diff --git a/apex/framework/java/android/provider/CloudMediaProviderContract.java b/apex/framework/java/android/provider/CloudMediaProviderContract.java
index cd4b434..5e610a8 100644
--- a/apex/framework/java/android/provider/CloudMediaProviderContract.java
+++ b/apex/framework/java/android/provider/CloudMediaProviderContract.java
@@ -88,8 +88,8 @@
         public static final String DATE_TAKEN_MILLIS = "date_taken_millis";
 
         /**
-         * Number associated with a media item indicating what generation or batch the media item
-         * was synced into the media collection.
+         * Non-negative number associated with a media item indicating what generation or batch the
+         * media item was synced into the media collection.
          * <p>
          * Providers should associate a monotonically increasing sync generation number to each
          * media item which is expected to increase for each atomic modification on the media item.
@@ -551,6 +551,22 @@
     public static final String EXTRA_ALBUM_ID = "android.provider.extra.ALBUM_ID";
 
     /**
+     * The maximum number of query results that should be included in a batch when syncing metadata
+     * with cloud provider.
+     *
+     * This extra can be passed as a {@link Bundle} parameter to the media or album query methods.
+     *
+     * It is optional for the provider to honor this extra and return results at max page size.
+     *
+     * @see CloudMediaProvider#onQueryMedia
+     * @see CloudMediaProvider#onQueryAlbums
+     *
+     * <p>
+     * Type: INTEGER
+     */
+    public static final String EXTRA_PAGE_SIZE = "android.provider.extra.PAGE_SIZE";
+
+    /**
      * Limits the query results to only media items less than the given file size in bytes.
      * <p>
      * This is only intended for the MediaProvider to implement for cross-user communication. Not
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 634d25f..b1172af 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -20,6 +20,7 @@
 import android.annotation.CurrentTimeMillisLong;
 import android.annotation.CurrentTimeSecondsLong;
 import android.annotation.DurationMillisLong;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -260,6 +261,8 @@
     /** {@hide} */
     public static final String GET_CLOUD_PROVIDER_RESULT = "get_cloud_provider_result";
     /** {@hide} */
+    public static final String SET_CLOUD_PROVIDER_RESULT = "set_cloud_provider_result";
+    /** {@hide} */
     public static final String SET_CLOUD_PROVIDER_CALL = "set_cloud_provider";
     /** {@hide} */
     public static final String EXTRA_CLOUD_PROVIDER = "cloud_provider";
@@ -272,10 +275,22 @@
     public static final String GRANT_MEDIA_READ_FOR_PACKAGE_CALL =
             "grant_media_read_for_package";
 
+    /** @hide */
+    public static final String REVOKE_READ_GRANT_FOR_PACKAGE_CALL =
+            "revoke_media_read_for_package";
+
     /** {@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 PICKER_MEDIA_INIT_CALL = "picker_media_init";
+    /** {@hide} */
+    public static final String EXTRA_LOCAL_ONLY = "is_local_only";
+    /** {@hide} */
+    public static final String EXTRA_ALBUM_ID = "album_id";
+    /** {@hide} */
+    public static final String EXTRA_ALBUM_AUTHORITY = "album_authority";
 
     /**
      * Only used for testing.
@@ -290,7 +305,14 @@
      * {@hide}
      */
     @VisibleForTesting
-    public static final String READ_BACKED_UP_FILE_PATHS = "read_backed_up_file_paths";
+    public static final String READ_BACKUP = "read_backup";
+
+    /**
+     * Only used for testing.
+     * {@hide}
+     */
+    @VisibleForTesting
+    public static final String GET_OWNER_PACKAGE_NAME = "get_owner_package_name";
 
     /**
      * Only used for testing.
@@ -304,6 +326,20 @@
      * {@hide}
      */
     @VisibleForTesting
+    public static final String GET_RECOVERY_DATA = "get_recovery_data";
+
+    /**
+     * Only used for testing.
+     * {@hide}
+     */
+    @VisibleForTesting
+    public static final String REMOVE_RECOVERY_DATA = "remove_recovery_data";
+
+    /**
+     * Only used for testing.
+     * {@hide}
+     */
+    @VisibleForTesting
     public static final String DELETE_BACKED_UP_FILE_PATHS = "delete_backed_up_file_paths";
 
     /** {@hide} */
@@ -482,18 +518,18 @@
     public static final String EXTRA_SHOW_ACTION_ICONS = "android.intent.extra.showActionIcons";
 
     /**
-     * The name of the Intent-extra used to control the onCompletion behavior of a MovieView.
-     * This is a boolean property that specifies whether or not to finish the MovieView activity
-     * when the movie completes playing. The default value is true, which means to automatically
-     * exit the movie player activity when the movie completes playing.
+     * The name of the Intent-extra used to control the onCompletion behavior of a MovieView. This
+     * is a boolean property that specifies whether or not to finish the MovieView activity when the
+     * movie completes playing. The default value is true, which means to automatically exit the
+     * movie player activity when the movie completes playing.
      */
-    public static final String EXTRA_FINISH_ON_COMPLETION = "android.intent.extra.finishOnCompletion";
+    public static final String EXTRA_FINISH_ON_COMPLETION =
+            "android.intent.extra.finishOnCompletion";
 
-    /**
-     * The name of the Intent action used to launch a camera in still image mode.
-     */
+    /** The name of the Intent action used to launch a camera in still image mode. */
     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
-    public static final String INTENT_ACTION_STILL_IMAGE_CAMERA = "android.media.action.STILL_IMAGE_CAMERA";
+    public static final String INTENT_ACTION_STILL_IMAGE_CAMERA =
+            "android.media.action.STILL_IMAGE_CAMERA";
 
     /**
      * Name under which an activity handling {@link #INTENT_ACTION_STILL_IMAGE_CAMERA} or
@@ -720,42 +756,39 @@
     public final static String EXTRA_OUTPUT = "output";
 
     /**
-     * Activity Action: Allow the user to select images or videos provided by
-     * system and return it. This is different than {@link Intent#ACTION_PICK}
-     * and {@link Intent#ACTION_GET_CONTENT} in that
+     * Activity Action: Allow the user to select images or videos provided by system and return it.
+     * This is different than {@link Intent#ACTION_PICK} and {@link Intent#ACTION_GET_CONTENT} in
+     * that
+     *
      * <ul>
-     * <li> the data for this action is provided by the system
-     * <li> this action is only used for picking images and videos
-     * <li> caller gets read access to user picked items even without storage
-     * permissions
+     *   <li>the data for this action is provided by the system
+     *   <li>this action is only used for picking images and videos
+     *   <li>caller gets read access to user picked items even without storage permissions
      * </ul>
-     * <p>
-     * Callers can optionally specify MIME type (such as {@code image/*} or
-     * {@code video/*}), resulting in a range of content selection that the
-     * caller is interested in. The optional MIME type can be requested with
-     * {@link Intent#setType(String)}.
-     * <p>
-     * If the caller needs multiple returned items (or caller wants to allow
-     * multiple selection), then it can specify
-     * {@link MediaStore#EXTRA_PICK_IMAGES_MAX} to indicate this.
-     * <p>
-     * When the caller requests multiple selection, the value of
-     * {@link MediaStore#EXTRA_PICK_IMAGES_MAX} must be a positive integer
-     * greater than 1 and less than or equal to
-     * {@link MediaStore#getPickImagesMaxLimit}, otherwise
-     * {@link Activity#RESULT_CANCELED} is returned.
-     * <p>
-     * Callers may use {@link Intent#EXTRA_LOCAL_ONLY} to limit content
-     * selection to local data.
-     * <p>
-     * Output: MediaStore content URI(s) of the item(s) that was picked.
-     * Unlike other MediaStore URIs, these are referred to as 'picker' URIs and
-     * 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,
-     * {@link #ACTION_PICK_IMAGES} is now the recommended option for images and videos,
-     * since it offers a better user experience.
+     *
+     * <p>Callers can optionally specify MIME type (such as {@code image/*} or {@code video/*}),
+     * resulting in a range of content selection that the caller is interested in. The optional MIME
+     * type can be requested with {@link Intent#setType(String)}.
+     *
+     * <p>If the caller needs multiple returned items (or caller wants to allow multiple selection),
+     * then it can specify {@link MediaStore#EXTRA_PICK_IMAGES_MAX} to indicate this.
+     *
+     * <p>When the caller requests multiple selection, the value of {@link
+     * MediaStore#EXTRA_PICK_IMAGES_MAX} must be a positive integer greater than 1 and less than or
+     * equal to {@link MediaStore#getPickImagesMaxLimit}, otherwise {@link Activity#RESULT_CANCELED}
+     * is returned. Use {@link MediaStore#EXTRA_PICK_IMAGES_IN_ORDER} in multiple selection mode to
+     * allow the user to pick images in order.
+     *
+     * <p>Callers may use {@link Intent#EXTRA_LOCAL_ONLY} to limit content selection to local data.
+     *
+     * <p>Output: MediaStore content URI(s) of the item(s) that was picked. Unlike other MediaStore
+     * URIs, these are referred to as 'picker' URIs and 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, {@link
+     * #ACTION_PICK_IMAGES} is now the recommended option for images and videos, since it offers a
+     * better user experience.
      */
     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
     public static final String ACTION_PICK_IMAGES = "android.provider.action.PICK_IMAGES";
@@ -803,6 +836,20 @@
             "android.provider.action.PICK_IMAGES_SETTINGS";
 
     /**
+     * The name of an optional intent-extra used to allow ordered selection of items. Set this extra
+     * to true to allow the user to see the order of their selected items. The result returned to
+     * the caller will be the same as the user selected order. This extra is only allowed via the
+     * {@link MediaStore#ACTION_PICK_IMAGES}.
+     *
+     * <p>The value of this intent-extra should be a boolean. Default value is false.
+     *
+     * @see #ACTION_PICK_IMAGES
+     */
+    @FlaggedApi("com.android.providers.media.flags.pick_ordered_images")
+    public static final String EXTRA_PICK_IMAGES_IN_ORDER =
+            "android.provider.extra.PICK_IMAGES_IN_ORDER";
+
+    /**
      * The name of an optional intent-extra used to allow multiple selection of
      * items and constrain maximum number of items that can be returned by
      * {@link MediaStore#ACTION_PICK_IMAGES}, action may still return nothing
@@ -4721,10 +4768,23 @@
      * {@hide}
      */
     @VisibleForTesting
-    public static String[] readBackedUpFilePaths(@NonNull ContentResolver resolver,
-            String volumeName) {
-        Bundle bundle = resolver.call(AUTHORITY, READ_BACKED_UP_FILE_PATHS, volumeName, null);
-        return bundle.getStringArray(READ_BACKED_UP_FILE_PATHS);
+    public static String readBackup(@NonNull ContentResolver resolver,
+            String volumeName, String filePath) {
+        Bundle extras = new Bundle();
+        extras.putString(Files.FileColumns.DATA, filePath);
+        Bundle bundle = resolver.call(AUTHORITY, READ_BACKUP, volumeName, extras);
+        return bundle.getString(READ_BACKUP);
+    }
+
+    /**
+     * Only used for testing.
+     * {@hide}
+     */
+    @VisibleForTesting
+    public static String getOwnerPackageName(@NonNull ContentResolver resolver, int ownerId) {
+        Bundle bundle = resolver.call(AUTHORITY, GET_OWNER_PACKAGE_NAME, String.valueOf(ownerId),
+                null);
+        return bundle.getString(GET_OWNER_PACKAGE_NAME);
     }
 
     /**
@@ -4748,6 +4808,25 @@
     }
 
     /**
+     * Only used for testing.
+     * {@hide}
+     */
+    @VisibleForTesting
+    public static String[] getRecoveryData(@NonNull ContentResolver resolver) {
+        Bundle bundle = resolver.call(AUTHORITY, GET_RECOVERY_DATA, null, null);
+        return bundle.getStringArray(GET_RECOVERY_DATA);
+    }
+
+    /**
+     * Only used for testing.
+     * {@hide}
+     */
+    @VisibleForTesting
+    public static void removeRecoveryData(@NonNull ContentResolver resolver) {
+        resolver.call(AUTHORITY, REMOVE_RECOVERY_DATA, null, null);
+    }
+
+    /**
      * Block until any pending operations have finished, such as
      * {@link #scanFile} or {@link #scanVolume} requests.
      *
@@ -4914,4 +4993,29 @@
             throw e.rethrowAsRuntimeException();
         }
     }
+
+    /**
+     * Revoke {@link com.android.providers.media.MediaGrants} for the given package, for the
+     * list of local (to the device) content uris. These must be valid picker uris.
+     *
+     * @hide
+     */
+    public static void revokeMediaReadForPackages(
+            @NonNull Context context, int packageUid, @NonNull List<Uri> uris) {
+        Objects.requireNonNull(uris);
+        if (uris.isEmpty()) {
+            return;
+        }
+        final ContentResolver resolver = context.getContentResolver();
+        try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) {
+            final Bundle extras = new Bundle();
+            extras.putInt(Intent.EXTRA_UID, packageUid);
+            extras.putParcelableArrayList(EXTRA_URI_LIST, new ArrayList<Uri>(uris));
+            client.call(REVOKE_READ_GRANT_FOR_PACKAGE_CALL,
+                    /* arg= */ null,
+                    /* extras= */ extras);
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
 }
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index e5fb23e..c155229 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -2581,6 +2581,16 @@
     }
 }
 
+void FuseDaemon::SetupPublicVolumeLevelDbInstance(const std::string& volume_name) {
+    if (android::base::StartsWith(fuse->root->GetIoPath(), PRIMARY_VOLUME_PREFIX)) {
+        // Setup leveldb instance for both external primary and internal volume.
+        fuse->level_db_mutex.lock();
+        // Create level db instance for public volume
+        SetupLevelDbConnection(volume_name);
+        fuse->level_db_mutex.unlock();
+    }
+}
+
 std::string deriveVolumeName(const std::string& path) {
     std::string volume_name;
     if (!android::base::StartsWith(path, STORAGE_PREFIX)) {
@@ -2588,8 +2598,10 @@
     } else if (android::base::StartsWith(path, PRIMARY_VOLUME_PREFIX)) {
         volume_name = VOLUME_EXTERNAL_PRIMARY;
     } else {
-        size_t size = sizeof(STORAGE_PREFIX) / sizeof(STORAGE_PREFIX[0]);
-        volume_name = volume_name.substr(size);
+        // Return "C58E-1702" from the path like "/storage/C58E-1702/Download/1935694997673.png"
+        volume_name = path.substr(9, 9);
+        // Convert to lowercase
+        std::transform(volume_name.begin(), volume_name.end(), volume_name.begin(), ::tolower);
     }
     return volume_name;
 }
@@ -2597,7 +2609,7 @@
 void FuseDaemon::DeleteFromLevelDb(const std::string& key) {
     std::string volume_name = deriveVolumeName(key);
     if (!CheckLevelDbConnection(volume_name)) {
-        LOG(ERROR) << "Failure in leveldb delete in volume:" << volume_name << " for key:" << key;
+        LOG(ERROR) << "DeleteFromLevelDb: Missing leveldb connection.";
         return;
     }
 
@@ -2609,10 +2621,10 @@
     }
 }
 
-void FuseDaemon::InsertInLevelDb(const std::string& key, const std::string& value) {
-    std::string volume_name = deriveVolumeName(key);
+void FuseDaemon::InsertInLevelDb(const std::string& volume_name, const std::string& key,
+                                 const std::string& value) {
     if (!CheckLevelDbConnection(volume_name)) {
-        LOG(ERROR) << "Failure in leveldb insert in volume:" << volume_name << " for key:" << key;
+        LOG(ERROR) << "InsertInLevelDb: Missing leveldb connection.";
         return;
     }
 
@@ -2620,6 +2632,7 @@
     status = fuse->level_db_connection_map[volume_name]->Put(leveldb::WriteOptions(), key, value);
     if (!status.ok()) {
         LOG(ERROR) << "Failure in leveldb insert for key: " << key << " in volume:" << volume_name;
+        LOG(ERROR) << status.ToString();
     }
 }
 
@@ -2630,7 +2643,7 @@
     std::vector<std::string> file_paths;
 
     if (!CheckLevelDbConnection(volume_name)) {
-        LOG(ERROR) << "Failure in leveldb file paths read for volume:" << volume_name;
+        LOG(ERROR) << "ReadFilePathsFromLevelDb: Missing leveldb connection.";
         return file_paths;
     }
 
@@ -2655,16 +2668,16 @@
     std::string data = "";
     std::string volume_name = deriveVolumeName(filePath);
     if (!CheckLevelDbConnection(volume_name)) {
-        LOG(ERROR) << "Failure in leveldb data read for key:" << filePath;
+        LOG(ERROR) << "ReadBackedUpDataFromLevelDb: Missing leveldb connection.";
         return data;
     }
 
     leveldb::Status status = fuse->level_db_connection_map[volume_name]->Get(leveldb::ReadOptions(),
                                                                              filePath, &data);
-    if (!status.ok()) {
-        LOG(WARNING) << "Failure in leveldb read for key: " << filePath << status.ToString();
-    } else {
-        LOG(DEBUG) << "Read successful for key: " << filePath;
+    if (status.IsNotFound()) {
+        LOG(VERBOSE) << "Key is not found in leveldb: " << filePath << " " << status.ToString();
+    } else if (!status.ok()) {
+        LOG(WARNING) << "Failure in leveldb read for key: " << filePath << " " << status.ToString();
     }
     return data;
 }
@@ -2672,22 +2685,26 @@
 std::string FuseDaemon::ReadOwnership(const std::string& key) {
     // Return empty string if key not found
     std::string data = "";
-    if (CheckLevelDbConnection(OWNERSHIP_RELATION)) {
-        leveldb::Status status = fuse->level_db_connection_map[OWNERSHIP_RELATION]->Get(
-                leveldb::ReadOptions(), key, &data);
-        if (!status.ok()) {
-            LOG(WARNING) << "Failure in leveldb read for key: " << key << status.ToString();
-        } else {
-            LOG(DEBUG) << "Read successful for key: " << key;
-        }
+    if (!CheckLevelDbConnection(OWNERSHIP_RELATION)) {
+        LOG(ERROR) << "ReadOwnership: Missing leveldb connection.";
+        return data;
     }
+
+    leveldb::Status status = fuse->level_db_connection_map[OWNERSHIP_RELATION]->Get(
+            leveldb::ReadOptions(), key, &data);
+    if (status.IsNotFound()) {
+        LOG(VERBOSE) << "Key is not found in leveldb: " << key << " " << status.ToString();
+    } else if (!status.ok()) {
+        LOG(WARNING) << "Failure in leveldb read for key: " << key << " " << status.ToString();
+    }
+
     return data;
 }
 
 void FuseDaemon::CreateOwnerIdRelation(const std::string& ownerId,
                                        const std::string& ownerPackageIdentifier) {
     if (!CheckLevelDbConnection(OWNERSHIP_RELATION)) {
-        LOG(ERROR) << "Failure in leveldb insert for ownership relation.";
+        LOG(ERROR) << "CreateOwnerIdRelation: Missing leveldb connection.";
         return;
     }
 
@@ -2710,7 +2727,7 @@
 void FuseDaemon::RemoveOwnerIdRelation(const std::string& ownerId,
                                        const std::string& ownerPackageIdentifier) {
     if (!CheckLevelDbConnection(OWNERSHIP_RELATION)) {
-        LOG(ERROR) << "Failure in leveldb delete for ownership relation.";
+        LOG(ERROR) << "RemoveOwnerIdRelation: Missing leveldb connection.";
         return;
     }
 
@@ -2736,7 +2753,7 @@
 std::map<std::string, std::string> FuseDaemon::GetOwnerRelationship() {
     std::map<std::string, std::string> resultMap;
     if (!CheckLevelDbConnection(OWNERSHIP_RELATION)) {
-        LOG(ERROR) << "Failure in leveldb read for ownership relation.";
+        LOG(ERROR) << "GetOwnerRelationship: Missing leveldb connection.";
         return resultMap;
     }
 
@@ -2754,7 +2771,7 @@
 
 bool FuseDaemon::CheckLevelDbConnection(const std::string& instance_name) {
     if (fuse->level_db_connection_map.find(instance_name) == fuse->level_db_connection_map.end()) {
-        LOG(ERROR) << "Leveldb setup is missing for :" << instance_name;
+        LOG(ERROR) << "Leveldb setup is missing for: " << instance_name;
         return false;
     }
     return true;
diff --git a/jni/FuseDaemon.h b/jni/FuseDaemon.h
index a634812..a9eaf22 100644
--- a/jni/FuseDaemon.h
+++ b/jni/FuseDaemon.h
@@ -80,6 +80,11 @@
     void SetupLevelDbInstances();
 
     /**
+     * Setup leveldb instances for public volume.
+     */
+    void SetupPublicVolumeLevelDbInstance(const std::string& volume_name);
+
+    /**
      * Creates a leveldb instance and sets up a connection.
      */
     void SetupLevelDbConnection(const std::string& instance_name);
@@ -90,9 +95,10 @@
     void DeleteFromLevelDb(const std::string& key);
 
     /**
-     * Inserts in leveldb instance of volume derived from path.
+     * Inserts in leveldb instance of provided volume.
      */
-    void InsertInLevelDb(const std::string& key, const std::string& value);
+    void InsertInLevelDb(const std::string& volume_name, const std::string& key,
+                         const std::string& value);
 
     /**
      * Reads file paths for given volume from leveldb for given range.
diff --git a/jni/com_android_providers_media_FuseDaemon.cpp b/jni/com_android_providers_media_FuseDaemon.cpp
index 97a6a6e..2b78645 100644
--- a/jni/com_android_providers_media_FuseDaemon.cpp
+++ b/jni/com_android_providers_media_FuseDaemon.cpp
@@ -196,6 +196,18 @@
     daemon->SetupLevelDbInstances();
 }
 
+void com_android_providers_media_FuseDaemon_setup_public_volume_db_backup(JNIEnv* env, jobject self,
+                                                                   jlong java_daemon,
+                                                                   jstring volume_name) {
+    fuse::FuseDaemon* const daemon = reinterpret_cast<fuse::FuseDaemon*>(java_daemon);
+    ScopedUtfChars utf_chars_volumeName(env, volume_name);
+    if (!utf_chars_volumeName.c_str()) {
+        LOG(WARNING) << "Couldn't initialise FUSE device id for " << volume_name;
+        return;
+    }
+    daemon->SetupPublicVolumeLevelDbInstance(utf_chars_volumeName.c_str());
+}
+
 void com_android_providers_media_FuseDaemon_delete_db_backup(JNIEnv* env, jobject self,
                                                              jlong java_daemon, jstring java_path) {
     fuse::FuseDaemon* const daemon = reinterpret_cast<fuse::FuseDaemon*>(java_daemon);
@@ -209,15 +221,19 @@
 
 void com_android_providers_media_FuseDaemon_backup_volume_db_data(JNIEnv* env, jobject self,
                                                                   jlong java_daemon,
-                                                                  jstring java_path, jstring value) {
+                                                                  jstring volume_name,
+                                                                  jstring java_path,
+                                                                  jstring value) {
     fuse::FuseDaemon* const daemon = reinterpret_cast<fuse::FuseDaemon*>(java_daemon);
     ScopedUtfChars utf_chars_path(env, java_path);
     ScopedUtfChars utf_chars_value(env, value);
+    ScopedUtfChars utf_chars_volumeName(env, volume_name);
     if (!utf_chars_path.c_str()) {
         LOG(WARNING) << "Couldn't initialise FUSE device id";
         return;
     }
-    daemon->InsertInLevelDb(utf_chars_path.c_str(), utf_chars_value.c_str());
+    daemon->InsertInLevelDb(utf_chars_volumeName.c_str(), utf_chars_path.c_str(),
+                            utf_chars_value.c_str());
 }
 
 bool com_android_providers_media_FuseDaemon_is_fuse_thread(JNIEnv* env, jclass clazz) {
@@ -328,9 +344,11 @@
          reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_initialize_device_id)},
         {"native_setup_volume_db_backup", "(J)V",
          reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_setup_volume_db_backup)},
+         {"native_setup_public_volume_db_backup", "(JLjava/lang/String;)V",
+         reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_setup_public_volume_db_backup)},
         {"native_delete_db_backup", "(JLjava/lang/String;)V",
          reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_delete_db_backup)},
-        {"native_backup_volume_db_data", "(JLjava/lang/String;Ljava/lang/String;)V",
+        {"native_backup_volume_db_data", "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
          reinterpret_cast<void*>(com_android_providers_media_FuseDaemon_backup_volume_db_data)},
         {"native_read_backed_up_file_paths",
          "(JLjava/lang/String;Ljava/lang/String;I)[Ljava/lang/String;",
diff --git a/jni/node.cpp b/jni/node.cpp
index 31e4970..25f732d 100644
--- a/jni/node.cpp
+++ b/jni/node.cpp
@@ -115,6 +115,14 @@
     std::lock_guard<std::recursive_mutex> guard(*tree->lock_);
 
     if (tree) {
+        // Guarantee this node not be released while deleting its children.
+        // pf_forget could be called for a parent node first not its children
+        // when evicting file system inodes by shrinker, so the parent node
+        // could exist without its own reference but having a children node.
+        // In this case, this node could be deleted during executing
+        // DeleteTree(child), and it causes double free for the node.
+        tree->Acquire();
+
         // Make a copy of the list of children because calling Delete tree
         // will modify the list of children, which will cause issues while
         // iterating over them.
diff --git a/jni/node_test.cpp b/jni/node_test.cpp
index f687cad..6afdd75 100644
--- a/jni/node_test.cpp
+++ b/jni/node_test.cpp
@@ -526,6 +526,22 @@
     test_fn("BaZ", baz1.get(), baz2.get());
 }
 
+TEST_F(NodeTest, DestroyDoesntDoubleFree) {
+    node* root = node::Create(nullptr, "root", "", true, 0, 0, &lock_, 0, &tracker_);
+    node* child = node::Create(root, "child", "", true, 0, 0, &lock_, 0, &tracker_);
+    node* grandchild = node::Create(child, "grandchild", "", true, 0, 0, &lock_, 0, &tracker_);
+
+    // 'child' is referenced by itself and by 'grandchild'
+    ASSERT_EQ(2, GetRefCount(child));
+    // Kernel forgets about child only
+    ASSERT_FALSE(child->Release(1));
+    // Child only referenced by 'grandchild'
+    ASSERT_EQ(1, GetRefCount(child));
+
+    // Now, destroying the filesystem shouldn't result in a double free
+    node::DeleteTree(root);
+}
+
 TEST_F(NodeTest, ForChild) {
     unique_node_ptr parent = CreateNode(nullptr, "/path");
     unique_node_ptr foo1 = CreateNode(parent.get(), "FoO");
diff --git a/src/com/android/providers/media/LegacyDatabaseHelper.java b/legacy/src/com/android/providers/media/LegacyDatabaseHelper.java
similarity index 99%
rename from src/com/android/providers/media/LegacyDatabaseHelper.java
rename to legacy/src/com/android/providers/media/LegacyDatabaseHelper.java
index 0e21878..9f85763 100644
--- a/src/com/android/providers/media/LegacyDatabaseHelper.java
+++ b/legacy/src/com/android/providers/media/LegacyDatabaseHelper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 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.
@@ -1103,3 +1103,4 @@
         return "LegacyDH[" + getDatabaseName() + "]." + method;
     }
 }
+
diff --git a/mediaprovider_flags.aconfig b/mediaprovider_flags.aconfig
new file mode 100644
index 0000000..85bc07b
--- /dev/null
+++ b/mediaprovider_flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.providers.media.flags"
+
+flag {
+    name: "pick_ordered_images"
+    namespace: "mediaprovider"
+    description: "This flag controls whether to enable ordered selection in photopicker"
+    bug: "303784642"
+}
diff --git a/mediaproviderutils.sh b/mediaproviderutils.sh
index f25640d..f0b3dc6 100644
--- a/mediaproviderutils.sh
+++ b/mediaproviderutils.sh
@@ -1,5 +1,6 @@
 # Shell utility functions for mediaprovider developers.
 # sudo apt-get install rlwrap to have a more fully featured sqlite CLI
+# sudo apt-get install sqlitebrowser to navigate the database with a GUI
 set -x # enable debugging
 
 function add-media-grant () {
@@ -63,14 +64,9 @@
   fi
 }
 
-function sqlite3-pull () {
-    adb root
-    if [ -z "$1" ]
-    then
-        dir=$(pwd)
-    else
-        dir=$1
-    fi
+function media-pull () {
+    adb root && adb wait-for-device
+    dir=$(get-dir $1)
     package=$(get-package)
 
     if [ -f "$dir/external.db" ]; then
@@ -86,10 +82,21 @@
     sqlite3 $dir/external.db "drop trigger files_insert"
     sqlite3 $dir/external.db "drop trigger files_update"
     sqlite3 $dir/external.db "drop trigger files_delete"
-
-    rlwrap sqlite3 $dir/external.db
 }
 
+function sqlite3-pull () {
+      dir="$(get-dir $1)"
+      media-pull "$dir"
+      rlwrap sqlite3 "$dir"/external.db
+}
+
+function sqlitebrowser-pull () {
+    dir="$(get-dir "$1")"
+    media-pull "$dir"
+    sqlitebrowser "$dir"/external.db
+}
+
+
 function sqlite3-push () {
     adb root
     if [ -z "$1" ]
@@ -145,6 +152,16 @@
     adb shell sqlite3 $dir $clause
 }
 
+function get-dir (){
+    if [ -z "$1" ]
+    then
+        dir=$(pwd)
+    else
+        dir=$1
+    fi
+    echo "$dir"
+}
+
 function get-package() {
     if [ -z "$(adb shell pm list package com.android.providers.media.module)" ]
     then
diff --git a/pdf/apk/Android.bp b/pdf/apk/Android.bp
new file mode 100644
index 0000000..42e5f60
--- /dev/null
+++ b/pdf/apk/Android.bp
@@ -0,0 +1,40 @@
+// Copyright (C) 2023 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 {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app_certificate {
+    name: "com.android.graphics.pdf.certificate",
+    certificate: "com.android.graphics.pdf",
+}
+
+filegroup {
+    name: "pdfviewer-sources",
+    srcs: [
+        "src/**/*.java",
+    ],
+}
+
+android_app {
+    name: "PdfViewer",
+    srcs: [":pdfviewer-sources"],
+    updatable: true,
+    certificate: ":com.android.graphics.pdf.certificate",
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    apex_available: ["com.android.mediaprovider"],
+}
\ No newline at end of file
diff --git a/pdf/apk/AndroidManifest.xml b/pdf/apk/AndroidManifest.xml
new file mode 100644
index 0000000..369bb56
--- /dev/null
+++ b/pdf/apk/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 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.graphics.pdf">
+    <application
+        android:label="PdfViewer">
+
+    </application>
+</manifest>
\ No newline at end of file
diff --git a/pdf/apk/com.android.graphics.pdf.pk8 b/pdf/apk/com.android.graphics.pdf.pk8
new file mode 100644
index 0000000..6004032
--- /dev/null
+++ b/pdf/apk/com.android.graphics.pdf.pk8
Binary files differ
diff --git a/pdf/apk/com.android.graphics.pdf.x509.pem b/pdf/apk/com.android.graphics.pdf.x509.pem
new file mode 100644
index 0000000..16e1628
--- /dev/null
+++ b/pdf/apk/com.android.graphics.pdf.x509.pem
@@ -0,0 +1,35 @@
+-----BEGIN CERTIFICATE-----
+MIIGETCCA/mgAwIBAgIUP9mjBKPNP1+BI6mngJol0t6leZIwDQYJKoZIhvcNAQEL
+BQAwgZYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH
+DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy
+b2lkMRIwEAYDVQQDDAlQZGZWaWV3ZXIxIjAgBgkqhkiG9w0BCQEWE2FuZHJvaWRA
+YW5kcm9pZC5jb20wIBcNMjMxMDEyMDQzNjI0WhgPNDc2MTA5MDcwNDM2MjRaMIGW
+MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91
+bnRhaW4gVmlldzEQMA4GA1UECgwHQW5kcm9pZDEQMA4GA1UECwwHQW5kcm9pZDES
+MBAGA1UEAwwJUGRmVmlld2VyMSIwIAYJKoZIhvcNAQkBFhNhbmRyb2lkQGFuZHJv
+aWQuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAmSU7yvmLqo/x
+B4l9mrVeHxPeX9ok5cSkP9VoDPL/uTCKE7RFG7A6yIogwNMGL4aKLLUTpdDsJoKZ
+bgZEHhJMuy/HE4XEaLJtzr9lO+Ys9RXgqKORZsBQrg/O5/pnepZvzSpZkpWiPHtm
+HaREFe/j5pmjsjW3p9JYEbauawUvrg8KsBBAlPBwGckHsIuxQfPusW3EobqlXB0y
+8mkT1UbJE+HRwWlazyfuR95vvGaS1g0dii03QVYTB6mNoGyM884YsSC95w/RtqYf
+B7s9hbpUeefmjMILxD+ArJdiYHxFVD8j43tOvnc+y61Pz5o9zam6Rs+qLw7GNgfe
+wOQg+BKMFc8JO2fiNhUmuXH5oN7lTpz7c1s2RGfnj5A+2nK+BPevbZDyILr/bRQp
+Sxg7J7XL3LuvhBnx/bLxvLuaRSmp73KIJDZwEM4EUsXuueVw4bsoTQUsogL1sU9v
+ahZjCL92Vc5iEfll9hrAJcMTERKjG09KU9DWfWeaMst/5V5UxoEhq4ZmhSI6h7uj
+cy4vvQKouqYYyZ8R598lprp7FcraBcVZSp2qBvTtQQpEG+y5SBm3E/n5DtcNHZId
+L6hHAmQciIdDv+oxbEp1rigCejWgcRfxWkNcVbR5TeucLZOUmNyWXmrCEd5Iz0vd
+SeO2NMCUreP18PSuwEzXJ83rytj0bCMCAwEAAaNTMFEwHQYDVR0OBBYEFKD9ou8w
+qANyOhdhBamcysOWrWiSMB8GA1UdIwQYMBaAFKD9ou8wqANyOhdhBamcysOWrWiS
+MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAAXXcTDfW6GVz/SH
+UvjUvTIYbZ7Kl9Fcca8nY0m89bq8dt+/3ElxSrFzVojF2NgiQGdpY5/cI3x8XXLC
+neKtfv/YElF+ikC11UMOdRThLEmP/MGG96i5bv5O085Nbi7/e3Jy+kALgc5rOgvX
+JyhvinYpiIu5OkdZqQuNBKCELosxBMTRY5d32ABEYRz/Vf2s5v2jzU5zxo3+9rbb
+NYGu0rkd1/2/Rkxzt32Xs4mUsgZjttqx4Wmvo0k3WP2ZwH4te79JUZGO7JvsiDjo
+Cx8f+cvhCdMa+I4l7+BrcBNyUNQ+S6WDKTzRObXZPUQ/XmMCp1wecG6yxMlMzvFk
+fzY2PbZC5u6GmvETG6lk0Xwq3hMljcko+A8F9P3wzLUlSwwLJG5OY7VuQtMDfYBW
+48aGns339ROJlt+bv5THhzZBSpgYZarfRTYG/uVzs+Sk4jutpPI76doYCBMQ6CEm
+X7OdTWrWdbQGBIyVOZJzhAOFD2YUE9eUrOgo1vNVUjMIPVVXdqhBn0h5alho58Xc
+cdhLJlps9UEzRm9RDomhET9WFVtWuh4WVL5IZ6IOo4ghPcvWDKtb8Py/xeBl5eJQ
+izAl7kqk3+PgY5bRriSS90o5qfJLQlCOka9sxvwSg/ovCmeUGla2R6xunH368fmG
+1TGq80KKLslZgDyfoxzi1wosrFkN
+-----END CERTIFICATE-----
diff --git a/pdf/apk/src/com/android/graphics/pdf/Placeholder.java b/pdf/apk/src/com/android/graphics/pdf/Placeholder.java
new file mode 100644
index 0000000..7de45b3
--- /dev/null
+++ b/pdf/apk/src/com/android/graphics/pdf/Placeholder.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2023 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.graphics.pdf;
+
+/**
+ * Placeholder class for new PDF viewer apk inside MediaProvider module.
+ *
+ * @hide
+ *
+ */
+public class Placeholder {
+}
diff --git a/apex/pdf/framework/Android.bp b/pdf/framework/Android.bp
similarity index 100%
rename from apex/pdf/framework/Android.bp
rename to pdf/framework/Android.bp
diff --git a/apex/pdf/framework/api/current.txt b/pdf/framework/api/current.txt
similarity index 100%
rename from apex/pdf/framework/api/current.txt
rename to pdf/framework/api/current.txt
diff --git a/apex/pdf/framework/api/module-lib-current.txt b/pdf/framework/api/module-lib-current.txt
similarity index 100%
rename from apex/pdf/framework/api/module-lib-current.txt
rename to pdf/framework/api/module-lib-current.txt
diff --git a/apex/pdf/framework/api/module-lib-removed.txt b/pdf/framework/api/module-lib-removed.txt
similarity index 100%
rename from apex/pdf/framework/api/module-lib-removed.txt
rename to pdf/framework/api/module-lib-removed.txt
diff --git a/apex/pdf/framework/api/removed.txt b/pdf/framework/api/removed.txt
similarity index 100%
rename from apex/pdf/framework/api/removed.txt
rename to pdf/framework/api/removed.txt
diff --git a/apex/pdf/framework/api/system-current.txt b/pdf/framework/api/system-current.txt
similarity index 100%
rename from apex/pdf/framework/api/system-current.txt
rename to pdf/framework/api/system-current.txt
diff --git a/apex/pdf/framework/api/system-removed.txt b/pdf/framework/api/system-removed.txt
similarity index 100%
rename from apex/pdf/framework/api/system-removed.txt
rename to pdf/framework/api/system-removed.txt
diff --git a/apex/pdf/framework/java/android/graphics/pdf/Placeholder.java b/pdf/framework/java/android/graphics/pdf/Placeholder.java
similarity index 100%
rename from apex/pdf/framework/java/android/graphics/pdf/Placeholder.java
rename to pdf/framework/java/android/graphics/pdf/Placeholder.java
diff --git a/res/drawable/error_icon.xml b/res/drawable/error_icon.xml
new file mode 100644
index 0000000..e9433b7
--- /dev/null
+++ b/res/drawable/error_icon.xml
@@ -0,0 +1,17 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <group>
+        <clip-path
+            android:pathData="M0,0h24v24h-24z"/>
+        <path
+            android:pathData="M1,21L12,2L23,21H1ZM4.45,19H19.55L12,6L4.45,19ZM12,18C12.283,18
+            12.517,17.908 12.7,17.725C12.9,17.525 13,17.283 13,17C13,16.717 12.9,16.483
+            12.7,16.3C12.517,16.1 12.283,16 12,16C11.717,16 11.475,16.1 11.275,16.3C11.092,16.483
+            11,16.717 11,17C11,17.283 11.092,17.525 11.275,17.725C11.475,17.908 11.717,18 12,
+            18ZM11,15H13V10H11V15Z"
+            android:fillColor="#775A0B"/>
+    </group>
+</vector>
diff --git a/res/drawable/ic_artwork_camera.xml b/res/drawable/ic_artwork_camera.xml
index dc22c49..9c39e64 100644
--- a/res/drawable/ic_artwork_camera.xml
+++ b/res/drawable/ic_artwork_camera.xml
@@ -14,9 +14,11 @@
      limitations under the License.
 -->
 
+<!-- This vector draws the camera graphic that is displayed in the picker when the
+     device has no images/videos i.e. the picker is empty -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="120dp"
-    android:height="80dp"
+    android:width="100dp"
+    android:height="66.67dp"
     android:viewportWidth="120"
     android:viewportHeight="80">
   <path
diff --git a/res/drawable/ic_background_circle.xml b/res/drawable/ic_background_circle.xml
new file mode 100644
index 0000000..ec6f524
--- /dev/null
+++ b/res/drawable/ic_background_circle.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="oval">
+        <solid android:color="?attr/categoryDefaultThumbnailCircleColor" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/picker_app_icon.xml b/res/drawable/picker_app_icon.xml
new file mode 100644
index 0000000..9f83344
--- /dev/null
+++ b/res/drawable/picker_app_icon.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <path
+        android:pathData="M8,2H20C21.1,2 22,2.9 22,4V16C22,17.1 21.1,18 20,18H8C6.9,18 6,17.1 6,16V4C6,2.9 6.9,2 8,2ZM20,16V4H8V16H20ZM2,6V20C2,21.1 2.9,22 4,22H18V20H4V6H2ZM13.17,13.98L15.67,11L19,15H9L11.5,11.8L13.17,13.98Z"
+        android:fillColor="#5F6368"
+        android:fillType="evenOdd"/>
+</vector>
diff --git a/res/drawable/picker_item_check.xml b/res/drawable/picker_item_check.xml
index fb0ef88..c73c699 100644
--- a/res/drawable/picker_item_check.xml
+++ b/res/drawable/picker_item_check.xml
@@ -1,31 +1,31 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2021 The Android Open Source Project
+    <!-- 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
+         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
+              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.
--->
+         Unless required by applicable law or agreed to in writing, software
+         distributed under the License is distributed on an "AS IS" BASIS,
+         WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+         See the License for the specific language governing permissions and
+         limitations under the License.
+    -->
 
 <selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:state_selected="true">
-        <layer-list>
-            <item android:gravity="center"
-                  android:width="18dp"
-                  android:height="18dp">
-                <shape android:shape="oval">
-                    <solid android:color="@color/picker_background_color"/>
-                </shape>
-            </item>
-            <item android:drawable="@drawable/ic_check_circle_filled"/>
-        </layer-list>
-    </item>
-    <item android:drawable="@drawable/ic_radio_button_unchecked"/>
-</selector>
+<item android:state_selected="true">
+    <layer-list>
+        <item android:gravity="center"
+              android:width="18dp"
+              android:height="18dp">
+            <shape android:shape="oval">
+                <solid android:color="@color/picker_background_color"/>
+            </shape>
+        </item>
+        <item android:drawable="@drawable/ic_check_circle_filled"/>
+    </layer-list>
+</item>
+<item android:drawable="@drawable/ic_radio_button_unchecked"/>
+</selector>
\ No newline at end of file
diff --git a/res/drawable/picker_item_order.xml b/res/drawable/picker_item_order.xml
new file mode 100644
index 0000000..ceeae40
--- /dev/null
+++ b/res/drawable/picker_item_order.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_selected="true">
+        <layer-list>
+            <item android:gravity="center"
+                  android:width="18dp"
+                  android:height="18dp">
+                <shape android:shape="oval">
+                    <solid android:color="?attr/pickerSelectedColor"/>
+                </shape>
+            </item>
+        </layer-list>
+    </item>
+    <item android:drawable="@drawable/ic_radio_button_unchecked"/>
+</selector>
\ No newline at end of file
diff --git a/res/drawable/thumbnail_favorites.xml b/res/drawable/thumbnail_favorites.xml
new file mode 100644
index 0000000..a1a8101
--- /dev/null
+++ b/res/drawable/thumbnail_favorites.xml
@@ -0,0 +1,25 @@
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <path
+        android:pathData="M14.81,8.62L22,9.24L16.55,13.97L18.18,21L12,17.27L5.82,21L7.46,13.97L2,9.24L9.19,8.63L12,2L14.81,8.62ZM8.24,17.67L12,15.4L15.77,17.68L14.77,13.4L18.09,10.52L13.71,10.14L12,6.1L10.3,10.13L5.92,10.51L9.24,13.39L8.24,17.67Z"
+        android:fillColor="#5B631D"
+        android:fillType="evenOdd"/>
+</vector>
diff --git a/res/drawable/thumbnail_videos.xml b/res/drawable/thumbnail_videos.xml
new file mode 100644
index 0000000..e5944d5
--- /dev/null
+++ b/res/drawable/thumbnail_videos.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <path
+        android:pathData="M18,6V10.48L22,6.5V17.5L18,13.52V18C18,19.1 17.1,20 16,20H4C2.9,20 2,19.1 2,18V6C2,4.9 2.9,4 4,4H16C17.1,4 18,4.9 18,6ZM16,6H4V18H16V6Z"
+        android:fillColor="#5B631D"
+        android:fillType="evenOdd"/>
+</vector>
+
diff --git a/res/layout/error_dialog.xml b/res/layout/error_dialog.xml
new file mode 100644
index 0000000..5fa23d1
--- /dev/null
+++ b/res/layout/error_dialog.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:padding="20dp"
+    android:gravity="center">
+    <ImageView
+        android:layout_width="34dp"
+        android:layout_height="34dp"
+        android:src="@drawable/error_icon"
+        android:layout_gravity="center_horizontal"
+        android:layout_marginBottom="16dp"
+        android:importantForAccessibility="no"/>
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/dialog_error_title"
+        android:textSize="24sp"
+        android:layout_gravity="center_horizontal"
+        android:gravity="center"
+        android:textColor="?android:attr/textColorPrimary"/>
+
+    <TextView
+        android:id="@+id/message"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/dialog_error_message"
+        android:layout_marginTop="16dp"
+        android:layout_gravity="center_horizontal"
+        android:gravity="center"
+        android:textSize="16sp"
+        android:textColor="?android:attr/textColorSecondary"/>
+
+    <Button
+        android:id="@+id/okButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/dialog_button_text"
+        android:layout_marginTop="16dp"
+        android:layout_gravity="end"
+        android:textColor="?attr/pickerHighlightTextColor"
+        android:backgroundTint="?attr/pickerHighlightColor"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/fragment_picker_tab.xml b/res/layout/fragment_picker_tab.xml
index ae3180d..7dd6ea9 100644
--- a/res/layout/fragment_picker_tab.xml
+++ b/res/layout/fragment_picker_tab.xml
@@ -20,34 +20,44 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
-    <LinearLayout
+    <!-- The nested scroll view holds the layout that is made visible when
+    the picker is empty. It has been wrapped in the scroll view to tackle
+    bugs where the "empty_text_view" gets rolled off the screen partially
+    or completely in small screen devices -->
+    <androidx.core.widget.NestedScrollView
         android:id="@android:id/empty"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_marginTop="80dp"
-        android:orientation="vertical"
         android:visibility="gone">
 
-        <ImageView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center_horizontal"
-            android:scaleType="fitCenter"
-            android:src="@drawable/ic_artwork_camera"
-            android:contentDescription="@null"/>
-
-        <TextView
-            android:id="@+id/empty_text_view"
+        <LinearLayout
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:layout_marginTop="@dimen/picker_empty_text_margin"
-            android:gravity="center_horizontal"
-            android:text="@string/picker_photos_empty_message"
-            android:textColor="?android:attr/textColorSecondary"
-            android:textSize="@dimen/picker_empty_text_size"
-            style="?android:attr/textAppearanceListItem"/>
+            android:orientation="vertical">
 
-    </LinearLayout>
+            <ImageView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal"
+                android:scaleType="fitCenter"
+                android:src="@drawable/ic_artwork_camera"
+                android:contentDescription="@null"/>
+
+            <TextView
+                android:id="@+id/empty_text_view"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/picker_empty_text_margin"
+                android:gravity="center_horizontal"
+                android:text="@string/picker_photos_empty_message"
+                android:textColor="?android:attr/textColorSecondary"
+                android:textSize="@dimen/picker_empty_text_size"
+                style="?android:attr/textAppearanceListItem"/>
+
+        </LinearLayout>
+
+    </androidx.core.widget.NestedScrollView>
 
     <com.android.providers.media.photopicker.ui.AutoFitRecyclerView
         android:id="@+id/picker_tab_recyclerview"
@@ -57,4 +67,24 @@
         android:drawSelectorOnTop="true"
         android:overScrollMode="never"/>
 
+    <TextView
+        android:id="@+id/loading_text_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:text="@string/picker_loading_photos_message"
+        android:textColor="?android:attr/textColorPrimary"
+        android:textSize="@dimen/picker_tab_loading_message_text_size"
+        style="?android:attr/textAppearanceListItem"
+        android:visibility="gone"/>
+
+    <ProgressBar
+        android:id="@+id/progress_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/picker_progress_bar_margin_top"
+        style="@style/android:Widget.Material.ProgressBar.Horizontal"
+        android:indeterminate="true"
+        android:visibility="gone"/>
+
 </FrameLayout>
diff --git a/res/layout/item_album_grid.xml b/res/layout/item_album_grid.xml
index 4c04fff..c09beaf 100644
--- a/res/layout/item_album_grid.xml
+++ b/res/layout/item_album_grid.xml
@@ -38,6 +38,16 @@
             android:scaleType="centerCrop"
             android:contentDescription="@null"/>
 
+        <ImageView
+            android:id="@+id/icon_default_thumbnail"
+            android:layout_width="56dp"
+            android:layout_height="56dp"
+            android:scaleType="centerCrop"
+            android:tint="?attr/categoryDefaultThumbnailColor"
+            android:contentDescription="@null"
+            android:layout_gravity="center"
+            android:background="@drawable/ic_background_circle"
+            android:padding="16dp"/>
     </com.google.android.material.card.MaterialCardView>
 
     <TextView
@@ -57,6 +67,7 @@
         android:minHeight="@dimen/picker_album_item_count_height"
         android:layout_marginTop="@dimen/picker_album_item_count_margin"
         android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small"
-        android:textColor="?android:attr/textColorSecondary"/>
+        android:textColor="?android:attr/textColorSecondary"
+        android:visibility="gone"/>
 
 </LinearLayout>
diff --git a/res/layout/item_photo_grid.xml b/res/layout/item_photo_grid.xml
index f28305c..cd19343 100644
--- a/res/layout/item_photo_grid.xml
+++ b/res/layout/item_photo_grid.xml
@@ -108,4 +108,17 @@
         android:layout_gravity="top|start"
         android:scaleType="fitCenter"/>
 
+    <TextView
+        android:id="@+id/selected_order"
+        android:layout_height="@dimen/picker_item_check_size"
+        android:layout_width="@dimen/picker_item_check_size"
+        android:layout_marginStart="@dimen/picker_item_check_margin"
+        android:layout_marginTop="@dimen/picker_item_check_margin"
+        android:background="@drawable/picker_item_order"
+        android:layout_gravity="top|start"
+        android:gravity="center"
+        android:textSize="12dp"
+        android:textColor="?attr/pickerHighlightTextColor"
+        android:scaleType="fitCenter"/>
+
 </FrameLayout>
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index 804d612..e3d1c1e 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Media"</string>
     <string name="storage_description" msgid="4081716890357580107">"Plaaslike berging"</string>
-    <string name="app_label" msgid="9035307001052716210">"Mediaberging"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Media"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Mediakieser"</string>
     <string name="artist_label" msgid="8105600993099120273">"Kunstenaar"</string>
     <string name="unknown" msgid="2059049215682829375">"Onbekend"</string>
     <string name="root_images" msgid="5861633549189045666">"Prente"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Kry toegang tot wolkmedia vanaf"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Geen"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Kon nie wolkmedia-app op dié tydstip verander nie."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Mediakieser"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Mediakieser"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Sinkroniseer tans media …"</string>
     <string name="add" msgid="2894574044585549298">"Voeg by"</string>
     <string name="deselect" msgid="4297825044827769490">"Ontkies"</string>
     <string name="deselected" msgid="8488133193326208475">"Ontkies"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Geen albums nie"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Bekyk geselekteerde"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Foto\'s"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albums"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Voorskou"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Skakel oor na werk"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> items}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Voeg by (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Laat toe (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Laat geen toe nie"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Aflaaie"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Gunstelinge"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Sukkel om video te speel"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Gaan jou internetverbinding na en probeer weer"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Herprobeer"</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="preloading_dialog_title" msgid="4974348221848532887">"Berei tans jou geselekteerde media voor"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> van <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> is gereed"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Kanselleer"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Gerugsteunde foto\'s word nou ingesluit"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Jy kan foto\'s van <xliff:g id="APP_NAME">%1$s</xliff:g>-rekening <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> af kies"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g>-rekening is opgedateer"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Kies app"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Kies rekening"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Verander rekening"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Kry tans al jou foto’s"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Laat <xliff:g id="APP_NAME_0">^1</xliff:g> toe om hierdie video te wysig?}other{Laat <xliff:g id="APP_NAME_1">^1</xliff:g> toe om <xliff:g id="COUNT">^2</xliff:g> video\'s te wysig?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Veiligheidbeskerming"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Toestelspesifieke kodewisselingopletberigte"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Toestelspesifieke kodewisselingvordering"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Probeer later weer. Jou foto’s sal beskikbaar wees sodra die kwessie opgelos is."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Sommige foto’s kan nie laai nie"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Het dit"</string>
 </resources>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index ed4ad73..32b2d97 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"ማህደረመረጃ"</string>
     <string name="storage_description" msgid="4081716890357580107">"አካባቢያዊ ማከማቻ"</string>
-    <string name="app_label" msgid="9035307001052716210">"ማህደረ መረጃ ማከማቻ"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"ሚዲያ"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"የሚዲያ መራጭ"</string>
     <string name="artist_label" msgid="8105600993099120273">"አርቲስት"</string>
     <string name="unknown" msgid="2059049215682829375">"የማይታወቅ"</string>
     <string name="root_images" msgid="5861633549189045666">"ምስሎች"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"የደመና ሚዲያን ይድረሱ ከ"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"ምንም"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"በዚህ ጊዜ የደመና የሚዲያ መተግበሪያን መለወጥ አልተቻለም።"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"የሚዲያ መራጭ"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"የሚዲያ መራጭ"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"ሚዲያ በማስመር ላይ…"</string>
     <string name="add" msgid="2894574044585549298">"አክል"</string>
     <string name="deselect" msgid="4297825044827769490">"አትምረጥ"</string>
     <string name="deselected" msgid="8488133193326208475">"አልተመረጠም"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"ምንም አልበሞች የሉም"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"የተመረጡትን አሳይ"</string>
     <string name="picker_photos" msgid="7415035516411087392">"ፎቶዎች"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"አልበሞች"</string>
     <string name="picker_preview" msgid="6257414886055861039">"ቅድመ-ዕይታ"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"ወደ የሥራ ቀይር"</string>
@@ -72,10 +76,11 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ንጥል}one{<xliff:g id="COUNT_1">^1</xliff:g> ንጥል}other{<xliff:g id="COUNT_1">^1</xliff:g> ንጥሎች}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>) አክል"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"ለ(<xliff:g id="COUNT">^1</xliff:g>) ፍቀድ"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"ምንም አትፍቀድ"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"ካሜራ"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"ውርዶች"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"ተወዳጆች"</string>
-    <string name="picker_category_screenshots" msgid="7216102327587644284">"ቅጽበታዊ ገጽ እይታዎች"</string>
+    <string name="picker_category_screenshots" msgid="7216102327587644284">"ቅጽበታዊ ገፅ እይታዎች"</string>
     <!-- no translation found for picker_category_videos (1478458836380241356) -->
     <skip />
     <string name="picker_motion_photo_text" msgid="5016603812468180816">"የእንቅስቃሴ ፎቶ"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"ቪድዮን ማጫወት ላይ ችግር"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"የበይነመረብዎን ግንኙነት ይፈትሹ እና እንደገና ይሞክሩ"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"እንደገና ሞክር"</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="preloading_dialog_title" msgid="4974348221848532887">"የእርስዎን የተመረጠ ሚዲያ በማዘጋጀት ላይ"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> ከ<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ዝግጁ"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"ይቅር"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"ምትኬ የተቀመጠላቸው ፎቶዎች አሁን ተካትተዋል"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"ከ<xliff:g id="APP_NAME">%1$s</xliff:g> መለያ <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> ፎቶዎችን መምረጥ ይችላሉ"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> መለያ ተዘምኗል"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"መተግበሪያ ይምረጡ"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"መለያ ይምረጡ"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"መለያ ቀይር"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"ሁሉንም ፎቶዎችዎን በማምጣት ላይ"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"የደህንነት ጥበቃ"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"የቤተኛ ትራንስኮድ ማንቂያዎች"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"የቤተኛ ትራንስኮድ ሂደት"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"ቆይተው እንደገና ይሞክሩ። የእርስዎ ፎቶዎች አንዴ ችግሩ ከተፈታ በኋላ ይገኛሉ።"</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"አንዳንድ ፎቶዎችን መጫን አይቻለም"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"ገባኝ"</string>
 </resources>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 8bd02ca..9ff9503 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"الوسائط"</string>
     <string name="storage_description" msgid="4081716890357580107">"التخزين المحلي"</string>
-    <string name="app_label" msgid="9035307001052716210">"تخزين الوسائط"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"الوسائط"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"أداة اختيار الوسائط"</string>
     <string name="artist_label" msgid="8105600993099120273">"الفنان"</string>
     <string name="unknown" msgid="2059049215682829375">"غير معروف"</string>
     <string name="root_images" msgid="5861633549189045666">"الصور"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"الوصول إلى الوسائط في السحابة الإلكترونية من"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"بلا تطبيق"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"تعذر تغيير تطبيق وسائط في السحابة الإلكترونية حاليًا"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"أداة اختيار الوسائط"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"أداة اختيار الوسائط"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"جارٍ مزامنة الوسائط…"</string>
     <string name="add" msgid="2894574044585549298">"إضافة"</string>
     <string name="deselect" msgid="4297825044827769490">"إلغاء الاختيار"</string>
     <string name="deselected" msgid="8488133193326208475">"تم إلغاء الاختيار"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"ما مِن ألبومات"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"عرض ما تم اختياره"</string>
     <string name="picker_photos" msgid="7415035516411087392">"الصور"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"الألبومات"</string>
     <string name="picker_preview" msgid="6257414886055861039">"معاينة"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"التبديل إلى الملف الشخصي للعمل"</string>
@@ -69,9 +73,10 @@
     <string name="picker_profile_work_paused_msg" msgid="6321552322125246726">"لفتح صور العمل، عليك تفعيل تطبيقات العمل ثم إعادة المحاولة."</string>
     <string name="picker_privacy_message" msgid="9132700451027116817">"يمكن لهذا التطبيق الوصول إلى الصور التي تختارها فقط."</string>
     <string name="picker_header_permissions" msgid="675872774407768495">"اختَر الصور والفيديوهات التي تريد السماح لهذا التطبيق بالوصول إليها"</string>
-    <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{عنصر واحد (<xliff:g id="COUNT_0">^1</xliff:g>)}zero{<xliff:g id="COUNT_1">^1</xliff:g> عنصر}two{عنصران (<xliff:g id="COUNT_1">^1</xliff:g>)}few{<xliff:g id="COUNT_1">^1</xliff:g> عناصر}many{<xliff:g id="COUNT_1">^1</xliff:g> عنصرًا}other{<xliff:g id="COUNT_1">^1</xliff:g> عنصر}}"</string>
+    <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{صورة واحدة (<xliff:g id="COUNT_0">^1</xliff:g>)}zero{<xliff:g id="COUNT_1">^1</xliff:g> صورة}two{صورتان (<xliff:g id="COUNT_1">^1</xliff:g>)}few{<xliff:g id="COUNT_1">^1</xliff:g> صور}many{<xliff:g id="COUNT_1">^1</xliff:g> صورة}other{<xliff:g id="COUNT_1">^1</xliff:g> صورة}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"إضافة (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"السماح (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"لم يتم اختيار أي صورة"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"الكاميرا"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"العناصر التي تم تنزيلها"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"العناصر المفضّلة"</string>
@@ -92,11 +97,12 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"مشكلة في تشغيل الفيديو"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"يُرجى التحقّق من الاتصال بالإنترنت ثم إعادة المحاولة."</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"إعادة المحاولة"</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="preloading_dialog_title" msgid="4974348221848532887">"جارٍ تحضير الوسائط التي تم اختيارها"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> من إجمالي <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> صورة جاهزة"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"إلغاء"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"تم الآن تضمين الصور التي تم الاحتفاظ بنسخة احتياطية منها"</string>
-    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"يمكنك اختيار صور من حساب <xliff:g id="APP_NAME">%1$s</xliff:g> للمستخدم <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>."</string>
+    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"يمكنك اختيار صور من حساب \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" للمستخدم <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>."</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"تم تعديل الحساب <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"تم الآن تضمين الصور من <xliff:g id="USER_ACCOUNT">%1$s</xliff:g> هنا."</string>
     <string name="picker_banner_cloud_choose_app_title" msgid="3165966147547974251">"اختيار تطبيق موسيقى على السحابة الإلكترونية"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"اختيار تطبيق"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"اختيار حساب"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"تبديل الحساب"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"جارٍ تحميل جميع الصور"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"حماية الأمن الشخصي"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"‏تنبيهات Native Transcode"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"‏مدى تقدُّم Native Transcode"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"يُرجى إعادة المحاولة لاحقًا. ستتوفّر صورك عند حل المشكلة."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"يتعذّر تحميل بعض الصور"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"حسنًا"</string>
 </resources>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index d0bbafd..f97b5c1 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"মিডিয়া"</string>
     <string name="storage_description" msgid="4081716890357580107">"স্থানীয় ষ্ট’ৰেজ"</string>
-    <string name="app_label" msgid="9035307001052716210">"মিডিয়া ষ্ট’ৰেজ"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"মিডিয়া"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"মিডিয়া বাছনিকৰ্তা"</string>
     <string name="artist_label" msgid="8105600993099120273">"শিল্পী"</string>
     <string name="unknown" msgid="2059049215682829375">"অজ্ঞাত"</string>
     <string name="root_images" msgid="5861633549189045666">"প্ৰতিচ্ছবি"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"ইয়াৰ পৰা ক্লাউড মিডিয়া এক্সেছ কৰক"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"নাই"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"এই সময়ত ক্লাউড মিডিয়া এপ্ সলনি কৰিব নোৱাৰি"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"মিডিয়া বাছনিকৰ্তা"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"মিডিয়া বাছনিকৰ্তা"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"মিডিয়া ছিংক কৰি থকা হৈছে…"</string>
     <string name="add" msgid="2894574044585549298">"যোগ দিয়ক"</string>
     <string name="deselect" msgid="4297825044827769490">"বাছনিৰ পৰা আঁতৰাওক"</string>
     <string name="deselected" msgid="8488133193326208475">"বাছনিৰ পৰা আঁতৰোৱা হ’ল"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"কোনো এলবাম নাই"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"ভিউ বাছনি কৰা হৈছে"</string>
     <string name="picker_photos" msgid="7415035516411087392">"ফট’"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"এলবাম"</string>
     <string name="picker_preview" msgid="6257414886055861039">"পূৰ্বদৰ্শন কৰক"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"কৰ্মস্থানৰ প্ৰ’ফাইললৈ সলনি কৰক"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> টা বস্তু}one{<xliff:g id="COUNT_1">^1</xliff:g> টা বস্তু}other{<xliff:g id="COUNT_1">^1</xliff:g> টা বস্তু}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g> টা) যোগ দিয়ক"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"অনুমতি দিয়ক (<xliff:g id="COUNT">^1</xliff:g> টা)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"এখনৰো অনুমতি নিদিব"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"কেমেৰা"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"ডাউনল’ড"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"প্ৰিয়"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"ভিডিঅ’ প্লে’ কৰাত সমস্যা হৈছে"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"আপোনাৰ ইণ্টাৰনেট সংযোগ পৰীক্ষা কৰক আৰু পুনৰ চেষ্টা কৰক"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"পুনৰ চেষ্টা কৰক"</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="preloading_dialog_title" msgid="4974348221848532887">"আপুনি বাছনি কৰা মিডিয়া সাজু কৰি থকা হৈছে"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> টা বস্তুৰ ভিতৰত <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> টা সাজু"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"বাতিল কৰক"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"এতিয়া বেকআপ লোৱা ফট’সমূহ অন্তৰ্ভুক্ত কৰা হৈছে"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"আপুনি <xliff:g id="APP_NAME">%1$s</xliff:g>ৰ একাউণ্টৰ <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> পৰা ফট’সমূহ বাছনি কৰিব পাৰে"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> একাউণ্টটো আপডে’ট কৰা হৈছে"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"সুৰক্ষিত নিৰাপত্তা"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"স্থানীয় ট্ৰেন্সক’ড সতৰ্কবাৰ্তা"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"স্থানীয় ট্ৰেন্সক’ড অগ্ৰগতি"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"পাছত পুনৰ চেষ্টা কৰক। সমস্যাটো সমাধান হোৱাৰ পাছত আপোনাৰ ফট’সমূহ উপলব্ধ হ’ব।"</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"কিছুমান ফট’ ল’ড কৰিব নোৱাৰি"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"বুজি পালোঁ"</string>
 </resources>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index e1eec60..88c4c05 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Media"</string>
     <string name="storage_description" msgid="4081716890357580107">"Yerli yaddaş"</string>
-    <string name="app_label" msgid="9035307001052716210">"Media Yaddaşı"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Media"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Media seçici"</string>
     <string name="artist_label" msgid="8105600993099120273">"Sənətçi"</string>
     <string name="unknown" msgid="2059049215682829375">"Naməlum"</string>
     <string name="root_images" msgid="5861633549189045666">"Təsvirlər"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Bulud mediasına buradan giriş edin:"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Heç biri"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"İndi bulud media tətbiqini dəyişmək mümkün deyil."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Media seçici"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Media seçici"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Media sinxronlaşdırılır…"</string>
     <string name="add" msgid="2894574044585549298">"Əlavə edin"</string>
     <string name="deselect" msgid="4297825044827769490">"Seçimi ləğv edin"</string>
     <string name="deselected" msgid="8488133193326208475">"Seçimi ləğv edilib"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Albom yoxdur"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Seçilənə baxın"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotolar"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albomlar"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Önbaxış"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"İş profilinə keçirin"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> element}other{<xliff:g id="COUNT_1">^1</xliff:g> element}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Əlavə edin (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"İcazə verin (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Heç birinə icazə verməyin"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Endirmələr"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Sevimlilər"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Videonu oxudarkən xəta oldu"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"İnternet bağlantınızı yoxlayın və yenidən sınayın"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Yenidən cəhd edin"</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="preloading_dialog_title" msgid="4974348221848532887">"Seçilmiş media hazırlanır"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>/<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> hazırdır"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Ləğv edin"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Yedəklənmiş fotolar indi daxildir"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"<xliff:g id="APP_NAME">%1$s</xliff:g> hesabından (<xliff:g id="USER_ACCOUNT">%2$s</xliff:g>) fotoları seçə bilərsiniz"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> hesabı güncəlləndi"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Tətbiq seçin"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Hesab seçin"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Hesabı dəyişdirin"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Bütün fotolar əldə edilir"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> tətbiqinə bu videoya 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> videoya dəyişiklik etmək icazəsi verilsin?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Güvənlik qoruması"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Orijinal Transkod Xəbərdarlıqları"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Orijinal Transkod İrəliləyişi"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Sonra cəhd edin. Problem həll edildikdən sonra fotolar əlçatan olacaq."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Bəzi fotolar yüklənmir"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Anladım"</string>
 </resources>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index d83a562..0c40717 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Mediji"</string>
     <string name="storage_description" msgid="4081716890357580107">"Lokalni memorijski prostor"</string>
-    <string name="app_label" msgid="9035307001052716210">"Memorijski prostor za medije"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Mediji"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Birač medija"</string>
     <string name="artist_label" msgid="8105600993099120273">"Izvođač"</string>
     <string name="unknown" msgid="2059049215682829375">"Nepoznato"</string>
     <string name="root_images" msgid="5861633549189045666">"Slike"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Pristupajte medijima u klaudu iz"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Ništa"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Promena aplikacije za medije u klaudu nije uspela."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Birač medija"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Birač medija"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Mediji se sinhronizuju…"</string>
     <string name="add" msgid="2894574044585549298">"Dodaj"</string>
     <string name="deselect" msgid="4297825044827769490">"Opozovi izbor"</string>
     <string name="deselected" msgid="8488133193326208475">"Opozvan je izbor"</string>
@@ -53,11 +55,13 @@
     <string name="selected" msgid="9151797369975828124">"Izabrano"</string>
     <string name="select_up_to" msgid="6994294169508439957">"{count,plural, =1{Izaberite najviše <xliff:g id="COUNT_0">^1</xliff:g> stavku}one{Izaberite najviše <xliff:g id="COUNT_1">^1</xliff:g> stavku}few{Izaberite najviše <xliff:g id="COUNT_1">^1</xliff:g> stavke}other{Izaberite najviše <xliff:g id="COUNT_1">^1</xliff:g> stavki}}"</string>
     <string name="recent" msgid="6694613584743207874">"Nedavno"</string>
-    <string name="picker_photos_empty_message" msgid="5980619500554575558">"Nema slika niti video snimaka"</string>
-    <string name="picker_album_media_empty_message" msgid="7061850698189881671">"Nema podržanih slika niti video snimaka"</string>
+    <string name="picker_photos_empty_message" msgid="5980619500554575558">"Nema slika niti videa"</string>
+    <string name="picker_album_media_empty_message" msgid="7061850698189881671">"Nema podržanih slika niti videa"</string>
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Nema albuma"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Prikaži izabrano"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Slike"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albumi"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Pregled"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Pređi na poslovni profil"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> stavka}one{<xliff:g id="COUNT_1">^1</xliff:g> stavka}few{<xliff:g id="COUNT_1">^1</xliff:g> stavke}other{<xliff:g id="COUNT_1">^1</xliff:g> stavki}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Dodaj (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Dozvoli (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Ne dozvoli nijednu"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Preuzeto"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Omiljeno"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Došlo je do greške pri puštanju videa"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Proverite internet vezu i probajte ponovo"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Probaj ponovo"</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="preloading_dialog_title" msgid="4974348221848532887">"Pripremaju se odabrani medijski fajlovi"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Spremno:<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> od <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Otkaži"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Sada su uvrštene rezervne kopije slika"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Možete da izaberete slike sa naloga <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> za <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Nalog za <xliff:g id="APP_NAME">%1$s</xliff:g> je ažuriran"</string>
@@ -107,36 +113,35 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Odaberi aplikaciju"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Odaberi nalog"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Promeni nalog"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Preuzimaju se sve slike"</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>
-    <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> izmeni ovaj video?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> video?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> video snimka?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> video snimaka?}}"</string>
-    <string name="permission_progress_write_video" msgid="7014908418349819148">"{count,plural, =1{Menja se video…}one{Menja se <xliff:g id="COUNT">^1</xliff:g> video…}few{Menjaju se <xliff:g id="COUNT">^1</xliff:g> video snimka…}other{Menja se <xliff:g id="COUNT">^1</xliff:g> video snimaka…}}"</string>
+    <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> izmeni ovaj video?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> video?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> video snimka?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> videa?}}"</string>
+    <string name="permission_progress_write_video" msgid="7014908418349819148">"{count,plural, =1{Menja se video…}one{Menja se <xliff:g id="COUNT">^1</xliff:g> video…}few{Menjaju se <xliff:g id="COUNT">^1</xliff:g> video snimka…}other{Menja se <xliff:g id="COUNT">^1</xliff:g> videa…}}"</string>
     <string name="permission_write_image" msgid="3518991791620523786">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> izmeni ovu sliku?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> sliku?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> slike?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> slika?}}"</string>
     <string name="permission_progress_write_image" msgid="3623580315590025262">"{count,plural, =1{Menja se slika…}one{Menja se <xliff:g id="COUNT">^1</xliff:g> slika…}few{Menjaju se <xliff:g id="COUNT">^1</xliff:g> slike…}other{Menja se <xliff:g id="COUNT">^1</xliff:g> slika…}}"</string>
     <string name="permission_write_generic" msgid="7431128739233656991">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> izmeni ovu stavku?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> stavku?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> stavke?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izmeni <xliff:g id="COUNT">^2</xliff:g> stavki?}}"</string>
     <string name="permission_progress_write_generic" msgid="2806560971318391443">"{count,plural, =1{Menja se stavka…}one{Menja se <xliff:g id="COUNT">^1</xliff:g> stavka…}few{Menjaju se <xliff:g id="COUNT">^1</xliff:g> stavke…}other{Menja se <xliff:g id="COUNT">^1</xliff:g> stavki…}}"</string>
     <string name="permission_trash_audio" msgid="6554672354767742206">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> premesti ovaj audio fajl u otpad?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> audio fajl u otpad?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> audio fajla u otpad?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> audio fajlova u otpad?}}"</string>
     <string name="permission_progress_trash_audio" msgid="3116279868733641329">"{count,plural, =1{Audio fajl se premešta u otpad…}one{<xliff:g id="COUNT">^1</xliff:g> audio fajl se premešta u otpad…}few{<xliff:g id="COUNT">^1</xliff:g> audio fajla se premeštaju u otpad…}other{<xliff:g id="COUNT">^1</xliff:g> audio fajlova se premešta u otpad…}}"</string>
-    <string name="permission_trash_video" msgid="7555850843259959642">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> premesti ovaj video u otpad?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> video u otpad?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> video snimka u otpad?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> video snimaka u otpad?}}"</string>
-    <string name="permission_progress_trash_video" msgid="4637821778329459681">"{count,plural, =1{Video se premešta u otpad…}one{<xliff:g id="COUNT">^1</xliff:g> video se premešta u otpad…}few{<xliff:g id="COUNT">^1</xliff:g> video snimka se premeštaju u otpad…}other{<xliff:g id="COUNT">^1</xliff:g> video snimaka se premešta u otpad…}}"</string>
+    <string name="permission_trash_video" msgid="7555850843259959642">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> premesti ovaj video u otpad?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> video u otpad?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> video snimka u otpad?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> videa u otpad?}}"</string>
+    <string name="permission_progress_trash_video" msgid="4637821778329459681">"{count,plural, =1{Video se premešta u otpad…}one{<xliff:g id="COUNT">^1</xliff:g> video se premešta u otpad…}few{<xliff:g id="COUNT">^1</xliff:g> video snimka se premeštaju u otpad…}other{<xliff:g id="COUNT">^1</xliff:g> videa se premešta u otpad…}}"</string>
     <string name="permission_trash_image" msgid="3333128084684156675">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> premesti ovu sliku u otpad?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> sliku u otpad?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> slike u otpad?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> slika u otpad?}}"</string>
     <string name="permission_progress_trash_image" msgid="3063857679090024764">"{count,plural, =1{Slika se premešta u otpad…}one{<xliff:g id="COUNT">^1</xliff:g> slika se premešta u otpad…}few{<xliff:g id="COUNT">^1</xliff:g> slike se premeštaju u otpad…}other{<xliff:g id="COUNT">^1</xliff:g> slika se premešta u otpad…}}"</string>
     <string name="permission_trash_generic" msgid="5545420534785075362">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> premesti ovu stavku u otpad?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> stavku u otpad?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> stavke u otpad?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> stavki u otpad?}}"</string>
     <string name="permission_progress_trash_generic" msgid="7815124979717814057">"{count,plural, =1{Stavka se premešta u otpad…}one{<xliff:g id="COUNT">^1</xliff:g> stavka se premešta u otpad…}few{<xliff:g id="COUNT">^1</xliff:g> stavke se premeštaju u otpad…}other{<xliff:g id="COUNT">^1</xliff:g> stavki se premešta u otpad…}}"</string>
     <string name="permission_untrash_audio" msgid="8404597563284002472">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> premesti ovaj audio fajl iz otpada?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> audio fajl iz otpada?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> audio fajla iz otpada?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> audio fajlova iz otpada?}}"</string>
     <string name="permission_progress_untrash_audio" msgid="2775372344946464508">"{count,plural, =1{Audio fajl se premešta iz otpada…}one{<xliff:g id="COUNT">^1</xliff:g> audio fajl se premešta iz otpada…}few{<xliff:g id="COUNT">^1</xliff:g> audio fajla se premeštaju iz otpada…}other{<xliff:g id="COUNT">^1</xliff:g> audio fajlova se premešta iz otpada…}}"</string>
-    <string name="permission_untrash_video" msgid="3178914827607608162">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> premesti ovaj video iz otpada?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> video iz otpada?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> video snimka iz otpada?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> video snimaka iz otpada?}}"</string>
-    <string name="permission_progress_untrash_video" msgid="5500929409733841567">"{count,plural, =1{Video se premešta iz otpada…}one{<xliff:g id="COUNT">^1</xliff:g> video se premešta iz otpada…}few{<xliff:g id="COUNT">^1</xliff:g> video snimka se premeštaju iz otpada…}other{<xliff:g id="COUNT">^1</xliff:g> video snimaka se premešta iz otpada…}}"</string>
+    <string name="permission_untrash_video" msgid="3178914827607608162">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> premesti ovaj video iz otpada?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> video iz otpada?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> video snimka iz otpada?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> videa iz otpada?}}"</string>
+    <string name="permission_progress_untrash_video" msgid="5500929409733841567">"{count,plural, =1{Video se premešta iz otpada…}one{<xliff:g id="COUNT">^1</xliff:g> video se premešta iz otpada…}few{<xliff:g id="COUNT">^1</xliff:g> video snimka se premeštaju iz otpada…}other{<xliff:g id="COUNT">^1</xliff:g> videa se premešta iz otpada…}}"</string>
     <string name="permission_untrash_image" msgid="3397523279351032265">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> premesti ovu sliku iz otpada?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> sliku iz otpada?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> slike iz otpada?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> slika iz otpada?}}"</string>
     <string name="permission_progress_untrash_image" msgid="5295061520504846264">"{count,plural, =1{Slika se premešta iz otpada…}one{<xliff:g id="COUNT">^1</xliff:g> slika se premešta iz otpada…}few{<xliff:g id="COUNT">^1</xliff:g> slike se premeštaju iz otpada…}other{<xliff:g id="COUNT">^1</xliff:g> slika se premešta iz otpada…}}"</string>
     <string name="permission_untrash_generic" msgid="2118366929431671046">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> premesti ovu stavku iz otpada?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> stavku iz otpada?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> stavke iz otpada?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> premesti <xliff:g id="COUNT">^2</xliff:g> stavki iz otpada?}}"</string>
     <string name="permission_progress_untrash_generic" msgid="1489511601966842579">"{count,plural, =1{Stavka se premešta iz otpada…}one{<xliff:g id="COUNT">^1</xliff:g> stavka se premešta iz otpada…}few{<xliff:g id="COUNT">^1</xliff:g> stavke se premeštaju iz otpada…}other{<xliff:g id="COUNT">^1</xliff:g> stavki se premešta iz otpada…}}"</string>
     <string name="permission_delete_audio" msgid="3326674742892796627">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> izbriše ovaj audio fajl?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> audio fajl?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> audio fajla?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> audio fajlova?}}"</string>
     <string name="permission_progress_delete_audio" msgid="1734871539021696401">"{count,plural, =1{Briše se audio fajl…}one{Briše se <xliff:g id="COUNT">^1</xliff:g> audio fajl…}few{Brišu se <xliff:g id="COUNT">^1</xliff:g> audio fajla…}other{Briše se <xliff:g id="COUNT">^1</xliff:g> audio fajlova…}}"</string>
-    <string name="permission_delete_video" msgid="604024971828349279">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> izbriše ovaj video?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> video?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> video snimka?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> video snimaka?}}"</string>
-    <string name="permission_progress_delete_video" msgid="1846702435073793157">"{count,plural, =1{Briše se video…}one{Briše se <xliff:g id="COUNT">^1</xliff:g> video…}few{Brišu se <xliff:g id="COUNT">^1</xliff:g> video snimka…}other{Briše se <xliff:g id="COUNT">^1</xliff:g> video snimaka…}}"</string>
+    <string name="permission_delete_video" msgid="604024971828349279">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> izbriše ovaj video?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> video?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> video snimka?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> videa?}}"</string>
+    <string name="permission_progress_delete_video" msgid="1846702435073793157">"{count,plural, =1{Briše se video…}one{Briše se <xliff:g id="COUNT">^1</xliff:g> video…}few{Brišu se <xliff:g id="COUNT">^1</xliff:g> video snimka…}other{Briše se <xliff:g id="COUNT">^1</xliff:g> videa…}}"</string>
     <string name="permission_delete_image" msgid="3109056012794330510">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> izbriše ovu sliku?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> sliku?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> slike?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> slika?}}"</string>
     <string name="permission_progress_delete_image" msgid="8580517204901148906">"{count,plural, =1{Briše se slika…}one{Briše se <xliff:g id="COUNT">^1</xliff:g> slika…}few{Brišu se <xliff:g id="COUNT">^1</xliff:g> slike…}other{Briše se <xliff:g id="COUNT">^1</xliff:g> slika…}}"</string>
     <string name="permission_delete_generic" msgid="7891939881065520271">"{count,plural, =1{Želite li da dozvolite da <xliff:g id="APP_NAME_0">^1</xliff:g> izbriše ovu stavku?}one{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> stavku?}few{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> stavke?}other{Želite li da dozvolite da <xliff:g id="APP_NAME_1">^1</xliff:g> izbriše <xliff:g id="COUNT">^2</xliff:g> stavki?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Sigurnosna zaštita"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Obaveštenja o osnovnom transkodiranju"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Tok osnovnog transkodiranja"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Probajte ponovo kasnije. Slike će biti dostupne kada se problem reši."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Učitavanje nekih slika nije uspelo"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Važi"</string>
 </resources>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index cb32adc..23af8f2 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Медыя"</string>
     <string name="storage_description" msgid="4081716890357580107">"Лакальнае сховішча"</string>
-    <string name="app_label" msgid="9035307001052716210">"Медыясховішча"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Мультымедыя"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Сродак выбару мультымедыя"</string>
     <string name="artist_label" msgid="8105600993099120273">"Выканаўца"</string>
     <string name="unknown" msgid="2059049215682829375">"Невядома"</string>
     <string name="root_images" msgid="5861633549189045666">"Відарысы"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Доступ да воблачных мультымедыя з:"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Няма"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Воблачныя мультымедыйныя праграмы не зменены."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Сродак выбару мультымедыя"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Сродак выбару мультымедыя"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Ідзе сінхранізацыя мультымедыя…"</string>
     <string name="add" msgid="2894574044585549298">"Дадаць"</string>
     <string name="deselect" msgid="4297825044827769490">"Адмяніць выбар"</string>
     <string name="deselected" msgid="8488133193326208475">"Выбар скасаваны"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Няма альбомаў"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Праглядзець выбранае"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Фота"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Альбомы"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Перадпрагляд"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Пераключыцца на працоўны"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> элемент}one{<xliff:g id="COUNT_1">^1</xliff:g> элемент}few{<xliff:g id="COUNT_1">^1</xliff:g> элементы}many{<xliff:g id="COUNT_1">^1</xliff:g> элементаў}other{<xliff:g id="COUNT_1">^1</xliff:g> элемента}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Дадаць (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Дазволіць (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Не дазваляць ніякія"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Камера"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Спампоўкі"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Абранае"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Праблемы з прайграваннем відэа"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Праверце падключэнне да інтэрнэту і паўтарыце спробу"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Паўтарыць спробу"</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="preloading_dialog_title" msgid="4974348221848532887">"Ідзе падрыхтоўка выбраных вамі медыяфайлаў"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Гатова: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> з <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Скасаваць"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Цяпер дададзены рэзервовыя копіі фотаздымкаў"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Вы можаце выбраць фотаздымкі з уліковага запісу <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> для праграмы \"<xliff:g id="APP_NAME">%1$s</xliff:g>\""</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Зменены ўліковы запіс для праграмы \"<xliff:g id="APP_NAME">%1$s</xliff:g>\""</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Выбраць праграму"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Выбраць уліковы запіс"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Змяніць уліковы запіс"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Вашы фота загружаюцца"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Ахова бяспекі"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Абвесткі пра ўбудаванае перакадзіраванне"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Ход убудаванага перакадзіравання"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Паўтарыце спробу пазней. Калі праблема будзе вырашана, вашы фота стануць даступнымі."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Некаторыя фота не ўдалося загрузіць"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
 </resources>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index fbb9f34..9ff2d77 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Мултимедия"</string>
     <string name="storage_description" msgid="4081716890357580107">"Локално хранилище"</string>
-    <string name="app_label" msgid="9035307001052716210">"Мултимедийно хранилище"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Мултимедия"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Инструмент за избор на носители"</string>
     <string name="artist_label" msgid="8105600993099120273">"Изпълнител"</string>
     <string name="unknown" msgid="2059049215682829375">"Неизвестно"</string>
     <string name="root_images" msgid="5861633549189045666">"Изображения"</string>
@@ -39,16 +38,19 @@
     <string name="allow" msgid="8885707816848569619">"Разрешаване"</string>
     <string name="deny" msgid="6040983710442068936">"Отказ"</string>
     <string name="picker_browse" msgid="5554477454636075934">"Преглед…"</string>
-    <string name="picker_settings" msgid="6443463167344790260">"Медийно приложение в облака"</string>
+    <string name="picker_settings" msgid="6443463167344790260">"Прил. за мултимедия в облака"</string>
     <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Приложение за мултимедия в облака"</string>
     <string name="picker_settings_title" msgid="5647700706470673258">"Приложение за мултимедия в облака"</string>
     <string name="picker_settings_description" msgid="2916686824777214585">"Осъществяване на достъп до мултимедията в облака, когато приложение или уебсайт иска от вас да изберете снимки или видеоклипове"</string>
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Достъп до мултимедия в облака от"</string>
-    <string name="picker_settings_no_provider" msgid="2582311853680058223">"Няма"</string>
+    <string name="picker_settings_no_provider" msgid="2582311853680058223">"Нищо"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Медийното приложение в облака не бе променено."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Инструмент за избор на мултимедия"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Инструмент за избор на мултимедия"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Мултимедията се синхронизира…"</string>
     <string name="add" msgid="2894574044585549298">"Добавяне"</string>
     <string name="deselect" msgid="4297825044827769490">"Премахване на избора"</string>
-    <string name="deselected" msgid="8488133193326208475">"Неизбрано"</string>
+    <string name="deselected" msgid="8488133193326208475">"Отменен избор"</string>
     <string name="select" msgid="2704765470563027689">"Избиране"</string>
     <string name="selected" msgid="9151797369975828124">"Избрано"</string>
     <string name="select_up_to" msgid="6994294169508439957">"{count,plural, =1{Изберете най-много <xliff:g id="COUNT_0">^1</xliff:g> елемент}other{Изберете най-много <xliff:g id="COUNT_1">^1</xliff:g> елемента}}"</string>
@@ -58,10 +60,12 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Няма албуми"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Преглед на избраното"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Снимки"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Албуми"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Визуализация"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Превкл. към служ. пoтр. профил"</string>
-    <string name="picker_personal_profile" msgid="639484258397758406">"Превключване към личния потребителски профил"</string>
+    <string name="picker_personal_profile" msgid="639484258397758406">"Превкл. към личния потр. профил"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"Блокирано от администратора ви"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"Достъпът до служебни данни от лично приложение не е разрешен"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"Достъпът до лични данни от служебно приложение не е разрешен"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> елемент}other{<xliff:g id="COUNT_1">^1</xliff:g> елемента}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Добавяне (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Разрешаване (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Забраняване на всички"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Камера"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Изтегляния"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Любими"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Проблем при възпроизвеждането на видеосъдържание"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Проверете връзката си с интернет и опитайте отново"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Нов опит"</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="preloading_dialog_title" msgid="4974348221848532887">"Избраната от вас мултимедия се подготвя"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Готови: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> от <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Отказ"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Снимките, за които е създадено резервно копие, вече са добавени"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Можете да избирате снимки от профила <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> в(ъв) <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Профилът в(ъв) <xliff:g id="APP_NAME">%1$s</xliff:g> е актуализиран"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Избиране на приложение"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Избиране на профил"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Промяна на профила"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Всичките ви снимки се извличат"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Защита на безопасността"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Стандартни сигнали за прекодиране"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Стандартен прогрес при прекодиране"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Опитайте отново по-късно. Снимките ви ще бъдат налице, след като проблемът бъде разрешен."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Някои снимки не могат да се заредят"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Разбрах"</string>
 </resources>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index 2f4826a..0d9f12d 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"মিডিয়া"</string>
     <string name="storage_description" msgid="4081716890357580107">"স্থানীয় স্টোরেজ"</string>
-    <string name="app_label" msgid="9035307001052716210">"মিডিয়া স্টোরেজ"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"মিডিয়া"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"মিডিয়া বাছাইকারি"</string>
     <string name="artist_label" msgid="8105600993099120273">"শিল্পী"</string>
     <string name="unknown" msgid="2059049215682829375">"অজানা"</string>
     <string name="root_images" msgid="5861633549189045666">"ছবি"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"এখান থেকে ক্লাউড মিডিয়া অ্যাক্সেস করুন"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"কোনওটিই নয়"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"এই মুহূর্তে ক্লাউড মিডিয়া অ্যাপ বদল করা যায়নি।"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"মিডিয়া বাছাইকারী"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"মিডিয়া বাছাইকারী"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"মিডিয়া সিঙ্ক করছে…"</string>
     <string name="add" msgid="2894574044585549298">"যোগ করুন"</string>
     <string name="deselect" msgid="4297825044827769490">"টিক চিহ্নটি সরিয়ে দিন"</string>
     <string name="deselected" msgid="8488133193326208475">"টিকচিহ্ন সরিয়ে দিন"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"কোনও অ্যালবাম নেই"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"কোনগুলি বাছা হয়েছে দেখুন"</string>
     <string name="picker_photos" msgid="7415035516411087392">"ফটো"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"অ্যালবাম"</string>
     <string name="picker_preview" msgid="6257414886055861039">"প্রিভিউ"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"অফিস প্রোফাইলে সুইচ করুন"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g>টি আইটেম}one{<xliff:g id="COUNT_1">^1</xliff:g>টি আইটেম}other{<xliff:g id="COUNT_1">^1</xliff:g>টি আইটেম}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>)টি যোগ করুন"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"অনুমতি দিন (<xliff:g id="COUNT">^1</xliff:g>টি)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"কারও অনুমতি নেই"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"ক্যামেরা"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"ডাউনলোড করা আইটেম"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"পছন্দসই আইটেম"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"ভিডিও প্লে করতে সমস্যা হয়েছে"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"ইন্টারনেট কানেকশন ঠিক আছে কিনা দেখে নিয়ে আবার চেষ্টা করুন"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"আবার চেষ্টা করুন"</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="preloading_dialog_title" msgid="4974348221848532887">"আপনার বেছে নেওয়া মিডিয়া রেডি করা হচ্ছে"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>টির মধ্যে <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> নম্বর আইটেম রেডি আছে"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"বাতিল করুন"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"ব্যাক-আপ নেওয়া ফটো এখন যোগ করা হয়েছে"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"আপনি <xliff:g id="APP_NAME">%1$s</xliff:g>-এর অ্যাকাউন্ট <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> থেকে ফটো বেছে নিতে পারবেন"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g>-এর অ্যাকাউন্ট আপডেট করা হয়েছে"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"অ্যাপ বেছে নিন"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"অ্যাকাউন্ট বেছে নিন"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"অ্যাকাউন্ট পরিবর্তন করুন"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"আপনার সব ফটো লোড করা হচ্ছে"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"নিরাপত্তার সুরক্ষা"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"নেটিভ ট্রান্সকোড অ্যালার্ট"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"নেটিভ ট্রান্সকোড প্রোগ্রেস"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"পরে আবার চেষ্টা করুন। সমস্যার সমাধান হয়ে গেলে আপনার ফটো উপলভ্য হবে।"</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"কিছু ফটো লোড করা যাচ্ছে না"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"বুঝেছি"</string>
 </resources>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index e04120d..557107e 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Mediji"</string>
     <string name="storage_description" msgid="4081716890357580107">"Lokalna pohrana"</string>
-    <string name="app_label" msgid="9035307001052716210">"Medijska pohrana"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Medij"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Izbornik medijskog sadržaja"</string>
     <string name="artist_label" msgid="8105600993099120273">"Umjetnik"</string>
     <string name="unknown" msgid="2059049215682829375">"Nepoznato"</string>
     <string name="root_images" msgid="5861633549189045666">"Slike"</string>
@@ -39,16 +38,19 @@
     <string name="allow" msgid="8885707816848569619">"Dozvoli"</string>
     <string name="deny" msgid="6040983710442068936">"Odbij"</string>
     <string name="picker_browse" msgid="5554477454636075934">"Pregledajte…"</string>
-    <string name="picker_settings" msgid="6443463167344790260">"Apl. za med. sadržaje u oblaku"</string>
-    <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Aplikacija za medijske sadržaje u oblaku"</string>
+    <string name="picker_settings" msgid="6443463167344790260">"Aplikacija za medije u oblaku"</string>
+    <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Aplikacija za medije u oblaku"</string>
     <string name="picker_settings_title" msgid="5647700706470673258">"Aplikacija za medijske sadržaje u oblaku"</string>
-    <string name="picker_settings_description" msgid="2916686824777214585">"Pristupite medijima na oblaku kada vam aplikacija ili web lokacija zatraži da odaberete fotografije ili videozapise"</string>
-    <string name="picker_settings_selection_message" msgid="245453573086488596">"Pristupite medijskom sadržaju u oblaku iz"</string>
+    <string name="picker_settings_description" msgid="2916686824777214585">"Pristupite medijima u oblaku kada vam aplikacija ili web lokacija zatraži da odaberete fotografije ili videozapise"</string>
+    <string name="picker_settings_selection_message" msgid="245453573086488596">"Pristupite medijima u oblaku iz oblaku iz"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Ništa"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Promjena medijske aplikacije u oblaku nije uspjela."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Izbornik medijskog sadržaja"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Izbornik medijskog sadržaja"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Sinhroniziranje medijskog sadržaja…"</string>
     <string name="add" msgid="2894574044585549298">"Dodaj"</string>
     <string name="deselect" msgid="4297825044827769490">"Poništi odabir"</string>
-    <string name="deselected" msgid="8488133193326208475">"Odabir poništen"</string>
+    <string name="deselected" msgid="8488133193326208475">"Odabir je poništen"</string>
     <string name="select" msgid="2704765470563027689">"Odaberi"</string>
     <string name="selected" msgid="9151797369975828124">"Odabrano"</string>
     <string name="select_up_to" msgid="6994294169508439957">"{count,plural, =1{Odaberite najviše <xliff:g id="COUNT_0">^1</xliff:g> stavku}one{Odaberite najviše <xliff:g id="COUNT_1">^1</xliff:g> stavku}few{Odaberite najviše <xliff:g id="COUNT_1">^1</xliff:g> stavke}other{Odaberite najviše <xliff:g id="COUNT_1">^1</xliff:g> stavki}}"</string>
@@ -58,10 +60,12 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Nema albuma"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Prikaži odabrano"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotografije"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albumi"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Pregled"</string>
-    <string name="picker_work_profile" msgid="2083221066869141576">"Prebacite se na radni"</string>
-    <string name="picker_personal_profile" msgid="639484258397758406">"Prebacite se na lični"</string>
+    <string name="picker_work_profile" msgid="2083221066869141576">"Prebacite se na radni profil"</string>
+    <string name="picker_personal_profile" msgid="639484258397758406">"Prebacite se na lični profil profil"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"Blokirao je administrator"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"Pristupanje poslovnim podacima iz lične aplikacije nije dozvoljeno"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"Pristupanje ličnim podacima iz poslovne aplikacije nije dozvoljeno"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> stavka}one{<xliff:g id="COUNT_1">^1</xliff:g> stavka}few{<xliff:g id="COUNT_1">^1</xliff:g> stavke}other{<xliff:g id="COUNT_1">^1</xliff:g> stavki}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Dodaj (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Dozvoli (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Nemoj dozvoliti ništa"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Preuzimanja"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Omiljeno"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Poteškoće prilikom reprodukcije videozapisa"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Provjerite internetsku vezu i pokušajte ponovo"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Pokušaj ponovo"</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="preloading_dialog_title" msgid="4974348221848532887">"Pripremanje odabranih medijskih fajlova"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Spremno: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> od <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Otkaži"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Sigurnosne kopije fotografija su sada uključene"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Možete odabrati fotografije s računa <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> u aplikaciji <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Račun u aplikaciji <xliff:g id="APP_NAME">%1$s</xliff:g> je ažuriran"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Zaštita sigurnosti"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Obavještenja o izvornom konvertiranju"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Napredak izvornog konvertiranja"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Pokušajte ponovo kasnije. Fotografije će biti dostupne čim se problem riješi."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Nije moguće učitati određene fotografije"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Razumijem"</string>
 </resources>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 580d157..704fc78 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Multimèdia"</string>
     <string name="storage_description" msgid="4081716890357580107">"Emmagatzematge local"</string>
-    <string name="app_label" msgid="9035307001052716210">"Emmagatzematge multimèdia"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Contingut multimèdia"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Selector de mitjans"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artista"</string>
     <string name="unknown" msgid="2059049215682829375">"Desconegut"</string>
     <string name="root_images" msgid="5861633549189045666">"Imatges"</string>
@@ -40,12 +39,15 @@
     <string name="deny" msgid="6040983710442068936">"Denega"</string>
     <string name="picker_browse" msgid="5554477454636075934">"Navega…"</string>
     <string name="picker_settings" msgid="6443463167344790260">"Aplicació multimèdia al núvol"</string>
-    <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Aplicació multimèdia al núvol"</string>
+    <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"App multimèdia al núvol"</string>
     <string name="picker_settings_title" msgid="5647700706470673258">"Aplicació multimèdia al núvol"</string>
     <string name="picker_settings_description" msgid="2916686824777214585">"Accedeix al contingut multimèdia al núvol si una aplicació o un lloc web et demana que seleccionis fotos o vídeos"</string>
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Accedeix al contingut multimèdia al núvol des de"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Cap"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"No s\'ha pogut canviar l\'app multimèdia al núvol."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Selector de mitjans"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Selector de mitjans"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"S\'està sincronitzant el contingut multimèdia…"</string>
     <string name="add" msgid="2894574044585549298">"Afegeix"</string>
     <string name="deselect" msgid="4297825044827769490">"Desselecciona"</string>
     <string name="deselected" msgid="8488133193326208475">"Desseleccionat"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"No hi ha cap àlbum"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Mostra la selecció"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotos"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Àlbums"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Previsualitza"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Canvia al perfil de treball"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> element}many{<xliff:g id="COUNT_1">^1</xliff:g> elements}other{<xliff:g id="COUNT_1">^1</xliff:g> elements}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Afegeix (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Permet (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"No en permetis cap"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Càmera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Baixades"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Preferits"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Hi ha hagut un problema en reproduir el vídeo"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Comprova la connexió a Internet i torna-ho a provar"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Torna-ho a provar"</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="preloading_dialog_title" msgid="4974348221848532887">"S\'està preparant el contingut multimèdia seleccionat"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> a punt"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Cancel·la"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Ara s\'ha inclòs la còpia de seguretat de les fotos"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Pots seleccionar fotos del compte de <xliff:g id="APP_NAME">%1$s</xliff:g> de <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"S\'ha actualitzat el compte de <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Tria una aplicació"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Tria un compte"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Canvia de compte"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"S\'estan obtenint totes les teves fotos"</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?}many{Vols permetre que <xliff:g id="APP_NAME_1">^1</xliff:g> modifiqui <xliff:g id="COUNT">^2</xliff:g> fitxers 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…}many{S\'estan modificant <xliff:g id="COUNT">^1</xliff:g> fitxers d\'àudio…}other{S\'estan modificant <xliff:g id="COUNT">^1</xliff:g> fitxers d\'àudio…}}"</string>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Vols permetre que <xliff:g id="APP_NAME_0">^1</xliff:g> modifiqui aquest vídeo?}many{Vols permetre que <xliff:g id="APP_NAME_1">^1</xliff:g> modifiqui <xliff:g id="COUNT">^2</xliff:g> vídeos?}other{Vols permetre que <xliff:g id="APP_NAME_1">^1</xliff:g> modifiqui <xliff:g id="COUNT">^2</xliff:g> vídeos?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Protecció de seguretat"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Alertes de transcodificació nativa"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Progrés de la transcodificació nativa"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Torna-ho a provar més tard. Les teves fotos estaran disponibles un cop el problema s\'hagi resolt."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"No es poden carregar algunes fotos"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Entesos"</string>
 </resources>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 9a919a2..dd3f2af 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Média"</string>
     <string name="storage_description" msgid="4081716890357580107">"Místní úložiště"</string>
-    <string name="app_label" msgid="9035307001052716210">"Úložiště médií"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Média"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Nástroj pro výběr médií"</string>
     <string name="artist_label" msgid="8105600993099120273">"Interpret"</string>
     <string name="unknown" msgid="2059049215682829375">"Neznámý"</string>
     <string name="root_images" msgid="5861633549189045666">"Obrázky"</string>
@@ -42,10 +41,13 @@
     <string name="picker_settings" msgid="6443463167344790260">"Aplikace pro cloudová média"</string>
     <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Aplikace pro cloudová média"</string>
     <string name="picker_settings_title" msgid="5647700706470673258">"Aplikace pro cloudová média"</string>
-    <string name="picker_settings_description" msgid="2916686824777214585">"Když vás aplikace nebo web požádá o výběr fotografií nebo videí, přejít na vaše cloudová média"</string>
+    <string name="picker_settings_description" msgid="2916686824777214585">"Když vás aplikace nebo web požádá o výběr fotografií nebo videí, můžete přejít na svoje cloudová média"</string>
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Přístup ke cloudovým médiím z"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Žádný"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Aplikaci pro cloudová média nyní nelze změnit."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Nástroj pro výběr médií"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Nástroj pro výběr médií"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Synchronizace médií…"</string>
     <string name="add" msgid="2894574044585549298">"Přidat"</string>
     <string name="deselect" msgid="4297825044827769490">"Zrušit výběr"</string>
     <string name="deselected" msgid="8488133193326208475">"Výběr zrušen"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Žádná alba"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Zobrazit vybrané"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotky"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Alba"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Náhled"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Přepnout na pracovní profil"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> položka}few{<xliff:g id="COUNT_1">^1</xliff:g> položky}many{<xliff:g id="COUNT_1">^1</xliff:g> položky}other{<xliff:g id="COUNT_1">^1</xliff:g> položek}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Přidat (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Povolit (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Nepovolit nic"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Fotoaparát"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Stažené soubory"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Oblíbené"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Při přehrávání videa došlo k potížím"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Zkontrolujte připojení k internetu a zkuste to znovu"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Zkusit znovu"</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="preloading_dialog_title" msgid="4974348221848532887">"Příprava vámi vybraných médií"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Připraveno: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> z <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Zrušit"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Teď jsou zde zahrnuty zálohované fotky"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Můžete vybrat fotky z účtu <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> aplikace <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Účet <xliff:g id="APP_NAME">%1$s</xliff:g> byl aktualizován"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Vybrat aplikaci"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Vybrat účet"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Změnit účet"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Načítání všech fotek"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Povolit aplikaci <xliff:g id="APP_NAME_0">^1</xliff:g> upravit toto video?}few{Povolit aplikaci <xliff:g id="APP_NAME_1">^1</xliff:g> upravit <xliff:g id="COUNT">^2</xliff:g> videa?}many{Povolit aplikaci <xliff:g id="APP_NAME_1">^1</xliff:g> upravit <xliff:g id="COUNT">^2</xliff:g> videa?}other{Povolit aplikaci <xliff:g id="APP_NAME_1">^1</xliff:g> upravit <xliff:g id="COUNT">^2</xliff:g> videí?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Bezpečnostní ochrana"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Upozornění na nativní překódování"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Průběh nativního překódování"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Zkuste to později. Fotky budou k dispozici po vyřešení tohoto problému."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Některé fotografie nelze načíst"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Rozumím"</string>
 </resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 0d72f65..fbf5071 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Medier"</string>
     <string name="storage_description" msgid="4081716890357580107">"Lokalt lager"</string>
-    <string name="app_label" msgid="9035307001052716210">"Medielagring"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Mediefiler"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Medievælger"</string>
     <string name="artist_label" msgid="8105600993099120273">"Kunstner"</string>
     <string name="unknown" msgid="2059049215682829375">"Ukendt"</string>
     <string name="root_images" msgid="5861633549189045666">"Billeder"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Få adgang til medier i skyen via"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Ingen"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Skymedieappen kunne ikke ændres"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Medievælger"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Medievælger"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Mediet synkroniseres…"</string>
     <string name="add" msgid="2894574044585549298">"Tilføj"</string>
     <string name="deselect" msgid="4297825044827769490">"Fravælg"</string>
     <string name="deselected" msgid="8488133193326208475">"Fravalgt"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Ingen album"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Se valgte"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Billeder"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Album"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Forhåndsvisning"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Skift til arbejdsprofil"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> element}one{<xliff:g id="COUNT_1">^1</xliff:g> element}other{<xliff:g id="COUNT_1">^1</xliff:g> elementer}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Tilføj (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Tillad (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Tillad ingen"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favoritter"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Problemer med at afspille video"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Tjek din internetforbindelse, og prøv igen"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Prøv igen"</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="preloading_dialog_title" msgid="4974348221848532887">"Dine valgte medier gøres klar"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> af <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> er klar"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Annuller"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Sikkerhedskopierede billeder er nu inkluderet"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Du kan vælge billeder fra <xliff:g id="APP_NAME">%1$s</xliff:g>-kontoen <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g>-kontoen er opdateret"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Vælg app"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Vælg en konto"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Skift konto"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Indlæser alle dine billeder"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Vil du give <xliff:g id="APP_NAME_0">^1</xliff:g> tilladelse til at ændre denne video?}one{Vil du give <xliff:g id="APP_NAME_1">^1</xliff:g> tilladelse til at ændre <xliff:g id="COUNT">^2</xliff:g> video?}other{Vil du give <xliff:g id="APP_NAME_1">^1</xliff:g> tilladelse til at ændre <xliff:g id="COUNT">^2</xliff:g> videoer?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Beskyttelse"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Underretninger om indbygget omkodning"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Status på indbygget omkodning"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Prøv igen senere. Dine billeder bliver tilgængelige, så snart problemet er løst."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Nogle billeder kan ikke indlæses"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
 </resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index b7645d3..658e3f0 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Medien"</string>
     <string name="storage_description" msgid="4081716890357580107">"Lokaler Speicher"</string>
-    <string name="app_label" msgid="9035307001052716210">"Medienspeicher"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Medien"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Media-Auswahl"</string>
     <string name="artist_label" msgid="8105600993099120273">"Interpret"</string>
     <string name="unknown" msgid="2059049215682829375">"Unbekannt"</string>
     <string name="root_images" msgid="5861633549189045666">"Bilder"</string>
@@ -42,10 +41,13 @@
     <string name="picker_settings" msgid="6443463167344790260">"Cloud-Medien-App"</string>
     <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Cloud-Medien-App"</string>
     <string name="picker_settings_title" msgid="5647700706470673258">"Cloud-Medien-App"</string>
-    <string name="picker_settings_description" msgid="2916686824777214585">"Zugriff auf Cloudmedien, wenn dich eine App oder Website darum bittet, Fotos oder Videos auszuwählen"</string>
+    <string name="picker_settings_description" msgid="2916686824777214585">"Zugriff auf Cloud-Medien, wenn dich eine App oder Website darum bittet, Fotos oder Videos auszuwählen"</string>
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Zugriff auf Cloud-Medien aus"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Keine"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Ändern der Cloud-Medien-App derzeit nicht möglich."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Media-Auswahl"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Media-Auswahl"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Medien werden synchronisiert…"</string>
     <string name="add" msgid="2894574044585549298">"Hinzufügen"</string>
     <string name="deselect" msgid="4297825044827769490">"Auswahl aufheben"</string>
     <string name="deselected" msgid="8488133193326208475">"Auswahl aufgehoben"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Keine Alben"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Auswahl ansehen"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotos"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Alben"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Vorschau"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Zum Arbeitsprofil wechseln"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> Element}other{<xliff:g id="COUNT_1">^1</xliff:g> Elemente}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Hinzufügen (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Erlauben (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Keines zulassen"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favoriten"</string>
@@ -92,23 +97,23 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Probleme beim Abspielen des Videos"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Prüfe deine Internetverbindung und versuche es noch einmal"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Wiederholen"</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="preloading_dialog_title" msgid="4974348221848532887">"Ausgewählte Medien werden vorbereitet"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> von <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> fertig"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Abbrechen"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Gesicherte Fotos jetzt mit berücksichtigt"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Du kannst Fotos aus dem <xliff:g id="APP_NAME">%1$s</xliff:g>-Konto <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> auswählen"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g>-Konto aktualisiert"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"Fotos von <xliff:g id="USER_ACCOUNT">%1$s</xliff:g> sind jetzt hier mit berücksichtigt"</string>
-    <string name="picker_banner_cloud_choose_app_title" msgid="3165966147547974251">"Cloudmedien-App auswählen"</string>
-    <string name="picker_banner_cloud_choose_app_desc" msgid="2359212653555524926">"Damit gesicherte Fotos hier mit berücksichtigt werden, wähle eine Cloudmedien-App in den Einstellungen aus"</string>
+    <string name="picker_banner_cloud_choose_app_title" msgid="3165966147547974251">"Cloud-Medien-App auswählen"</string>
+    <string name="picker_banner_cloud_choose_app_desc" msgid="2359212653555524926">"Damit gesicherte Fotos hier mit berücksichtigt werden, wähle eine Cloud-Medien-App in den Einstellungen aus"</string>
     <string name="picker_banner_cloud_choose_account_title" msgid="5010901185639577685">"<xliff:g id="APP_NAME">%1$s</xliff:g>-Konto auswählen"</string>
     <string name="picker_banner_cloud_choose_account_desc" msgid="8868134443673142712">"Damit Fotos von <xliff:g id="APP_NAME">%1$s</xliff:g> hier mit berücksichtigt werden, wähle eine Konto in der App aus"</string>
     <string name="picker_banner_cloud_dismiss_button" msgid="2935903078288463882">"Schließen"</string>
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"App auswählen"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Konto auswählen"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Konto ändern"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Alle deine Fotos werden geladen"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Darf <xliff:g id="APP_NAME_0">^1</xliff:g> dieses Video ändern?}other{Darf <xliff:g id="APP_NAME_1">^1</xliff:g> <xliff:g id="COUNT">^2</xliff:g> Videos ändern?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Schutz"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Warnmeldungen bei nativer Transcodierung"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Fortschritt bei nativer Transcodierung"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Versuch es später noch einmal. Deine Fotos sind verfügbar, sobald das Problem gelöst wurde."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Einige Fotos konnten nicht geladen werden"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Ok"</string>
 </resources>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 3f4fb52..6957172 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Μέσα"</string>
     <string name="storage_description" msgid="4081716890357580107">"Τοπικός χώρος αποθήκευσης"</string>
-    <string name="app_label" msgid="9035307001052716210">"Αποθηκευτικός χώρος μέσων"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Μέσα"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Εργαλείο επιλογής μέσων"</string>
     <string name="artist_label" msgid="8105600993099120273">"Καλλιτέχνης"</string>
     <string name="unknown" msgid="2059049215682829375">"Άγνωστο"</string>
     <string name="root_images" msgid="5861633549189045666">"Εικόνες"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Πρόσβαση σε μέσα στο cloud από"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Καμία"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Η αλλαγή της εφαρμογής μέσων cloud ήταν αδύνατη."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Εργαλείο επιλογής μέσων"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Εργαλείο επιλογής μέσων"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Συγχρονισμός μέσων…"</string>
     <string name="add" msgid="2894574044585549298">"Προσθήκη"</string>
     <string name="deselect" msgid="4297825044827769490">"Αποεπιλογή"</string>
     <string name="deselected" msgid="8488133193326208475">"Αποεπιλέχθηκε"</string>
@@ -55,9 +57,11 @@
     <string name="recent" msgid="6694613584743207874">"Πρόσφατα"</string>
     <string name="picker_photos_empty_message" msgid="5980619500554575558">"Δεν υπάρχουν φωτογραφίες ή βίντεο"</string>
     <string name="picker_album_media_empty_message" msgid="7061850698189881671">"Δεν υπάρχουν υποστηριζόμενες φωτογραφίες ή βίντεο"</string>
-    <string name="picker_albums_empty_message" msgid="8341079772950966815">"Δεν υπάρχουν λευκώματα"</string>
+    <string name="picker_albums_empty_message" msgid="8341079772950966815">"Δεν υπάρχουν άλμπουμ"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Προβολή επιλεγμένων"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Φωτογραφίες"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Άλμπουμ"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Προεπισκόπηση"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Μετάβαση σε προφίλ εργασίας"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> στοιχείο}other{<xliff:g id="COUNT_1">^1</xliff:g> στοιχεία}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Προσθήκη (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Αποδοχή (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Δεν επιτρέπεται καμία"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Κάμερα"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Λήψεις"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Αγαπημένα"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Πρόβλημα με την αναπαραγωγή βίντεο"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Ελέγξτε τη σύνδεσή σας στο διαδίκτυο και δοκιμάστε ξανά"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Επανάληψη"</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="preloading_dialog_title" msgid="4974348221848532887">"Προετοιμασία των μέσων που επιλέξατε"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> από <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> έτοιμα"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Ακύρωση"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Συμπεριλαμβάνονται πλέον φωτογραφίες που έχουν αντίγραφα ασφαλείας"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Μπορείτε να επιλέξετε φωτογραφίες από τον λογαριασμό <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> στην εφαρμογή <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Ο λογαριασμός <xliff:g id="APP_NAME">%1$s</xliff:g> ενημερώθηκε"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Επιλογή εφαρμογής"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Επιλογή λογαριασμού"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Αλλαγή λογαριασμού"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Γίνεται λήψη όλων των φωτογραφιών σας"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Safety Protection"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Ειδοποιήσεις εγγενούς διακωδικοποίησης"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Πρόοδος εγγενούς διακωδικοποίησης"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Δοκιμάστε ξανά αργότερα. Οι φωτογραφίες σας θα καταστούν διαθέσιμες μόλις επιλυθεί το πρόβλημα."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Δεν είναι δυνατή η φόρτωση ορισμένων φωτογραφιών"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Το κατάλαβα"</string>
 </resources>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index 3b6aab2..afa1aa4 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Media"</string>
     <string name="storage_description" msgid="4081716890357580107">"Local storage"</string>
-    <string name="app_label" msgid="9035307001052716210">"Media Storage"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Media"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Media picker"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artist"</string>
     <string name="unknown" msgid="2059049215682829375">"Unknown"</string>
     <string name="root_images" msgid="5861633549189045666">"Images"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Access cloud media from"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"None"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Could not change cloud media app at this time."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Media Picker"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Media Picker"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Syncing media…"</string>
     <string name="add" msgid="2894574044585549298">"Add"</string>
     <string name="deselect" msgid="4297825044827769490">"Deselect"</string>
     <string name="deselected" msgid="8488133193326208475">"Deselected"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"No albums"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"View selected"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Photos"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albums"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Preview"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Switch to work"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> items}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Add (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Allow (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Allow none"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Camera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favourites"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Trouble playing video"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Please check your Internet connection and try again"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Retry"</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="preloading_dialog_title" msgid="4974348221848532887">"Preparing your selected media"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> of <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ready"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Cancel"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Backed up photos now included"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"You can select photos from <xliff:g id="APP_NAME">%1$s</xliff:g> account <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> account updated"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Safety protection"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Native transcode alerts"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Native transcode progress"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Please try again later. Your photos will be available once the issue is resolved."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Can\'t load some photos"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Got it"</string>
 </resources>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index 97981a5..4470b43 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Media"</string>
     <string name="storage_description" msgid="4081716890357580107">"Local storage"</string>
-    <string name="app_label" msgid="9035307001052716210">"Media Storage"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Media"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Media picker"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artist"</string>
     <string name="unknown" msgid="2059049215682829375">"Unknown"</string>
     <string name="root_images" msgid="5861633549189045666">"Images"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Access cloud media from"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"None"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Could not change cloud media app at this time."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Media picker"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Media picker"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Syncing media…"</string>
     <string name="add" msgid="2894574044585549298">"Add"</string>
     <string name="deselect" msgid="4297825044827769490">"Deselect"</string>
     <string name="deselected" msgid="8488133193326208475">"Deselected"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"No albums"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"View selected"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Photos"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albums"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Preview"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Switch to work"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> items}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Add (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Allow (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Allow none"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Camera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favorites"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Trouble playing video"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Check your internet connection and try again"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Retry"</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="preloading_dialog_title" msgid="4974348221848532887">"Preparing your selected media"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> of <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ready"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Cancel"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Backed up photos now included"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"You can select photos from <xliff:g id="APP_NAME">%1$s</xliff:g> account <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> account updated"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Safety protection"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Native Transcode Alerts"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Native Transcode Progress"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Try again later. Your photos will be available once the issue is resolved."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Can\'t load some Photos"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Got it"</string>
 </resources>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index 3b6aab2..afa1aa4 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Media"</string>
     <string name="storage_description" msgid="4081716890357580107">"Local storage"</string>
-    <string name="app_label" msgid="9035307001052716210">"Media Storage"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Media"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Media picker"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artist"</string>
     <string name="unknown" msgid="2059049215682829375">"Unknown"</string>
     <string name="root_images" msgid="5861633549189045666">"Images"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Access cloud media from"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"None"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Could not change cloud media app at this time."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Media Picker"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Media Picker"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Syncing media…"</string>
     <string name="add" msgid="2894574044585549298">"Add"</string>
     <string name="deselect" msgid="4297825044827769490">"Deselect"</string>
     <string name="deselected" msgid="8488133193326208475">"Deselected"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"No albums"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"View selected"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Photos"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albums"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Preview"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Switch to work"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> items}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Add (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Allow (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Allow none"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Camera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favourites"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Trouble playing video"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Please check your Internet connection and try again"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Retry"</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="preloading_dialog_title" msgid="4974348221848532887">"Preparing your selected media"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> of <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ready"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Cancel"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Backed up photos now included"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"You can select photos from <xliff:g id="APP_NAME">%1$s</xliff:g> account <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> account updated"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Safety protection"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Native transcode alerts"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Native transcode progress"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Please try again later. Your photos will be available once the issue is resolved."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Can\'t load some photos"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Got it"</string>
 </resources>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index 3b6aab2..afa1aa4 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Media"</string>
     <string name="storage_description" msgid="4081716890357580107">"Local storage"</string>
-    <string name="app_label" msgid="9035307001052716210">"Media Storage"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Media"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Media picker"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artist"</string>
     <string name="unknown" msgid="2059049215682829375">"Unknown"</string>
     <string name="root_images" msgid="5861633549189045666">"Images"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Access cloud media from"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"None"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Could not change cloud media app at this time."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Media Picker"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Media Picker"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Syncing media…"</string>
     <string name="add" msgid="2894574044585549298">"Add"</string>
     <string name="deselect" msgid="4297825044827769490">"Deselect"</string>
     <string name="deselected" msgid="8488133193326208475">"Deselected"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"No albums"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"View selected"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Photos"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albums"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Preview"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Switch to work"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> items}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Add (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Allow (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Allow none"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Camera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favourites"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Trouble playing video"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Please check your Internet connection and try again"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Retry"</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="preloading_dialog_title" msgid="4974348221848532887">"Preparing your selected media"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> of <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ready"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Cancel"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Backed up photos now included"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"You can select photos from <xliff:g id="APP_NAME">%1$s</xliff:g> account <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> account updated"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Safety protection"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Native transcode alerts"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Native transcode progress"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Please try again later. Your photos will be available once the issue is resolved."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Can\'t load some photos"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Got it"</string>
 </resources>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index 8666707..100a978 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‎‎‎‎‎‏‏‎‏‏‎‏‎‏‎‎‏‎‎‎‎‎‎‎‎‏‏‎‏‎‎‎‎‎‎‏‏‎‏‎‎‏‏‏‏‏‏‎‎‏‏‎‎‎Media‎‏‎‎‏‎"</string>
     <string name="storage_description" msgid="4081716890357580107">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‎‎‏‎‏‎‎‏‎‏‎‎‏‎‏‎‏‏‏‏‏‎‎‎‏‎‏‏‎‎‏‎‏‏‎‏‏‏‏‎‎‏‏‏‎‎‎‎‎‏‎‏‎‎‏‎‏‏‎Local storage‎‏‎‎‏‎"</string>
-    <string name="app_label" msgid="9035307001052716210">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‎‏‎‏‏‎‎‎‏‏‏‏‎‏‏‎‏‏‏‏‎‏‏‎‎‎‏‎‏‎‏‎‏‎‎‏‎‏‏‎‎‎‏‏‎‏‏‏‎‎‏‎‏‏‎‎‏‎‎Media Storage‎‏‎‎‏‎"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‏‏‎‎‎‎‏‎‎‏‎‏‏‎‎‎‏‎‎‎‎‎‎‎‏‏‎‏‎‏‎‏‏‎‎‏‏‎‎‏‏‏‎‎‎‏‎‏‏‏‎‎‎‏‏‎‎‏‎Media‎‏‎‎‏‎"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‎‎‏‎‎‏‎‏‏‏‎‎‎‎‎‎‎‎‎‎‏‎‎‏‎‎‎‎‎‏‎‏‎‎‏‎‎‎‏‎‏‎‎‏‏‏‏‏‎‏‏‎‎‏‎‎‏‎‎Media picker‎‏‎‎‏‎"</string>
     <string name="artist_label" msgid="8105600993099120273">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‏‏‏‎‎‏‏‏‎‎‎‎‏‎‎‏‎‏‎‎‎‎‏‏‎‏‏‎‎‏‎‏‏‏‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‎‎‎‏‎Artist‎‏‎‎‏‎"</string>
     <string name="unknown" msgid="2059049215682829375">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‎‎‏‎‎‏‎‎‏‏‎‎‏‏‎‏‏‎‎‏‎‏‏‎‏‎‎‎‏‏‎‎‏‏‏‏‎‏‎‎‏‎‎‏‎‏‏‏‎‎‎‎‏‏‏‏‏‏‎Unknown‎‏‎‎‏‎"</string>
     <string name="root_images" msgid="5861633549189045666">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‎‏‎‏‎‏‏‎‎‎‏‎‏‏‎‏‎‎‏‎‏‎‏‎‎‎‎‎‎‏‏‏‏‎‏‏‏‎‏‏‎‎‏‏‎‏‏‏‎‏‏‎‏‎‎‎‏‎‎Images‎‏‎‎‏‎"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‎‏‏‎‏‏‎‏‎‎‎‎‎‎‎‎‏‏‎‏‎‏‏‏‎‎‏‎‏‏‎‏‎‎‎‏‎‏‎‏‎‏‏‏‎‏‎‏‎‎‎‎‎‎‏‎‏‎‎‎Access cloud media from‎‏‎‎‏‎"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‏‏‏‏‎‏‎‏‏‎‎‎‏‏‎‏‏‎‏‏‏‏‎‎‏‏‎‎‎‎‎‎‎‏‏‏‏‎‎‎‎‏‎‏‎‏‏‏‏‏‎‏‏‎‏‏‏‏‎None‎‏‎‎‏‎"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‎‏‏‎‏‎‏‏‎‏‎‎‏‏‎‏‏‏‎‏‏‎‏‎‎‏‏‏‏‏‎‎‏‏‏‎‏‏‏‎‏‎‎‏‏‎‎‏‎‎‎‎‎‎‏‏‎‏‎Could not change cloud media app at this time.‎‏‎‎‏‎"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎‎‏‏‏‏‎‏‎‎‏‎‏‎‎‏‎‏‎‏‎‏‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‎‏‎‏‏‏‎‏‎‏‎‎‎‏‎‎‎‏‏‎‎‏‎Media picker‎‏‎‎‏‎"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‏‏‏‎‎‏‎‏‎‎‏‎‏‎‏‏‎‏‏‏‎‏‏‏‏‏‎‏‏‎‎‏‎‏‏‏‏‏‎‏‏‎‎‏‏‎‎‏‏‎‎‏‎‎‏‏‏‎‎Media picker‎‏‎‎‏‎"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‎‏‏‏‎‏‏‎‏‏‏‏‏‏‏‎‎‎‎‎‎‏‎‏‎‏‏‎‎‏‎‎‎‏‏‏‏‏‎‏‎‏‏‏‏‏‏‏‎‎‏‎‎‎‎‏‏‎‎Syncing media…‎‏‎‎‏‎"</string>
     <string name="add" msgid="2894574044585549298">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‎‎‎‎‏‎‏‎‏‏‏‎‎‏‎‏‏‏‏‏‎‎‎‎‎‏‏‏‏‎‎‏‏‏‏‎‎‏‎‏‏‎‎‏‎‏‏‏‎‏‏‏‏‏‎‎‏‎‎Add‎‏‎‎‏‎"</string>
     <string name="deselect" msgid="4297825044827769490">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‏‏‏‎‏‎‎‏‎‎‏‏‏‏‎‎‎‏‎‎‎‏‏‎‏‏‎‏‏‏‏‏‏‏‏‎‏‏‏‏‎‎‏‎‎‎‏‎‏‎‏‎‎‏‎‎‏‎‎Deselect‎‏‎‎‏‎"</string>
     <string name="deselected" msgid="8488133193326208475">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‏‏‏‎‎‏‎‏‏‏‏‏‎‏‎‎‎‎‎‏‎‏‎‏‎‏‏‎‏‎‎‏‎‎‏‏‎‏‎‎‏‎‎‏‎‎‏‎‏‏‏‎‏‏‎‏‏‎Deselected‎‏‎‎‏‎"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‏‎‎‎‎‎‏‎‏‏‏‎‏‏‏‏‏‎‏‏‏‎‎‎‏‎‎‏‏‎‏‏‏‏‏‎‎‎‎‎‎‏‏‎‎‏‎‎‎‎‏‏‏‏‏‎No albums‎‏‎‎‏‎"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‏‏‎‏‏‏‎‎‏‎‏‎‎‎‏‏‏‏‏‎‎‎‏‎‎‏‏‏‎‎‎‏‎‎‏‏‎‏‎‎‎‏‏‎‎‏‏‏‎‏‎‎‎‎‏‎‏‏‎View selected‎‏‎‎‏‎"</string>
     <string name="picker_photos" msgid="7415035516411087392">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‏‏‎‏‏‏‎‎‏‏‏‎‏‏‏‏‏‏‏‏‎‎‎‎‏‎‎‎‎‎‎‎‎‎‎‏‎‎‏‎‏‏‎‏‎‎‎‏‎‏‎‎‎‏‎‎‎‎‎‎Photos‎‏‎‎‏‎"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‏‎‏‏‏‎‏‏‎‏‎‎‎‎‎‎‎‏‎‎‎‎‎‏‏‎‎‎‏‏‎‎‏‎‏‎‏‎‏‏‎‎‏‏‏‏‏‏‏‏‎‏‎‎‎‏‏‎‎Albums‎‏‎‎‏‎"</string>
     <string name="picker_preview" msgid="6257414886055861039">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‏‎‏‎‏‏‎‏‏‎‎‏‏‎‏‏‎‏‎‏‏‏‏‏‎‎‎‎‏‏‏‏‎‎‏‏‎‎‏‏‏‎‏‏‎‏‏‎‎‏‎‏‏‏‏‎Preview‎‏‎‎‏‎"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‎‎‏‏‏‎‏‎‎‏‎‎‎‏‎‏‏‎‏‎‎‎‎‏‎‏‏‏‎‏‏‏‏‏‎‏‎‏‏‎‎‎‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‎Switch to work‎‏‎‎‏‎"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‏‎‏‎‏‎‏‏‎‎‏‏‎‎‏‎‎‎‎‎‏‏‎‏‎‏‏‏‏‏‏‎‎‎‏‏‏‎‏‎‏‏‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‎‎‎‎‏‎‎‏‏‎<xliff:g id="COUNT_0">^1</xliff:g>‎‏‎‎‏‏‏‎ item‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‏‎‏‎‏‎‏‏‎‎‏‏‎‎‏‎‎‎‎‎‏‏‎‏‎‏‏‏‏‏‏‎‎‎‏‏‏‎‏‎‏‏‏‎‏‏‎‎‎‏‏‎‎‏‏‏‏‎‎‎‎‏‎‎‏‏‎<xliff:g id="COUNT_1">^1</xliff:g>‎‏‎‎‏‏‏‎ items‎‏‎‎‏‎}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‏‏‏‎‎‏‎‏‎‏‎‎‏‏‎‎‏‏‏‎‎‎‎‏‎‏‏‎‎‎‏‎‏‏‎‎‏‏‏‎‏‏‎‏‏‎‏‎‏‏‏‎‏‏‏‏‏‏‎Add (‎‏‎‎‏‏‎<xliff:g id="COUNT">^1</xliff:g>‎‏‎‎‏‏‏‎)‎‏‎‎‏‎"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‏‏‎‏‎‏‎‎‎‎‏‎‎‎‎‎‏‎‏‏‏‎‎‏‏‏‏‎‎‎‏‎‏‎‎‏‎‏‏‏‎‎‏‏‏‏‎‎‎‎‎‏‎‏‎‏‏‎‎Allow (‎‏‎‎‏‏‎<xliff:g id="COUNT">^1</xliff:g>‎‏‎‎‏‏‏‎)‎‏‎‎‏‎"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‏‏‎‎‏‏‎‏‎‏‎‎‎‎‏‎‏‎‎‏‎‎‏‎‎‏‎‎‏‏‏‎‏‏‏‏‏‏‏‎‏‎‎‎‎‎‎‎‎‎‏‎‏‏‎Allow none‎‏‎‎‏‎"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‏‏‎‏‏‎‏‎‎‎‏‏‎‏‎‏‎‏‏‎‎‏‏‎‎‏‎‎‏‎‏‏‏‏‏‎‏‏‏‏‏‏‎‎‎‏‎‎‏‎‎‎‎‏‎‎‎‎‎Camera‎‏‎‎‏‎"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‏‏‎‎‎‎‎‏‎‎‎‏‏‎‎‎‎‏‏‎‎‎‎‏‎‏‎‏‏‏‏‏‎‏‎‏‏‏‎‏‎‎‎‎‏‏‎‎‏‏‎‏‏‎‏‏‎‎‎Downloads‎‏‎‎‏‎"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‎‎‏‎‏‎‎‎‎‏‏‎‎‏‎‏‏‎‏‎‏‏‎‏‏‏‎‏‎‎‎‎‏‎‎‏‏‏‏‎‏‎‏‏‎‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎Favorites‎‏‎‎‏‎"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‏‏‏‎‎‎‎‎‎‎‏‏‎‏‎‏‎‎‎‏‏‏‎‎‏‎‏‏‎‏‎‏‏‏‎‎‎‏‎‎‏‏‎‏‎‏‏‏‎‎‎‏‎‎‎‎‏‎‏‎Trouble playing video‎‏‎‎‏‎"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‏‎‏‏‏‎‏‎‎‏‏‎‏‏‎‎‏‎‏‏‎‎‏‎‎‏‏‏‎‏‎‏‎‏‎‏‏‏‏‎‏‏‏‏‏‎‏‎‏‏‎‎‏‏‏‏‎‏‎Check your internet connection and try again‎‏‎‎‏‎"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‏‎‎‏‏‎‎‏‏‎‏‏‏‎‏‎‏‎‎‏‏‎‏‎‏‎‎‎‎‎‎‏‎‏‎‏‏‎‏‎‏‏‎‎‏‎‏‏‎‏‎‎‏‏‎‎‎‎‎Retry‎‏‎‎‏‎"</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="preloading_dialog_title" msgid="4974348221848532887">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‎‏‎‎‎‎‏‎‎‎‎‏‏‎‏‏‏‏‎‏‎‏‏‎‏‎‎‏‎‎‎‎‎‎‎‏‏‎‎‎‎‎‏‎‏‏‎‏‏‏‏‎‎‏‎‏‏‏‎Preparing your selected media‎‏‎‎‏‎"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‏‎‎‏‏‎‎‏‎‎‏‎‎‏‏‏‏‏‎‏‎‎‏‎‏‎‏‎‎‏‎‎‏‎‏‎‎‎‎‏‏‎‏‎‎‎‎‎‎‏‎‎‏‏‎‎‎‏‎‎‏‏‎<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>‎‏‎‎‏‏‏‎ of ‎‏‎‎‏‏‎<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>‎‏‎‎‏‏‏‎ ready‎‏‎‎‏‎"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‏‏‎‏‏‎‏‏‏‏‏‎‏‎‎‎‎‎‎‏‎‎‏‏‏‏‏‏‎‎‎‏‎‏‎‏‏‎‏‏‏‏‎‎‎‏‎‏‎‏‏‎‎‎‎‎‎‏‎Cancel‎‏‎‎‏‎"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‎‎‎‎‎‏‏‏‏‎‎‎‏‏‎‏‎‎‏‎‎‏‎‏‎‏‏‎‎‏‎‎‏‏‏‎‎‎‎‏‏‏‏‏‎‎‏‏‎‎‏‏‎‏‎‏‏‎Backed up photos now included‎‏‎‎‏‎"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‎‏‎‏‎‎‏‏‏‏‏‏‎‏‏‏‏‏‎‎‏‎‎‎‎‏‎‏‎‏‎‏‎‏‏‏‎‏‏‏‏‏‎‎‎‎‎‏‏‏‏‏‎‏‎‏‏‏‎You can select photos from ‎‏‎‎‏‏‎<xliff:g id="APP_NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎ account ‎‏‎‎‏‏‎<xliff:g id="USER_ACCOUNT">%2$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‏‎‏‏‏‏‎‏‏‎‎‎‎‎‏‏‎‏‎‎‎‏‏‏‏‎‎‏‎‎‎‎‎‏‏‏‎‎‏‎‏‏‎‎‏‎‎‏‎‎‏‎‎‎‏‏‏‏‎‎‏‎‎‏‏‎<xliff:g id="APP_NAME">%1$s</xliff:g>‎‏‎‎‏‏‏‎ account updated‎‏‎‎‏‎"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‎‏‎‎‏‎‏‏‏‎‎‎‏‎‏‏‎‏‎‏‏‎‏‏‏‏‏‏‎‏‎‎‏‎‎‎‏‏‏‎‏‏‏‎‎‎‎‎‏‏‎‏‏‎‎‏‏‏‎Safety protection‎‏‎‎‏‎"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‎‏‏‏‎‏‎‏‏‏‎‎‏‏‏‏‎‎‏‎‎‎‎‎‏‎‎‎‎‎‏‏‎‏‏‎‎‏‏‏‎‏‎‏‎‎‎‏‏‏‎‏‎‏‏‏‏‎‎Native Transcode Alerts‎‏‎‎‏‎"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‏‏‏‏‎‏‎‎‏‏‏‏‏‏‏‎‎‏‎‏‎‏‎‎‏‎‏‏‎‏‏‎‏‎‎‎‏‏‏‎‏‏‎‏‎‏‎‏‎‏‎‏‎‏‎‎‏‏‎Native Transcode Progress‎‏‎‎‏‎"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‏‏‎‎‎‎‏‏‏‏‎‏‏‎‏‏‎‏‏‏‏‏‎‏‏‎‎‎‎‎‎‏‎‎‎‎‏‏‎‏‏‏‏‎‏‎‎‏‏‎‎‏‎‎‎‏‏‎‎Try again later. Your photos will be available once the issue is resolved.‎‏‎‎‏‎"</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‎‎‏‏‎‏‎‏‎‎‏‏‎‎‎‏‎‎‎‏‎‎‏‏‎‏‎‏‏‎‎‏‏‎‏‎‎‎‏‎‎‎‎‏‎‏‏‎‏‎‏‏‎‏‏‏‎‎‎Can\'t load some Photos‎‏‎‎‏‎"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‎‎‏‏‏‎‎‎‎‎‎‏‎‎‏‏‎‏‏‏‏‏‎‏‎‎‏‏‏‎‏‎‏‎‏‏‎‎‎‎‏‎‎‎‏‎‏‏‏‏‎‎‏‏‏‎‎‎‎Got it‎‏‎‎‏‎"</string>
 </resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index 3c244c9..03771b7 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Multimedia"</string>
     <string name="storage_description" msgid="4081716890357580107">"Almacenamiento local"</string>
-    <string name="app_label" msgid="9035307001052716210">"Almacenamiento multimedia"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Multimedia"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Selector de medios"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artista"</string>
     <string name="unknown" msgid="2059049215682829375">"Desconocido"</string>
     <string name="root_images" msgid="5861633549189045666">"Imágenes"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Accede a los medios en la nube desde"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Ninguna"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"No se pudo cambiar la app de música en la nube."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Selector de medios"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Selector de medios"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Sincronizando contenido multimedia…"</string>
     <string name="add" msgid="2894574044585549298">"Agregar"</string>
     <string name="deselect" msgid="4297825044827769490">"Anular la selección"</string>
     <string name="deselected" msgid="8488133193326208475">"Sin seleccionar"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"No hay álbumes"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Ver seleccionados"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotos"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Álbumes"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Vista previa"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Cambiar al perfil de trabajo"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> elemento}many{<xliff:g id="COUNT_1">^1</xliff:g> elementos}other{<xliff:g id="COUNT_1">^1</xliff:g> elementos}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Agregar (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Permitir (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"No permitir ninguna"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Cámara"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Descargas"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favoritos"</string>
@@ -92,11 +97,12 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Se produjo un problema al reproducir el video"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Revisa la conexión a Internet y vuelve a intentarlo"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Reintentar"</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="preloading_dialog_title" msgid="4974348221848532887">"Preparando el contenido multimedia seleccionado"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> listos"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Cancelar"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Ahora se incluyen las fotos con copia de seguridad"</string>
-    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Puedes seleccionar fotos de <xliff:g id="APP_NAME">%1$s</xliff:g> desde la cuenta de <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Puedes seleccionar imágenes de <xliff:g id="APP_NAME">%1$s</xliff:g> desde la cuenta de <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Se actualizó la cuenta de <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"Ahora se incluyen aquí las fotos de <xliff:g id="USER_ACCOUNT">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_choose_app_title" msgid="3165966147547974251">"Elige una app multimedia en la nube"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Elegir una app"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Elegir cuenta"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Cambiar cuenta"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Obteniendo todas tus fotos"</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?}many{¿Deseas permitir que <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> archivos 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…}many{Modificando <xliff:g id="COUNT">^1</xliff:g> archivos de audio…}other{Modificando <xliff:g id="COUNT">^1</xliff:g> archivos de audio…}}"</string>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{¿Deseas permitir que <xliff:g id="APP_NAME_0">^1</xliff:g> modifique este video?}many{¿Deseas permitir que <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> videos?}other{¿Deseas permitir que <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> videos?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Protección de seguridad"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Native Transcode Alerts"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Native Transcode Progress"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Vuelve a intentarlo más tarde. Tus fotos estarán disponibles una vez que se resuelva el problema."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Se produjo un error durante la carga de algunas fotos"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Entendido"</string>
 </resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index d407a5c..532b10b 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Multimedia"</string>
     <string name="storage_description" msgid="4081716890357580107">"Almacenamiento local"</string>
-    <string name="app_label" msgid="9035307001052716210">"Almacenamiento multimedia"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Multimedia"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Selector de medios"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artista"</string>
     <string name="unknown" msgid="2059049215682829375">"Desconocido"</string>
     <string name="root_images" msgid="5861633549189045666">"Imágenes"</string>
@@ -46,22 +45,27 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Accede al contenido multimedia en la nube desde"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Ninguna"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"No se puede cambiar la app multimedia en la nube."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Selector de medios"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Selector de medios"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Sincronizando contenido multimedia…"</string>
     <string name="add" msgid="2894574044585549298">"Añadir"</string>
     <string name="deselect" msgid="4297825044827769490">"Desmarcar"</string>
     <string name="deselected" msgid="8488133193326208475">"Desmarcado"</string>
     <string name="select" msgid="2704765470563027689">"Seleccionar"</string>
     <string name="selected" msgid="9151797369975828124">"Seleccionado"</string>
     <string name="select_up_to" msgid="6994294169508439957">"{count,plural, =1{Selecciona hasta <xliff:g id="COUNT_0">^1</xliff:g> elemento}many{Selecciona hasta <xliff:g id="COUNT_1">^1</xliff:g> elementos}other{Selecciona hasta <xliff:g id="COUNT_1">^1</xliff:g> elementos}}"</string>
-    <string name="recent" msgid="6694613584743207874">"Reciente"</string>
+    <string name="recent" msgid="6694613584743207874">"Recientes"</string>
     <string name="picker_photos_empty_message" msgid="5980619500554575558">"No hay fotos ni vídeos"</string>
     <string name="picker_album_media_empty_message" msgid="7061850698189881671">"No hay fotos ni vídeos compatibles"</string>
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"No hay ningún álbum"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Ver seleccionado"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotos"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Álbumes"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Vista previa"</string>
-    <string name="picker_work_profile" msgid="2083221066869141576">"Cambiar al de trabajo"</string>
-    <string name="picker_personal_profile" msgid="639484258397758406">"Cambiar al personal"</string>
+    <string name="picker_work_profile" msgid="2083221066869141576">"Cambiar a perfil de trabajo"</string>
+    <string name="picker_personal_profile" msgid="639484258397758406">"Cambiar a perfil personal"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"Bloqueado por tu administrador"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"No se puede acceder a datos de trabajo desde una aplicación personal"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"No se puede acceder a datos personales desde una aplicación de trabajo"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> elemento}many{<xliff:g id="COUNT_1">^1</xliff:g> elementos}other{<xliff:g id="COUNT_1">^1</xliff:g> elementos}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Añadir (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Permitir (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"No permitir ninguna"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Cámara"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Descargas"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favoritos"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Hay problemas para reproducir el vídeo"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Comprueba tu conexión a Internet y vuelve a intentarlo"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Reintentar"</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="preloading_dialog_title" msgid="4974348221848532887">"Preparando el contenido multimedia seleccionado"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> listos"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Cancelar"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Ahora se incluye la copia de seguridad de las fotos"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Puedes seleccionar fotos de la cuenta de <xliff:g id="APP_NAME">%1$s</xliff:g> de <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Cuenta de <xliff:g id="APP_NAME">%1$s</xliff:g> actualizada"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Elegir aplicación"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Elegir cuenta"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Cambiar de cuenta"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Cargando todas tus fotos"</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?}many{¿Permitir que <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> archivos 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…}many{Modificando <xliff:g id="COUNT">^1</xliff:g> archivos de audio…}other{Modificando <xliff:g id="COUNT">^1</xliff:g> archivos de audio…}}"</string>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{¿Permitir que <xliff:g id="APP_NAME_0">^1</xliff:g> modifique este vídeo?}many{¿Permitir que <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> vídeos?}other{¿Permitir que <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> vídeos?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Protección de seguridad"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Alertas de transcodificación nativa"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Progreso de transcodificación nativa"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Inténtalo de nuevo más tarde. Tus fotos estarán disponibles cuando se resuelva el problema."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"No se pueden cargar algunas fotos"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Entendido"</string>
 </resources>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 39a435a..51ff309 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Meedia"</string>
     <string name="storage_description" msgid="4081716890357580107">"Kohalik salvestusruum"</string>
-    <string name="app_label" msgid="9035307001052716210">"Meediumi salvestusruum"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Meedia"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Meediavalija"</string>
     <string name="artist_label" msgid="8105600993099120273">"Esitaja"</string>
     <string name="unknown" msgid="2059049215682829375">"Teadmata"</string>
     <string name="root_images" msgid="5861633549189045666">"Pildid"</string>
@@ -39,13 +38,16 @@
     <string name="allow" msgid="8885707816848569619">"Luba"</string>
     <string name="deny" msgid="6040983710442068936">"Keela"</string>
     <string name="picker_browse" msgid="5554477454636075934">"Sirvimine …"</string>
-    <string name="picker_settings" msgid="6443463167344790260">"Pilvemeediarakendus"</string>
-    <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Pilvemeediarakendus"</string>
-    <string name="picker_settings_title" msgid="5647700706470673258">"Pilvemeediarakendus"</string>
+    <string name="picker_settings" msgid="6443463167344790260">"Pilvemeedia rakendus"</string>
+    <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Pilvemeedia rakendus"</string>
+    <string name="picker_settings_title" msgid="5647700706470673258">"Pilvemeedia rakendus"</string>
     <string name="picker_settings_description" msgid="2916686824777214585">"Juurdepääs teie pilves olevale meediale, kui rakendus või veebisait palub teil fotosid või videoid valida"</string>
-    <string name="picker_settings_selection_message" msgid="245453573086488596">"Pilvemeediarakendus:"</string>
+    <string name="picker_settings_selection_message" msgid="245453573086488596">"Pilvemeedia rakendus:"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Pole"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Pilvepõhist meediarakendust ei saanud muuta."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Meediavalija"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Meediavalija"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Meediumi sünkroonimine …"</string>
     <string name="add" msgid="2894574044585549298">"Lisa"</string>
     <string name="deselect" msgid="4297825044827769490">"Tühista valik"</string>
     <string name="deselected" msgid="8488133193326208475">"Valik on tühistatud"</string>
@@ -58,12 +60,14 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Albumeid pole"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Kuva valitud"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotod"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albumid"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Eelvaade"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Lülituge tööprofiilile"</string>
     <string name="picker_personal_profile" msgid="639484258397758406">"Lülituge isiklikule profiilile"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"Blokeeris teie administraator"</string>
-    <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"Juurdepääs tööandmetele isikliku rakenduse kaudu pole lubatud"</string>
+    <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"Juurdepääs tööandmetele isikliku rakenduse kaudu pole lubatud."</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"Juurdepääs isiklikele andmetele töörakenduse kaudu pole lubatud"</string>
     <string name="picker_profile_work_paused_title" msgid="382212880704235925">"Töörakendused on peatatud"</string>
     <string name="picker_profile_work_paused_msg" msgid="6321552322125246726">"Tööfotode avamiseks lülitage töörakendused sisse ja proovige uuesti"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> üksus}other{<xliff:g id="COUNT_1">^1</xliff:g> üksust}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Lisa (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Luba (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Ära luba ühtki"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kaamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Allalaadimised"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Lemmikud"</string>
@@ -92,11 +97,12 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Probleem video esitamisel"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Kontrollige internetiühendust ja proovige uuesti"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Proovi uuesti"</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="preloading_dialog_title" msgid="4974348221848532887">"Valitud meedia ettevalmistamine"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>-st on valmis"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Tühista"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Varundatud fotod on nüüd kaasatud"</string>
-    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Saate valida fotosid rakendusest <xliff:g id="APP_NAME">%1$s</xliff:g> konto <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> kaudu"</string>
+    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Saate valida konto <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> fotosid rakendusest <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Rakenduse <xliff:g id="APP_NAME">%1$s</xliff:g> kontot värskendati"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"Kasutaja <xliff:g id="USER_ACCOUNT">%1$s</xliff:g> fotod on nüüd siia kaasatud"</string>
     <string name="picker_banner_cloud_choose_app_title" msgid="3165966147547974251">"Valige pilvemeediarakendus"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Ohutuskaitse"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Omakoodi transkodeerimise hoiatused"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Omakoodi transkodeerimise edenemine"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Proovige hiljem uuesti. Teie fotod on saadaval pärast probleemi lahendamist."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Mõnda fotot ei saa laadida"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Selge"</string>
 </resources>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index 10f871d..be1db85 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Multimedia-edukia"</string>
     <string name="storage_description" msgid="4081716890357580107">"Biltegi lokala"</string>
-    <string name="app_label" msgid="9035307001052716210">"Multimediaren memoria-unitatea"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Multimedia-edukia"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Multimedia-edukiaren hautatzailea"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artista"</string>
     <string name="unknown" msgid="2059049215682829375">"Ezezaguna"</string>
     <string name="root_images" msgid="5861633549189045666">"Irudiak"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Atzitu honen bidez gordetako hodeiko multimedia-edukia:"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Bat ere ez"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Ezin izan da aldatu hodeiko multimedia-aplikazioa."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Multimedia-edukiaren hautatzailea"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Multimedia-edukiaren hautatzailea"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Multimedia-edukia sinkronizatzen…"</string>
     <string name="add" msgid="2894574044585549298">"Gehitu"</string>
     <string name="deselect" msgid="4297825044827769490">"Desautatu"</string>
     <string name="deselected" msgid="8488133193326208475">"Desautatuta"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Ez dago albumik"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Ikusi hautatutakoak"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Argazkiak"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albumak"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Aurrebista"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Aldatu laneko profilera"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> elementu}other{<xliff:g id="COUNT_1">^1</xliff:g> elementu}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Gehitu (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Eman baimena (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Ez eman baimenik"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Deskargak"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Gogokoak"</string>
@@ -92,10 +97,11 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Arazoren bat izan da bideoa erreproduzitzean"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Egiaztatu Internetera konektatuta zaudela eta saiatu berriro"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Saiatu berriro"</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="preloading_dialog_title" msgid="4974348221848532887">"Hautatutako multimedia-edukia prestatzen"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> prest"</string>
-    <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Orain, babeskopiak dituzten argazkiak sartuta daude"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Utzi"</string>
+    <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Orain, barnean hartzen dira babeskopiak dituzten argazkiak"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"<xliff:g id="APP_NAME">%1$s</xliff:g> aplikazioko <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> kontuko argazkiak hauta ditzakezu"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Eguneratu da <xliff:g id="APP_NAME">%1$s</xliff:g> aplikazioko kontua"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"Orain, <xliff:g id="USER_ACCOUNT">%1$s</xliff:g> kontuko argazkiak hemen sartuta daude"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Aukeratu aplikazio bat"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Aukeratu kontu bat"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Aldatu kontua"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Argazki guztiak eskuratzen"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Bideoari aldaketak egiteko baimena eman nahi diozu <xliff:g id="APP_NAME_0">^1</xliff:g> aplikazioari?}other{<xliff:g id="COUNT">^2</xliff:g> bideori aldaketak egiteko baimena eman nahi diozu <xliff:g id="APP_NAME_1">^1</xliff:g> aplikazioari?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Segurtasun-babesa"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Transkodetze-alerta natiboak"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Transkodetze natiboaren garapena"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Saiatu berriro geroago. Arazoa konpondu ondoren egongo dira erabilgarri argazkiak."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Ezin dira kargatu argazki batzuk"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Ados"</string>
 </resources>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index c6bd780..5e515e0 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -18,12 +18,11 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"رسانه"</string>
     <string name="storage_description" msgid="4081716890357580107">"فضای ذخیره‌سازی محلی"</string>
-    <string name="app_label" msgid="9035307001052716210">"فضای ذخیره‌سازی رسانه"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"رسانه"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"انتخابگر رسانه"</string>
     <string name="artist_label" msgid="8105600993099120273">"هنرمند"</string>
     <string name="unknown" msgid="2059049215682829375">"نامشخص"</string>
     <string name="root_images" msgid="5861633549189045666">"تصویر"</string>
-    <string name="root_videos" msgid="8792703517064649453">"ویدئو"</string>
+    <string name="root_videos" msgid="8792703517064649453">"ویدیوها"</string>
     <string name="root_audio" msgid="3505830755201326018">"صوت"</string>
     <string name="root_documents" msgid="3829103301363849237">"اسناد"</string>
     <string name="permission_required" msgid="1460820436132943754">"برای اصلاح یا حذف این مورد مجوز لازم است."</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"دسترسی به رسانه ابری از"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"هیچ‌کدام"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"اکنون نمی‌توانید برنامه رسانه ابری را تغییر دهید."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"انتخابگر رسانه"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"انتخابگر رسانه"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"درحال همگام‌سازی رسانه…"</string>
     <string name="add" msgid="2894574044585549298">"افزودن"</string>
     <string name="deselect" msgid="4297825044827769490">"لغو انتخاب"</string>
     <string name="deselected" msgid="8488133193326208475">"لغو انتخاب‌شده"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"آلبومی موجود نیست"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"مشاهده موارد انتخاب‌شده"</string>
     <string name="picker_photos" msgid="7415035516411087392">"عکس‌ها"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"آلبوم‌ها"</string>
     <string name="picker_preview" msgid="6257414886055861039">"پیش‌نما"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"رفتن به نمایه کاری"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> مورد}one{<xliff:g id="COUNT_1">^1</xliff:g> مورد}other{<xliff:g id="COUNT_1">^1</xliff:g> مورد}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"افزودن (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"اجازه دادن (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"مجاز کردن عدم بارگذاری"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"دوربین"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"بارگیری‌ها"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"موارد دلخواه"</string>
@@ -92,10 +97,11 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"پخش ویدیو با مشکل روبه‌رو شد"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"اتصال اینترنت را بررسی کنید و دوباره امتحان کنید"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"امتحان مجدد"</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="preloading_dialog_title" msgid="4974348221848532887">"درحال آماده‌سازی رسانه انتخاب‌شده"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> مورد از <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> مورد آماده است"</string>
-    <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"عکس‌های پشتیبان‌گیری‌شده اکنون اضافه می‌شود"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"لغو کردن"</string>
+    <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"عکس‌های پشتیبان‌گیری‌شده اکنون اضافه شده‌اند"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"می‌توانید عکس‌های حساب <xliff:g id="APP_NAME">%1$s</xliff:g>‏ (<xliff:g id="USER_ACCOUNT">%2$s</xliff:g>) را انتخاب کنید"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"حساب <xliff:g id="APP_NAME">%1$s</xliff:g> به‌روز شد"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"عکس‌های <xliff:g id="USER_ACCOUNT">%1$s</xliff:g> اکنون در اینجا اضافه می‌شود"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"انتخاب برنامه"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"انتخاب حساب"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"تغییر حساب"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"درحال دریافت همه عکس‌های شما"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"محافظت امنیتی"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"هشدارهای تراتبدیل محلی"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"پیشرفت تراتبدیل محلی"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"بعداً دوباره امتحان کنید. عکس‌هایتان پس‌از رفع مشکل دردسترس خواهد بود."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"برخی‌از عکس‌ها را نمی‌توان بار کرد"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"متوجه‌ام"</string>
 </resources>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index e7f77c6..bfd4d84 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Media"</string>
     <string name="storage_description" msgid="4081716890357580107">"Paikallinen tallennustila"</string>
-    <string name="app_label" msgid="9035307001052716210">"Median tallennustila"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Media"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Median valitsin"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artisti"</string>
     <string name="unknown" msgid="2059049215682829375">"Tuntematon"</string>
     <string name="root_images" msgid="5861633549189045666">"Kuvat"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Pääsy pilvimediaan:"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"–"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Pilvimediasovellusta ei voitu vaihtaa."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Median valitsin"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Median valitsin"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Synkronoidaan mediaa…"</string>
     <string name="add" msgid="2894574044585549298">"Lisää"</string>
     <string name="deselect" msgid="4297825044827769490">"Poista valinta"</string>
     <string name="deselected" msgid="8488133193326208475">"Valinta poistettu"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Ei albumeita"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Katso valitut"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Kuvat"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albumit"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Esikatselu"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Siirry työprofiiliin"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> kohde}other{<xliff:g id="COUNT_1">^1</xliff:g> kohdetta}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Lisää (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Salli (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Älä salli mitään"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Lataukset"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Suosikit"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Videon toisto ei onnistu"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Tarkista internetyhteytesi ja yritä uudelleen"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Yritä uudelleen"</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="preloading_dialog_title" msgid="4974348221848532887">"Valitsemaasi mediaa valmistellaan"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> valmiina"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Peruuta"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Varmuuskopioidut kuvat löytyvät nyt täältä"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Voit valita sovelluksen <xliff:g id="APP_NAME">%1$s</xliff:g> kuvat tililtä <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Tili päivitetty: <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Valitse sovellus"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Valitse tili"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Vaihda tiliä"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Ladataan kaikkia kuvia"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Saako <xliff:g id="APP_NAME_0">^1</xliff:g> muokata tätä videota?}other{Saako <xliff:g id="APP_NAME_1">^1</xliff:g> muokata <xliff:g id="COUNT">^2</xliff:g> videota?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Turvallisuuden varmistaminen"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Natiivin transkoodin ilmoitukset"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Natiivin transkoodin edistyminen"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Yritä myöhemmin uudelleen. Kuvat ovat saatavilla, kun ongelma on korjattu."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Joitain kuvia ei voi ladata"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
 </resources>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index 72a8011..a351916 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Multimédia"</string>
     <string name="storage_description" msgid="4081716890357580107">"Stockage local"</string>
-    <string name="app_label" msgid="9035307001052716210">"Stockage multimédia"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Contenu multimédia"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Sélecteur d\'éléments multimédias"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artiste"</string>
     <string name="unknown" msgid="2059049215682829375">"Inconnu"</string>
     <string name="root_images" msgid="5861633549189045666">"Images"</string>
@@ -40,12 +39,15 @@
     <string name="deny" msgid="6040983710442068936">"Refuser"</string>
     <string name="picker_browse" msgid="5554477454636075934">"Parcourir…"</string>
     <string name="picker_settings" msgid="6443463167344790260">"Appli multimédia infonuagique"</string>
-    <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Application multimédia infonuagique"</string>
-    <string name="picker_settings_title" msgid="5647700706470673258">"Application multimédia infonuagique"</string>
+    <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Appli multimédia infonuagique"</string>
+    <string name="picker_settings_title" msgid="5647700706470673258">"Appli multimédia infonuagique"</string>
     <string name="picker_settings_description" msgid="2916686824777214585">"Accédez à votre contenu multimédia infonuagique lorsqu\'une application ou un site Web vous demande de sélectionner des photos ou des vidéos"</string>
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Accéder au contenu multimédia infonuagique à partir de"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Aucune"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Changement appli multimédia infonuagique imposs."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Sélecteur d\'éléments multimédias"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Sélecteur d\'éléments multimédias"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Synchronisation du contenu multimédia en cours…"</string>
     <string name="add" msgid="2894574044585549298">"Ajouter"</string>
     <string name="deselect" msgid="4297825044827769490">"Désélectionner"</string>
     <string name="deselected" msgid="8488133193326208475">"Désélectionné"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Aucun album"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Afficher la sélection"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Photos"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albums"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Aperçu"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Passez au profil professionnel"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> élément}one{<xliff:g id="COUNT_1">^1</xliff:g> élément}many{<xliff:g id="COUNT_1">^1</xliff:g> éléments}other{<xliff:g id="COUNT_1">^1</xliff:g> éléments}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Ajouter (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Autoriser (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Ne rien autoriser"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Appareil photo"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Téléchargements"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favoris"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Difficulté à lire la vidéo"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Vérifiez votre connexion Internet et réessayez"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Réessayer"</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="preloading_dialog_title" msgid="4974348221848532887">"Préparation du contenu multimédia sélectionné en cours…"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> sur <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> prêt(s)"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Annuler"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Les photos sauvegardées sont maintenant incluses"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Vous pouvez sélectionner des photos du compte <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> de <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Compte de <xliff:g id="APP_NAME">%1$s</xliff:g> mis à jour"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Choisir une application"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Choisir un compte"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Changer de compte"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Chargement de vos photos en cours…"</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?}many{Autoriser <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> fichiers 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…}many{Modification de <xliff:g id="COUNT">^1</xliff:g> fichiers audio en cours…}other{Modification de <xliff:g id="COUNT">^1</xliff:g> fichiers audio en cours…}}"</string>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Autoriser <xliff:g id="APP_NAME_0">^1</xliff:g> à modifier cette vidéo?}one{Autoriser <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> vidéo?}many{Autoriser <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> vidéos?}other{Autoriser <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> vidéos?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Protection de sécurité"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Alertes de transcodage natif"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Progression du transcodage natif"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Réessayez plus tard. Vos photos seront accessibles dès que le problème sera résolu."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Impossible de charger certaines photos"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
 </resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 7c8edae..131c795 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Multimédia"</string>
     <string name="storage_description" msgid="4081716890357580107">"Stockage local"</string>
-    <string name="app_label" msgid="9035307001052716210">"Stockage multimédia"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Multimédia"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Sélecteur de fichiers multimédias"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artiste"</string>
     <string name="unknown" msgid="2059049215682829375">"Inconnu"</string>
     <string name="root_images" msgid="5861633549189045666">"Images"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Accéder aux contenus multimédias cloud à partir de"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Aucune"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Impossible de changer l\'appli multimédia cloud."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Sélecteur de fichiers multimédias"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Sélecteur de fichiers multimédias"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Synchronisation des fichiers multimédias…"</string>
     <string name="add" msgid="2894574044585549298">"Ajouter"</string>
     <string name="deselect" msgid="4297825044827769490">"Désélectionner"</string>
     <string name="deselected" msgid="8488133193326208475">"Désélectionné"</string>
@@ -58,10 +60,12 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Aucun album"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Afficher la sélection"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Photos"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albums"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Aperçu"</string>
-    <string name="picker_work_profile" msgid="2083221066869141576">"Passer au professionnel"</string>
-    <string name="picker_personal_profile" msgid="639484258397758406">"Passer au personnel"</string>
+    <string name="picker_work_profile" msgid="2083221066869141576">"Passer au profil professionnel"</string>
+    <string name="picker_personal_profile" msgid="639484258397758406">"Passer au profil personnel"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"Bloqué par votre administrateur"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"Vous n\'êtes pas autorisé à accéder à des données professionnelles depuis une appli personnelle"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"Vous n\'êtes pas autorisé à accéder à des données à caractère personnel depuis une appli professionnelle"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> élément}one{<xliff:g id="COUNT_1">^1</xliff:g> élément}many{<xliff:g id="COUNT_1">^1</xliff:g> éléments}other{<xliff:g id="COUNT_1">^1</xliff:g> éléments}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Ajouter (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Autoriser (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Ne rien autoriser"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Appareil photo"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Téléchargements"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favoris"</string>
@@ -92,11 +97,12 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Problème de lecture vidéo"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Vérifiez votre connexion Internet, puis réessayez"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Réessayer"</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="preloading_dialog_title" msgid="4974348221848532887">"Préparation des fichiers multimédias que vous avez sélectionnés"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Prêt(s) : <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> sur <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Annuler"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Les photos sauvegardées sont désormais incluses"</string>
-    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Vous pouvez sélectionner des photos de <xliff:g id="APP_NAME">%1$s</xliff:g>, compte <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Vous pouvez sélectionner des photos issues du compte <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> dans l\'appli <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Compte <xliff:g id="APP_NAME">%1$s</xliff:g> mis à jour"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"Les photos de <xliff:g id="USER_ACCOUNT">%1$s</xliff:g> sont désormais incluses ici"</string>
     <string name="picker_banner_cloud_choose_app_title" msgid="3165966147547974251">"Sélectionner une appli multimédia cloud"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Sélectionner une appli"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Sélectionner un compte"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Changer de compte"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Chargement de toutes vos photos…"</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 ?}many{Autoriser <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> fichiers 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…}many{Modification de <xliff:g id="COUNT">^1</xliff:g> fichiers audio…}other{Modification de <xliff:g id="COUNT">^1</xliff:g> fichiers audio…}}"</string>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Autoriser <xliff:g id="APP_NAME_0">^1</xliff:g> à modifier cette vidéo ?}one{Autoriser <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> vidéo ?}many{Autoriser <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> vidéos ?}other{Autoriser <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> vidéos ?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Protection de sécurité"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Alertes de transcodage natif"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Progression du transcodage natif"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Réessayez plus tard. Vos photos seront disponibles une fois le problème résolu."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Impossible de charger certaines photos"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
 </resources>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index bd03ef3..b0fda91 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Multimedia"</string>
     <string name="storage_description" msgid="4081716890357580107">"Almacenamento local"</string>
-    <string name="app_label" msgid="9035307001052716210">"Almacenamento multimedia"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Contido multimedia"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Seleccionador multimedia"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artista"</string>
     <string name="unknown" msgid="2059049215682829375">"Descoñecida"</string>
     <string name="root_images" msgid="5861633549189045666">"Imaxes"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Acceder ao contido multimedia gardado na nube desde"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Ningunha"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"App multimedia con servizo na nube non cambiada."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Seleccionador de contido multimedia"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Seleccionador de contido multimedia"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Sincronizando contido multimedia…"</string>
     <string name="add" msgid="2894574044585549298">"Engadir"</string>
     <string name="deselect" msgid="4297825044827769490">"Anular selección"</string>
     <string name="deselected" msgid="8488133193326208475">"Anulouse a selección"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Non hai álbums"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Ver selección"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotos"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Álbums"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Vista previa"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Cambiar ao perfil de traballo"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> elemento}other{<xliff:g id="COUNT_1">^1</xliff:g> elementos}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Engadir (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Permitir (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Non permitir ningunha"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Cámara"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Descargas"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favoritos"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Problemas ao reproducir o vídeo"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Comproba a conexión a Internet e téntao de novo"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Tentar de novo"</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="preloading_dialog_title" msgid="4974348221848532887">"Preparando recursos seleccionados"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Elementos listos: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Cancelar"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Agora inclúense as fotos con copia de seguranza"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Podes seleccionar fotos da seguinte conta de <xliff:g id="APP_NAME">%1$s</xliff:g>: <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Actualizouse a conta de <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Escoller aplicación"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Seleccionar conta"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Cambiar de conta"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Cargando todas as fotos"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Queres permitir que <xliff:g id="APP_NAME_0">^1</xliff:g> modifique este vídeo?}other{Queres permitir que <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> vídeos?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Protección de seguranza"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Alertas de transcodificación nativa"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Progreso da transcodificación nativa"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Téntao de novo máis tarde. As túas fotos estarán dispoñibles en canto se resolva o problema."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Non se poden cargar algunhas fotos"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Entendido"</string>
 </resources>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 57f7808..b46ad96 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"મીડિયા"</string>
     <string name="storage_description" msgid="4081716890357580107">"સ્થાનિક સ્ટોરેજ"</string>
-    <string name="app_label" msgid="9035307001052716210">"મીડિયા સ્ટોરેજ"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"મીડિયા"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"મીડિયા પિકર"</string>
     <string name="artist_label" msgid="8105600993099120273">"કલાકાર"</string>
     <string name="unknown" msgid="2059049215682829375">"અજાણ"</string>
     <string name="root_images" msgid="5861633549189045666">"છબીઓ"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"આમાંથી ક્લાઉડ મીડિયાને ઍક્સેસ કરો"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"એકપણ નહીં"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"આ સમયે ક્લાઉડ મીડિયા ઍપને બદલી શકાઈ નથી."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"મીડિયા પિકર"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"મીડિયા પિકર"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"મીડિયા સિંક કરી રહ્યાં છે…"</string>
     <string name="add" msgid="2894574044585549298">"ઉમેરો"</string>
     <string name="deselect" msgid="4297825044827769490">"નાપસંદ કરો"</string>
     <string name="deselected" msgid="8488133193326208475">"નાપસંદ કર્યું"</string>
@@ -58,11 +60,13 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"કોઈ આલ્બમ નથી"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"પસંદ કરેલા ફોટા જુઓ"</string>
     <string name="picker_photos" msgid="7415035516411087392">"ફોટા"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"આલ્બમ"</string>
     <string name="picker_preview" msgid="6257414886055861039">"પ્રીવ્યૂ કરો"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"ઑફિસની પ્રોફાઇલ પર સ્વિચ કરો"</string>
     <string name="picker_personal_profile" msgid="639484258397758406">"વ્યક્તિગત પ્રોફાઇલ પર સ્વિચ કરો"</string>
-    <string name="picker_profile_admin_title" msgid="4172022376418293777">"તમારા વ્યવસ્થાપકે સુવિધા બ્લૉક કરી છે"</string>
+    <string name="picker_profile_admin_title" msgid="4172022376418293777">"તમારા ઍડમિને સુવિધા બ્લૉક કરી છે"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"વ્યક્તિગત ઍપ પરથી ઑફિસનો ડેટા ઍક્સેસ કરવાની પરવાનગી નથી"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"ઑફિસ માટેની ઍપ પરથી વ્યક્તિગત ડેટા ઍક્સેસ કરવાની પરવાનગી નથી"</string>
     <string name="picker_profile_work_paused_title" msgid="382212880704235925">"ઑફિસ માટેની ઍપ થોભાવવામાં આવી છે"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> આઇટમ}one{<xliff:g id="COUNT_1">^1</xliff:g> આઇટમ}other{<xliff:g id="COUNT_1">^1</xliff:g> આઇટમ}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"ઉમેરો (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"મંજૂરી આપો (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"કોઈને મંજૂરી આપશો નહીં"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"કૅમેરા"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"ડાઉનલોડ"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"મનપસંદ"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"વીડિયો ચલાવવામાં સમસ્યા આવી"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"તમારું ઇન્ટરનેટ કનેક્શન ચેક કરો અને ફરી પ્રયાસ કરો"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"ફરી પ્રયાસ કરો"</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="preloading_dialog_title" msgid="4974348221848532887">"તમે પસંદ કરેલું મીડિયા તૈયાર કરી રહ્યાં છીએ"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>માંથી <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> તૈયાર"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"રદ કરો"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"બૅકઅપ લીધેલા ફોટા હવે શામેલ કરવામાં આવ્યા છે"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"તમે <xliff:g id="APP_NAME">%1$s</xliff:g> એકાઉન્ટના <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> પરથી ફોટા પસંદ કરી શકો છો"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> એકાઉન્ટ અપડેટ કરવામાં આવ્યું"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"ઍપ પસંદ કરો"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"એકાઉન્ટ પસંદ કરો"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"એકાઉન્ટ બદલો"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"તમારા બધા ફોટા મેળવવામાં આવી રહ્યાં છે"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"સલામતી સંરક્ષણ"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Native Transcode Alerts"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Native Transcode Progress"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"થોડા સમય પછી ફરી પ્રયાસ કરો. એકવાર સમસ્યાનું નિરાકરણ થઈ જાય, તે પછી તમારા ફોટા ઉપલબ્ધ થશે."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"અમુક ફોટા લોડ કરી શકાતા નથી"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"સમજાઈ ગયું"</string>
 </resources>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index e99c1b3..a493629 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"मीडिया"</string>
     <string name="storage_description" msgid="4081716890357580107">"स्थानीय जगह"</string>
-    <string name="app_label" msgid="9035307001052716210">"मीडिया मेमोरी"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"मीडिया"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"मीडिया पिकर"</string>
     <string name="artist_label" msgid="8105600993099120273">"कलाकार"</string>
     <string name="unknown" msgid="2059049215682829375">"अज्ञात"</string>
     <string name="root_images" msgid="5861633549189045666">"इमेज"</string>
@@ -46,9 +45,12 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"क्लाउड पर मौजूद मीडिया को यहां से ऐक्सेस करें:"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"कोई नहीं"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"इस समय क्लाउड मीडिया ऐप्लिकेशन नहीं बदला जा सका."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"मीडिया पिकर"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"मीडिया पिकर"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"मीडिया को सिंक किया जा रहा है…"</string>
     <string name="add" msgid="2894574044585549298">"जोड़ें"</string>
-    <string name="deselect" msgid="4297825044827769490">"चुना हुआ हटाएं"</string>
-    <string name="deselected" msgid="8488133193326208475">"चुना हुआ हटाया गया"</string>
+    <string name="deselect" msgid="4297825044827769490">"चुने हुए का निशान हटाएं"</string>
+    <string name="deselected" msgid="8488133193326208475">"चुने हुए का निशान हटाया गया"</string>
     <string name="select" msgid="2704765470563027689">"चुनें"</string>
     <string name="selected" msgid="9151797369975828124">"चुना गया"</string>
     <string name="select_up_to" msgid="6994294169508439957">"{count,plural, =1{ज़्यादा से ज़्यादा <xliff:g id="COUNT_0">^1</xliff:g> आइटम चुनें}one{ज़्यादा से ज़्यादा <xliff:g id="COUNT_1">^1</xliff:g> आइटम चुनें}other{ज़्यादा से ज़्यादा <xliff:g id="COUNT_1">^1</xliff:g> आइटम चुनें}}"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"कोई एल्बम नहीं है"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"चुनी गई फ़ोटो या वीडियो देखें"</string>
     <string name="picker_photos" msgid="7415035516411087392">"फ़ोटो"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"एल्बम"</string>
     <string name="picker_preview" msgid="6257414886055861039">"झलक"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"वर्क प्रोफ़ाइल पर जाएं"</string>
@@ -67,13 +71,14 @@
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"निजी डेटा को ऑफ़िस के काम से जुड़े ऐप्लिकेशन से ऐक्सेस करने की अनुमति नहीं है"</string>
     <string name="picker_profile_work_paused_title" msgid="382212880704235925">"वर्क ऐप्लिकेशन रोक दिए गए हैं"</string>
     <string name="picker_profile_work_paused_msg" msgid="6321552322125246726">"वर्क फ़ोटो देखने के लिए, ऑफ़िस के काम से जुड़े ऐप्लिकेशन चालू करें और दोबारा कोशिश करें"</string>
-    <string name="picker_privacy_message" msgid="9132700451027116817">"इस ऐप्लिकेशन के पास आपकी उन फ़ोटो का ही ऐक्सेस होता है जिन्हें आपने चुना हो"</string>
+    <string name="picker_privacy_message" msgid="9132700451027116817">"इस ऐप्लिकेशन के पास आपकी उन फ़ोटो का ही ऐक्सेस होता है जिन्हें आपने चुना है"</string>
     <string name="picker_header_permissions" msgid="675872774407768495">"वे फ़ोटो और वीडियो चुनें जिनका ऐक्सेस इस ऐप्लिकेशन को देना है"</string>
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> आइटम}one{<xliff:g id="COUNT_1">^1</xliff:g> आइटम}other{<xliff:g id="COUNT_1">^1</xliff:g> आइटम}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>) जोड़ें"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"अनुमति दें (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"कोई फ़ोटो न चुनें"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"कैमरा"</string>
-    <string name="picker_category_downloads" msgid="793866660287361900">"डाउनलोड की गई चीज़ें"</string>
+    <string name="picker_category_downloads" msgid="793866660287361900">"डाउनलोड किए गए आइटम"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"पसंदीदा"</string>
     <string name="picker_category_screenshots" msgid="7216102327587644284">"स्क्रीनशॉट"</string>
     <!-- no translation found for picker_category_videos (1478458836380241356) -->
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"वीडियो चलाने में समस्या हो रही है"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"अपने इंटरनेट कनेक्शन की जांच करें और फिर से कोशिश करें"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"फिर से कोशिश करें"</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="preloading_dialog_title" msgid="4974348221848532887">"आपकी चुनी गई मीडिया तैयार की जा रही है"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> में से <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> तैयार हैं"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"रद्द करें"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"बैक अप ली गई फ़ोटो अब जोड़ दी गई हैं"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"आपके पास, <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> वाले <xliff:g id="APP_NAME">%1$s</xliff:g> खाते से फ़ोटो चुनने का विकल्प है"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> खाता अपडेट किया गया"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"सुरक्षा के लिए बचाव"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"नेटिव ट्रांसकोड सूचना"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"नेटिव ट्रांसकोड स्थिति"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"कुछ देर बाद कोशिश करें. समस्या हल होते ही आपकी फ़ोटो उपलब्ध हो जाएंगी."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"कुछ फ़ोटो लोड नहीं की जा सकीं"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"ठीक है"</string>
 </resources>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index d7014e8..c989254 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Mediji"</string>
     <string name="storage_description" msgid="4081716890357580107">"Lokalna pohrana"</string>
-    <string name="app_label" msgid="9035307001052716210">"Pohranjivanje na mediju"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Mediji"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Alat za izbor medija"</string>
     <string name="artist_label" msgid="8105600993099120273">"Izvođač"</string>
     <string name="unknown" msgid="2059049215682829375">"Nepoznato"</string>
     <string name="root_images" msgid="5861633549189045666">"Slike"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Pristupi medijima u oblaku putem"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Ništa"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Aplikaciju za medijske sadržaje u oblaku trenutačno nije moguće promijeniti."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Alat za izbor medija"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Alat za izbor medija"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Sinkroniziranje medija…"</string>
     <string name="add" msgid="2894574044585549298">"Dodaj"</string>
     <string name="deselect" msgid="4297825044827769490">"Poništi odabir"</string>
     <string name="deselected" msgid="8488133193326208475">"Odabir poništen"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Nema albuma"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Prikaži odabrano"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotografije"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albumi"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Pregled"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Prijeđite na poslovni"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> stavka}one{<xliff:g id="COUNT_1">^1</xliff:g> stavka}few{<xliff:g id="COUNT_1">^1</xliff:g> stavke}other{<xliff:g id="COUNT_1">^1</xliff:g> stavki}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Dodaj (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Dopusti (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Nemoj dopustiti prijenos"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Preuzimanja"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Omiljeno"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Poteškoće s reprodukcijom videozapisa"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Provjerite internetsku vezu i pokušajte ponovo"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Pokušaj ponovo"</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="preloading_dialog_title" msgid="4974348221848532887">"Priprema odabranih medija"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Spremno: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> od <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Odustani"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Sad su uključene sigurnosno kopirane fotografije"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Možete odabrati aplikacije s računa <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> za aplikaciju <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Ažuriran je račun za aplikaciju <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Odaberite aplikaciju"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Odaberite račun"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Promijenite račun"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Dohvaćanje svih vaših fotografija"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Želite li dopustiti aplikaciji <xliff:g id="APP_NAME_0">^1</xliff:g> da izmijeni taj videozapis?}one{Želite li dopustiti aplikaciji <xliff:g id="APP_NAME_1">^1</xliff:g> da izmijeni <xliff:g id="COUNT">^2</xliff:g> videozapis?}few{Želite li dopustiti aplikaciji <xliff:g id="APP_NAME_1">^1</xliff:g> da izmijeni <xliff:g id="COUNT">^2</xliff:g> videozapisa?}other{Želite li dopustiti aplikaciji <xliff:g id="APP_NAME_1">^1</xliff:g> da izmijeni <xliff:g id="COUNT">^2</xliff:g> videozapisa?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Osiguranje"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Upozorenja nativnog konvertiranja"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Napredak nativnog konvertiranja"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Pokušajte ponovo poslije. Vaše fotografije bit će dostupne kad se problem riješi."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Neke fotografije ne mogu se učitati"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Shvaćam"</string>
 </resources>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index bbf77be..c5d7870 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Média"</string>
     <string name="storage_description" msgid="4081716890357580107">"Helyi tárhely"</string>
-    <string name="app_label" msgid="9035307001052716210">"Médiatároló"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Média"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Médiaválasztó"</string>
     <string name="artist_label" msgid="8105600993099120273">"Előadó"</string>
     <string name="unknown" msgid="2059049215682829375">"Ismeretlen"</string>
     <string name="root_images" msgid="5861633549189045666">"Képek"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Hozzáférés a következő szolgáltatásban tárolt felhőbeli médiatartalmakhoz:"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Nincs"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Most nem módosítható a felhőbeli médiaalkalmazás."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Médiaválasztó"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Médiaválasztó"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Médiatartalom szinkronizálása…"</string>
     <string name="add" msgid="2894574044585549298">"Hozzáadás"</string>
     <string name="deselect" msgid="4297825044827769490">"Jelölés törlése"</string>
     <string name="deselected" msgid="8488133193326208475">"Kijelölés megszüntetve"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Nincsenek albumok"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Kijelöltek megnézése"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotók"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albumok"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Előnézet"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Átváltás munkaprofilra"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> elem}other{<xliff:g id="COUNT_1">^1</xliff:g> elem}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Hozzáadás (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Engedélyezés (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Egy se legyen engedélyezve"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Letöltések"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Kedvencek"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Probléma merült fel a videó lejátszásakor"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Ellenőrizze internetkapcsolatát, és próbálkozzon újra"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Újra"</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="preloading_dialog_title" msgid="4974348221848532887">"A kiválasztott média előkészítése…"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>/<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> kész"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Mégse"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Mostantól rendelkezésre állnak a fotók, amelyekről biztonsági másolat készült"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Kiválaszthat fotókat a(z) <xliff:g id="APP_NAME">%1$s</xliff:g> alkalmazásból (<xliff:g id="USER_ACCOUNT">%2$s</xliff:g>-fiók)"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g>-fiók frissítve"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Válasszon alkalmazást"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Fiók kiválasztása"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Másik fiók választása"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Folyamatban van az összes fotó lekérése"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Engedélyezi a(z) <xliff:g id="APP_NAME_0">^1</xliff:g> számára ennek a videónak 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> videó módosítását?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Biztonsági védelem"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Natív átkódolási értesítések"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Natív átkódolási folyamat"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Próbálkozzon újra később. Fotói hozzáférhetők lesznek a probléma elhárítását követően."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Egyes fotók nem tölthetők be"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Értem"</string>
 </resources>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 40268d9..6fee32e 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Մեդիա"</string>
     <string name="storage_description" msgid="4081716890357580107">"Սարքի հիշողություն"</string>
-    <string name="app_label" msgid="9035307001052716210">"Մեդիա կրիչ"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Մեդիա"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Մուլտիմեդիա ընտրիչ"</string>
     <string name="artist_label" msgid="8105600993099120273">"Կատարող"</string>
     <string name="unknown" msgid="2059049215682829375">"Անհայտ"</string>
     <string name="root_images" msgid="5861633549189045666">"Պատկերներ"</string>
@@ -46,9 +45,12 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Օգտվեք ամպային մեդիա բովանդակությունից հետևյալ մատակարարից՝"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Չկա"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Չհաջողվեց փոխել ամպային մուլտիմեդիա հավելվածը։"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Մուլտիմեդիա ընտրիչ"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Մուլտիմեդիա ընտրիչ"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Մեդիաֆայլերը համաժամացվում են…"</string>
     <string name="add" msgid="2894574044585549298">"Ավելացնել"</string>
-    <string name="deselect" msgid="4297825044827769490">"Ապընտրել"</string>
-    <string name="deselected" msgid="8488133193326208475">"Ապընտրված"</string>
+    <string name="deselect" msgid="4297825044827769490">"Չեղարկել ընտրությունը"</string>
+    <string name="deselected" msgid="8488133193326208475">"Ընտրությունը չեղարկված է"</string>
     <string name="select" msgid="2704765470563027689">"Ընտրել"</string>
     <string name="selected" msgid="9151797369975828124">"Ընտրված"</string>
     <string name="select_up_to" msgid="6994294169508439957">"{count,plural, =1{Ընտրեք մինչև <xliff:g id="COUNT_0">^1</xliff:g> տարր}one{Ընտրեք մինչև <xliff:g id="COUNT_1">^1</xliff:g> տարր}other{Ընտրեք մինչև <xliff:g id="COUNT_1">^1</xliff:g> տարր}}"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Ալբոմներ չկան"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Դիտել ընտրվածը"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Լուսանկարներ"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Ալբոմներ"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Նախադիտում"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Բացել աշխատանքային պրոֆիլը"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> տարր}one{<xliff:g id="COUNT_1">^1</xliff:g> տարր}other{<xliff:g id="COUNT_1">^1</xliff:g> տարր}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Ավելացնել (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Թույլատրել (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Արգելել բոլորը"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Տեսախցիկ"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Ներբեռնումներ"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Ընտրանի"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Տեսանյութի նվագարկման սխալ"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Ստուգեք ձեր ինտերնետ կապը և նորից փորձեք"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Նորից փորձել"</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="preloading_dialog_title" msgid="4974348221848532887">"Ձեր ընտրած մեդիաֆայլերի նախապատրաստում"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> պատրաստ է"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Չեղարկել"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Պահուստավորված լուսանկարներն այժմ ավելացված են"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Դուք կարող եք լուսանկարներ ընտրել «<xliff:g id="APP_NAME">%1$s</xliff:g>» հավելվածի <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> հաշվից"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> հավելվածի հաշիվը թարմացվեց"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Ընտրել հավելված"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Ընտրել հաշիվը"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Փոխել հաշիվը"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Ձեր բոլոր լուսանկարները բեռնվում են"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Անվտանգության պաշտպանություն"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Տրանսկոդավորման մասին հիմնական ծանուցումներ"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Տրանսկոդավորման հիմնական գործընթաց"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Փորձեք ավելի ուշ։ Ձեր լուսանկարները հասանելի կլինեն, երբ խնդիրը լուծվի։"</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Չհաջողվեց բեռնել որոշ լուսանկարներ"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Եղավ"</string>
 </resources>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 778892b..e655fcb 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Media"</string>
     <string name="storage_description" msgid="4081716890357580107">"Penyimpanan lokal"</string>
-    <string name="app_label" msgid="9035307001052716210">"Penyimpanan Media"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Media"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Pemilih media"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artis"</string>
     <string name="unknown" msgid="2059049215682829375">"Tidak diketahui"</string>
     <string name="root_images" msgid="5861633549189045666">"Gambar"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Akses media cloud dari"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Tidak ada"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Saat ini tidak bisa mengubah aplikasi media cloud."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Pemilih media"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Pemilih media"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Menyinkronkan media…"</string>
     <string name="add" msgid="2894574044585549298">"Tambahkan"</string>
     <string name="deselect" msgid="4297825044827769490">"Batalkan pilihan"</string>
     <string name="deselected" msgid="8488133193326208475">"Batal dipilih"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Tidak ada album"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Lihat yang dipilih"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Foto"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Album"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Pratinjau"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Beralih ke profil kerja"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> item}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Tambahkan (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Izinkan (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Tidak ada"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Hasil download"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favorit"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Terjadi masalah saat memutar video"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Periksa koneksi internet Anda, lalu coba lagi"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Coba lagi"</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="preloading_dialog_title" msgid="4974348221848532887">"Menyiapkan media yang dipilih"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> dari <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> siap"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Batal"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Foto yang dicadangkan kini disertakan"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Anda dapat memilih foto dari akun <xliff:g id="APP_NAME">%1$s</xliff:g> <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Akun <xliff:g id="APP_NAME">%1$s</xliff:g> diperbarui"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Pilih aplikasi"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Pilih akun"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Ubah akun"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Mengambil semua foto Anda"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Izinkan <xliff:g id="APP_NAME_0">^1</xliff:g> mengubah video ini?}other{Izinkan <xliff:g id="APP_NAME_1">^1</xliff:g> mengubah <xliff:g id="COUNT">^2</xliff:g> video?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Perlindungan keselamatan"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Peringatan Transcoding Native"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Progres Transcoding Native"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Coba lagi nanti. Foto Anda akan tersedia setelah masalah diselesaikan."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Tidak dapat memuat beberapa Foto"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Oke"</string>
 </resources>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index 9a90ead..02ea74b 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Margmiðlun"</string>
     <string name="storage_description" msgid="4081716890357580107">"Staðbundin vistun"</string>
-    <string name="app_label" msgid="9035307001052716210">"Efnisgeymsla"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Efni"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Efnisval"</string>
     <string name="artist_label" msgid="8105600993099120273">"Flytjandi"</string>
     <string name="unknown" msgid="2059049215682829375">"Óþekkt"</string>
     <string name="root_images" msgid="5861633549189045666">"Myndir"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Opna skýjaefni frá"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Ekkert"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Ekki tókst að breyta efnisforriti í skýi."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Efnisval"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Efnisval"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Samstillir efni…"</string>
     <string name="add" msgid="2894574044585549298">"Bæta við"</string>
     <string name="deselect" msgid="4297825044827769490">"Afvelja"</string>
     <string name="deselected" msgid="8488133193326208475">"Afvalið"</string>
@@ -58,10 +60,12 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Engin albúm"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Skoða valið"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Myndir"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albúm"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Forskoða"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Skipta yfir í vinnusnið"</string>
-    <string name="picker_personal_profile" msgid="639484258397758406">"Skipta yfir í eigið snið"</string>
+    <string name="picker_personal_profile" msgid="639484258397758406">"Skipta yfir í einkasnið"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"Útilokað af kerfisstjóra"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"Óheimilt er að opna vinnugögn í forriti til einkanota"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"Óheimilt er að opna einkagögn í vinnuforriti"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> atriði}one{<xliff:g id="COUNT_1">^1</xliff:g> atriði}other{<xliff:g id="COUNT_1">^1</xliff:g> atriði}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Bæta við (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Leyfa (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Ekki leyfa neitt"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Myndavél"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Niðurhal"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Uppáhald"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Vandamál við spilun myndskeiðs"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Athugaðu nettenginguna og reyndu aftur"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Reyna aftur"</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="preloading_dialog_title" msgid="4974348221848532887">"Undirbýr valið efni"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> af <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> til reiðu"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Hætta við"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Afritaðar myndir eru nú hafðar með"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Þú getur valið myndir af reikningnum <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> í: <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g>: reikningur uppfærður"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Velja forrit"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Veldu reikning"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Skipta um reikning"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Sækir allar myndirnar þínar"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Leyfa <xliff:g id="APP_NAME_0">^1</xliff:g> að breyta þessu myndskeiði?}one{Leyfa <xliff:g id="APP_NAME_1">^1</xliff:g> að breyta <xliff:g id="COUNT">^2</xliff:g> myndskeiði?}other{Leyfa <xliff:g id="APP_NAME_1">^1</xliff:g> að breyta <xliff:g id="COUNT">^2</xliff:g> myndskeiðum?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Öryggisbúnaður"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Viðvaranir fyrir sérforritaðar umkóðanir"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Sérforritað umkóðunarferli"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Reyndu aftur síðar. Myndirnar þínar verða tiltækar um leið og vandamálið er leyst."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Ekki tekst að hlaða sumum myndum"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Ég skil"</string>
 </resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index d0278be..e86dc97 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Supporti multimediali"</string>
     <string name="storage_description" msgid="4081716890357580107">"Archiviazione locale"</string>
-    <string name="app_label" msgid="9035307001052716210">"Media Storage"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Contenuti multimediali"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Selettore media"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artista"</string>
     <string name="unknown" msgid="2059049215682829375">"Sconosciuto"</string>
     <string name="root_images" msgid="5861633549189045666">"Immagini"</string>
@@ -42,10 +41,13 @@
     <string name="picker_settings" msgid="6443463167344790260">"App multimediale cloud"</string>
     <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"App multimediale cloud"</string>
     <string name="picker_settings_title" msgid="5647700706470673258">"App multimediale con funzionalità cloud"</string>
-    <string name="picker_settings_description" msgid="2916686824777214585">"Accedi ai tuoi contenuti multimediali cloud quando un\'app o un sito web ti chiede di selezionare foto o video"</string>
+    <string name="picker_settings_description" msgid="2916686824777214585">"Accedi ai tuoi contenuti multimediali sul cloud quando un\'app o un sito web ti chiede di selezionare foto o video"</string>
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Accedi ai contenuti multimediali sul cloud da"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Nessuna"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Ora è impossibile cambiare app multimediale cloud."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Selettore media"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Selettore media"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Sincronizzazione dei media in corso…"</string>
     <string name="add" msgid="2894574044585549298">"Aggiungi"</string>
     <string name="deselect" msgid="4297825044827769490">"Deseleziona"</string>
     <string name="deselected" msgid="8488133193326208475">"Deselezionato"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Nessun album"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Visualizza selezione"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Foto"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Album"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Anteprima"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Passa al profilo di lavoro"</string>
@@ -67,11 +71,12 @@
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"Non è consentito accedere ai dati personali da un\'app di lavoro"</string>
     <string name="picker_profile_work_paused_title" msgid="382212880704235925">"Le app di lavoro sono in pausa"</string>
     <string name="picker_profile_work_paused_msg" msgid="6321552322125246726">"Per aprire le foto relative al lavoro, attiva le app di lavoro e riprova"</string>
-    <string name="picker_privacy_message" msgid="9132700451027116817">"Questa app può accedere soltanto alle foto selezionate da te"</string>
+    <string name="picker_privacy_message" msgid="9132700451027116817">"Questa app può accedere soltanto alle foto che selezioni"</string>
     <string name="picker_header_permissions" msgid="675872774407768495">"Seleziona le foto e i video a cui può accedere questa app"</string>
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> elemento}many{<xliff:g id="COUNT_1">^1</xliff:g> elementi}other{<xliff:g id="COUNT_1">^1</xliff:g> elementi}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Aggiungi (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Consenti (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Non consentire foto"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Fotocamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Download"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Preferiti"</string>
@@ -92,18 +97,19 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Errore durante la riproduzione del video"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Controlla la connessione a Internet e riprova"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Riprova"</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="preloading_dialog_title" msgid="4974348221848532887">"Preparazione dei contenuti multimediali selezionati in corso…"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> su <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> pronti"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Annulla"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Ora sono incluse le foto di cui hai eseguito il backup"</string>
-    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Puoi selezionare foto dall\'account <xliff:g id="APP_NAME">%1$s</xliff:g> di <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Puoi selezionare foto dall\'account <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> nell\'app <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Account <xliff:g id="APP_NAME">%1$s</xliff:g> aggiornato"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"Ora le foto di <xliff:g id="USER_ACCOUNT">%1$s</xliff:g> sono incluse qui"</string>
     <string name="picker_banner_cloud_choose_app_title" msgid="3165966147547974251">"Scegli un\'app multimediale con funzionalità cloud"</string>
     <string name="picker_banner_cloud_choose_app_desc" msgid="2359212653555524926">"Per includere qui le foto di cui hai eseguito il backup, scegli un\'app multimediale con funzionalità cloud nelle impostazioni"</string>
     <string name="picker_banner_cloud_choose_account_title" msgid="5010901185639577685">"Scegli un account <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_choose_account_desc" msgid="8868134443673142712">"Per includere qui le foto di <xliff:g id="APP_NAME">%1$s</xliff:g>, scegli un account nell\'app"</string>
-    <string name="picker_banner_cloud_dismiss_button" msgid="2935903078288463882">"Ignora"</string>
+    <string name="picker_banner_cloud_dismiss_button" msgid="2935903078288463882">"Chiudi"</string>
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Scegli app"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Scegli account"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Cambia account"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Protezione di sicurezza"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Avvisi di transcodifica nativa"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Avanzamento di transcodifica nativa"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Riprova più tardi. Le tue foto saranno disponibili dopo aver risolto il problema."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Impossibile caricare alcune foto"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
 </resources>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 62771db..31c3197 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"מדיה"</string>
     <string name="storage_description" msgid="4081716890357580107">"אחסון מקומי"</string>
-    <string name="app_label" msgid="9035307001052716210">"אחסון מדיה"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"מדיה"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"הכלי לבחירת מדיה"</string>
     <string name="artist_label" msgid="8105600993099120273">"אומן"</string>
     <string name="unknown" msgid="2059049215682829375">"לא ידוע"</string>
     <string name="root_images" msgid="5861633549189045666">"תמונות"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"גישה למדיה בענן מתוך"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"ללא"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"לא ניתן להחליף את אפליקציית המדיה בענן כרגע."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"הכלי לבחירת מדיה"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"הכלי לבחירת מדיה"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"המדיה בתהליך סנכרון…"</string>
     <string name="add" msgid="2894574044585549298">"הוספה"</string>
     <string name="deselect" msgid="4297825044827769490">"ביטול הבחירה"</string>
     <string name="deselected" msgid="8488133193326208475">"הבחירה בוטלה"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"אין אלבומים"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"הצגת הפריטים שנבחרו"</string>
     <string name="picker_photos" msgid="7415035516411087392">"תמונות"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"אלבומים"</string>
     <string name="picker_preview" msgid="6257414886055861039">"תצוגה מקדימה"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"לפרופיל העבודה"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{פריט אחד (<xliff:g id="COUNT_0">^1</xliff:g>)}one{<xliff:g id="COUNT_1">^1</xliff:g> פריטים}two{<xliff:g id="COUNT_1">^1</xliff:g> פריטים}other{<xliff:g id="COUNT_1">^1</xliff:g> פריטים}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"הוספה (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"אישור (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"לא להוסיף"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"מצלמה"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"הורדות"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"מועדפים"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"בעיות בהפעלת הסרטון"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"מומלץ לבדוק את החיבור לאינטרנט ולנסות שוב"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"ניסיון נוסף"</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="preloading_dialog_title" msgid="4974348221848532887">"המדיה שבחרת בתהליך הכנה"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> מתוך <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> מוכנים"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"ביטול"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"התמונות שעברו גיבוי נכללות עכשיו"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"ניתן לבחור תמונות מחשבון <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> באפליקציה <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"החשבון באפליקציה <xliff:g id="APP_NAME">%1$s</xliff:g> עודכן"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"בחירת אפליקציה"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"בחירת חשבון"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"החלפת חשבון"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"איסוף התמונות מתבצע"</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> קובצי אודיו?}two{לאפשר לאפליקציה <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> קובצי אודיו…}two{מתבצע שינוי ב-<xliff:g id="COUNT">^1</xliff:g> קובצי אודיו…}other{מתבצע שינוי ב-<xliff:g id="COUNT">^1</xliff:g> קובצי אודיו…}}"</string>
     <string name="permission_write_video" msgid="103902551603700525">"{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> סרטונים?}two{לאפשר לאפליקציה <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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"הגנה על בטיחות"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"התראות של המרת קידוד מקורית"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"התקדמות של המרת קידוד מקורית"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"כדאי לנסות שוב אחר כך. התמונות שלך יהיו זמינות כשהבעיה תיפתר."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"יש תמונות שאי אפשר לטעון כרגע"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"הבנתי"</string>
 </resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 11f7849..30a9d9f 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"メディア"</string>
     <string name="storage_description" msgid="4081716890357580107">"ローカル ストレージ"</string>
-    <string name="app_label" msgid="9035307001052716210">"メディア ストレージ"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"メディア"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"メディアの選択"</string>
     <string name="artist_label" msgid="8105600993099120273">"アーティスト"</string>
     <string name="unknown" msgid="2059049215682829375">"不明"</string>
     <string name="root_images" msgid="5861633549189045666">"画像"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"クラウド メディアへのアクセス元"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"なし"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"クラウド メディアアプリを変更できませんでした。"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"メディアの選択"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"メディアの選択"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"メディアを同期しています…"</string>
     <string name="add" msgid="2894574044585549298">"追加"</string>
     <string name="deselect" msgid="4297825044827769490">"選択を解除"</string>
     <string name="deselected" msgid="8488133193326208475">"選択解除済み"</string>
@@ -58,20 +60,23 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"アルバムはありません"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"選択した写真を見る"</string>
     <string name="picker_photos" msgid="7415035516411087392">"写真"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"アルバム"</string>
     <string name="picker_preview" msgid="6257414886055861039">"プレビュー"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"仕事用に切り替える"</string>
     <string name="picker_personal_profile" msgid="639484258397758406">"個人用に切り替える"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"管理者によりブロックされています"</string>
-    <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"個人用アプリから仕事用データにアクセスすることは認められていません"</string>
+    <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"個人用アプリから仕事用データにアクセスすることは許可されていません"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"仕事用アプリから個人データにアクセスすることは認められていません"</string>
-    <string name="picker_profile_work_paused_title" msgid="382212880704235925">"仕事用アプリは一時停止されています"</string>
+    <string name="picker_profile_work_paused_title" msgid="382212880704235925">"仕事用アプリ一時停止中"</string>
     <string name="picker_profile_work_paused_msg" msgid="6321552322125246726">"仕事用の写真を開くには、仕事用アプリを有効にしてからもう一度試してください。"</string>
     <string name="picker_privacy_message" msgid="9132700451027116817">"このアプリは、選択した写真にのみアクセスできます。"</string>
     <string name="picker_header_permissions" msgid="675872774407768495">"このアプリにアクセスを許可する写真と動画を選択してください"</string>
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> 件のアイテム}other{<xliff:g id="COUNT_1">^1</xliff:g> 件のアイテム}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"追加(<xliff:g id="COUNT">^1</xliff:g> 件)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"許可(<xliff:g id="COUNT">^1</xliff:g> 件)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"すべて許可しない"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"カメラ"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"ダウンロード"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"お気に入り"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"動画を再生できません"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"インターネット接続を確認してもう一度お試しください"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"再試行"</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="preloading_dialog_title" msgid="4974348221848532887">"選択したメディアの準備をする"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> 件準備完了"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"キャンセル"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"バックアップした写真が追加されました"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"<xliff:g id="APP_NAME">%1$s</xliff:g> のアカウント <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> の写真を選択できます"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> アカウントが更新されました"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"安全保護"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"ネイティブ コード変換アラート"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"ネイティブ コード変換進行状況"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"しばらくしてからもう一度お試しください。問題が解決されると、写真をご利用いただけるようになります。"</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"読み込めなかった写真があります"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
 </resources>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index 58b1aee..27398e9 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"მედია"</string>
     <string name="storage_description" msgid="4081716890357580107">"ადგილობრივი მეხსიერება"</string>
-    <string name="app_label" msgid="9035307001052716210">"მედიის საცავი"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"მედია"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"მედიის ამომრჩევი"</string>
     <string name="artist_label" msgid="8105600993099120273">"შემსრულებელი"</string>
     <string name="unknown" msgid="2059049215682829375">"უცნობი"</string>
     <string name="root_images" msgid="5861633549189045666">"სურათები"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"ღრუბლოვან მედიაზე წვდომა აქედან:"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"არცერთი"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"ღრუბლური მედია აპის შეცვლა ამჯერად ვერ ხერხდება."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"მედიის ამომრჩევი"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"მედიის ამომრჩევი"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"მიმდინარეობს მედიის სინქრონიზაცია…"</string>
     <string name="add" msgid="2894574044585549298">"დამატება"</string>
     <string name="deselect" msgid="4297825044827769490">"არჩევის გაუქმება"</string>
     <string name="deselected" msgid="8488133193326208475">"არჩევა გაუქმებულია"</string>
@@ -58,9 +60,11 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"ალბომები არ არის"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"არჩეულის ნახვა"</string>
     <string name="picker_photos" msgid="7415035516411087392">"ფოტოები"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"ალბომები"</string>
     <string name="picker_preview" msgid="6257414886055861039">"გადახედვა"</string>
-    <string name="picker_work_profile" msgid="2083221066869141576">"სამსახურის პროფილზე გადართვა"</string>
+    <string name="picker_work_profile" msgid="2083221066869141576">"სამსახურზე გადართვა"</string>
     <string name="picker_personal_profile" msgid="639484258397758406">"პირად პროფილზე გადართვა"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"დაბლოკილია თქვენი ადმინისტრატორის მიერ"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"პირადი აპიდან სამუშაო სამსახურებრივ მონაცემებზე წვდომა დაუშვებელია"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ერთეული}other{<xliff:g id="COUNT_1">^1</xliff:g> ერთეული}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"დამატება (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"დაშვება (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"არცერთის დაშვება"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"კამერა"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"ჩამოტვირთვები"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"რჩეულები"</string>
@@ -92,10 +97,11 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"ვიდეოს იკვრება პრობლემურად"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"შეამოწმეთ ინტერნეტთან კავშირი და სცადეთ ხელახლა"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"ცდა"</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="preloading_dialog_title" msgid="4974348221848532887">"მიმდინარეობს თქვენ მიერ არჩეული მედიის მომზადება"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> სულ <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>-დან მზადაა"</string>
-    <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"ფოტოების სარეზერვო ასლები ახლა განთავსებულია"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"გაუქმება"</string>
+    <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"უკვე ფოტოების სარეზერვო ასლების ჩათვლით"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"შეგიძლიათ აირჩიოთ ფოტოები <xliff:g id="APP_NAME">%1$s</xliff:g>-ის ანგარიშიდან <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> ანგარიში განახლებულია"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"<xliff:g id="USER_ACCOUNT">%1$s</xliff:g>-ის ფოტოები ახლა აქ არის განთავსებული"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"აპის არჩევა"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"აირჩიეთ ანგარიში"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"ანგარიშის შეცვლა"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"თქვენი ყველა ფოტოს მიღება"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"უსაფრთხოების დაცვა"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"ტრანსკოდირების ადგილობრივი გაფრთხილება"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"ტრანსკოდირების ადგილობრივი პროგრესი"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"ცადეთ მოგვიანებით. თქვენი ფოტოები ხარვეზის აღმოფხვრის შემდეგ იქნება ხელმისაწვდომი."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"ზოგიერთი ფოტოს ჩატვირთვა ვერ ხერხდება"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"გასაგებია"</string>
 </resources>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index c6397d3..28cd54b 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Мультимeдиа"</string>
     <string name="storage_description" msgid="4081716890357580107">"Жергілікті жад"</string>
-    <string name="app_label" msgid="9035307001052716210">"Мультимедиа қоймасы"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Meдиа"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Meдиафайл таңдағыш"</string>
     <string name="artist_label" msgid="8105600993099120273">"Орындаушы"</string>
     <string name="unknown" msgid="2059049215682829375">"Белгісіз"</string>
     <string name="root_images" msgid="5861633549189045666">"Кескіндер"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Бұлттық мультимедиа қолданбасына келесі жерден кіріңіз:"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Жоқ"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Бұлттық мультимедиа қолданбасы өзгертілмейді."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Meдиафайл таңдағыш"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Meдиафайл таңдағыш"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Медиафайл синхрондалып жатыр…"</string>
     <string name="add" msgid="2894574044585549298">"Қосу"</string>
     <string name="deselect" msgid="4297825044827769490">"Таңдамау"</string>
     <string name="deselected" msgid="8488133193326208475">"Таңдау алынған"</string>
@@ -58,8 +60,10 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Альбомдар жоқ."</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Таңдалғанды көру"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Фотосуреттер"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Aльбомдар"</string>
-    <string name="picker_preview" msgid="6257414886055861039">"Алдын ала көру"</string>
+    <string name="picker_preview" msgid="6257414886055861039">"Алғы көрініс"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Жұмыс профиліне ауысу"</string>
     <string name="picker_personal_profile" msgid="639484258397758406">"Жеке профильге ауысу"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"Әкімшіңіз бөгеген"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> элемент}other{<xliff:g id="COUNT_1">^1</xliff:g> элемент}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Қосу (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Рұқсат (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Ешқайсысына рұқсат етпеу"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Камера"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Жүктеп алынғандар"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Таңдаулылар"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Бейнені ойнату кезінде қиындық туындады"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Интернет байланысын тексеріп, әрекетті қайталаңыз."</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Қайталау"</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="preloading_dialog_title" msgid="4974348221848532887">"Таңдалған мультимедиа әзірленіп жатыр"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> дайын"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Бас тарту"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Сақтық көшірмесі жасалған фотосуреттер қосылды"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Фотосуреттерді <xliff:g id="APP_NAME">%1$s</xliff:g> қолданбасының <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> аккаунтынан таңдай аласыз."</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> аккаунты жаңартылды"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Қолданба таңдау"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Аккаунт таңдау"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Аккаунтты өзгерту"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Барлық сурет алынып жатыр."</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Қауіпсіздікті қорғау"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Түбірлік транскодтау туралы хабарландырулар"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Түбірлік транскодтау прогресі"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Кейінірек қайталап көріңіз. Мәселе шешілген соң, фотосуреттеріңіз қолжетімді болады."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Кейбір фотосуреттерді жүктеу мүмкін емес"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Түсінікті"</string>
 </resources>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index eca2100..6b10fc5 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"មេឌៀ"</string>
     <string name="storage_description" msgid="4081716890357580107">"ទំហំផ្ទុកមូលដ្ឋាន"</string>
-    <string name="app_label" msgid="9035307001052716210">"ទំហំផ្ទុកមេឌៀ"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"មេឌៀ"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"មុខងារ​ជ្រើសរើស​មេឌៀ"</string>
     <string name="artist_label" msgid="8105600993099120273">"សិល្បករ"</string>
     <string name="unknown" msgid="2059049215682829375">"មិនស្គាល់"</string>
     <string name="root_images" msgid="5861633549189045666">"រូបភាព"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"ចូលប្រើប្រាស់មេឌៀលើពពកពី"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"គ្មាន"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"មិនអាចប្ដូរ​កម្មវិធី​មេឌៀ​ពពក​នៅពេលនេះបានទេ។"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"មុខងារ​ជ្រើសរើស​មេឌៀ"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"មុខងារ​ជ្រើសរើស​មេឌៀ"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"កំពុងធ្វើ​សមកាលកម្ម​មេឌៀ…"</string>
     <string name="add" msgid="2894574044585549298">"បញ្ចូល"</string>
     <string name="deselect" msgid="4297825044827769490">"ដក​ការជ្រើសរើស"</string>
     <string name="deselected" msgid="8488133193326208475">"បានដក​ការជ្រើសរើស"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"គ្មាន​អាល់ប៊ុម​ទេ"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"មើលអ្វីដែលបានជ្រើសរើស"</string>
     <string name="picker_photos" msgid="7415035516411087392">"រូបថត"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"អាល់ប៊ុម"</string>
     <string name="picker_preview" msgid="6257414886055861039">"មើល​សាកល្បង"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"ប្ដូរទៅ​កម្រងព័ត៌មាន​ការងារ"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{ធាតុ <xliff:g id="COUNT_0">^1</xliff:g>}other{ធាតុ <xliff:g id="COUNT_1">^1</xliff:g>}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"បញ្ចូល (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"អនុញ្ញាត (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"អនុញ្ញាត \"គ្មាន\""</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"កាមេរ៉ា"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"ការទាញយក"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"សំណព្វ"</string>
@@ -92,11 +97,12 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"មានបញ្ហាក្នុងការចាក់វីដេអូ"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"ពិនិត្យ​ការតភ្ជាប់​អ៊ីនធឺណិត​របស់អ្នក រួចព្យាយាម​ម្ដង​ទៀត"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"ព្យាយាមម្ដងទៀត"</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="preloading_dialog_title" msgid="4974348221848532887">"កំពុងរៀបចំមេឌៀដែលអ្នកបានជ្រើសរើស"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> នៃ <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> រួចរាល់ហើយ"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"បោះបង់"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"ឥឡូវនេះមានរួមបញ្ចូលរូបថតដែលបានបម្រុងទុក"</string>
-    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"អ្នកអាចជ្រើសរើសរូបថតពីគណនី <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> របស់ <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"អ្នកអាចជ្រើសរើសរូបថតពីគណនី <xliff:g id="APP_NAME">%1$s</xliff:g> <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"បានធ្វើបច្ចុប្បន្នភាពគណនី <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"ឥឡូវនេះ រូបថតពី <xliff:g id="USER_ACCOUNT">%1$s</xliff:g> ត្រូវបានរួមបញ្ចូលនៅទីនេះ"</string>
     <string name="picker_banner_cloud_choose_app_title" msgid="3165966147547974251">"ជ្រើសរើសកម្មវិធី​មេឌៀលើពពក"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"ការការពារសុវត្ថិភាព"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"ការជូន​ដំណឹង​អំពី​ការបំប្លែង​កូដ​ដើម"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"ដំណើរ​វិវឌ្ឍ​នៃ​ការបំប្លែង​កូដ​ដើម"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"សូមព្យាយាមម្តងទៀតនៅពេលក្រោយ។ រូបថត​របស់អ្នក​នឹងអាចប្រើបាន បន្ទាប់ពី​ដោះស្រាយ​បញ្ហា។"</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"មិនអាចផ្ទុករូបថតមួយចំនួនបានទេ"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"យល់ហើយ"</string>
 </resources>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index c162efc..7e3af0b 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"ಮಾಧ್ಯಮ"</string>
     <string name="storage_description" msgid="4081716890357580107">"ಸ್ಥಳೀಯ ಸಂಗ್ರಹಣೆ"</string>
-    <string name="app_label" msgid="9035307001052716210">"ಮಾಧ್ಯಮ ಸಂಗ್ರಹಣೆ"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"ಮಾಧ್ಯಮ"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"ಮಾಧ್ಯಮ ಪಿಕರ್"</string>
     <string name="artist_label" msgid="8105600993099120273">"ಕಲಾವಿದರು"</string>
     <string name="unknown" msgid="2059049215682829375">"ಅಪರಿಚಿತ"</string>
     <string name="root_images" msgid="5861633549189045666">"ಚಿತ್ರಗಳು"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"ಇದರಿಂದ ಕ್ಲೌಡ್ ಮಾಧ್ಯಮವನ್ನು ಪ್ರವೇಶಿಸಿ"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"ಯಾವುದೂ ಅಲ್ಲ"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"ಇದೀಗ ಕ್ಲೌಡ್ ಮೀಡಿಯಾ ಆ್ಯಪ್ ಬದಲಾಯಿಸಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"ಮಾಧ್ಯಮ ಪಿಕರ್"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"ಮಾಧ್ಯಮ ಪಿಕರ್"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"ಮಾಧ್ಯಮವನ್ನು ಸಿಂಕ್ ಮಾಡಲಾಗುತ್ತಿದೆ…"</string>
     <string name="add" msgid="2894574044585549298">"ಸೇರಿಸಿ"</string>
     <string name="deselect" msgid="4297825044827769490">"ಆಯ್ಕೆ ರದ್ದುಮಾಡಿ"</string>
     <string name="deselected" msgid="8488133193326208475">"ಆಯ್ಕೆ ರದ್ದುಮಾಡಲಾಗಿದೆ"</string>
@@ -58,20 +60,23 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"ಯಾವುದೇ ಆಲ್ಬಮ್‌ಗಳಿಲ್ಲ"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"ಆಯ್ಕೆಮಾಡಿರುವುದನ್ನು ವೀಕ್ಷಿಸಿ"</string>
     <string name="picker_photos" msgid="7415035516411087392">"ಫೋಟೋಗಳು"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"ಆಲ್ಬಮ್‌ಗಳು"</string>
     <string name="picker_preview" msgid="6257414886055861039">"ಪೂರ್ವವೀಕ್ಷಣೆ"</string>
-    <string name="picker_work_profile" msgid="2083221066869141576">"ಕೆಲಸಕ್ಕೆ ಬದಲಿಸಿ"</string>
-    <string name="picker_personal_profile" msgid="639484258397758406">"ವೈಯಕ್ತಿಕಕ್ಕೆ ಬದಲಿಸಿ"</string>
+    <string name="picker_work_profile" msgid="2083221066869141576">"ಉದ್ಯೋಗ ಪ್ರೊಫೈಲ್‌ಗೆ ಬದಲಿಸಿ"</string>
+    <string name="picker_personal_profile" msgid="639484258397758406">"ವೈಯಕ್ತಿಕ ಪ್ರೊಫೈಲ್‌ಗೆ ಬದಲಿಸಿ"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"ನಿಮ್ಮ ನಿರ್ವಾಹಕರು ನಿರ್ಬಂಧಿಸಿದ್ದಾರೆ"</string>
-    <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"ವೈಯಕ್ತಿಕ ಆ್ಯಪ್ ಮೂಲಕ ಅಧಿಕೃತ ಡೇಟಾವನ್ನು ಪ್ರವೇಶಿಸಲು ಅನುಮತಿಸಲಾಗುವುದಿಲ್ಲ"</string>
+    <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"ವೈಯಕ್ತಿಕ ಆ್ಯಪ್‌ನಿಂದ ಉದ್ಯೋಗದ ಡೇಟಾವನ್ನು ಆ್ಯಕ್ಸೆಸ್ ಮಾಡಲು ಅನುಮತಿಸಲಾಗುವುದಿಲ್ಲ"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"ಕೆಲಸಕ್ಕೆ ಸಂಬಂಧಿಸಿದ ಆ್ಯಪ್ ಮೂಲಕ ಅಧಿಕೃತ ಡೇಟಾವನ್ನು ಪ್ರವೇಶಿಸಲು ಅನುಮತಿಸಲಾಗುವುದಿಲ್ಲ"</string>
     <string name="picker_profile_work_paused_title" msgid="382212880704235925">"ಕೆಲಸಕ್ಕೆ ಸಂಬಂಧಿಸಿದ ಆ್ಯಪ್‌ಗಳನ್ನು ವಿರಾಮಗೊಳಿಸಲಾಗಿದೆ"</string>
     <string name="picker_profile_work_paused_msg" msgid="6321552322125246726">"ಕೆಲಸದ ಫೋಟೋಗಳನ್ನು ತೆರೆಯಲು, ನಿಮ್ಮ ಕೆಲಸಕ್ಕೆ ಸಂಬಂಧಿಸಿದ ಆ್ಯಪ್‌ಗಳನ್ನು ಆನ್ ಮಾಡಿ ನಂತರ ಪುನಃ ಪ್ರಯತ್ನಿಸಿ"</string>
-    <string name="picker_privacy_message" msgid="9132700451027116817">"ಈ ಆ್ಯಪ್ ನೀವು ಆಯ್ಕೆಮಾಡಿದ ಫೋಟೋಗಳನ್ನು ಮಾತ್ರ ಪ್ರವೇಶಿಸಬಹುದು"</string>
+    <string name="picker_privacy_message" msgid="9132700451027116817">"ಈ ಆ್ಯಪ್ ನೀವು ಆಯ್ಕೆಮಾಡಿದ ಫೋಟೋಗಳನ್ನು ಮಾತ್ರ ಆ್ಯಕ್ಸೆಸ್ ಮಾಡಬಹುದು"</string>
     <string name="picker_header_permissions" msgid="675872774407768495">"ಈ ಆ್ಯಪ್ ಅನ್ನು ಆ್ಯಕ್ಸೆಸ್ ಮಾಡಲು ನೀವು ಅನುಮತಿಸುವ ಫೋಟೋಗಳು ಮತ್ತು ವೀಡಿಯೊಗಳನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
-    <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g>ಐಟಂ}one{<xliff:g id="COUNT_1">^1</xliff:g> ಐಟಂಗಳು}other{<xliff:g id="COUNT_1">^1</xliff:g> ಐಟಂಗಳು}}"</string>
+    <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ಐಟಂ}one{<xliff:g id="COUNT_1">^1</xliff:g> ಐಟಂಗಳು}other{<xliff:g id="COUNT_1">^1</xliff:g> ಐಟಂಗಳು}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"ಸೇರಿಸಿ (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"(<xliff:g id="COUNT">^1</xliff:g>) ಅನುಮತಿಸಿ"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"ಯಾವುದನ್ನೂ ಅನುಮತಿಸಬೇಡಿ"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"ಕ್ಯಾಮರಾ"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"ಡೌನ್‌ಲೋಡ್‌ಗಳು"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"ಮೆಚ್ಚಿನವುಗಳು"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"ವೀಡಿಯೊ ಪ್ಲೇ ಮಾಡಲು ಸಮಸ್ಯೆಯಾಗಿದೆ"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"ನಿಮ್ಮ ಇಂಟರ್ನೆಟ್ ಕನೆಕ್ಷನ್ ಅನ್ನು ಪರಿಶೀಲಿಸಿ ಹಾಗೂ ಪುನಃ ಪ್ರಯತ್ನಿಸಿ"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"ಮರುಪ್ರಯತ್ನಿಸಿ"</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="preloading_dialog_title" msgid="4974348221848532887">"ನಿಮ್ಮ ಆಯ್ಕೆಮಾಡಿದ ಮೀಡಿಯಾವನ್ನು ಸಿದ್ಧಪಡಿಸಲಾಗುತ್ತಿದೆ"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ರಲ್ಲಿ <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> ಸಿದ್ಧವಾಗಿವೆ"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"ರದ್ದುಮಾಡಿ"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"ಬ್ಯಾಕಪ್ ಮಾಡಲಾದ ಫೋಟೋಗಳನ್ನು ಈಗ ಸೇರಿಸಲಾಗಿದೆ"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"<xliff:g id="APP_NAME">%1$s</xliff:g> ಖಾತೆಯ <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> ನಲ್ಲಿರುವ ಫೋಟೋಗಳನ್ನು ನೀವು ಆಯ್ಕೆಮಾಡಬಹುದು"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> ಖಾತೆಯನ್ನು ಅಪ್‌ಡೇಟ್ ಮಾಡಲಾಗಿದೆ"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"ಆ್ಯಪ್ ಅನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"ಖಾತೆಯನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"ಖಾತೆಯನ್ನು ಬದಲಾಯಿಸಿ"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"ನಿಮ್ಮ ಎಲ್ಲಾ ಫೋಟೋಗಳನ್ನು ಪಡೆಯಲಾಗುತ್ತಿದೆ"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"ಭದ್ರತಾ ರಕ್ಷಣೆ"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"ನೇಟಿವ್ ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಅಲರ್ಟ್‌ಗಳು"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"ನೇಟಿವ್ ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಪ್ರಗತಿ"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"ನಂತರ ಪುನಃ ಪ್ರಯತ್ನಿಸಿ. ಸಮಸ್ಯೆ ಬಗೆಹರಿದ ನಂತರ ನಿಮ್ಮ ಫೋಟೋಗಳು ಲಭ್ಯವಿರುತ್ತವೆ."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"ಕೆಲವು ಫೋಟೋಗಳನ್ನು ಲೋಡ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗುತ್ತಿಲ್ಲ"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"ಅರ್ಥವಾಯಿತು"</string>
 </resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 9e23b09..3b7432a 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"미디어"</string>
     <string name="storage_description" msgid="4081716890357580107">"로컬 저장소"</string>
-    <string name="app_label" msgid="9035307001052716210">"미디어 저장소"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"미디어"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"미디어 선택 도구"</string>
     <string name="artist_label" msgid="8105600993099120273">"아티스트"</string>
     <string name="unknown" msgid="2059049215682829375">"알 수 없음"</string>
     <string name="root_images" msgid="5861633549189045666">"이미지"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"클라우드 미디어 액세스 위치"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"없음"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"현재 클라우드 미디어 앱을 변경할 수 없습니다."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"미디어 선택 도구"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"미디어 선택 도구"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"미디어 동기화 중…"</string>
     <string name="add" msgid="2894574044585549298">"추가"</string>
     <string name="deselect" msgid="4297825044827769490">"선택 해제"</string>
     <string name="deselected" msgid="8488133193326208475">"선택 해제됨"</string>
@@ -58,10 +60,12 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"앨범 없음"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"선택 항목 보기"</string>
     <string name="picker_photos" msgid="7415035516411087392">"사진"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"앨범"</string>
     <string name="picker_preview" msgid="6257414886055861039">"미리보기"</string>
-    <string name="picker_work_profile" msgid="2083221066869141576">"직장으로 전환"</string>
-    <string name="picker_personal_profile" msgid="639484258397758406">"개인으로 전환"</string>
+    <string name="picker_work_profile" msgid="2083221066869141576">"직장 프로필로 전환"</string>
+    <string name="picker_personal_profile" msgid="639484258397758406">"개인 프로필로 전환"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"관리자가 차단함"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"개인 앱에서는 업무 데이터에 액세스할 수 없습니다."</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"직장 앱에서는 개인 데이터에 액세스할 수 없습니다."</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{항목 <xliff:g id="COUNT_0">^1</xliff:g>개}other{항목 <xliff:g id="COUNT_1">^1</xliff:g>개}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"추가(<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"허용(<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"허용 안 함"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"카메라"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"다운로드"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"즐겨찾기"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"동영상 재생 중 문제 발생"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"인터넷 연결 상태를 확인하고 다시 시도해 주세요."</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"다시 시도"</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="preloading_dialog_title" msgid="4974348221848532887">"선택한 미디어를 준비하는 중"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>개 준비됨"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"취소"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"이제 백업된 사진이 포함됨"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"<xliff:g id="APP_NAME">%1$s</xliff:g> 계정(<xliff:g id="USER_ACCOUNT">%2$s</xliff:g>)에서 사진을 선택할 수 있습니다."</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> 계정 업데이트됨"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"앱 선택"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"계정 선택"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"계정 변경"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"모든 사진 가져오는 중"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"안전 보안"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"네이티브 트랜스코드 알림"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"네이티브 트랜스코드 진행 상황"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"나중에 다시 시도해 주세요. 문제가 해결된 후 사진을 사용할 수 있습니다."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"일부 사진을 로드할 수 없음"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"확인"</string>
 </resources>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index 469592e..4757855 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Мультимедия"</string>
     <string name="storage_description" msgid="4081716890357580107">"Жергиликтүү сактагыч"</string>
-    <string name="app_label" msgid="9035307001052716210">"Медиа сактагыч"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Медиа"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Медиа файлдарды тандагыч"</string>
     <string name="artist_label" msgid="8105600993099120273">"Аткаруучу"</string>
     <string name="unknown" msgid="2059049215682829375">"Белгисиз"</string>
     <string name="root_images" msgid="5861633549189045666">"Сүрөттөр"</string>
@@ -46,18 +45,23 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Төмөнкүдөн алынган булуттагы мультимедианы колдонуу:"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Жок"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Мультимедиа колдонмосу өзгөргөн жок."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Медиа файлдарды тандагыч"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Медиа файлдарды тандагыч"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Медиа файлдар шайкештирилүүдө…"</string>
     <string name="add" msgid="2894574044585549298">"Кошуу"</string>
     <string name="deselect" msgid="4297825044827769490">"Тандоодон чыгаруу"</string>
     <string name="deselected" msgid="8488133193326208475">"Тандоодон чыгарылды"</string>
     <string name="select" msgid="2704765470563027689">"Тандоо"</string>
     <string name="selected" msgid="9151797369975828124">"Тандалды"</string>
-    <string name="select_up_to" msgid="6994294169508439957">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> объектке чейин тандаңыз}other{<xliff:g id="COUNT_1">^1</xliff:g> объектке чейин тандаңыз}}"</string>
+    <string name="select_up_to" msgid="6994294169508439957">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> нерсеге чейин тандаңыз}other{<xliff:g id="COUNT_1">^1</xliff:g> нерсеге чейин тандаңыз}}"</string>
     <string name="recent" msgid="6694613584743207874">"Акыркы"</string>
     <string name="picker_photos_empty_message" msgid="5980619500554575558">"Сүрөттөр же видеолор жок"</string>
     <string name="picker_album_media_empty_message" msgid="7061850698189881671">"Колдоого алынган сүрөттөр же видеолор жок"</string>
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Альбомдор жок"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Тандалганды көрүү"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Сүрөттөр"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Альбомдор"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Алдын ала көрүү"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Жумуш профилине которулуу"</string>
@@ -67,11 +71,12 @@
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"Жеке маалыматка жумуш колдонмосунан кирүүгө тыюу салынат"</string>
     <string name="picker_profile_work_paused_title" msgid="382212880704235925">"Жумуш колдонмолору тындырылды"</string>
     <string name="picker_profile_work_paused_msg" msgid="6321552322125246726">"Жумуш сүрөттөрүн ачуу үчүн жумуш колдонмолорун иштетип, кайра аракет кылыңыз"</string>
-    <string name="picker_privacy_message" msgid="9132700451027116817">"Бул колдонмо сиз тандаган сүрөттөргө гана кире алат"</string>
+    <string name="picker_privacy_message" msgid="9132700451027116817">"Бул колдонмого сиз тандаган сүрөттөр гана жеткиликтүү"</string>
     <string name="picker_header_permissions" msgid="675872774407768495">"Бул колдонмо кире турган сүрөттөрдү жана видеолорду тандаңыз"</string>
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> нерсе}other{<xliff:g id="COUNT_1">^1</xliff:g> нерсе}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Кошуу (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Уруксат берүү (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Уруксат берилбейт"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Камера"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Жүктөлүп алынгандар"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Тандалмалар"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Видеону ойнотууда маселе келип чыкты"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Интернет байланышыңызды текшерип, кайталап көрүңүз"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Кайталоо"</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="preloading_dialog_title" msgid="4974348221848532887">"Тандаган медиа файлдарыңыз даярдалууда"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ичинен <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> даяр"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Жокко чыгаруу"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Эми камдык көчүрмөсү сакталган сүрөттөр камтылат"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"<xliff:g id="APP_NAME">%1$s</xliff:g> аккаунтундагы (<xliff:g id="USER_ACCOUNT">%2$s</xliff:g>) сүрөттөрдү тандай аласыз"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> аккаунту жаңыртылды"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Колдонмо тандаңыз"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Аккаунт тандоо"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Аккаунтту өзгөртүү"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Бардык сүрөттөрүңүз алынууда"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Коопсуздукту коргоо"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Камтылган транскоддоо эскертүүлөрү"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Камтылган транскоддоо жүргүзүлүүдө"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Бир аздан кийин кайталап көрүңүз. Сүрөттөрүңүздү маселе чечилгенден кийин көрө аласыз."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Айрым сүрөттөр жүктөлбөй жатат"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Түшүндүм"</string>
 </resources>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index e07d1a3..2ba9646 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"ມີເດຍ"</string>
     <string name="storage_description" msgid="4081716890357580107">"ບ່ອນຈັດເກັບຂໍ້ມູນໃນເຄື່ອງ"</string>
-    <string name="app_label" msgid="9035307001052716210">"ພື້ນທີ່ຈັດເກັບຂໍ້ມູນມີເດຍ"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"ສື່"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"ຕົວເລືອກມີເດຍ"</string>
     <string name="artist_label" msgid="8105600993099120273">"ສິນລະປິນ"</string>
     <string name="unknown" msgid="2059049215682829375">"ບໍ່ຮູ້ຈັກ"</string>
     <string name="root_images" msgid="5861633549189045666">"ຮູບພາບ"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"ເຂົ້າເຖິງມີເດຍໃນລະບົບຄລາວຈາກ"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"ບໍ່ມີ"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"ບໍ່ສາມາດປ່ຽນແອັບມີເດຍໃນລະບົບຄລາວໄດ້ໃນຕອນນີ້."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"ຕົວເລືອກມີເດຍ"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"ຕົວເລືອກມີເດຍ"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"ກຳລັງຊິ້ງຂໍ້ມູນມີເດຍ…"</string>
     <string name="add" msgid="2894574044585549298">"ເພີ່ມ"</string>
     <string name="deselect" msgid="4297825044827769490">"ເຊົາເລືອກ"</string>
     <string name="deselected" msgid="8488133193326208475">"ເຊົາເລືອກແລ້ວ"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"ບໍ່ມີອະລະບ້ຳ"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"ເບິ່ງອັນທີ່ເລືອກໄວ້"</string>
     <string name="picker_photos" msgid="7415035516411087392">"ຮູບພາບ"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"ອະລະບ້ຳ"</string>
     <string name="picker_preview" msgid="6257414886055861039">"ຕົວຢ່າງ"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"ສະຫຼັບໄປໂປຣໄຟລ໌ວຽກ"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ລາຍການ}other{<xliff:g id="COUNT_1">^1</xliff:g> ລາຍການ}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"ເພີ່ມ (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"ອະນຸຍາດ (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"ບໍ່ອະນຸຍາດ"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"ກ້ອງຖ່າຍຮູບ"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"ດາວໂຫຼດ"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"ລາຍການທີ່ມັກ"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"ບັນຫາໃນການຫຼິ້ນວິດີໂອ"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"ກະລຸນາກວດສອບການເຊື່ອມຕໍ່ອິນເຕີເນັດຂອງທ່ານແລ້ວລອງໃໝ່"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"ລອງໃໝ່"</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="preloading_dialog_title" msgid="4974348221848532887">"ກຳລັງກຽມມີເດຍທີ່ທ່ານເລືອກໄວ້"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"ພ້ອມແລ້ວ <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> ຈາກທັງໝົດ <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"ຍົກເລີກ"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"ຕອນນີ້ຮວມຮູບພາບທີ່ສຳຮອງຂໍ້ມູນໄວ້ແລ້ວ"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"ທ່ານສາມາດເລືອກຮູບພາບໄດ້ຈາກບັນຊີ <xliff:g id="APP_NAME">%1$s</xliff:g> <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"ອັບເດດບັນຊີ <xliff:g id="APP_NAME">%1$s</xliff:g> ແລ້ວ"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"ເລືອກແອັບ"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"ເລືອກບັນຊີ"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"ປ່ຽນບັນຊີ"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"ກຳລັງໂຫຼດຮູບພາບທັງໝົດຂອງທ່ານ"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"ການປ້ອງກັນຄວາມປອດໄພ"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"ແຈ້ງເຕືອນການປ່ຽນຮູບແບບລະຫັດເດີມ"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"ຄວາມຄືບໜ້າຂອງການປ່ຽນຮູບແບບລະຫັດເດີມ"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"ກະລຸນາລອງໃໝ່ໃນພາຍຫຼັງ. ຈະມີການສະແດງຮູບພາບຂອງທ່ານເມື່ອບັນຫາໄດ້ຮັບການແກ້ໄຂແລ້ວ."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"ບໍ່ສາມາດໂຫຼດບາງຮູບພາບໄດ້"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"ເຂົ້າໃຈແລ້ວ"</string>
 </resources>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index f491e7e..29a093f 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Medija"</string>
     <string name="storage_description" msgid="4081716890357580107">"Vietinė saugykla"</string>
-    <string name="app_label" msgid="9035307001052716210">"Medijos saugykla"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Medija"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Medijos pasirinkimo priemonė"</string>
     <string name="artist_label" msgid="8105600993099120273">"Atlikėjas"</string>
     <string name="unknown" msgid="2059049215682829375">"Nežinoma"</string>
     <string name="root_images" msgid="5861633549189045666">"Vaizdai"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Pasiekti mediją debesyje iš"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Nėra"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Šiuo metu nepavyko pakeisti debesies medijos programos."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Medijos pasirinkimo priemonė"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Medijos pasirinkimo priemonė"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Sinchronizuojama medija…"</string>
     <string name="add" msgid="2894574044585549298">"Pridėti"</string>
     <string name="deselect" msgid="4297825044827769490">"Panaikinti pasirinkimą"</string>
     <string name="deselected" msgid="8488133193326208475">"Pasirinkimas panaikintas"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Nėra jokių albumų"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Žiūrėti pasirinktus"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Nuotraukos"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albumai"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Peržiūra"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Perjungti į darbo profilį"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> elementas}one{<xliff:g id="COUNT_1">^1</xliff:g> elementas}few{<xliff:g id="COUNT_1">^1</xliff:g> elementai}many{<xliff:g id="COUNT_1">^1</xliff:g> elemento}other{<xliff:g id="COUNT_1">^1</xliff:g> elementų}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Pridėti (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Leisti (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Niekas neleidžiama"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Vaizdo kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Atsisiuntimai"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Mėgstamiausi"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Paleidžiant vaizdo įrašą kilo problema"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Patikrinkite interneto ryšį ir bandykite dar kartą"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Bandyti dar kartą"</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="preloading_dialog_title" msgid="4974348221848532887">"Ruošiami pasirinkti medijos failai"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Paruošta: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> iš <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Atšaukti"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Dabar įtraukiamos atsarginės nuotraukų kopijos"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Galite pasirinkti nuotraukas iš „<xliff:g id="APP_NAME">%1$s</xliff:g>“ paskyros <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"„<xliff:g id="APP_NAME">%1$s</xliff:g>“ paskyra atnaujinta"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Pasirinkti programą"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Pasirinkti paskyrą"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Pakeisti paskyrą"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Gaunamos visos nuotraukos"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Leisti programai „<xliff:g id="APP_NAME_0">^1</xliff:g>“ keisti šį vaizdo įrašą?}one{Leisti programai „<xliff:g id="APP_NAME_1">^1</xliff:g>“ keisti <xliff:g id="COUNT">^2</xliff:g> vaizdo įrašą?}few{Leisti programai „<xliff:g id="APP_NAME_1">^1</xliff:g>“ keisti <xliff:g id="COUNT">^2</xliff:g> vaizdo įrašus?}many{Leisti programai „<xliff:g id="APP_NAME_1">^1</xliff:g>“ keisti <xliff:g id="COUNT">^2</xliff:g> vaizdo įrašo?}other{Leisti programai „<xliff:g id="APP_NAME_1">^1</xliff:g>“ keisti <xliff:g id="COUNT">^2</xliff:g> vaizdo įrašų?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Apsauga"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Native Transcode Alerts"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Native Transcode Progress"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Vėliau bandykite dar kartą. Nuotraukos bus pasiekiamos išsprendus problemą."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Nepavyko įkelti kai kurių nuotraukų"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Supratau"</string>
 </resources>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 4af22b7..ad857f8 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Multivide"</string>
     <string name="storage_description" msgid="4081716890357580107">"Lokālā krātuve"</string>
-    <string name="app_label" msgid="9035307001052716210">"Multivides krātuve"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Multivide"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Multivides atlasītājs"</string>
     <string name="artist_label" msgid="8105600993099120273">"Izpildītājs"</string>
     <string name="unknown" msgid="2059049215682829375">"Nezināms"</string>
     <string name="root_images" msgid="5861633549189045666">"Attēli"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Piekļūstiet mākoņa multivides saturam no"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Nav"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Nevarēja mainīt mākoņa multivides lietotni."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Multivides atlasītājs"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Multivides atlasītājs"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Notiek multivides satura sinhronizēšana…"</string>
     <string name="add" msgid="2894574044585549298">"Pievienot"</string>
     <string name="deselect" msgid="4297825044827769490">"Noņemt atlasi"</string>
     <string name="deselected" msgid="8488133193326208475">"Atlase noņemta"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Nav albumu"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Skatīt atlasīto"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotoattēli"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albumi"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Priekšskatījums"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Pārslēgties uz darba profilu"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> vienums}zero{<xliff:g id="COUNT_1">^1</xliff:g> vienumu}one{<xliff:g id="COUNT_1">^1</xliff:g> vienums}other{<xliff:g id="COUNT_1">^1</xliff:g> vienumi}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Pievienot <xliff:g id="COUNT">^1</xliff:g>"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Atļaut (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Neatļaut nevienu"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Lejupielādes"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Izlase"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Atskaņojot videoklipu, radās kļūda"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Pārbaudiet interneta savienojumu un mēģiniet vēlreiz"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Mēģināt vēlreiz"</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="preloading_dialog_title" msgid="4974348221848532887">"Notiek jūsu atlasītā multivides satura sagatavošana"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Gatavs: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> no <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Atcelt"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Dublētie fotoattēli ir iekļauti"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Varat atlasīt fotoattēlus no lietotnes “<xliff:g id="APP_NAME">%1$s</xliff:g>” konta <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Lietotnes “<xliff:g id="APP_NAME">%1$s</xliff:g>” konts ir atjaunināts"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Izvēlēties lietotni"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Izvēlēties kontu"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Mainīt kontu"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Notiek visu jūsu fotoattēlu ielāde…"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Vai atļaut lietotnei <xliff:g id="APP_NAME_0">^1</xliff:g> pārveidot šo videoklipu?}zero{Vai atļaut lietotnei <xliff:g id="APP_NAME_1">^1</xliff:g> pārveidot <xliff:g id="COUNT">^2</xliff:g> videoklipus?}one{Vai atļaut lietotnei <xliff:g id="APP_NAME_1">^1</xliff:g> pārveidot <xliff:g id="COUNT">^2</xliff:g> videoklipu?}other{Vai atļaut lietotnei <xliff:g id="APP_NAME_1">^1</xliff:g> pārveidot <xliff:g id="COUNT">^2</xliff:g> videoklipus?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Drošības aizsardzība"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Brīdinājumi par mantotā formāta pārkodēšanu"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Mantotā formāta pārkodēšanas norise"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Vēlāk mēģiniet vēlreiz. Fotoattēli būs pieejami, tiklīdz problēma būs novērsta."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Nevar ielādēt dažus fotoattēlus"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Labi"</string>
 </resources>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index 1c852ca..cb95f54 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Аудио-визуелни содржини"</string>
     <string name="storage_description" msgid="4081716890357580107">"Локална меморија"</string>
-    <string name="app_label" msgid="9035307001052716210">"Капацитет за аудиовизуелни содржини"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Аудиовизуелни содржини"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Избирач на аудиовизуелни содржини"</string>
     <string name="artist_label" msgid="8105600993099120273">"Изведувач"</string>
     <string name="unknown" msgid="2059049215682829375">"Непознат"</string>
     <string name="root_images" msgid="5861633549189045666">"Слики"</string>
@@ -42,10 +41,13 @@
     <string name="picker_settings" msgid="6443463167344790260">"Апликација за содржини во облак"</string>
     <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Апликација за содржини во облак"</string>
     <string name="picker_settings_title" msgid="5647700706470673258">"Апликација за аудиовизуелни содржини во облак"</string>
-    <string name="picker_settings_description" msgid="2916686824777214585">"Пристап до вашите аудиовизуелни содржини во облак кога некоја апликација или веб-сајт ќе ве праша да изберете фотографии или видеа"</string>
-    <string name="picker_settings_selection_message" msgid="245453573086488596">"Пристапете до аудиовизуелните содржини во облак од"</string>
+    <string name="picker_settings_description" msgid="2916686824777214585">"Пристапувајте до вашите аудиовизуелни содржини во облак кога некоја апликација или веб-сајт ќе побара да изберете фотографии или видеа"</string>
+    <string name="picker_settings_selection_message" msgid="245453573086488596">"Пристапувајте до аудиовизуелните содржини во облак од"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Нема"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Не може да се промени апликацијата за аудиовизуелни содржини во облак во моментов."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Избирач на аудиовизуелни содржини"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Избирач на аудиовизуелни содржини"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Се синхронизираат аудиовизуелните содржини…"</string>
     <string name="add" msgid="2894574044585549298">"Додај"</string>
     <string name="deselect" msgid="4297825044827769490">"Поништи го изборот"</string>
     <string name="deselected" msgid="8488133193326208475">"Изборот е поништен"</string>
@@ -58,10 +60,12 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Нема албуми"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Прикажи ги избраните"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Фотографии"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Албуми"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Преглед"</string>
-    <string name="picker_work_profile" msgid="2083221066869141576">"Префрли на работен профил"</string>
-    <string name="picker_personal_profile" msgid="639484258397758406">"Префрли на личен профил"</string>
+    <string name="picker_work_profile" msgid="2083221066869141576">"Префрлете се на работен профил"</string>
+    <string name="picker_personal_profile" msgid="639484258397758406">"Префрлете се на личен профил"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"Блокирано од администраторот"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"Не е дозволено пристапување до работни податоци од лична апликација"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"Не е дозволено пристапување до лични податоци од работна апликација"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ставка}one{<xliff:g id="COUNT_1">^1</xliff:g> ставка}other{<xliff:g id="COUNT_1">^1</xliff:g> ставки}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Додај (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Дозволи (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Ниедна"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Камера"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Преземања"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Омилени"</string>
@@ -92,10 +97,11 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Проблем со пуштањето видео"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Проверете ја интернет-врската и обидете се повторно"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Повторно"</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="preloading_dialog_title" msgid="4974348221848532887">"Вашите избрани аудиовизуелни содржини се подготвуваат"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Подготвени: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> од <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
-    <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Бекап од фотографиите сега е влучен"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Откажи"</string>
+    <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Сега се опфатени фотографии од бекап"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Можете да изберете фотографии од сметката <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> на <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Сметката на <xliff:g id="APP_NAME">%1$s</xliff:g> е ажурирана"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"Фотографиите од <xliff:g id="USER_ACCOUNT">%1$s</xliff:g> сега се вклучени овде"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Изберете апликација"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Изберете сметка"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Променете ја сметката"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Се вчитуваат сите ваши фотографии"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Безбедносна заштита"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Предупредувања за матичното транскодирање"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Напредок на матичното транскодирање"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Обидете се повторно подоцна. Вашите фотографии ќе бидат достапни откако ќе се реши проблемот."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Некои фотографии не може да се вчитаат"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Сфатив"</string>
 </resources>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index 42b9c43..5def22b 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"മീഡിയ"</string>
     <string name="storage_description" msgid="4081716890357580107">"ലോക്കൽ സ്റ്റോറേജ്"</string>
-    <string name="app_label" msgid="9035307001052716210">"മീഡിയ സ്റ്റോറേജ്"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"മീഡിയ"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"മീഡിയ പിക്കർ"</string>
     <string name="artist_label" msgid="8105600993099120273">"ആർട്ടിസ്‌റ്റ്"</string>
     <string name="unknown" msgid="2059049215682829375">"അജ്ഞാതം"</string>
     <string name="root_images" msgid="5861633549189045666">"ചിത്രങ്ങൾ"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"ക്ലൗഡ് മീഡിയ ആപ്പിൽ നിന്ന്"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"ഒന്നുമില്ല"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"ക്ലൗഡ് മീഡിയ ആപ്പ് ഇപ്പോൾ മാറ്റാനാകുന്നില്ല."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"മീഡിയാ പിക്കർ"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"മീഡിയാ പിക്കർ"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"മീഡിയാ സമന്വയിപ്പിക്കുന്നു…"</string>
     <string name="add" msgid="2894574044585549298">"ചേർക്കുക"</string>
     <string name="deselect" msgid="4297825044827769490">"തിരഞ്ഞെടുത്തത് മാറ്റുക"</string>
     <string name="deselected" msgid="8488133193326208475">"തിരഞ്ഞെടുത്തത് മാറ്റി"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"ആൽബങ്ങളൊന്നുമില്ല"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"തിരഞ്ഞെടുത്തത് കാണുക"</string>
     <string name="picker_photos" msgid="7415035516411087392">"ഫോട്ടോകൾ"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"ആൽബങ്ങൾ"</string>
     <string name="picker_preview" msgid="6257414886055861039">"പ്രിവ്യു"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"ഔദ്യോഗിക പ്രൊഫൈലിലേക്ക് മാറുക"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ഇനം}other{<xliff:g id="COUNT_1">^1</xliff:g> ഇനങ്ങൾ}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>) ചേർക്കുക"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"(<xliff:g id="COUNT">^1</xliff:g>) എണ്ണത്തെ അനുവദിക്കുക"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"ഒന്നും അനുവദിക്കരുത്"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"ക്യാമറ"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"ഡൗൺലോഡുകൾ"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"പ്രിയപ്പെട്ടവ"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"വീഡിയോ പ്ലേ ചെയ്യുന്നതിൽ പ്രശ്നം"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"നിങ്ങളുടെ ഇന്റർനെറ്റ് കണക്ഷൻ പരിശോധിച്ച് വീണ്ടും ശ്രമിക്കുക"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"വീണ്ടും ശ്രമിക്കുക"</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="preloading_dialog_title" msgid="4974348221848532887">"നിങ്ങൾ തിരഞ്ഞെടുത്ത മീഡിയ തയ്യാറാക്കുന്നു"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>-ൽ <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> എണ്ണം തയ്യാറാണ്"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"റദ്ദാക്കുക"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"ബാക്കപ്പ് ചെയ്ത ഫോട്ടോകൾ ഇപ്പോൾ ഉൾപ്പെടുത്തിയിരിക്കുന്നു"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"നിങ്ങൾക്ക് <xliff:g id="APP_NAME">%1$s</xliff:g> അക്കൗണ്ടിൽ <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> നിന്ന് ഫോട്ടോകൾ തിരഞ്ഞെടുക്കാം"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> അക്കൗണ്ട് അപ്ഡേറ്റ് ചെയ്തു"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"സുരക്ഷാ പരിരക്ഷ"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"നേറ്റീവ് ട്രാൻസ്കോഡ് മുന്നറിയിപ്പുകൾ"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"നേറ്റീവ് ട്രാൻസ്കോഡ് പുരോഗതി"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"പിന്നീട് വീണ്ടും ശ്രമിക്കുക. പ്രശ്‌നം പരിഹരിച്ച് കഴിഞ്ഞ് നിങ്ങളുടെ ഫോട്ടോകൾ ലഭ്യമാകും."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"ചില ഫോട്ടോകൾ ലോഡ് ചെയ്യാനാകുന്നില്ല"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"മനസ്സിലായി"</string>
 </resources>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 3dc2d7d..e1a5246 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -18,12 +18,11 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Медиа"</string>
     <string name="storage_description" msgid="4081716890357580107">"Дотоод сан"</string>
-    <string name="app_label" msgid="9035307001052716210">"Медиа санах ой"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Медиа"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Медиа сонгогч"</string>
     <string name="artist_label" msgid="8105600993099120273">"Уран бүтээлч"</string>
     <string name="unknown" msgid="2059049215682829375">"Тодорхойгүй"</string>
     <string name="root_images" msgid="5861633549189045666">"Зураг"</string>
-    <string name="root_videos" msgid="8792703517064649453">"Бичлэг"</string>
+    <string name="root_videos" msgid="8792703517064649453">"Видео"</string>
     <string name="root_audio" msgid="3505830755201326018">"Аудио"</string>
     <string name="root_documents" msgid="3829103301363849237">"Документ"</string>
     <string name="permission_required" msgid="1460820436132943754">"Энэ зүйлийг өөрчлөх эсвэл устгахад зөвшөөрөл шаардлагатай."</string>
@@ -42,10 +41,13 @@
     <string name="picker_settings" msgid="6443463167344790260">"Үүлэн медиа апп"</string>
     <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Үүлэн медиа апп"</string>
     <string name="picker_settings_title" msgid="5647700706470673258">"Үүлэн медиа апп"</string>
-    <string name="picker_settings_description" msgid="2916686824777214585">"Апп эсвэл вебсайт танаас зураг эсвэл видео сонгохыг хүсэх үед таны үүлэн медиадаа хандана уу"</string>
+    <string name="picker_settings_description" msgid="2916686824777214585">"Апп эсвэл вебсайт танаас зураг эсвэл видео сонгохыг хүсвэл үүлэн медиадаа хандана уу"</string>
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Дараахаас үүлэн медиад хандах"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Байхгүй"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Энэ удаад үүлэн медиа аппыг өөрчилж чадсангүй."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Медиа сонгогч"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Медиа сонгогч"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Медиаг синк хийж байна…"</string>
     <string name="add" msgid="2894574044585549298">"Нэмэх"</string>
     <string name="deselect" msgid="4297825044827769490">"Сонголтыг цуцлах"</string>
     <string name="deselected" msgid="8488133193326208475">"Сонголтыг цуцалсан"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Цомог алга"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Сонгосныг харах"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Зураг"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Цомог"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Урьдчилан үзэх"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Ажлын профайл руу сэлгэх"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> зүйл}other{<xliff:g id="COUNT_1">^1</xliff:g> зүйл}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Нэмэх (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Зөвшөөрөх (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Юуг ч бүү зөвшөөр"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Камер"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Таталтууд"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Дуртай зүйлс"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Видеог тоглуулахад асуудал гарлаа"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Интернэт холболтоо шалгаад, дахин оролдоно уу"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Дахин оролдох"</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="preloading_dialog_title" msgid="4974348221848532887">"Таны сонгосон медиаг бэлтгэж байна"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>-с <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> бэлэн"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Цуцлах"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Одоо хуулбарласан зургийг багтаасан"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Та <xliff:g id="APP_NAME">%1$s</xliff:g> <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> бүртгэлээс зураг сонгох боломжтой"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> бүртгэлийг шинэчилсэн"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Апп сонгох"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Бүртгэл сонгох"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Бүртгэл өөрчлөх"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Таны бүх зургийг авч байна"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Аюулгүй байдлын хамгаалалт"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Уугуул хөрвүүлгийн сэрэмжлүүлэг"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Уугуул хөрвүүлгийн явц"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Дараа дахин оролдоно уу. Асуудлыг шийдвэрлэсний дараа таны зургууд боломжтой болно."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Зарим зургийг ачаалах боломжгүй"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Ойлголоо"</string>
 </resources>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index 0b68570..184f72b 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"मीडिया"</string>
     <string name="storage_description" msgid="4081716890357580107">"स्थानिक स्टोरेज"</string>
-    <string name="app_label" msgid="9035307001052716210">"मीडिया स्टोरेज"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"मीडिया"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"मीडिया पिकर"</string>
     <string name="artist_label" msgid="8105600993099120273">"कलाकार"</string>
     <string name="unknown" msgid="2059049215682829375">"अज्ञात"</string>
     <string name="root_images" msgid="5861633549189045666">"इमेज"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"येथून क्लाउड मीडिया अ‍ॅक्सेस करा"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"काहीही नाही"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"क्लाउड मीडिया अ‍ॅप या क्षणी बदलू शकत नाही."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"मीडिया पिकर"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"मीडिया पिकर"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"मीडिया सिंक करत आहे…"</string>
     <string name="add" msgid="2894574044585549298">"जोडा"</string>
     <string name="deselect" msgid="4297825044827769490">"निवड रद्द करा"</string>
     <string name="deselected" msgid="8488133193326208475">"निवड रद्द केली आहे"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"कोणतेही अल्बम नाहीत"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"निवडलेले पहा"</string>
     <string name="picker_photos" msgid="7415035516411087392">"फोटो"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"अल्बम"</string>
     <string name="picker_preview" msgid="6257414886055861039">"पूर्वावलोकन"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"ऑफिसवर स्विच करा"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> आयटम}other{<xliff:g id="COUNT_1">^1</xliff:g> आयटम}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>) जोडा"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"(<xliff:g id="COUNT">^1</xliff:g>) ला अनुमती द्या"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"काहीही नाही याला अनुमती द्या"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"कॅमेरा"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"डाउनलोड"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"आवडते"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"व्हिडिओ प्ले करण्यात समस्या आली"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"तुमचे इंटरनेट कनेक्शन तपासा आणि पुन्हा प्रयत्न करा"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"पुन्हा प्रयत्न करा"</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="preloading_dialog_title" msgid="4974348221848532887">"तुमचा निवडलेला मीडिया तयार करत आहे"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> पैकी <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> तयार"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"रद्द करा"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"बॅकअप घेतलेल्या फोटोचा आता समावेश केला आहे"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"तुम्ही <xliff:g id="APP_NAME">%1$s</xliff:g> खात्याच्या <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> वरून फोटो निवडू शकता"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> खाते अपडेट केले"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"ॲप निवडा"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"खाते निवडा"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"खाते बदला"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"तुमचे सर्व फोटो मिळवत आहे"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"सुरक्षितता संरक्षण"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"मूळ ट्रान्सकोड सूचना"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"मूळ ट्रान्सकोड प्रगती"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"नंतर पुन्हा प्रयत्न करा. समस्येचे निराकरण झाल्यावर तुमचे फोटो उपलब्ध होतील."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"काही फोटो लोड करू शकत नाही"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"समजले"</string>
 </resources>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index a285a4d..9f48850 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Media"</string>
     <string name="storage_description" msgid="4081716890357580107">"Storan setempat"</string>
-    <string name="app_label" msgid="9035307001052716210">"Storan Media"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Media"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Pemilih Media"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artis"</string>
     <string name="unknown" msgid="2059049215682829375">"Tidak diketahui"</string>
     <string name="root_images" msgid="5861633549189045666">"Imej"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Akses media awan daripada"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Tiada"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Tidak dapat menukar apl media awan pada masa ini."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Pemilih media"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Pemilih media"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Menyegerakkan media…"</string>
     <string name="add" msgid="2894574044585549298">"Tambah"</string>
     <string name="deselect" msgid="4297825044827769490">"Nyahpilih"</string>
     <string name="deselected" msgid="8488133193326208475">"Dinyahpilih"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Tiada album"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Lihat terpilih"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Foto"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Album"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Pratonton"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Beralih kepada kerja"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> item}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Tambah (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Benarkan (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Tiada yang dibenarkan"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Muat turun"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Kegemaran"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Berlaku masalah semasa memainkan video"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Semak sambungan Internet anda, kemudian cuba lagi"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Cuba lagi"</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="preloading_dialog_title" msgid="4974348221848532887">"Menyediakan media pilihan anda"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> daripada <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> sudah bersedia"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Batal"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Foto yang disandarkan kini disertakan"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Anda boleh memilih foto daripada akaun <xliff:g id="APP_NAME">%1$s</xliff:g> <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Akaun <xliff:g id="APP_NAME">%1$s</xliff:g> dikemas kini"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Pilih apl"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Pilih akaun"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Tukar akaun"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Mendapatkan semua foto anda"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Benarkan <xliff:g id="APP_NAME_0">^1</xliff:g> mengubah suai video ini?}other{Benarkan <xliff:g id="APP_NAME_1">^1</xliff:g> mengubah suai <xliff:g id="COUNT">^2</xliff:g> video?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Perlindungan keselamatan"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Amaran Transkod Asal"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Kemajuan Transkod Asal"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Cuba sebentar lagi. Foto anda akan tersedia selepas masalah ini diselesaikan."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Tidak dapat memuatkan beberapa foto"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
 </resources>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index 84e1513..322eb0a 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"မီဒီယာ"</string>
     <string name="storage_description" msgid="4081716890357580107">"စက်တွင်း သိုလှောင်ခန်း"</string>
-    <string name="app_label" msgid="9035307001052716210">"မီဒီယာ သိုလှောင်ခန်း"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"မီဒီယာ"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"မီဒီယာရွေးရန်"</string>
     <string name="artist_label" msgid="8105600993099120273">"အနုပညာရှင်"</string>
     <string name="unknown" msgid="2059049215682829375">"အမျိုးအမည်မသိ"</string>
     <string name="root_images" msgid="5861633549189045666">"ပုံများ"</string>
@@ -42,10 +41,13 @@
     <string name="picker_settings" msgid="6443463167344790260">"Cloud မီဒီယာအက်ပ်"</string>
     <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Cloud မီဒီယာအက်ပ်"</string>
     <string name="picker_settings_title" msgid="5647700706470673258">"Cloud မီဒီယာအက်ပ်"</string>
-    <string name="picker_settings_description" msgid="2916686824777214585">"အက်ပ် (သို့) ဝဘ်ဆိုက်က သင့်အား ဓာတ်ပုံ (သို့) ဗီဒီယိုများ ရွေးခိုင်းသောအခါ သင်၏ cloud မီဒီယာကို ဝင်ပါ"</string>
+    <string name="picker_settings_description" msgid="2916686824777214585">"အက်ပ် (သို့) ဝဘ်ဆိုက်က သင့်အား ဓာတ်ပုံ (သို့) ဗီဒီယိုများ ရွေးခိုင်းသောအခါ သင်၏ cloud မီဒီယာကို ဝင်သည်"</string>
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Cloud မီဒီယာကို ဤနေရာမှ ဝင်သုံးရန်"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"မရှိ"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"လောလောဆယ် cloud မီဒီယာ အက်ပ်ကို ပြောင်း၍မရပါ။"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"မီဒီယာရွေးခြင်း"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"မီဒီယာရွေးခြင်း"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"မီဒီယာကို စင့်ခ်လုပ်နေသည်…"</string>
     <string name="add" msgid="2894574044585549298">"ထည့်ရန်"</string>
     <string name="deselect" msgid="4297825044827769490">"မရွေးပါနှင့်"</string>
     <string name="deselected" msgid="8488133193326208475">"ရွေးချယ်မထားပါ"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"အယ်လ်ဘမ်များ မရှိပါ"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"ပြသမှုကို ရွေးချယ်ထားသည်"</string>
     <string name="picker_photos" msgid="7415035516411087392">"ဓာတ်ပုံများ"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"အယ်လ်ဘမ်များ"</string>
     <string name="picker_preview" msgid="6257414886055861039">"အစမ်းကြည့်ရှုခြင်း"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"အလုပ်သို့ ပြောင်းပါ"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{ဖိုင် <xliff:g id="COUNT_0">^1</xliff:g> ခု}other{ဖိုင် <xliff:g id="COUNT_1">^1</xliff:g> ခု}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>) ခု ထည့်ရန်"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"(<xliff:g id="COUNT">^1</xliff:g>) ခု ခွင့်ပြုရန်"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"သုည ခွင့်ပြုရန်"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"ကင်မရာ"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"ဒေါင်းလုဒ်များ"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"စိတ်ကြိုက်များ"</string>
@@ -92,11 +97,12 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"ဗီဒီယိုဖွင့်ရာတွင် ပြဿနာရှိသည်"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"သင်၏ အင်တာနက် ချိတ်ဆက်မှုကို စစ်ဆေးပြီး ထပ်စမ်းကြည့်ပါ"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"ထပ်စမ်းကြည့်ရန်"</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="preloading_dialog_title" msgid="4974348221848532887">"သင်ရွေးထားသော မီဒီယာကို ပြင်ဆင်နေသည်"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> အနက် <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> အသင့်ဖြစ်ပြီ"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"မလုပ်တော့"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"အရန်သိမ်းထားသော ဓာတ်ပုံများ ယခုထည့်သွင်းထားပြီ"</string>
-    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"<xliff:g id="APP_NAME">%1$s</xliff:g> <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> အကောင့်မှ ဓာတ်ပုံများ ရွေးနိုင်သည်"</string>
+    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"<xliff:g id="APP_NAME">%1$s</xliff:g> အကောင့် <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> မှ ဓာတ်ပုံများ ရွေးနိုင်သည်"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> အကောင့် အပ်ဒိတ်လုပ်လိုက်သည်"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"<xliff:g id="USER_ACCOUNT">%1$s</xliff:g> မှ ဓာတ်ပုံများကို ဤနေရာတွင် ယခုထည့်သွင်းထားပါပြီ"</string>
     <string name="picker_banner_cloud_choose_app_title" msgid="3165966147547974251">"cloud မီဒီယာအက်ပ် ရွေးရန်"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"အက်ပ်ရွေးရန်"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"အကောင့်ရွေးရန်"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"အကောင့်ပြောင်းရန်"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"သင့်ဓာတ်ပုံအားလုံးကို ရယူနေသည်"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"လုံခြုံရေး ကာကွယ်မှု"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"မူရင်းမီဒီယာကုဒ်ပြောင်းသည့် သတိပေးချက်များ"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"မူရင်းမီဒီယာကုဒ်ပြောင်းသည့် အခြေအနေ"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"နောက်မှထပ်စမ်းပါ။ ပြဿနာကို ဖြေရှင်းပြီးသည့်အခါ သင့်ဓာတ်ပုံများကို ရနိုင်မည်။"</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"ဓာတ်ပုံအချို့ကို ဖွင့်၍ မရပါ"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"နားလည်ပြီ"</string>
 </resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 2fa7c43..060945b 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Medier"</string>
     <string name="storage_description" msgid="4081716890357580107">"Lokal lagring"</string>
-    <string name="app_label" msgid="9035307001052716210">"Medielagring"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Medier"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Medievelger"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artist"</string>
     <string name="unknown" msgid="2059049215682829375">"Ukjent"</string>
     <string name="root_images" msgid="5861633549189045666">"Bilder"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Åpne skymedier fra"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Ingen"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Kunne ikke endre skymedieappen akkurat nå."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Medievelger"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Medievelger"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Synkroniserer medieinnholdet …"</string>
     <string name="add" msgid="2894574044585549298">"Legg til"</string>
     <string name="deselect" msgid="4297825044827769490">"Fjern merking"</string>
     <string name="deselected" msgid="8488133193326208475">"Ikke valgt"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Ingen album"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Vis valgte"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Bilder"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Album"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Forhåndsvisning"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Bytt til jobbprofilen"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> element}other{<xliff:g id="COUNT_1">^1</xliff:g> elementer}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Legg til (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Tillat (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Tillat ingen"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Nedlastinger"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favoritter"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Problem med avspilling av videoen"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Sjekk internettilkoblingen og prøv på nytt"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Prøv på nytt"</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="preloading_dialog_title" msgid="4974348221848532887">"Klargjør det valgte medieinnholdet"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> av <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> er klare"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Avbryt"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Nå er sikkerhetskopierte bilder inkludert"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Du kan velge bilder fra <xliff:g id="APP_NAME">%1$s</xliff:g>-kontoen <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g>-kontoen er oppdatert"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Velg app"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Velg konto"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Bytt konto"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Laster inn alle bildene dine"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Vil du tillate at <xliff:g id="APP_NAME_0">^1</xliff:g> endrer denne videoen?}other{Vil du tillate at <xliff:g id="APP_NAME_1">^1</xliff:g> endrer <xliff:g id="COUNT">^2</xliff:g> videoer?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Beskyttelse"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Integrerte omkodingsvarsler"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Integrert omkodingsfremdrift"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Prøv på nytt senere. Bildene dine blir tilgjengelige når problemet er løst."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Noen bilder kan ikke lastes inn"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Greit"</string>
 </resources>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index ec01564..5aab7f0 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"मिडिया"</string>
     <string name="storage_description" msgid="4081716890357580107">"स्थानीय भण्डारण"</string>
-    <string name="app_label" msgid="9035307001052716210">"मिडिया भण्डारण"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"मिडिया"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"मिडिया पिकर"</string>
     <string name="artist_label" msgid="8105600993099120273">"कलाकार"</string>
     <string name="unknown" msgid="2059049215682829375">"अज्ञात"</string>
     <string name="root_images" msgid="5861633549189045666">"फोटो"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"यसबाट क्लाउड मिडिया प्रयोग गर्नुहोस्"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"कुनै पनि होइन"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"अहिले क्लाउड मिडिया एप परिवर्तन गर्न सकिएन।"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"मिडिया पिकर"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"मिडिया पिकर"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"मिडिया सिंक हुँदै छ…"</string>
     <string name="add" msgid="2894574044585549298">"हाल्नुहोस्"</string>
     <string name="deselect" msgid="4297825044827769490">"चयन रद्द गर्नुहोस्"</string>
     <string name="deselected" msgid="8488133193326208475">"चयन रद्द गरियो"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"कुनै पनि एल्बम छैन"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"चयन गरिएका सामग्री हेर्नुहोस्"</string>
     <string name="picker_photos" msgid="7415035516411087392">"फोटोहरू"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"एल्बमहरू"</string>
     <string name="picker_preview" msgid="6257414886055861039">"प्रिभ्यू"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"कार्य प्रोफाइल प्रयोग गर्नुहोस्"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> वटा वस्तु}other{<xliff:g id="COUNT_1">^1</xliff:g> वटा वस्तु}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"थप्नुहोस् (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"अनुमति दिनुहोस् (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"कुनै पनि फोटो प्रयोग गर्न नदिनुहोस्"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"क्यामेरा"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"डाउनलोडहरू"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"मन पर्ने कुराहरू"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"भिडियो प्ले गर्दा समस्या भयो"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"इन्टरनेट जाँच्नुहोस् र फेरि प्रयास गर्नुहोस्"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"फेरि प्रयास गर्नुहोस्"</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="preloading_dialog_title" msgid="4974348221848532887">"तपाईंले चयन गर्नुभएको मिडिया तयार गरिँदै छ"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> मध्ये <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> वटा फोटो तयार छन्"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"रद्द गर्नुहोस्"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"अब ब्याकअप गरिएका फोटोहरू समावेश गरिएका छन्"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"तपाईं <xliff:g id="APP_NAME">%1$s</xliff:g> मा <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> खाता प्रयोग गरी राखिएका फोटोहरू चयन गर्न सक्नुहुन्छ"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> खाता अपडेट गरियो"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"एप छनौट गर्नुहोस्"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"खाता छनौट गर्नुहोस्"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"खाता बदल्नुहोस्"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"तपाईंका सबै फोटोहरू प्राप्त गरिँदै छन्"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"सेफ्टी प्रोटेक्सन"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"नेटिभ ट्रान्स्कोड अलर्ट"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"नेटिभ ट्रान्स्कोड प्रोग्रेस"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"पछि फेरि प्रयास गर्नुहोस्। समस्या समाधान हुनेबित्तिकै तपाईंका फोटो उपलब्ध हुने छन्।"</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"केही फोटोहरू लोड गर्न सकिँदैन"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"बुझेँ"</string>
 </resources>
diff --git a/res/values-night-v31/styles.xml b/res/values-night-v31/styles.xml
index 2a936f0..8e58ad7 100644
--- a/res/values-night-v31/styles.xml
+++ b/res/values-night-v31/styles.xml
@@ -14,7 +14,8 @@
      limitations under the License.
 -->
 
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+           xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
 
     <style name="PickerMaterialTheme" parent="@style/Theme.Material3.DayNight.NoActionBar">
         <item name="materialAlertDialogTheme">@style/ProfileDialogTheme</item>
@@ -40,6 +41,8 @@
         <item name="pickerBannerPrimaryTextColor">?android:attr/textColorSecondary</item>
         <item name="pickerBannerSecondaryTextColor">?android:attr/textColorSecondary</item>
         <item name="pickerBannerButtonTextColor">@android:color/system_accent1_300</item>
+        <item name="categoryDefaultThumbnailColor">?attr/colorOnSurfaceVariant</item>
+        <item name="categoryDefaultThumbnailCircleColor">?attr/colorSurfaceVariant</item>
     </style>
 
 </resources>
diff --git a/res/values-night/styles.xml b/res/values-night/styles.xml
index 72f234d..7a16b59 100644
--- a/res/values-night/styles.xml
+++ b/res/values-night/styles.xml
@@ -14,7 +14,8 @@
      limitations under the License.
 -->
 
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+           xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
 
     <style name="PickerDialogTheme"
            parent="@android:style/Theme.DeviceDefault.Dialog.Alert">
@@ -35,7 +36,7 @@
         <item name="android:alertDialogTheme">@style/AlertDialogTheme</item>
     </style>
 
-    <style name="PickerMaterialTheme" parent="@style/Theme.MaterialComponents.DayNight.NoActionBar">
+    <style name="PickerMaterialTheme" parent="@style/Theme.Material3.DayNight.NoActionBar">
         <item name="materialAlertDialogTheme">@style/ProfileDialogTheme</item>
         <item name="pickerDragBarColor">#686868</item>
         <item name="pickerHighlightColor">?android:attr/colorAccent</item>
@@ -59,6 +60,8 @@
         <item name="pickerBannerPrimaryTextColor">?android:attr/textColorSecondary</item>
         <item name="pickerBannerSecondaryTextColor">?android:attr/textColorSecondary</item>
         <item name="pickerBannerButtonTextColor">?android:attr/colorAccent</item>
+        <item name="categoryDefaultThumbnailColor">?attr/colorOnSurfaceVariant</item>
+        <item name="categoryDefaultThumbnailCircleColor">?attr/colorSurfaceVariant</item>
     </style>
 
 </resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 5c685ae..df69610 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Media"</string>
     <string name="storage_description" msgid="4081716890357580107">"Lokale opslag"</string>
-    <string name="app_label" msgid="9035307001052716210">"Mediaopslag"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Media"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Mediakiezer"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artiest"</string>
     <string name="unknown" msgid="2059049215682829375">"Onbekend"</string>
     <string name="root_images" msgid="5861633549189045666">"Afbeeldingen"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Cloudmedia openen vanuit"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Geen"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Cloudmedia-app kan nu niet worden gewijzigd."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Mediakiezer"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Mediakiezer"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Media synchroniseren…"</string>
     <string name="add" msgid="2894574044585549298">"Toevoegen"</string>
     <string name="deselect" msgid="4297825044827769490">"Deselecteren"</string>
     <string name="deselected" msgid="8488133193326208475">"Gedeselecteerd"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Geen albums"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Selectie bekijken"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Foto\'s"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albums"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Voorbeeld"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Overschakelen naar werkprofiel"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> items}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Toevoegen (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Toestaan (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Geen toestaan"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Camera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favorieten"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Probleem bij video afspelen"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Check de internetverbinding en probeer het opnieuw"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Opnieuw proberen"</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="preloading_dialog_title" msgid="4974348221848532887">"Je geselecteerde media voorbereiden"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> van <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> klaar"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Annuleren"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Nu ook met foto\'s waarvan een back-up is gemaakt"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Je kunt foto\'s selecteren uit het <xliff:g id="APP_NAME">%1$s</xliff:g>-account <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Account voor <xliff:g id="APP_NAME">%1$s</xliff:g> geüpdatet"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"App selecteren"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Account kiezen"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Account wijzigen"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Al je foto\'s ophalen"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> toestaan deze video aan te passen?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> toestaan <xliff:g id="COUNT">^2</xliff:g> video\'s aan te passen?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Beveiliging"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Meldingen voor native transcodering"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Voortgang van native transcodering"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Probeer het later opnieuw. Je foto\'s komen beschikbaar nadat het probleem is opgelost."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Kan bepaalde foto\'s niet laden"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
 </resources>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index ff2c2b8..8533864 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"ମିଡିଆ"</string>
     <string name="storage_description" msgid="4081716890357580107">"ଲୋକାଲ୍‍ ଷ୍ଟୋରେଜ୍‍"</string>
-    <string name="app_label" msgid="9035307001052716210">"ମିଡିଆ ଷ୍ଟୋରେଜ୍"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"ମିଡିଆ"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"ମିଡିଆ ପିକର"</string>
     <string name="artist_label" msgid="8105600993099120273">"କଳାକାର"</string>
     <string name="unknown" msgid="2059049215682829375">"ଅଜଣା"</string>
     <string name="root_images" msgid="5861633549189045666">"ଇମେଜ୍‌"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"ଏଠାରୁ କ୍ଲାଉଡ ମିଡିଆକୁ ଆକ୍ସେସ କରନ୍ତୁ"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"କିଛି ନାହିଁ"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"ଏହି ସମୟରେ କ୍ଲାଉଡ ମିଡିଆ ଆପ ପରିବର୍ତ୍ତନ ହେଲା ନାହିଁ।"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"ମିଡିଆ ପିକର"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"ମିଡିଆ ପିକର"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"ମିଡିଆ ସିଙ୍କ କରାଯାଉଛି…"</string>
     <string name="add" msgid="2894574044585549298">"ଯୋଗ କରନ୍ତୁ"</string>
     <string name="deselect" msgid="4297825044827769490">"ଅଚୟନ କରନ୍ତୁ"</string>
     <string name="deselected" msgid="8488133193326208475">"ଅଚୟନ କରାଯାଇଛି"</string>
@@ -56,14 +58,16 @@
     <string name="picker_photos_empty_message" msgid="5980619500554575558">"କୌଣସି ଫଟୋ କିମ୍ବା ଭିଡିଓ ନାହିଁ"</string>
     <string name="picker_album_media_empty_message" msgid="7061850698189881671">"କୌଣସି ସମର୍ଥିତ ଫଟୋ କିମ୍ବା ଭିଡିଓ ନାହିଁ"</string>
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"କୌଣସି ଆଲବମ ନାହିଁ"</string>
-    <string name="picker_view_selected" msgid="2266031384396143883">"ଚୟନିତଗୁଡ଼ିକୁ ଦେଖନ୍ତୁ"</string>
+    <string name="picker_view_selected" msgid="2266031384396143883">"ଚୟନିତଗୁଡ଼ିକୁ ଭ୍ୟୁ କରନ୍ତୁ"</string>
     <string name="picker_photos" msgid="7415035516411087392">"ଫଟୋ"</string>
-    <string name="picker_albums" msgid="4822511902115299142">"ଆଲବମ୍"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
+    <string name="picker_albums" msgid="4822511902115299142">"ଆଲବମ"</string>
     <string name="picker_preview" msgid="6257414886055861039">"ପ୍ରିଭ୍ୟୁ"</string>
-    <string name="picker_work_profile" msgid="2083221066869141576">"ୱାର୍କକୁ ସ୍ୱିଚ୍ କରନ୍ତୁ"</string>
-    <string name="picker_personal_profile" msgid="639484258397758406">"ବ୍ୟକ୍ତିଗତକୁ ସ୍ୱିଚ୍ କରନ୍ତୁ"</string>
-    <string name="picker_profile_admin_title" msgid="4172022376418293777">"ଆପଣଙ୍କ ଆଡମିନଙ୍କ ଦ୍ୱାରା ବ୍ଲକ୍ କରାଯାଇଛି"</string>
-    <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"କୌଣସି ବ୍ୟକ୍ତିଗତ ଆପରୁ ୱାର୍କ ଡାଟାକୁ ଆକ୍ସେସ୍ କରିବା ପାଇଁ ଅନୁମତି ଦିଆଯାଇନାହିଁ"</string>
+    <string name="picker_work_profile" msgid="2083221066869141576">"ୱାର୍କକୁ ସୁଇଚ କରନ୍ତୁ"</string>
+    <string name="picker_personal_profile" msgid="639484258397758406">"ବ୍ୟକ୍ତିଗତକୁ ସୁଇଚ କରନ୍ତୁ"</string>
+    <string name="picker_profile_admin_title" msgid="4172022376418293777">"ଆପଣଙ୍କ ଆଡମିନଙ୍କ ଦ୍ୱାରା ବ୍ଲକ କରାଯାଇଛି"</string>
+    <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"କୌଣସି ବ୍ୟକ୍ତିଗତ ଆପରୁ ୱାର୍କ ଡାଟାକୁ ଆକ୍ସେସ କରିବା ପାଇଁ ଅନୁମତି ଦିଆଯାଇନାହିଁ"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"କୌଣସି ୱାର୍କ ଆପରୁ ବ୍ୟକ୍ତିଗତ ଡାଟାକୁ ଆକ୍ସେସ୍ କରିବା ପାଇଁ ଅନୁମତି ଦିଆଯାଇନାହିଁ"</string>
     <string name="picker_profile_work_paused_title" msgid="382212880704235925">"ୱାର୍କ ଆପଗୁଡ଼ିକୁ ବିରତ କରାଯାଇଛି"</string>
     <string name="picker_profile_work_paused_msg" msgid="6321552322125246726">"ୱାର୍କ ଫଟୋଗୁଡ଼ିକୁ ଖୋଲିବାକୁ, ଆପଣଙ୍କ ୱାର୍କ ଆପଗୁଡ଼ିକୁ ଚାଲୁ କରି ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ"</string>
@@ -72,7 +76,8 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g>ଟି ଆଇଟମ}other{<xliff:g id="COUNT_1">^1</xliff:g>ଟି ଆଇଟମ}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>)ଟି ଯୋଗ କରନ୍ତୁ"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"ଅନୁମତି ଦିଅନ୍ତୁ (<xliff:g id="COUNT">^1</xliff:g>)"</string>
-    <string name="picker_category_camera" msgid="4857367052026843664">"କ୍ୟାମେରା"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"କାହାରିକୁ ଅନୁମତି ଦିଅନ୍ତୁ ନାହିଁ"</string>
+    <string name="picker_category_camera" msgid="4857367052026843664">"କେମେରା"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"ଡାଉନଲୋଡଗୁଡ଼ିକ"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"ପସନ୍ଦଗୁଡ଼ିକ"</string>
     <string name="picker_category_screenshots" msgid="7216102327587644284">"ସ୍କ୍ରିନସଟଗୁଡ଼ିକ"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"ଭିଡିଓ ପ୍ଲେ କରିବାରେ ସମସ୍ୟା"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"ଆପଣଙ୍କ ଇଣ୍ଟରନେଟ କନେକ୍ସନ ଯାଞ୍ଚ କରି ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ"</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="preloading_dialog_title" msgid="4974348221848532887">"ଆପଣଙ୍କ ଚୟନିତ ମିଡିଆକୁ ପ୍ରସ୍ତୁତ କରାଯାଉଛି"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>ରୁ <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>ଟି ପ୍ରସ୍ତୁତ ଅଛି"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"ବାତିଲ କରନ୍ତୁ"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"ବ୍ୟାକଅପ ନିଆଯାଇଥିବା ଫଟୋଗୁଡ଼ିକୁ ବର୍ତ୍ତମାନ ଅନ୍ତର୍ଭୁକ୍ତ କରାଯାଇଛି"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"ଆପଣ <xliff:g id="APP_NAME">%1$s</xliff:g> ଆକାଉଣ୍ଟ <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>ରୁ ଫଟୋଗୁଡ଼ିକୁ ଚୟନ କରିପାରିବେ"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> ଆକାଉଣ୍ଟକୁ ଅପଡେଟ କରାଯାଇଛି"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"ଆପ ବାଛନ୍ତୁ"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"ଆକାଉଣ୍ଟ ବାଛନ୍ତୁ"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"ଆକାଉଣ୍ଟ ବଦଳାନ୍ତୁ"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"ଆପଣଙ୍କର ସମସ୍ତ ଫଟୋ ପ୍ରାପ୍ତ କରାଯାଉଛି"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"ସୁରକ୍ଷିତ ସୁରକ୍ଷା"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"ନେଟିଭ ଟ୍ରାନ୍ସକୋଡ ଆଲର୍ଟ"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"ନେଟିଭ ଟ୍ରାନ୍ସକୋଡ ପ୍ରୋଗ୍ରେସ"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"ପରେ ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ। ସମସ୍ୟାର ସମାଧାନ ହେବା ପରେ ଆପଣଙ୍କ ଫଟୋଗୁଡ଼ିକ ଉପଲବ୍ଧ ହେବ।"</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"କିଛି ଫଟୋ ଲୋଡ କରାଯାଇପାରିବ ନାହିଁ"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"ବୁଝିଗଲି"</string>
 </resources>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index f6922bb..beb933e 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"ਮੀਡੀਆ"</string>
     <string name="storage_description" msgid="4081716890357580107">"ਸਥਾਨਕ ਸਟੋਰੇਜ"</string>
-    <string name="app_label" msgid="9035307001052716210">"ਮੀਡੀਆ ਸਟੋਰੇਜ"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"ਮੀਡੀਆ"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"ਮੀਡੀਆ ਚੋਣਕਾਰ"</string>
     <string name="artist_label" msgid="8105600993099120273">"ਕਲਾਕਾਰ"</string>
     <string name="unknown" msgid="2059049215682829375">"ਅਗਿਆਤ"</string>
     <string name="root_images" msgid="5861633549189045666">"ਚਿੱਤਰ"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"ਇੱਥੋਂ ਕਲਾਊਡ ਮੀਡੀਆ ਤੱਕ ਪਹੁੰਚ ਕਰੋ"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"ਕੋਈ ਨਹੀਂ"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"ਇਸ ਸਮੇਂ ਕਲਾਊਡ ਮੀਡੀਆ ਐਪ ਨੂੰ ਬਦਲਿਆ ਨਹੀਂ ਜਾ ਸਕਿਆ।"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"ਮੀਡੀਆ ਚੋਣਕਾਰ"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"ਮੀਡੀਆ ਚੋਣਕਾਰ"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"ਮੀਡੀਆ ਸਿੰਕ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ…"</string>
     <string name="add" msgid="2894574044585549298">"ਸ਼ਾਮਲ ਕਰੋ"</string>
     <string name="deselect" msgid="4297825044827769490">"ਅਣ-ਚੁਣਿਆ ਕਰੋ"</string>
     <string name="deselected" msgid="8488133193326208475">"ਅਣ-ਚੁਣਿਆ"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"ਕੋਈ ਐਲਬਮ ਨਹੀਂ"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"ਚੁਣੀਆਂ ਗਈਆਂ ਆਈਟਮਾਂ ਦੇਖੋ"</string>
     <string name="picker_photos" msgid="7415035516411087392">"ਫ਼ੋਟੋਆਂ"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"ਐਲਬਮਾਂ"</string>
     <string name="picker_preview" msgid="6257414886055861039">"ਪੂਰਵ-ਝਲਕ"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"ਕਾਰਜ ਪ੍ਰੋਫਾਈਲ \'ਤੇ ਸਵਿੱਚ ਕਰੋ"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ਆਈਟਮ}one{<xliff:g id="COUNT_1">^1</xliff:g> ਆਈਟਮ}other{<xliff:g id="COUNT_1">^1</xliff:g> ਆਈਟਮਾਂ}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>) ਸ਼ਾਮਲ ਕਰੋ"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"ਆਗਿਆ ਦਿਓ (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"ਕੋਈ ਵੀ ਆਗਿਆ ਨਾ ਦਿਓ"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"ਕੈਮਰਾ"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"ਡਾਊਨਲੋਡ"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"ਮਨਪਸੰਦ"</string>
@@ -92,11 +97,12 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"ਵੀਡੀਓ ਚਲਾਉਣ ਵਿੱਚ ਸਮੱਸਿਆ ਆ ਰਹੀ ਹੈ"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"ਆਪਣੇ ਇੰਟਰਨੈੱਟ ਕਨੈਕਸ਼ਨ ਦੀ ਜਾਂਚ ਕਰ ਕੇ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"ਮੁੜ-ਕੋਸ਼ਿਸ਼ ਕਰੋ"</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="preloading_dialog_title" msgid="4974348221848532887">"ਤੁਹਾਡਾ ਚੁਣਿਆ ਗਿਆ ਮੀਡੀਆ ਤਿਆਰ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ਵਿੱਚੋਂ <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> ਤਿਆਰ"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"ਰੱਦ ਕਰੋ"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"ਬੈਕਅੱਪ ਕੀਤੀਆਂ ਫ਼ੋਟੋਆਂ ਨੂੰ ਹੁਣ ਸ਼ਾਮਲ ਕੀਤਾ ਗਿਆ"</string>
-    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"ਤੁਸੀਂ ਖਾਤੇ <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> ਦੀ <xliff:g id="APP_NAME">%1$s</xliff:g> ਵਿੱਚੋਂ ਫ਼ੋਟੋਆਂ ਨੂੰ ਚੁਣੋ"</string>
+    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"ਤੁਸੀਂ <xliff:g id="APP_NAME">%1$s</xliff:g> ਵਿੱਚੋਂ <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> ਖਾਤੇ ਤੋਂ ਫ਼ੋਟੋਆਂ ਚੁਣ ਸਕਦੇ ਹੋ"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> ਖਾਤੇ ਨੂੰ ਅੱਪਡੇਟ ਕੀਤਾ ਗਿਆ"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"<xliff:g id="USER_ACCOUNT">%1$s</xliff:g> ਦੀਆਂ ਫ਼ੋਟੋਆਂ ਨੂੰ ਹੁਣ ਇੱਥੇ ਸ਼ਾਮਲ ਕੀਤਾ ਗਿਆ ਹੈ"</string>
     <string name="picker_banner_cloud_choose_app_title" msgid="3165966147547974251">"ਕਲਾਊਡ ਮੀਡੀਆ ਐਪ ਨੂੰ ਚੁਣੋ"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"ਐਪ ਚੁਣੋ"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"ਖਾਤਾ ਚੁਣੋ"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"ਖਾਤਾ ਬਦਲੋ"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"ਤੁਹਾਡੀਆਂ ਸਾਰੀਆਂ ਫ਼ੋਟੋਆਂ ਪ੍ਰਾਪਤ ਕੀਤੀਆਂ ਜਾ ਰਹੀਆਂ ਹਨ"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"ਸੁਰੱਖਿਆ ਬਚਾਅ"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"ਨੇਟਿਵ ਟ੍ਰਾਂਸਕੋਡ ਸੁਚੇਤਨਾਵਾਂ"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"ਨੇਟਿਵ ਟ੍ਰਾਂਸਕੋਡ ਪ੍ਰਗਤੀ"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"ਬਾਅਦ ਵਿੱਚ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ। ਸਮੱਸਿਆ ਹੱਲ ਹੋਣ ਤੋਂ ਬਾਅਦ ਤੁਹਾਡੀਆਂ ਫ਼ੋਟੋਆਂ ਉਪਲਬਧ ਹੋ ਜਾਣਗੀਆਂ।"</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"ਕੁਝ ਫ਼ੋਟੋਆਂ ਨੂੰ ਲੋਡ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"ਸਮਝ ਲਿਆ"</string>
 </resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 432521b..6ac9ce3 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Multimedia"</string>
     <string name="storage_description" msgid="4081716890357580107">"Pamięć lokalna"</string>
-    <string name="app_label" msgid="9035307001052716210">"Przechowywanie multimediów"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Multimedia"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Wybór mediów"</string>
     <string name="artist_label" msgid="8105600993099120273">"Wykonawca"</string>
     <string name="unknown" msgid="2059049215682829375">"Nieznany"</string>
     <string name="root_images" msgid="5861633549189045666">"Obrazy"</string>
@@ -46,10 +45,13 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Otwieraj multimedia w chmurze za pomocą:"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Brak"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Nie udało się zmienić aplikacji do multimediów w chmurze."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Wybór mediów"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Wybór mediów"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Synchronizuję multimedia…"</string>
     <string name="add" msgid="2894574044585549298">"Dodaj"</string>
-    <string name="deselect" msgid="4297825044827769490">"Odznacz"</string>
+    <string name="deselect" msgid="4297825044827769490">"Usuń wybór"</string>
     <string name="deselected" msgid="8488133193326208475">"Usunięto wybór"</string>
-    <string name="select" msgid="2704765470563027689">"Zaznacz"</string>
+    <string name="select" msgid="2704765470563027689">"Wybierz"</string>
     <string name="selected" msgid="9151797369975828124">"Wybrano"</string>
     <string name="select_up_to" msgid="6994294169508439957">"{count,plural, =1{Wybierz maksymalnie <xliff:g id="COUNT_0">^1</xliff:g> element}few{Wybierz maksymalnie <xliff:g id="COUNT_1">^1</xliff:g> elementy}many{Wybierz maksymalnie <xliff:g id="COUNT_1">^1</xliff:g> elementów}other{Wybierz maksymalnie <xliff:g id="COUNT_1">^1</xliff:g> elementu}}"</string>
     <string name="recent" msgid="6694613584743207874">"Ostatnie"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Brak albumów"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Wyświetl wybrane"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Zdjęcia"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albumy"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Podgląd"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Włącz profil służbowy"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> element}few{<xliff:g id="COUNT_1">^1</xliff:g> elementy}many{<xliff:g id="COUNT_1">^1</xliff:g> elementów}other{<xliff:g id="COUNT_1">^1</xliff:g> elementu}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Dodaj (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Zezwól (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Nie zezwalaj na żadne"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Aparat"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Pobrane"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Ulubione"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Wystąpiły problemy przy odtwarzaniu filmu"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Sprawdź połączenie z internetem i spróbuj ponownie"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Ponów"</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="preloading_dialog_title" msgid="4974348221848532887">"Przygotowywanie wybranych multimediów"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Gotowe <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> z <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Anuluj"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Teraz znajdziesz tu kopie zapasowe zdjęć"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Możesz wybrać zdjęcia z aplikacji <xliff:g id="APP_NAME">%1$s</xliff:g>, z konta <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Konto aplikacji <xliff:g id="APP_NAME">%1$s</xliff:g> zostało zaktualizowane"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Wybierz aplikację"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Wybierz konto"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Zmień konto"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Pobieram wszystkie Twoje zdjęcia"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Zezwolić aplikacji <xliff:g id="APP_NAME_0">^1</xliff:g> na zmodyfikowanie tego filmu?}few{Zezwolić aplikacji <xliff:g id="APP_NAME_1">^1</xliff:g> na zmodyfikowanie <xliff:g id="COUNT">^2</xliff:g> filmów?}many{Zezwolić aplikacji <xliff:g id="APP_NAME_1">^1</xliff:g> na zmodyfikowanie <xliff:g id="COUNT">^2</xliff:g> filmów?}other{Zezwolić aplikacji <xliff:g id="APP_NAME_1">^1</xliff:g> na zmodyfikowanie <xliff:g id="COUNT">^2</xliff:g> filmu?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Sprzęt zabezpieczający"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Alerty dotyczące transkodowania natywnego"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Postępy transkodowania natywnego"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Spróbuj ponownie później. Zdjęcia będą dostępne po rozwiązaniu problemu."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Nie można wczytać niektórych zdjęć"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
 </resources>
diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml
index cd37d76..326fde2 100644
--- a/res/values-pt-rBR/strings.xml
+++ b/res/values-pt-rBR/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Mídia"</string>
     <string name="storage_description" msgid="4081716890357580107">"Armazenamento local"</string>
-    <string name="app_label" msgid="9035307001052716210">"Armazenamento de mídia"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Mídia"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Seletor de mídia"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artista"</string>
     <string name="unknown" msgid="2059049215682829375">"Desconhecido"</string>
     <string name="root_images" msgid="5861633549189045666">"Imagens"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Acessar a mídia em nuvem de"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Nenhum"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Não foi possível mudar o app de mídia em nuvem."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Seletor de mídia"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Seletor de mídia"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Sincronizando mídia…"</string>
     <string name="add" msgid="2894574044585549298">"Adicionar"</string>
     <string name="deselect" msgid="4297825044827769490">"Desmarcar"</string>
     <string name="deselected" msgid="8488133193326208475">"Desmarcada"</string>
@@ -58,10 +60,12 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Sem álbuns"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Mostrar selecionados"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotos"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Álbuns"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Visualização"</string>
-    <string name="picker_work_profile" msgid="2083221066869141576">"Mudar para \"Trabalho\""</string>
-    <string name="picker_personal_profile" msgid="639484258397758406">"Mudar para \"Pessoal\""</string>
+    <string name="picker_work_profile" msgid="2083221066869141576">"Mudar para Trabalho"</string>
+    <string name="picker_personal_profile" msgid="639484258397758406">"Mudar para Pessoal"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"Bloqueado pelo administrador"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"Não é permitido o acesso a dados de trabalho em um app pessoal"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"Não é permitido o acesso a dados pessoais em um app de trabalho"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}one{<xliff:g id="COUNT_1">^1</xliff:g> item}many{<xliff:g id="COUNT_1">^1</xliff:g> itens}other{<xliff:g id="COUNT_1">^1</xliff:g> itens}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Adicionar (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Permitir (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Não autorizar"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Câmera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favoritos"</string>
@@ -92,10 +97,11 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Ocorreu um problema ao iniciar o vídeo"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Confira sua conexão de Internet e tente de novo"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Tentar novamente"</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="preloading_dialog_title" msgid="4974348221848532887">"Preparando a mídia selecionada"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> itens prontos"</string>
-    <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Fotos salvas em backup agora estão incluídas"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Cancelar"</string>
+    <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"As fotos salvas em backup agora estão incluídas"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Selecione fotos da conta <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> do app <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"A conta do app <xliff:g id="APP_NAME">%1$s</xliff:g> foi atualizada"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"As fotos da conta <xliff:g id="USER_ACCOUNT">%1$s</xliff:g> agora estão incluídas aqui"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Proteção"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Alertas da transcodificação nativa"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Progresso da transcodificação nativa"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Tente de novo mais tarde. Suas fotos vão ficar disponíveis assim que o problema for resolvido."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Não é possível carregar algumas fotos"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Entendi"</string>
 </resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 83b7aad..dddf82c 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Multimédia"</string>
     <string name="storage_description" msgid="4081716890357580107">"Armazenamento local"</string>
-    <string name="app_label" msgid="9035307001052716210">"Armazenamento de multimédia"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Multimédia"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Seletor de meios"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artista"</string>
     <string name="unknown" msgid="2059049215682829375">"Desconhecido"</string>
     <string name="root_images" msgid="5861633549189045666">"Imagens"</string>
@@ -33,7 +32,7 @@
     <string name="permission_more_thumb" msgid="1938863829470531577">"{count,plural, =1{+<xliff:g id="COUNT_0">^1</xliff:g>}many{+<xliff:g id="COUNT_1">^1</xliff:g>}other{+<xliff:g id="COUNT_1">^1</xliff:g>}}"</string>
     <string name="permission_more_text" msgid="2471785045095597753">"{count,plural, =1{E <xliff:g id="COUNT_0">^1</xliff:g> item adicional}many{E <xliff:g id="COUNT_1">^1</xliff:g> itens adicionais}other{E <xliff:g id="COUNT_1">^1</xliff:g> itens adicionais}}"</string>
     <string name="cache_clearing_dialog_title" msgid="8907893815183913664">"Limpe ficheiros de apps temporários"</string>
-    <string name="cache_clearing_dialog_text" msgid="7057784635111940957">"A app <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> pretende limpar alguns ficheiros temporários. Isto pode resultar num aumento da utilização da bateria ou dos dados móveis."</string>
+    <string name="cache_clearing_dialog_text" msgid="7057784635111940957">"A app <xliff:g id="APP_SEEKING_PERMISSION">%s</xliff:g> quer limpar alguns ficheiros temporários. Isto pode resultar num aumento da utilização da bateria ou dos dados móveis."</string>
     <string name="cache_clearing_in_progress_title" msgid="6902220064511664209">"A limpar ficheiros temporários da app…"</string>
     <string name="clear" msgid="5524638938415865915">"Limpar"</string>
     <string name="allow" msgid="8885707816848569619">"Permitir"</string>
@@ -46,8 +45,11 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Aceda a multimédia na nuvem a partir de"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Nenhuma"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Impossível alterar a app de multimédia na nuvem."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Seletor de meios"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Seletor de meios"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"A sincronizar conteúdo multimédia…"</string>
     <string name="add" msgid="2894574044585549298">"Adicionar"</string>
-    <string name="deselect" msgid="4297825044827769490">"Desselecionar"</string>
+    <string name="deselect" msgid="4297825044827769490">"Desmarcar"</string>
     <string name="deselected" msgid="8488133193326208475">"Desmarcado"</string>
     <string name="select" msgid="2704765470563027689">"Selecionar"</string>
     <string name="selected" msgid="9151797369975828124">"Selecionado"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Nenhum álbum"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Ver selecionado(s)"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotos"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Álbuns"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Pré-visualizar"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Mudar para trabalho"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}many{<xliff:g id="COUNT_1">^1</xliff:g> itens}other{<xliff:g id="COUNT_1">^1</xliff:g> itens}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Adicionar (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Permitir (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Não permitir nenhuma"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Câmara"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Transferências"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favoritos"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Problema ao reproduzir o vídeo"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Verifique a ligação à Internet e tente novamente"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Tentar novamente"</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="preloading_dialog_title" msgid="4974348221848532887">"A preparar conteúdo multimédia selecionado"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> item(ns) pronto(s)"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Cancelar"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"As fotos com cópia de segurança já estão incluídas"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Pode selecionar fotos da app <xliff:g id="APP_NAME">%1$s</xliff:g> da conta <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Conta da app <xliff:g id="APP_NAME">%1$s</xliff:g> atualizada"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Escolher app"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Escolher conta"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Alterar conta"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"A obter todas as suas fotos"</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?}many{Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> ficheiros 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…}many{A modificar <xliff:g id="COUNT">^1</xliff:g> ficheiro(s) de áudio…}other{A modificar <xliff:g id="COUNT">^1</xliff:g> ficheiro(s) de áudio…}}"</string>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Permitir que a app <xliff:g id="APP_NAME_0">^1</xliff:g> modifique este vídeo?}many{Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> vídeos?}other{Permitir que a app <xliff:g id="APP_NAME_1">^1</xliff:g> modifique <xliff:g id="COUNT">^2</xliff:g> vídeos?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Proteção de segurança"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Alertas de transcodificação nativa"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Progresso de transcodificação nativa"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Tente mais tarde. As suas fotos vão estar disponíveis quando o problema estiver resolvido."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Não é possível carregar algumas fotos"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
 </resources>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index cd37d76..326fde2 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Mídia"</string>
     <string name="storage_description" msgid="4081716890357580107">"Armazenamento local"</string>
-    <string name="app_label" msgid="9035307001052716210">"Armazenamento de mídia"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Mídia"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Seletor de mídia"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artista"</string>
     <string name="unknown" msgid="2059049215682829375">"Desconhecido"</string>
     <string name="root_images" msgid="5861633549189045666">"Imagens"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Acessar a mídia em nuvem de"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Nenhum"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Não foi possível mudar o app de mídia em nuvem."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Seletor de mídia"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Seletor de mídia"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Sincronizando mídia…"</string>
     <string name="add" msgid="2894574044585549298">"Adicionar"</string>
     <string name="deselect" msgid="4297825044827769490">"Desmarcar"</string>
     <string name="deselected" msgid="8488133193326208475">"Desmarcada"</string>
@@ -58,10 +60,12 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Sem álbuns"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Mostrar selecionados"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotos"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Álbuns"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Visualização"</string>
-    <string name="picker_work_profile" msgid="2083221066869141576">"Mudar para \"Trabalho\""</string>
-    <string name="picker_personal_profile" msgid="639484258397758406">"Mudar para \"Pessoal\""</string>
+    <string name="picker_work_profile" msgid="2083221066869141576">"Mudar para Trabalho"</string>
+    <string name="picker_personal_profile" msgid="639484258397758406">"Mudar para Pessoal"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"Bloqueado pelo administrador"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"Não é permitido o acesso a dados de trabalho em um app pessoal"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"Não é permitido o acesso a dados pessoais em um app de trabalho"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}one{<xliff:g id="COUNT_1">^1</xliff:g> item}many{<xliff:g id="COUNT_1">^1</xliff:g> itens}other{<xliff:g id="COUNT_1">^1</xliff:g> itens}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Adicionar (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Permitir (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Não autorizar"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Câmera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Downloads"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favoritos"</string>
@@ -92,10 +97,11 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Ocorreu um problema ao iniciar o vídeo"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Confira sua conexão de Internet e tente de novo"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Tentar novamente"</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="preloading_dialog_title" msgid="4974348221848532887">"Preparando a mídia selecionada"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> itens prontos"</string>
-    <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Fotos salvas em backup agora estão incluídas"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Cancelar"</string>
+    <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"As fotos salvas em backup agora estão incluídas"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Selecione fotos da conta <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> do app <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"A conta do app <xliff:g id="APP_NAME">%1$s</xliff:g> foi atualizada"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"As fotos da conta <xliff:g id="USER_ACCOUNT">%1$s</xliff:g> agora estão incluídas aqui"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Proteção"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Alertas da transcodificação nativa"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Progresso da transcodificação nativa"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Tente de novo mais tarde. Suas fotos vão ficar disponíveis assim que o problema for resolvido."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Não é possível carregar algumas fotos"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Entendi"</string>
 </resources>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index a8137a9..d5306a8 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Conținut media"</string>
     <string name="storage_description" msgid="4081716890357580107">"Stocare locală"</string>
-    <string name="app_label" msgid="9035307001052716210">"Stocarea conținutului media"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Media"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Selector de suport"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artist"</string>
     <string name="unknown" msgid="2059049215682829375">"Necunoscut"</string>
     <string name="root_images" msgid="5861633549189045666">"Imagini"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Accesează conținutul media în cloud din"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Niciuna"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Nu s-a putut schimba aplicația media pentru cloud"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Selector de suport"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Selector de suport"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Se sincronizează conținutul media…"</string>
     <string name="add" msgid="2894574044585549298">"Adaugă"</string>
     <string name="deselect" msgid="4297825044827769490">"Debifează"</string>
     <string name="deselected" msgid="8488133193326208475">"Deselectat"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Niciun album"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Vezi elementele selectate"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotografii"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albume"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Previzualizare"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Comută la serviciu"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> element}few{<xliff:g id="COUNT_1">^1</xliff:g> elemente}other{<xliff:g id="COUNT_1">^1</xliff:g> de elemente}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Adaugă (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Permite (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Nu permite nimic"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Cameră"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Descărcări"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Preferate"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Probleme la redarea videoclipului"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Verifică-ți conexiunea la internet și încearcă din nou"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Încearcă din nou"</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="preloading_dialog_title" msgid="4974348221848532887">"Se pregătește conținutul media selectat"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Finalizate: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> din <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Anulează"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Fotografiile cu backup sunt incluse acum"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Poți selecta fotografii din contul <xliff:g id="APP_NAME">%1$s</xliff:g> <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Contul <xliff:g id="APP_NAME">%1$s</xliff:g> a fost actualizat"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Alege aplicația"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Alege un cont"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Schimbă contul"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Se încarcă toate fotografiile"</string>
     <string name="permission_write_audio" msgid="8819694245323580601">"{count,plural, =1{Permiți ca <xliff:g id="APP_NAME_0">^1</xliff:g> să modifice acest fișier audio?}few{Permiți ca <xliff:g id="APP_NAME_1">^1</xliff:g> să modifice <xliff:g id="COUNT">^2</xliff:g> fișiere audio?}other{Permiț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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Permiți ca <xliff:g id="APP_NAME_0">^1</xliff:g> să modifice acest videoclip?}few{Permiți ca <xliff:g id="APP_NAME_1">^1</xliff:g> să modifice <xliff:g id="COUNT">^2</xliff:g> videoclipuri?}other{Permiți ca <xliff:g id="APP_NAME_1">^1</xliff:g> să modifice <xliff:g id="COUNT">^2</xliff:g> de videoclipuri?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Protecția în caz de accidente"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Alerte privind transcodarea în codul nativ"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Progresul transcodării în codul nativ"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Încearcă din nou mai târziu. Fotografiile tale vor fi disponibile după ce se rezolvă problema."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Unele fotografii nu pot fi încărcate"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
 </resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 87a0859..18d77ea 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Мультимедиа"</string>
     <string name="storage_description" msgid="4081716890357580107">"Локальное хранилище"</string>
-    <string name="app_label" msgid="9035307001052716210">"Хранилище мультимедиа"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Мультимедиа"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Инструмент выбора медиа"</string>
     <string name="artist_label" msgid="8105600993099120273">"Исполнитель"</string>
     <string name="unknown" msgid="2059049215682829375">"Неизвестно"</string>
     <string name="root_images" msgid="5861633549189045666">"Изображения"</string>
@@ -43,9 +42,12 @@
     <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Приложение для мультимедиа в облаке"</string>
     <string name="picker_settings_title" msgid="5647700706470673258">"Приложение для мультимедиа в облаке"</string>
     <string name="picker_settings_description" msgid="2916686824777214585">"Выбирайте свои фото и видео из облака в приложениях или на сайтах."</string>
-    <string name="picker_settings_selection_message" msgid="245453573086488596">"Получите доступ к мультимедиа в облаке"</string>
+    <string name="picker_settings_selection_message" msgid="245453573086488596">"Какое приложение использовать для доступа к медиафайлам в облаке?"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Нет"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Не удалось изменить приложение для мультимедиа."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Инструмент выбора медиа"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Инструмент выбора медиа"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Синхронизация медиаконтента…"</string>
     <string name="add" msgid="2894574044585549298">"Добавить"</string>
     <string name="deselect" msgid="4297825044827769490">"Отменить выбор"</string>
     <string name="deselected" msgid="8488133193326208475">"Выбор отменен"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Альбомов нет."</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Посмотреть выбранное"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Фотографии"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Альбомы"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Предварительный просмотр"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Перейти в рабочий профиль"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> объект}one{<xliff:g id="COUNT_1">^1</xliff:g> объект}few{<xliff:g id="COUNT_1">^1</xliff:g> объекта}many{<xliff:g id="COUNT_1">^1</xliff:g> объектов}other{<xliff:g id="COUNT_1">^1</xliff:g> объекта}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Добавить (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Открыть доступ (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Запретить доступ всем"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Камера"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Скачанные"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Избранное"</string>
@@ -92,10 +97,11 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Не удалось воспроизвести видео"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Проверьте подключение к интернету и повторите попытку."</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Повторить"</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="preloading_dialog_title" msgid="4974348221848532887">"Подготовка выбранных медиафайлов"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Предзагрузка: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> из <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
-    <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Резервные копии фотографий добавлены"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Отмена"</string>
+    <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Теперь можно выбирать фотографии в облаке"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Вы можете выбрать фотографии из аккаунта <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> приложения \"<xliff:g id="APP_NAME">%1$s</xliff:g>\"."</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g>: аккаунт обновлен"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"Фотографии из аккаунта <xliff:g id="USER_ACCOUNT">%1$s</xliff:g> теперь хранятся здесь."</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Выбрать приложение"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Выбрать аккаунт"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Сменить аккаунт"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Ваши фотографии загружаются"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Защита безопасности"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Уведомления нативного перекодирования"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Прогресс нативного перекодирования"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Повторите попытку позже. Ваши фотографии станут доступны после устранения проблемы."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Не удается загрузить некоторые фотографии"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"ОК"</string>
 </resources>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index 53566bb..e7c1b3c 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"මාධ්‍ය"</string>
     <string name="storage_description" msgid="4081716890357580107">"පෙදෙසි ආචයනය"</string>
-    <string name="app_label" msgid="9035307001052716210">"මාධ්‍ය ගබඩාව"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"මාධ්‍ය"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"මාධ්‍ය තෝරනය"</string>
     <string name="artist_label" msgid="8105600993099120273">"කලාකරු"</string>
     <string name="unknown" msgid="2059049215682829375">"නොදනී"</string>
     <string name="root_images" msgid="5861633549189045666">"රූප"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"මෙයින් ක්ලවුඩ් මාධ්‍ය වෙත ප්‍රවේශ වන්න"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"කිසිවක් නැත"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"මෙම අවස්ථාවේ ක්ලවුඩ් මාධ්‍ය යෙදුම වෙනස් කළ නොහැක."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"මාධ්‍ය තෝරනය"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"මාධ්‍ය තෝරනය"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"මාධ්‍ය සමමුහුර්ත කරමින්…"</string>
     <string name="add" msgid="2894574044585549298">"එක් කරන්න"</string>
     <string name="deselect" msgid="4297825044827769490">"නොතෝරන්න"</string>
     <string name="deselected" msgid="8488133193326208475">"නොතෝරන ලද"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"ඇල්බම නැත"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"තෝරා ගත් දේවල් බලන්න"</string>
     <string name="picker_photos" msgid="7415035516411087392">"ඡායාරූප"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"ඇල්බම"</string>
     <string name="picker_preview" msgid="6257414886055861039">"පෙරදසුන"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"කාර්යාලය වෙත මාරු වන්න"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{අයිතම <xliff:g id="COUNT_0">^1</xliff:g>}one{අයිතම <xliff:g id="COUNT_1">^1</xliff:g>}other{අයිතම <xliff:g id="COUNT_1">^1</xliff:g>}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"එක් කරන්න (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"ඉඩ දෙන්න (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"කිසිවකට ඉඩ නොදෙන්න"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"කැමරාව"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"බාගැනීම්"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"ප්‍රියතමයන්"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"වීඩියෝව වාදනය කිරීමේ ගැටලුවකි"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"ඔබේ අන්තර්ජාල සබැඳුම පරීක්ෂා කර නැවත උත්සාහ කරන්න"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"යළි උත්සාහ කරන්න"</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="preloading_dialog_title" msgid="4974348221848532887">"ඔබ තෝරන ලද මාධ්‍ය සූදානම් කරමින්"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>කින් <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>ක් සූදානම්"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"අවලංගු කරන්න"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"උපස්ථ කළ ඡායාරූප දැන් ඇතුළත් කර ඇත"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"ඔබට <xliff:g id="APP_NAME">%1$s</xliff:g> ගිණුමෙන් <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> ඡායාරූප තෝරා ගත හැක"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> ගිණුම යාවත්කාලීන විය"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"යෙදුම තෝරා ගන්න"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"ගිණුම තෝරා ගන්න"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"ගිණුම වෙනස් කරන්න"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"ඔබේ සියලු ඡායාරූප ලබා ගැනීම"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"සුරක්ෂිතතා ආරක්ෂණය"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"සහජ ට්‍රාන්ස්කෝඩ් ඇඟවීම්"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"සහජ ට්‍රාන්ස්කෝඩ් ප්‍රගතිය"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"පසුව නැවත උත්සාහ කරන්න. ගැටලුව විසඳූ පසු ඔබේ ඡායාරූප ලබා ගත හැකි වනු ඇත."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"සමහර ඡායාරූප පූරණය කළ නොහැක"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"තේරුණා"</string>
 </resources>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index 0c28291..b77206a 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Médiá"</string>
     <string name="storage_description" msgid="4081716890357580107">"Miestne úložisko"</string>
-    <string name="app_label" msgid="9035307001052716210">"Úložisko médií"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Médiá"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Nástroj na výber médií"</string>
     <string name="artist_label" msgid="8105600993099120273">"Interpret"</string>
     <string name="unknown" msgid="2059049215682829375">"Neznáme"</string>
     <string name="root_images" msgid="5861633549189045666">"Obrázky"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Získavať prístup k médiám v cloude v aplikácii"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Žiadne"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Momentálne sa nepodarilo zmeniť cloudový prehrávač"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Nástroj na výber médií"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Nástroj na výber médií"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Synchronizujú sa médiá…"</string>
     <string name="add" msgid="2894574044585549298">"Pridať"</string>
     <string name="deselect" msgid="4297825044827769490">"Zrušiť výber"</string>
     <string name="deselected" msgid="8488133193326208475">"Výber bol zrušený"</string>
@@ -58,20 +60,23 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Žiadne albumy"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Zobraziť vybrané"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotky"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albumy"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Ukážka"</string>
-    <string name="picker_work_profile" msgid="2083221066869141576">"Prepnúť na pracovný"</string>
-    <string name="picker_personal_profile" msgid="639484258397758406">"Prepnúť na osobný"</string>
+    <string name="picker_work_profile" msgid="2083221066869141576">"Prepnúť na pracovný profil"</string>
+    <string name="picker_personal_profile" msgid="639484258397758406">"Prepnúť na osobný profil"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"Blokované vaším správcom"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"Prístup k pracovným údajom z osobnej aplikácie nie je povolený"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"Prístup k osobným údajom z pracovnej aplikácie nie je povolený"</string>
     <string name="picker_profile_work_paused_title" msgid="382212880704235925">"Pracovné aplikácie sú pozastavené"</string>
     <string name="picker_profile_work_paused_msg" msgid="6321552322125246726">"Ak chcete otvoriť pracovné fotky, zapnite pracovné aplikácie a skúste to znova"</string>
-    <string name="picker_privacy_message" msgid="9132700451027116817">"Táto aplikácia môže mať prístup iba k fotkám, ktoré vyberiete"</string>
+    <string name="picker_privacy_message" msgid="9132700451027116817">"Táto aplikácia má prístup iba k fotkám, ktoré vyberiete"</string>
     <string name="picker_header_permissions" msgid="675872774407768495">"Vyberte fotky a videá, ku ktorým má mať táto aplikácia prístup"</string>
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> položka}few{<xliff:g id="COUNT_1">^1</xliff:g> položky}many{<xliff:g id="COUNT_1">^1</xliff:g> items}other{<xliff:g id="COUNT_1">^1</xliff:g> položiek}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Pridať (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Povoliť (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Nepovoliť žiadne"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Stiahnuté"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Obľúbené"</string>
@@ -92,11 +97,12 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Ťažkosti s prehrávaním videa"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Skontrolujte internetové pripojenie a skúste to znova"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Skúsiť znova"</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="preloading_dialog_title" msgid="4974348221848532887">"Pripravujú sa vybrané médiá"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Pripravené: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> z <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Zrušiť"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Zálohované fotky sú teraz zahrnuté"</string>
-    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Môžete vybrať fotky z účtu <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> aplikácie <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Môžete vyberať fotky z účtu <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> aplikácie <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Účet <xliff:g id="APP_NAME">%1$s</xliff:g> bol aktualizovaný"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"Odteraz sú tu zahrnuté fotky z účtu <xliff:g id="USER_ACCOUNT">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_choose_app_title" msgid="3165966147547974251">"Vyberte cloudovú aplikáciu s médiami"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Bezpečnosť"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Upozornenia natívneho prekódovania"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Postup natívneho prekódovania"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Skúste to neskôr. Po vyriešení problému budú vaše fotky k dispozícii."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Niektoré fotky sa nedajú načítať"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Dobre"</string>
 </resources>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index 1e9d72e..119ad24 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Predstavnost"</string>
     <string name="storage_description" msgid="4081716890357580107">"Lokalna shramba"</string>
-    <string name="app_label" msgid="9035307001052716210">"Shramba za predstavnost"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Predstavnost"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Orodje za izbiranje predstavnosti"</string>
     <string name="artist_label" msgid="8105600993099120273">"Izvajalec"</string>
     <string name="unknown" msgid="2059049215682829375">"Neznano"</string>
     <string name="root_images" msgid="5861633549189045666">"Slike"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Dostop do predstavnosti v oblaku v storitvi"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Brez"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Zamenjava aplikacije za predstavnost v oblaku trenutno ni mogoča."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Orodje za izbiranje predstavnosti"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Orodje za izbiranje predstavnosti"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Sinhroniziranje predstavnosti …"</string>
     <string name="add" msgid="2894574044585549298">"Dodaj"</string>
     <string name="deselect" msgid="4297825044827769490">"Počisti izbiro"</string>
     <string name="deselected" msgid="8488133193326208475">"Izbor je preklican"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Ni albumov."</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Prikaži izbrano"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotografije"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albumi"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Predogled"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Preklop na delovni profil"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> element}one{<xliff:g id="COUNT_1">^1</xliff:g> element}two{<xliff:g id="COUNT_1">^1</xliff:g> elementa}few{<xliff:g id="COUNT_1">^1</xliff:g> elementi}other{<xliff:g id="COUNT_1">^1</xliff:g> elementov}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Dodaj (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Dovoli (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Dovoli brez izbire"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Fotoaparat"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Prenosi"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Priljubljeno"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Težave pri predvajanju videoposnetka"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Preverite internetno povezavo in poskusite znova."</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Poskusi znova"</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="preloading_dialog_title" msgid="4974348221848532887">"Pripravljanje izbranih predstavnostnih vsebin"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Pripravljenih: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> od <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Prekliči"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Varnostno kopirane fotografije so zdaj vključene"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Izberete lahko fotografije iz računa <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> za aplikacijo <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Račun za aplikacijo <xliff:g id="APP_NAME">%1$s</xliff:g> je posodobljen"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Varnostna zaščita"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Opozorila o izvornem prekodiranju"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Napredek izvornega prekodiranja"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Poskusite znova pozneje. Fotografije bodo na voljo, ko bo težava odpravljena."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Nekaterih fotografij ni mogoče naložiti"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Razumem"</string>
 </resources>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index 95870a4..a8e5c18 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Media"</string>
     <string name="storage_description" msgid="4081716890357580107">"Hapësira ruajtëse lokale"</string>
-    <string name="app_label" msgid="9035307001052716210">"Hapësira ruajtëse e medias"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Media"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Zgjedhësi i medias"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artisti"</string>
     <string name="unknown" msgid="2059049215682829375">"I panjohur"</string>
     <string name="root_images" msgid="5861633549189045666">"Fotografitë"</string>
@@ -42,10 +41,13 @@
     <string name="picker_settings" msgid="6443463167344790260">"Aplikacioni i medias në renë kompjuterike"</string>
     <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Aplikacion i medias në renë kompjuterike"</string>
     <string name="picker_settings_title" msgid="5647700706470673258">"Aplikacioni i medias në renë kompjuterike"</string>
-    <string name="picker_settings_description" msgid="2916686824777214585">"Qasu te media jote në renë kompjuterike kur një aplikacion ose sajt uebi të kërkon të zgjedhësh fotografitë ose videot"</string>
+    <string name="picker_settings_description" msgid="2916686824777214585">"Qasu te media jote në renë kompjuterike kur një aplikacion ose uebsajt të kërkon të zgjedhësh fotografitë ose videot"</string>
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Qasu te media në renë kompjuterike nga"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Asnjë"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Aplikacioni i medias në renë kompjuterike nuk mund të ndryshohej në këtë moment."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Zgjedhësi i medias"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Zgjedhësi i medias"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Media po sinkronizohet…"</string>
     <string name="add" msgid="2894574044585549298">"Shto"</string>
     <string name="deselect" msgid="4297825044827769490">"Hiq përzgjedhjen"</string>
     <string name="deselected" msgid="8488133193326208475">"Zgjedhja është hequr"</string>
@@ -58,10 +60,12 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Nuk ka albume"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Shiko të zgjedhurat"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotografitë"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albumet"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Pamja paraprake"</string>
-    <string name="picker_work_profile" msgid="2083221066869141576">"Ndryshoje te puna"</string>
-    <string name="picker_personal_profile" msgid="639484258397758406">"Ndryshoje te personale"</string>
+    <string name="picker_work_profile" msgid="2083221066869141576">"Kalo te profili i punës"</string>
+    <string name="picker_personal_profile" msgid="639484258397758406">"Kalo te profili personal"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"Bllokuar nga administratori yt"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"Qasja e të dhënave të punës nga një aplikacion personal nuk lejohet"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"Qasja e të dhënave personale nga një aplikacion pune nuk lejohet"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> artikull}other{<xliff:g id="COUNT_1">^1</xliff:g> artikuj}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Shto (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Lejo (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Mos lejo asnjë"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Shkarkimet"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Të preferuarat"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Problem me luajtjen e videos"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Kontrollo lidhjen e internetit dhe provo përsëri"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Riprovo"</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="preloading_dialog_title" msgid="4974348221848532887">"Media e zgjedhur po përgatitet"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> nga <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> gati"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Anulo"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Fotografitë e rezervuara tani janë të përfshira"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Mund të zgjedhësh fotografi nga llogaria e<xliff:g id="USER_ACCOUNT">%2$s</xliff:g> në <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Llogaria e <xliff:g id="APP_NAME">%1$s</xliff:g> u përditësua"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Zgjidh aplikacionin"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Zgjidh llogarinë"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Ndrysho llogarinë"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Po merren të gjitha fotografitë e tua"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Të lejohet <xliff:g id="APP_NAME_0">^1</xliff:g> që ta modifikojë këtë video?}other{Të lejohet <xliff:g id="APP_NAME_1">^1</xliff:g> që të modifikojë <xliff:g id="COUNT">^2</xliff:g> video?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Mbrojtja e sigurisë"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Sinjalizimet e transkodimit origjinal"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Progresi i transkodimit origjinal"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Provo sërish më vonë. Fotografitë e tua do të ofrohen pasi të zgjidhet problemi."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Disa fotografi nuk mund të ngarkohen"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"E kuptova"</string>
 </resources>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 4c3f2bf..bda0c5f 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Медији"</string>
     <string name="storage_description" msgid="4081716890357580107">"Локални меморијски простор"</string>
-    <string name="app_label" msgid="9035307001052716210">"Меморијски простор за медије"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Медији"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Бирач медија"</string>
     <string name="artist_label" msgid="8105600993099120273">"Извођач"</string>
     <string name="unknown" msgid="2059049215682829375">"Непознато"</string>
     <string name="root_images" msgid="5861633549189045666">"Слике"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Приступајте медијима у клауду из"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Ништа"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Промена апликације за медије у клауду није успела."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Бирач медија"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Бирач медија"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Медији се синхронизују…"</string>
     <string name="add" msgid="2894574044585549298">"Додај"</string>
     <string name="deselect" msgid="4297825044827769490">"Опозови избор"</string>
     <string name="deselected" msgid="8488133193326208475">"Опозван је избор"</string>
@@ -53,11 +55,13 @@
     <string name="selected" msgid="9151797369975828124">"Изабрано"</string>
     <string name="select_up_to" msgid="6994294169508439957">"{count,plural, =1{Изаберите највише <xliff:g id="COUNT_0">^1</xliff:g> ставку}one{Изаберите највише <xliff:g id="COUNT_1">^1</xliff:g> ставку}few{Изаберите највише <xliff:g id="COUNT_1">^1</xliff:g> ставке}other{Изаберите највише <xliff:g id="COUNT_1">^1</xliff:g> ставки}}"</string>
     <string name="recent" msgid="6694613584743207874">"Недавно"</string>
-    <string name="picker_photos_empty_message" msgid="5980619500554575558">"Нема слика нити видео снимака"</string>
-    <string name="picker_album_media_empty_message" msgid="7061850698189881671">"Нема подржаних слика нити видео снимака"</string>
+    <string name="picker_photos_empty_message" msgid="5980619500554575558">"Нема слика нити видеа"</string>
+    <string name="picker_album_media_empty_message" msgid="7061850698189881671">"Нема подржаних слика нити видеа"</string>
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Нема албума"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Прикажи изабранo"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Слике"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Албуми"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Преглед"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Пређи на пословни профил"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ставка}one{<xliff:g id="COUNT_1">^1</xliff:g> ставка}few{<xliff:g id="COUNT_1">^1</xliff:g> ставке}other{<xliff:g id="COUNT_1">^1</xliff:g> ставки}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Додај (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Дозволи (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Не дозволи ниједну"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Камера"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Преузето"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Омиљено"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Дошло је до грешке при пуштању видеа"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Проверите интернет везу и пробајте поново"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Пробај поново"</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="preloading_dialog_title" msgid="4974348221848532887">"Припремају се одабрани медијски фајлови"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Спремно:<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> од <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Откажи"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Сада су уврштене резервне копије слика"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Можете да изаберете слике са налога <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> за <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Налог за <xliff:g id="APP_NAME">%1$s</xliff:g> је ажуриран"</string>
@@ -107,36 +113,35 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Одабери апликацију"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Одабери налог"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Промени налог"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Преузимају се све слике"</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>
-    <string name="permission_write_video" msgid="103902551603700525">"{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_video" msgid="7014908418349819148">"{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>
+    <string name="permission_write_video" msgid="103902551603700525">"{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_video" msgid="7014908418349819148">"{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>
     <string name="permission_write_image" msgid="3518991791620523786">"{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_image" msgid="3623580315590025262">"{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>
     <string name="permission_write_generic" msgid="7431128739233656991">"{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_generic" msgid="2806560971318391443">"{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>
     <string name="permission_trash_audio" msgid="6554672354767742206">"{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_trash_audio" msgid="3116279868733641329">"{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>
-    <string name="permission_trash_video" msgid="7555850843259959642">"{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_trash_video" msgid="4637821778329459681">"{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>
+    <string name="permission_trash_video" msgid="7555850843259959642">"{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_trash_video" msgid="4637821778329459681">"{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>
     <string name="permission_trash_image" msgid="3333128084684156675">"{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_trash_image" msgid="3063857679090024764">"{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>
     <string name="permission_trash_generic" msgid="5545420534785075362">"{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_trash_generic" msgid="7815124979717814057">"{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>
     <string name="permission_untrash_audio" msgid="8404597563284002472">"{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_untrash_audio" msgid="2775372344946464508">"{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>
-    <string name="permission_untrash_video" msgid="3178914827607608162">"{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_untrash_video" msgid="5500929409733841567">"{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>
+    <string name="permission_untrash_video" msgid="3178914827607608162">"{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_untrash_video" msgid="5500929409733841567">"{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>
     <string name="permission_untrash_image" msgid="3397523279351032265">"{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_untrash_image" msgid="5295061520504846264">"{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>
     <string name="permission_untrash_generic" msgid="2118366929431671046">"{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_untrash_generic" msgid="1489511601966842579">"{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>
     <string name="permission_delete_audio" msgid="3326674742892796627">"{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_delete_audio" msgid="1734871539021696401">"{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>
-    <string name="permission_delete_video" msgid="604024971828349279">"{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_delete_video" msgid="1846702435073793157">"{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>
+    <string name="permission_delete_video" msgid="604024971828349279">"{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_delete_video" msgid="1846702435073793157">"{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>
     <string name="permission_delete_image" msgid="3109056012794330510">"{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_delete_image" msgid="8580517204901148906">"{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>
     <string name="permission_delete_generic" msgid="7891939881065520271">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Сигурносна заштита"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Обавештења о основном транскодирању"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Ток основног транскодирања"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Пробајте поново касније. Слике ће бити доступне када се проблем реши."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Учитавање неких слика није успело"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Важи"</string>
 </resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 1031806..4ebe034 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Media"</string>
     <string name="storage_description" msgid="4081716890357580107">"Lokal lagring"</string>
-    <string name="app_label" msgid="9035307001052716210">"Medialagring"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Media"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Medieväljaren"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artist"</string>
     <string name="unknown" msgid="2059049215682829375">"Okänd"</string>
     <string name="root_images" msgid="5861633549189045666">"Bilder"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Få tillgång till media i molnet från"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Inga"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Det går inte att byta molnmedieapp just nu."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Medieväljaren"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Medieväljaren"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Synkroniserar media …"</string>
     <string name="add" msgid="2894574044585549298">"Lägg till"</string>
     <string name="deselect" msgid="4297825044827769490">"Avmarkera"</string>
     <string name="deselected" msgid="8488133193326208475">"Avmarkerad"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Inga album"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Visa valda"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Foton"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Album"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Förhandsgranska"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Byt till jobbprofilen"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> objekt}other{<xliff:g id="COUNT_1">^1</xliff:g> objekt}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Lägg till (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Tillåt (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Tillåt inga"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Nedladdningar"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favoriter"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Det gick inte att spela upp videon"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Kontrollera internetanslutningen och försök igen"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Försök igen"</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="preloading_dialog_title" msgid="4974348221848532887">"Din valda media förbereds"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> av <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> är redo"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Avbryt"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Säkerhetskopierade foton tas nu med"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Du kan välja foton från <xliff:g id="APP_NAME">%1$s</xliff:g>-kontot <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g>-kontot har uppdaterats"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Välj app"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Välj konto"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Byt konto"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Läser in alla dina foton"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Vill du tillåta att <xliff:g id="APP_NAME_0">^1</xliff:g> ändrar den här videon?}other{Vill du tillåta att <xliff:g id="APP_NAME_1">^1</xliff:g> ändrar <xliff:g id="COUNT">^2</xliff:g> videor?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Säkerhet"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Omkodningsvarningar för Native"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Omkodningsförlopp för Native"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Försök igen senare. Dina foton blir tillgängliga när problemet har lösts."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Det gick inte att läsa in vissa foton"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
 </resources>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index a3f016f..67f8103 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Maudhui"</string>
     <string name="storage_description" msgid="4081716890357580107">"Hifadhi ya ndani"</string>
-    <string name="app_label" msgid="9035307001052716210">"Hifadhi ya Maudhui"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Maudhui"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Kiteua maudhui"</string>
     <string name="artist_label" msgid="8105600993099120273">"Msanii"</string>
     <string name="unknown" msgid="2059049215682829375">"Isiyojulikana"</string>
     <string name="root_images" msgid="5861633549189045666">"Picha"</string>
@@ -39,13 +38,16 @@
     <string name="allow" msgid="8885707816848569619">"Ruhusu"</string>
     <string name="deny" msgid="6040983710442068936">"Kataa"</string>
     <string name="picker_browse" msgid="5554477454636075934">"Vinjari…"</string>
-    <string name="picker_settings" msgid="6443463167344790260">"Programu ya maudhui ya Wingu"</string>
-    <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Programu ya maudhui ya Wingu"</string>
-    <string name="picker_settings_title" msgid="5647700706470673258">"Programu ya maudhui ya kwenye wingu"</string>
+    <string name="picker_settings" msgid="6443463167344790260">"Programu ya maudhui ya wingu"</string>
+    <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Programu ya maudhui ya wingu"</string>
+    <string name="picker_settings_title" msgid="5647700706470673258">"Programu ya maudhui ya wingu"</string>
     <string name="picker_settings_description" msgid="2916686824777214585">"Fikia maudhui kwenye wingu lako programu au tovuti inapokuomba uchague picha au video"</string>
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Fikia maudhui ya kwenye wingu katika"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Hamna"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Imeshindwa kubadilisha programu ya maudhui ya wingu kwa wakati huu."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Kiteua maudhui"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Kiteua maudhui"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Inasawazisha maudhui…"</string>
     <string name="add" msgid="2894574044585549298">"Weka"</string>
     <string name="deselect" msgid="4297825044827769490">"Acha kuchagua"</string>
     <string name="deselected" msgid="8488133193326208475">"Umeacha kuchagua"</string>
@@ -58,10 +60,12 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Hakuna albamu"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Angalia ulizochagua"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Picha"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albamu"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Onyesho la kukagua"</string>
-    <string name="picker_work_profile" msgid="2083221066869141576">"Badili uweke wasifu wa kazini"</string>
-    <string name="picker_personal_profile" msgid="639484258397758406">"Badili uweke wasifu wa binafsi"</string>
+    <string name="picker_work_profile" msgid="2083221066869141576">"Badili utumie wasifu wa kazini"</string>
+    <string name="picker_personal_profile" msgid="639484258397758406">"Badili utumie wasifu wa binafsi"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"Umezuiwa na msimamizi wako"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"Huruhusiwi kufikia data ya kazini kwenye programu ya binafsi"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"Huruhusiwi kufikia data binafsi kwenye programu ya kazini"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{Kipengee <xliff:g id="COUNT_0">^1</xliff:g>}other{Vipengee <xliff:g id="COUNT_1">^1</xliff:g>}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Weka (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Ruhusu (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Usiruhusu yoyote"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Vipakuliwa"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Vipendwa"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Tatizo limetokea wakati wa kucheza video"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Angalia muunganisho wako wa intaneti na ujaribu tena"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Jaribu tena"</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="preloading_dialog_title" msgid="4974348221848532887">"Inaandaa maudhui yako uliyochagua"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> kati ya <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ziko tayari"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Ghairi"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Picha zilizohifadhiwa nakala zimejumuishwa sasa"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Unaweza kuchagua picha zilizotoka kwenye akaunti ya <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> katika programu ya <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Umesasisha akaunti ya <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Chagua programu"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Chagua akaunti"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Badilisha akaunti"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Inapakia picha zako zote"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Ungependa kuruhusu <xliff:g id="APP_NAME_0">^1</xliff:g> ibadilishe video hii?}other{Ungependa kuruhusu <xliff:g id="APP_NAME_1">^1</xliff:g> ibadilishe video <xliff:g id="COUNT">^2</xliff:g>?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Ulinzi wa Usalama"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Arifa za Ubadilishaji Asilia wa Muundo wa Faili"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Maendeleo ya Ubadilishaji Asilia wa Muundo wa Faili"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Jaribu tena baadaye. Picha zako zitapatikana mara tu tatizo litakapotatuliwa."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Imeshindwa kupakia baadhi ya Picha"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Nimeelewa"</string>
 </resources>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index 5f29ea0..d53729d 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"மீடியா"</string>
     <string name="storage_description" msgid="4081716890357580107">"சாதனச் சேமிப்பகம்"</string>
-    <string name="app_label" msgid="9035307001052716210">"மீடியா சேமிப்பிடம்"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"மீடியா"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"மீடியா தேர்வுக் கருவி"</string>
     <string name="artist_label" msgid="8105600993099120273">"கலைஞர்"</string>
     <string name="unknown" msgid="2059049215682829375">"அறியாதது"</string>
     <string name="root_images" msgid="5861633549189045666">"Images"</string>
@@ -46,18 +45,23 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"கிளவுட் மீடியாவை இதிலிருந்து அணுகும்"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"எதுவுமில்லை"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"இப்போது கிளவுடு மீடியா ஆப்ஸை மாற்ற முடியாது"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"மீடியா தேர்வுக் கருவி"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"மீடியா தேர்வுக் கருவி"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"மீடியாவை ஒத்திசைக்கிறது…"</string>
     <string name="add" msgid="2894574044585549298">"சேர்"</string>
     <string name="deselect" msgid="4297825044827769490">"தேர்வுநீக்கு"</string>
     <string name="deselected" msgid="8488133193326208475">"தேர்வுநீக்கப்பட்டது"</string>
     <string name="select" msgid="2704765470563027689">"தேர்ந்தெடு"</string>
     <string name="selected" msgid="9151797369975828124">"தேர்ந்தெடுக்கப்பட்டது"</string>
-    <string name="select_up_to" msgid="6994294169508439957">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> படத்தைத் தேர்ந்தெடுங்கள்}other{<xliff:g id="COUNT_1">^1</xliff:g> படங்களைத் தேர்ந்தெடுங்கள்}}"</string>
+    <string name="select_up_to" msgid="6994294169508439957">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> படத்தைத் தேர்ந்தெடுங்கள்}other{<xliff:g id="COUNT_1">^1</xliff:g> படங்கள் வரை தேர்ந்தெடுங்கள்}}"</string>
     <string name="recent" msgid="6694613584743207874">"சமீபத்தியவை"</string>
     <string name="picker_photos_empty_message" msgid="5980619500554575558">"படங்களோ வீடியோக்களோ இல்லை"</string>
     <string name="picker_album_media_empty_message" msgid="7061850698189881671">"ஆதரிக்கப்படும் படங்களோ வீடியோக்களோ இல்லை"</string>
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"ஆல்பங்கள் இல்லை"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"தேர்ந்தெடுத்ததைக் காட்டு"</string>
     <string name="picker_photos" msgid="7415035516411087392">"படங்கள்"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"ஆல்பங்கள்"</string>
     <string name="picker_preview" msgid="6257414886055861039">"மாதிரிக்காட்சி"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"பணிச் சுயவிவரத்திற்கு மாறு"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ஆவணம்}other{<xliff:g id="COUNT_1">^1</xliff:g> ஆவணங்கள்}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>) படங்களைச் சேர்"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"அனுமதி (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"எதையும் அனுமதிக்காதே"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"கேமரா"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"பதிவிறக்கங்கள்"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"பிடித்தவை"</string>
@@ -92,11 +97,12 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"வீடியோவைப் பிளே செய்வதில் சிக்கல்"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"உங்கள் இணைய இணைப்பைச் சரிபார்த்துவிட்டு மீண்டும் முயலவும்"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"மீண்டும் முயல்க"</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="preloading_dialog_title" msgid="4974348221848532887">"நீங்கள் தேர்ந்தெடுத்த மீடியா தயாராகிறது"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> / <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> தயாராக உள்ளது"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"ரத்துசெய்"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"காப்புப் பிரதி எடுக்கப்பட்ட படங்கள் இப்போது சேர்க்கப்பட்டுள்ளன"</string>
-    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"<xliff:g id="APP_NAME">%1$s</xliff:g> ஆப்ஸிலிருந்தும் <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> கணக்கிலிருந்தும் படங்களைத் தேர்ந்தெடுக்கலாம்"</string>
+    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"<xliff:g id="APP_NAME">%1$s</xliff:g> ஆப்ஸில் <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> கணக்கிலிருந்தும் படங்களைத் தேர்ந்தெடுக்கலாம்"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> கணக்கு புதுப்பிக்கப்பட்டது"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"<xliff:g id="USER_ACCOUNT">%1$s</xliff:g> கணக்கிலிருந்த படங்களும் இப்போது சேர்க்கப்பட்டுள்ளன"</string>
     <string name="picker_banner_cloud_choose_app_title" msgid="3165966147547974251">"கிளவுட் மீடியா ஆப்ஸைத் தேர்வுசெய்தல்"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"ஆப்ஸைத் தேர்வுசெய்க"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"கணக்கைத் தேர்வுசெய்க"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"கணக்கை மாற்று"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"உங்கள் படங்கள் அனைத்தையும் பெறுகிறது"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"பாதுகாப்பு வளையம்"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"நேட்டிவ் குறிமாற்ற விழிப்பூட்டல்கள்"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"நேட்டிவ் குறிமாற்றச் செயல்நிலை"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"பிறகு மீண்டும் முயலவும். சிக்கல் சரியானதும் உங்கள் படங்கள் கிடைக்கும்."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"சில படங்களை ஏற்ற முடியவில்லை"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"சரி"</string>
 </resources>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 831eb4a..3e8469b 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"మీడియా"</string>
     <string name="storage_description" msgid="4081716890357580107">"స్థానిక స్టోరేజ్‌"</string>
-    <string name="app_label" msgid="9035307001052716210">"మీడియా స్టోరేజ్‌"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"మీడియా"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"మీడియా సెలెక్టర్"</string>
     <string name="artist_label" msgid="8105600993099120273">"కళాకారుడు"</string>
     <string name="unknown" msgid="2059049215682829375">"తెలియదు"</string>
     <string name="root_images" msgid="5861633549189045666">"ఇమేజ్‌లు"</string>
@@ -46,9 +45,12 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"దాని నుండి క్లౌడ్ మీడియాను యాక్సెస్ చేయండి"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"ఏవీ లేవు"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"ఈ సమయంలో క్లౌడ్ మీడియా యాప్ మార్చడం సాధ్యపడలేదు."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"మీడియా సెలెక్టర్"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"మీడియా సెలెక్టర్"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"మీడియాను సింక్ చేస్తోంది…"</string>
     <string name="add" msgid="2894574044585549298">"జోడించండి"</string>
     <string name="deselect" msgid="4297825044827769490">"ఎంపికను తొలగించండి"</string>
-    <string name="deselected" msgid="8488133193326208475">"ఎంపికను తొలగించండి"</string>
+    <string name="deselected" msgid="8488133193326208475">"ఎంపిక తొలగించబడింది"</string>
     <string name="select" msgid="2704765470563027689">"ఎంచుకోండి"</string>
     <string name="selected" msgid="9151797369975828124">"ఎంచుకోబడింది"</string>
     <string name="select_up_to" msgid="6994294169508439957">"{count,plural, =1{గరిష్ఠంగా <xliff:g id="COUNT_0">^1</xliff:g> ఐటెమ్‌ను ఎంచుకోండి}other{గరిష్ఠంగా <xliff:g id="COUNT_1">^1</xliff:g> ఐటెమ్‌లను ఎంచుకోండి}}"</string>
@@ -58,9 +60,11 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"ఆల్బమ్‌లు ఏవీ లేవు"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"ఎంచుకున్న వాటిని చూడండి"</string>
     <string name="picker_photos" msgid="7415035516411087392">"ఫోటోలు"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"ఆల్బమ్‌లు"</string>
     <string name="picker_preview" msgid="6257414886055861039">"ప్రివ్యూ"</string>
-    <string name="picker_work_profile" msgid="2083221066869141576">"ఆఫీస్ ప్రొఫైల్‌కు మార్చండి"</string>
+    <string name="picker_work_profile" msgid="2083221066869141576">"వర్క్ ప్రొఫైల్‌కు మార్చండి"</string>
     <string name="picker_personal_profile" msgid="639484258397758406">"వ్యక్తిగత ప్రొఫైల్‌కు మార్చండి"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"మీ అడ్మిన్ బ్లాక్ చేశారు"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"వ్యక్తిగత యాప్ నుండి వర్క్ డేటాను యాక్సెస్ చేయడం అనుమతించబడదు"</string>
@@ -68,10 +72,11 @@
     <string name="picker_profile_work_paused_title" msgid="382212880704235925">"వర్క్ యాప్‌లు పాజ్ చేయబడ్డాయి"</string>
     <string name="picker_profile_work_paused_msg" msgid="6321552322125246726">"వర్క్ ఫోటోలను తెరవడానికి, మీ వర్క్ యాప్‌లను ఆన్ చేసి, ఆపై మళ్లీ ట్రై చేయండి"</string>
     <string name="picker_privacy_message" msgid="9132700451027116817">"ఈ యాప్ మీరు ఎంచుకున్న ఫోటోలను మాత్రమే యాక్సెస్ చేయగలదు"</string>
-    <string name="picker_header_permissions" msgid="675872774407768495">"ఏ ఫోటోలు, వీడియోలను ఈ యాప్ యాక్సెస్ చేయవచ్చు అని మీరు అనుకుంటున్నారో వాటిని ఎంచుకోండి"</string>
+    <string name="picker_header_permissions" msgid="675872774407768495">"ఈ యాప్‌, యాక్సెస్ చేయడానికి మీరు అనుమతించే ఫోటోలను, వీడియోలను ఎంచుకోండి"</string>
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ఐటెమ్}other{<xliff:g id="COUNT_1">^1</xliff:g> ఐటెమ్‌లు}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"జోడించండి (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"అనుమతించండి (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"వేటినీ అనుమతించవద్దు"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"కెమెరా"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"డౌన్‌లోడ్‌లు"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"ఫేవరెట్స్"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"వీడియోను ప్లే చేయడంలో సమస్య ఏర్పడింది"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"మీ ఇంటర్నెట్ కనెక్షన్‌ను చెక్ చేసి, మళ్ళీ ట్రై చేయండి"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"మళ్లీ ట్రై చేయండి"</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="preloading_dialog_title" msgid="4974348221848532887">"మీరు ఎంచుకున్న మీడియాను సిద్ధం చేస్తోంది"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>లో <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> సిద్ధంగా ఉన్నాయి"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"రద్దు చేయండి"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"బ్యాకప్ చేసిన ఫోటోలు ఇప్పుడు చేర్చబడ్డాయి"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"మీరు <xliff:g id="APP_NAME">%1$s</xliff:g> ఖాతా <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> నుండి ఫోటోలను ఎంచుకోవచ్చు"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> ఖాతా అప్‌డేట్ చేయబడింది"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"భద్రత రక్షణ"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"స్థానిక ట్రాన్స్‌కోడ్ అలర్ట్‌లు"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"స్థానిక ట్రాన్స్‌కోడ్ ప్రోగ్రెస్"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"తర్వాత మళ్లీ ట్రై చేయండి. సమస్య పరిష్కరించబడిన తర్వాత మీ ఫోటోలు అందుబాటులో ఉంటాయి."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"కొన్ని ఫోటోలను లోడ్ చేయడం సాధ్యపడదు"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"సరే"</string>
 </resources>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 1b4e5d3..a2afd2a 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"สื่อ"</string>
     <string name="storage_description" msgid="4081716890357580107">"พื้นที่เก็บข้อมูลในเครื่อง"</string>
-    <string name="app_label" msgid="9035307001052716210">"พื้นที่เก็บข้อมูลสื่อ"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"สื่อ"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"เครื่องมือเลือกสื่อ"</string>
     <string name="artist_label" msgid="8105600993099120273">"ศิลปิน"</string>
     <string name="unknown" msgid="2059049215682829375">"ไม่ทราบ"</string>
     <string name="root_images" msgid="5861633549189045666">"รูปภาพ"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"เข้าถึงสื่อในระบบคลาวด์จาก"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"ไม่มี"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"เปลี่ยนแอปสื่อบนระบบคลาวด์ไม่ได้ในขณะนี้"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"เครื่องมือเลือกสื่อ"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"เครื่องมือเลือกสื่อ"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"กำลังซิงค์สื่อ…"</string>
     <string name="add" msgid="2894574044585549298">"เพิ่ม"</string>
     <string name="deselect" msgid="4297825044827769490">"ยกเลิกการเลือก"</string>
     <string name="deselected" msgid="8488133193326208475">"ยกเลิกการเลือก"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"ไม่มีอัลบั้ม"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"ดูรายการที่เลือก"</string>
     <string name="picker_photos" msgid="7415035516411087392">"รูปภาพ"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"อัลบั้ม"</string>
     <string name="picker_preview" msgid="6257414886055861039">"ตัวอย่าง"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"เปลี่ยนไปใช้โปรไฟล์งาน"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> รายการ}other{<xliff:g id="COUNT_1">^1</xliff:g> รายการ}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"เพิ่ม (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"อนุญาต (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"ไม่อนุญาต"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"กล้อง"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"การดาวน์โหลด"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"รายการโปรด"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"เกิดปัญหาขณะเล่นวิดีโอ"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"ตรวจสอบการเชื่อมต่ออินเทอร์เน็ตและลองอีกครั้ง"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"ลองใหม่"</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="preloading_dialog_title" msgid="4974348221848532887">"กำลังเตรียมสื่อที่คุณเลือก"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"พร้อมแล้ว <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> จาก <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"ยกเลิก"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"รวมรูปภาพที่สำรองข้อมูลไว้แล้ว"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"คุณเลือกรูปภาพได้จากบัญชี <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> ของ \"<xliff:g id="APP_NAME">%1$s</xliff:g>\""</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"อัปเดตบัญชี \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" แล้ว"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"เลือกแอป"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"เลือกบัญชี"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"เปลี่ยนบัญชี"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"กำลังโหลดรูปภาพทั้งหมด"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"การปกป้องเพื่อความปลอดภัย"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Native Transcode Alerts"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Native Transcode Progress"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"โปรดลองอีกครั้งในภายหลัง รูปภาพจะพร้อมใช้งานเมื่อปัญหาได้รับการแก้ไขแล้ว"</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"โหลดรูปภาพบางรูปไม่ได้"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"รับทราบ"</string>
 </resources>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index 594b1a3..473a335 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Media"</string>
     <string name="storage_description" msgid="4081716890357580107">"Lokal na storage"</string>
-    <string name="app_label" msgid="9035307001052716210">"Storage ng Media"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Media"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Tagapili ng media"</string>
     <string name="artist_label" msgid="8105600993099120273">"Artist"</string>
     <string name="unknown" msgid="2059049215682829375">"Hindi alam"</string>
     <string name="root_images" msgid="5861633549189045666">"Mga Larawan"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"I-access ang cloud media sa"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Wala"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Hindi mapalitan ang cloud media app sa ngayon."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Tagapili ng media"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Tagapili ng media"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Sini-sync ang media…"</string>
     <string name="add" msgid="2894574044585549298">"Magdagdag"</string>
     <string name="deselect" msgid="4297825044827769490">"I-deselect"</string>
     <string name="deselected" msgid="8488133193326208475">"Na-deselect"</string>
@@ -57,7 +59,9 @@
     <string name="picker_album_media_empty_message" msgid="7061850698189881671">"Walang sinusuportahang larawan o video"</string>
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Walang album"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Tingnan ang napili"</string>
-    <string name="picker_photos" msgid="7415035516411087392">"Photos"</string>
+    <string name="picker_photos" msgid="7415035516411087392">"Mga Larawan"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Mga Album"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Preview"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Lumipat sa para sa trabaho"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> item}one{<xliff:g id="COUNT_1">^1</xliff:g> item}other{<xliff:g id="COUNT_1">^1</xliff:g> na item}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Magdagdag (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Payagan (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Walang papayagan"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Camera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Mga Download"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Mga Paborito"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Nagkakaproblema sa pag-play ng video"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Tingnan ang iyong koneksyon sa internet at subukan ulit"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Subukan ulit"</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="preloading_dialog_title" msgid="4974348221848532887">"Inihahanda ang napili mong media"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Handa na ang <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> sa <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Kanselahin"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Kasama na ngayon ang mga na-back up na larawan"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Puwede kang pumili ng mga larawan mula sa <xliff:g id="APP_NAME">%1$s</xliff:g> account na <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Na-update ang <xliff:g id="APP_NAME">%1$s</xliff:g> account"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Pumili ng app"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Pumili ng account"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Magpalit ng account"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Kinukuha ang lahat ng iyong larawan"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Payagan ang <xliff:g id="APP_NAME_0">^1</xliff:g> na baguhin ang video na ito?}one{Payagan ang <xliff:g id="APP_NAME_1">^1</xliff:g> na baguhin ang <xliff:g id="COUNT">^2</xliff:g> video?}other{Payagan ang <xliff:g id="APP_NAME_1">^1</xliff:g> na baguhin ang <xliff:g id="COUNT">^2</xliff:g> na video?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Proteksyon sa kaligtasan"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Native Transcode Alerts"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Native Transcode Progress"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Subukan ulit sa ibang pagkakataon. Magiging available ang iyong mga larawan kapag nalutas na ang isyu."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Hindi ma-load ang ilang Larawan"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
 </resources>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index df9e144..037d32f 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Medya"</string>
     <string name="storage_description" msgid="4081716890357580107">"Yerel depolama"</string>
-    <string name="app_label" msgid="9035307001052716210">"Medya Deposu"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Medya"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Medya seçme aracı"</string>
     <string name="artist_label" msgid="8105600993099120273">"Sanatçı"</string>
     <string name="unknown" msgid="2059049215682829375">"Bilinmiyor"</string>
     <string name="root_images" msgid="5861633549189045666">"Resimler"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Şuradaki bulut medyasına erişin"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Yok"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Bulut medya uygulaması şu anda değiştirilemiyor."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Medya seçme aracı"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Medya seçme aracı"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Medya senkronize ediliyor…"</string>
     <string name="add" msgid="2894574044585549298">"Ekle"</string>
     <string name="deselect" msgid="4297825044827769490">"Seçimi kaldır"</string>
     <string name="deselected" msgid="8488133193326208475">"Seçimi kaldırıldı"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Albüm yok"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Seçilenleri görüntüle"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Fotoğraflar"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albümler"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Önizle"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"İş profiline geç"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> öğe}other{<xliff:g id="COUNT_1">^1</xliff:g> öğe}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Ekle (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"İzin ver (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Hiçbirine izin verme"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"İndirilenler"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Favoriler"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Video oynatılırken sorun oluştu"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"İnternet bağlantınızı kontrol edip tekrar deneyin"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Tekrar dene"</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="preloading_dialog_title" msgid="4974348221848532887">"Seçtiğiniz medyalar hazırlanıyor"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> adetten <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> adedi hazır"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"İptal"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Yedeklenen fotoğraflar artık dahil ediliyor"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"<xliff:g id="APP_NAME">%1$s</xliff:g> uygulamasındaki <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> hesabından fotoğraf seçebilirsiniz"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> hesabı güncellendi"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Uygulama seç"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Hesap seç"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Hesabı değiştir"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Tüm fotoğraflarınız alınıyor"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> uygulamasının bu videoyu 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> videoyu değiştirmesine izin verilsin mi?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Güvenlik koruması"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Yerel Kod Dönüştürme Uyarıları"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Yerel Kod Dönüştürme İlerleme Durumu"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Daha sonra tekrar deneyin. Fotoğraflarınız, sorun çözüldükten sonra kullanılabilir."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Bazı fotoğraflar yüklenemiyor"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Anladım"</string>
 </resources>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index 9c4dacd..b2b3947 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Медіа-файли"</string>
     <string name="storage_description" msgid="4081716890357580107">"Локальна пам’ять"</string>
-    <string name="app_label" msgid="9035307001052716210">"Сховище медіа-файлів"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Медіа"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Інструмент вибору медіаносія"</string>
     <string name="artist_label" msgid="8105600993099120273">"Виконавець"</string>
     <string name="unknown" msgid="2059049215682829375">"Невідомо"</string>
     <string name="root_images" msgid="5861633549189045666">"Зображення"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Постачальник медіаконтенту з хмари"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Немає"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Не вдалося змінити хмарний мультимедійний додаток."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Інструмент вибору медіаносія"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Інструмент вибору медіаносія"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Синхронізація медіаносіїв…"</string>
     <string name="add" msgid="2894574044585549298">"Додати"</string>
     <string name="deselect" msgid="4297825044827769490">"Не вибирати"</string>
     <string name="deselected" msgid="8488133193326208475">"Не вибрано"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Немає альбомів"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Переглянути вибране"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Фото"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Альбоми"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Попередній перегляд"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Перейти в робочий профіль"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> об’єкт}one{<xliff:g id="COUNT_1">^1</xliff:g> об’єкт}few{<xliff:g id="COUNT_1">^1</xliff:g> об’єкти}many{<xliff:g id="COUNT_1">^1</xliff:g> об’єктів}other{<xliff:g id="COUNT_1">^1</xliff:g> об’єкта}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Додати (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Дозволити (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Не дозволяти жодної фотографії"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Камера"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Завантаження"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Вибране"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Проблема з відтворенням відео"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Перевірте інтернет-з’єднання й повторіть спробу"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Повторити"</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="preloading_dialog_title" msgid="4974348221848532887">"Підготовка вибраних медіафайлів"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"Готово: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> з <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Скасувати"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Резервні копії фотографій додано"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Ви можете вибрати фотографії з облікового запису <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> у додатку <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Обліковий запис у додатку <xliff:g id="APP_NAME">%1$s</xliff:g> оновлено"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Вибрати додаток"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Вибрати обліковий запис"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Змінити обліковий запис"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Завантажуються всі ваші фото"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Захист"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Cповіщення про перекодування нативного коду"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Перебіг перекодування нативного коду"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Повторіть спробу пізніше. Ваші фотографії будуть доступні після вирішення проблеми."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Не вдається завантажити деякі фотографії"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
 </resources>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index 69bcef6..dc215af 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"میڈیا"</string>
     <string name="storage_description" msgid="4081716890357580107">"مقامی اسٹوریج"</string>
-    <string name="app_label" msgid="9035307001052716210">"میڈیا اسٹوریج"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"میڈیا"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"میڈیا منتخب کنندہ"</string>
     <string name="artist_label" msgid="8105600993099120273">"فنکار"</string>
     <string name="unknown" msgid="2059049215682829375">"نامعلوم"</string>
     <string name="root_images" msgid="5861633549189045666">"تصاوير"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"اس سے کلاؤڈ میڈیا تک رسائی حاصل کریں"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"کوئی نہیں"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"کلاؤڈ میڈیا ایپ کو اس وقت تبدیل نہیں کیا جا سکا۔"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"میڈیا منتخب کنندہ"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"میڈیا منتخب کنندہ"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"میڈیا کی مطابقت پذیری کی جا رہی ہے…"</string>
     <string name="add" msgid="2894574044585549298">"شامل کریں"</string>
     <string name="deselect" msgid="4297825044827769490">"غیر منتخب کریں"</string>
     <string name="deselected" msgid="8488133193326208475">"غیر منتخب کردہ"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"کوئی البم نہیں"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"منتخب کردہ دیکھیں"</string>
     <string name="picker_photos" msgid="7415035516411087392">"تصاویر"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"البمز"</string>
     <string name="picker_preview" msgid="6257414886055861039">"پیش منظر"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"کام پر سوئچ کریں"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> آئٹم}other{<xliff:g id="COUNT_1">^1</xliff:g> آئٹمز}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"(<xliff:g id="COUNT">^1</xliff:g>) شامل کریں"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"(<xliff:g id="COUNT">^1</xliff:g>) کو اجازت دیں"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"کسی کو اجازت نہ دیں"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"کیمرا"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"ڈاؤن لوڈز"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"پسندیدہ"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"ویڈیو چلانے میں دشواری"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"اپنا انٹرنیٹ کنکشن چیک کریں اور دوبارہ کوشش کریں"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"پھر کوشش کریں"</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="preloading_dialog_title" msgid="4974348221848532887">"آپ کا منتخب کردہ میڈیا تیار کیا جا رہا ہے"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> میں سے <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> تیار ہیں"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"منسوخ کریں"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"بیک اپ لی گئی تصاویر اب شامل ہیں"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"آپ <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> کے <xliff:g id="APP_NAME">%1$s</xliff:g> اکاؤنٹ سے تصاویر منتخب کر سکتے ہیں"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> اکاؤنٹ اپ ڈیٹ کیا گیا"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"ایپ منتخب کریں"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"اکاؤنٹ منتخب کریں"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"اکاؤنٹ تبدیل کریں"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"آپ کی تمام تصاویر حاصل کی جا رہی ہیں"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"سیفٹی پروٹیکشن"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"مقامی ٹرانسکوڈ کے الرٹس"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"مقامی ٹرانسکوڈ کی پیشرفت"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"بعد میں دوبارہ کوشش کریں۔ مسئلہ حل ہو جانے کے بعد آپ کی تصاویر دستیاب ہوں گی۔"</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"کچھ تصاویر لوڈ نہیں کی جا سکتیں"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"سمجھ آ گئی"</string>
 </resources>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index 512bae3..ac34311 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Multimedia"</string>
     <string name="storage_description" msgid="4081716890357580107">"Mahalliy xotira"</string>
-    <string name="app_label" msgid="9035307001052716210">"Multimedia xotirasi"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Media"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Rasm tanlash"</string>
     <string name="artist_label" msgid="8105600993099120273">"Ijrochi"</string>
     <string name="unknown" msgid="2059049215682829375">"Noaniq"</string>
     <string name="root_images" msgid="5861633549189045666">"Rasmlar"</string>
@@ -46,22 +45,27 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Bulutli media kontentni ochish"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Hech qanday"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Hozirda bulutli media ilovasi oʻzgarmadi"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Rasm tanlash"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Rasm tanlash"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Media sinxronlanmoqda…"</string>
     <string name="add" msgid="2894574044585549298">"Kiritish"</string>
     <string name="deselect" msgid="4297825044827769490">"Tanlovni bekor qilish"</string>
     <string name="deselected" msgid="8488133193326208475">"Tanlovi yechilgan"</string>
     <string name="select" msgid="2704765470563027689">"Tanlash"</string>
     <string name="selected" msgid="9151797369975828124">"Tanlangan"</string>
-    <string name="select_up_to" msgid="6994294169508439957">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> tagcha elementni tanlang}other{<xliff:g id="COUNT_1">^1</xliff:g> tagcha elementni tanlang}}"</string>
+    <string name="select_up_to" msgid="6994294169508439957">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> tagacha elementni tanlang}other{<xliff:g id="COUNT_1">^1</xliff:g> tagacha elementni tanlang}}"</string>
     <string name="recent" msgid="6694613584743207874">"Oxirgi"</string>
     <string name="picker_photos_empty_message" msgid="5980619500554575558">"Surat yoki video kiritilmagan"</string>
     <string name="picker_album_media_empty_message" msgid="7061850698189881671">"Qabul qilinmaydigan rasm va videolar"</string>
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Albom kiritilmagan"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Belgilanganlarni ochish"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Suratlar"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Albomlar"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Razm solish"</string>
-    <string name="picker_work_profile" msgid="2083221066869141576">"Ish profiliga oʻtish"</string>
-    <string name="picker_personal_profile" msgid="639484258397758406">"Shaxsiy profilga oʻtish"</string>
+    <string name="picker_work_profile" msgid="2083221066869141576">"Ish profiliga almashish"</string>
+    <string name="picker_personal_profile" msgid="639484258397758406">"Shaxsiy profilga almashish"</string>
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"Administratoringiz tomonidan bloklangan"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"Shaxsiy ilovadan ishga oid maʼlumotlarga kirish taqiqlangan"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"Ishga oid ilovadan shaxsiy maʼlumotlarga kirish taqiqlangan"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> ta narsa}other{<xliff:g id="COUNT_1">^1</xliff:g> ta narsa}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Kiritish (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Ruxsat (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Hech qanday ruxsat berilmasin"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Kamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Yuklanmalar"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Sevimlilar"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Video ijrosida muammo"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Internet aloqasini tekshiring va qayta urining"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Qayta urinish"</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="preloading_dialog_title" msgid="4974348221848532887">"Tanlangan media tayyorlanmoqda"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> tayyor"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Bekor qilish"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Zaxiralangan suratlar qoʻshildi"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"<xliff:g id="USER_ACCOUNT">%2$s</xliff:g> hisobidagi <xliff:g id="APP_NAME">%1$s</xliff:g> rasmlarini tanlashingiz mumkin"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g> hisobi yangilandi"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Ilovani tanlash"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Hisobni tanlang"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Hisobni almashtirish"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Barcha rasmlaringizni yuklab oling"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{<xliff:g id="APP_NAME_0">^1</xliff:g> ilovasiga bu videoni oʻzgartirishi uchun ruxsat berilsinmi?}other{<xliff:g id="APP_NAME_1">^1</xliff:g> ilovasiga <xliff:g id="COUNT">^2</xliff:g> ta videoni oʻzgartirishi uchun ruxsat berilsinmi?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Xavfsizlik himoyasi"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Nativ transkodlash signallari"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Nativ transkodlash jarayoni"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Keyinroq qayta urining. Suratlaringiz muammo hal boʻlgandan keyin chiqadi."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Ayrim suratlar yuklanmadi"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
 </resources>
diff --git a/res/values-v31/styles.xml b/res/values-v31/styles.xml
index 3dec1da..3992eba 100644
--- a/res/values-v31/styles.xml
+++ b/res/values-v31/styles.xml
@@ -14,7 +14,8 @@
      limitations under the License.
 -->
 
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+           xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
 
     <style name="PickerMaterialTheme" parent="@style/Theme.Material3.DayNight.NoActionBar">
         <item name="materialAlertDialogTheme">@style/ProfileDialogTheme</item>
@@ -40,6 +41,8 @@
         <item name="pickerBannerPrimaryTextColor">?android:attr/textColorSecondary</item>
         <item name="pickerBannerSecondaryTextColor">?android:attr/textColorPrimary</item>
         <item name="pickerBannerButtonTextColor">@android:color/system_accent1_600</item>
+        <item name="categoryDefaultThumbnailColor">?attr/colorOnSurfaceVariant</item>
+        <item name="categoryDefaultThumbnailCircleColor">?attr/colorSurfaceVariant</item>
     </style>
 
 </resources>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index d7b3b22..ae0b4e5 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Phương tiện"</string>
     <string name="storage_description" msgid="4081716890357580107">"Bộ nhớ cục bộ"</string>
-    <string name="app_label" msgid="9035307001052716210">"Bộ nhớ phương tiện"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Nội dung nghe nhìn"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Công cụ chọn nội dung đa phương tiện"</string>
     <string name="artist_label" msgid="8105600993099120273">"Nghệ sĩ"</string>
     <string name="unknown" msgid="2059049215682829375">"Không xác định"</string>
     <string name="root_images" msgid="5861633549189045666">"Hình ảnh"</string>
@@ -39,13 +38,16 @@
     <string name="allow" msgid="8885707816848569619">"Cho phép"</string>
     <string name="deny" msgid="6040983710442068936">"Từ chối"</string>
     <string name="picker_browse" msgid="5554477454636075934">"Duyệt qua…"</string>
-    <string name="picker_settings" msgid="6443463167344790260">"Ứng dụng đa phương tiện trên đám mây"</string>
-    <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Ứng dụng đa phương tiện trên đám mây"</string>
-    <string name="picker_settings_title" msgid="5647700706470673258">"Ứng dụng nội dung phương tiện trên đám mây"</string>
-    <string name="picker_settings_description" msgid="2916686824777214585">"Truy cập vào nội dung nghe nhìn trên đám mây của bạn khi ứng dụng hoặc trang web yêu cầu bạn chọn ảnh hoặc video"</string>
-    <string name="picker_settings_selection_message" msgid="245453573086488596">"Truy cập nội dung phương tiện trên đám mây qua"</string>
+    <string name="picker_settings" msgid="6443463167344790260">"Ứng dụng nghe nhìn trên đám mây"</string>
+    <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"Ứng dụng nghe nhìn trên đám mây"</string>
+    <string name="picker_settings_title" msgid="5647700706470673258">"Ứng dụng nghe nhìn trên đám mây"</string>
+    <string name="picker_settings_description" msgid="2916686824777214585">"Truy cập vào nội dung nghe nhìn của bạn trên đám mây khi có ứng dụng hay trang web yêu cầu bạn chọn ảnh hoặc video"</string>
+    <string name="picker_settings_selection_message" msgid="245453573086488596">"Truy cập nội dung nghe nhìn trên đám mây qua"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Không có"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Hiện không thay đổi được ứng dụng đa phương tiện đám mây."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Công cụ chọn nội dung đa phương tiện"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Công cụ chọn nội dung đa phương tiện"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Đang đồng bộ hoá nội dung đa phương tiện…"</string>
     <string name="add" msgid="2894574044585549298">"Thêm"</string>
     <string name="deselect" msgid="4297825044827769490">"Bỏ chọn"</string>
     <string name="deselected" msgid="8488133193326208475">"Đã bỏ chọn"</string>
@@ -55,14 +57,16 @@
     <string name="recent" msgid="6694613584743207874">"Gần đây"</string>
     <string name="picker_photos_empty_message" msgid="5980619500554575558">"Không có ảnh hoặc video nào"</string>
     <string name="picker_album_media_empty_message" msgid="7061850698189881671">"Không có ảnh hoặc video nào được hỗ trợ"</string>
-    <string name="picker_albums_empty_message" msgid="8341079772950966815">"Không có đĩa nhạc nào"</string>
+    <string name="picker_albums_empty_message" msgid="8341079772950966815">"Không có album nào"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Xem các mục được chọn"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Ảnh"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Album"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Xem trước"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Chuyển sang hồ sơ công việc"</string>
     <string name="picker_personal_profile" msgid="639484258397758406">"Chuyển sang hồ sơ cá nhân"</string>
-    <string name="picker_profile_admin_title" msgid="4172022376418293777">"Bị quản trị viên của bạn chặn"</string>
+    <string name="picker_profile_admin_title" msgid="4172022376418293777">"Quản trị viên của bạn đã chặn thao tác này"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"Bạn không được phép truy cập dữ liệu công việc từ một ứng dụng cá nhân"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"Bạn không được phép truy cập dữ liệu cá nhân từ một ứng dụng công việc"</string>
     <string name="picker_profile_work_paused_title" msgid="382212880704235925">"Ứng dụng công việc đã tạm dừng"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> mục}other{<xliff:g id="COUNT_1">^1</xliff:g> mục}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Thêm (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Cho phép (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Không cho phép"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Máy ảnh"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Tệp đã tải xuống"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Mục yêu thích"</string>
@@ -92,10 +97,11 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Sự cố khi phát video"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Hãy kiểm tra kết nối Internet rồi thử lại"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Thử lại"</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="preloading_dialog_title" msgid="4974348221848532887">"Đang chuẩn bị nội dung đa phương tiện mà bạn chọn"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> mục đã sẵn sàng"</string>
-    <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Ảnh được sao lưu giờ đã xuất hiện"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Huỷ"</string>
+    <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Ảnh được sao lưu giờ đã có mặt"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Bạn có thể chọn ảnh của <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> trên <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"Đã cập nhật tài khoản <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"Ảnh của <xliff:g id="USER_ACCOUNT">%1$s</xliff:g> giờ sẽ xuất hiện tại đây"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Chọn ứng dụng"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Chọn tài khoản"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Thay đổi tài khoản"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Tải tất cả các ảnh"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Cho phép <xliff:g id="APP_NAME_0">^1</xliff:g> sửa đổi video 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> video?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Bảo vệ an toàn"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Cảnh báo chuyển mã gốc"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Tiến trình chuyển mã gốc"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Hãy thử lại sau. Ảnh của bạn sẽ xuất hiện sau khi vấn đề được giải quyết."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Không tải được một số ảnh"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Tôi hiểu"</string>
 </resources>
diff --git a/res/values-watch/dimens.xml b/res/values-watch/dimens.xml
deleted file mode 100644
index ed5fa00..0000000
--- a/res/values-watch/dimens.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?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 2ad13a4..a2c02a2 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"媒体"</string>
     <string name="storage_description" msgid="4081716890357580107">"本地存储空间"</string>
-    <string name="app_label" msgid="9035307001052716210">"媒体存储设备"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"媒体"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"媒体选择工具"</string>
     <string name="artist_label" msgid="8105600993099120273">"音乐人"</string>
     <string name="unknown" msgid="2059049215682829375">"未知"</string>
     <string name="root_images" msgid="5861633549189045666">"图片"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"从以下位置访问云端媒体:"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"无"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"目前无法更改云端媒体应用。"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"媒体选择工具"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"媒体选择工具"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"正在同步媒体…"</string>
     <string name="add" msgid="2894574044585549298">"添加"</string>
     <string name="deselect" msgid="4297825044827769490">"取消选择"</string>
     <string name="deselected" msgid="8488133193326208475">"已取消选中"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"无影集"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"查看所选内容"</string>
     <string name="picker_photos" msgid="7415035516411087392">"照片"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"影集"</string>
     <string name="picker_preview" msgid="6257414886055861039">"预览"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"切换到工作资料"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> 个}other{<xliff:g id="COUNT_1">^1</xliff:g> 个}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"添加(<xliff:g id="COUNT">^1</xliff:g> 项)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"允许 (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"全部不允许"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"相机"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"下载内容"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"收藏"</string>
@@ -92,23 +97,23 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"播放视频时遇到问题"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"请检查互联网连接,然后重试"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"重试"</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="preloading_dialog_title" msgid="4974348221848532887">"正在准备您选择的媒体"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> 个已准备就绪,共 <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> 个"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"取消"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"备份照片现已添加完成"</string>
-    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"您可以选择来自<xliff:g id="APP_NAME">%1$s</xliff:g>帐号 <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> 的照片"</string>
-    <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g>帐号已更新"</string>
+    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"您可以选择来自<xliff:g id="APP_NAME">%1$s</xliff:g>账号 <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> 的照片"</string>
+    <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"<xliff:g id="APP_NAME">%1$s</xliff:g>账号已更新"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"来自 <xliff:g id="USER_ACCOUNT">%1$s</xliff:g> 的照片已添加到此处"</string>
     <string name="picker_banner_cloud_choose_app_title" msgid="3165966147547974251">"选择云端媒体应用"</string>
     <string name="picker_banner_cloud_choose_app_desc" msgid="2359212653555524926">"如需将备份照片添加到此处,请在“设置”中选择一个云端媒体应用"</string>
-    <string name="picker_banner_cloud_choose_account_title" msgid="5010901185639577685">"选择<xliff:g id="APP_NAME">%1$s</xliff:g>帐号"</string>
-    <string name="picker_banner_cloud_choose_account_desc" msgid="8868134443673142712">"如需将来自<xliff:g id="APP_NAME">%1$s</xliff:g>的照片添加到此处,请在应用中选择一个帐号"</string>
+    <string name="picker_banner_cloud_choose_account_title" msgid="5010901185639577685">"选择<xliff:g id="APP_NAME">%1$s</xliff:g>账号"</string>
+    <string name="picker_banner_cloud_choose_account_desc" msgid="8868134443673142712">"如需将来自<xliff:g id="APP_NAME">%1$s</xliff:g>的照片添加到此处,请在应用中选择一个账号"</string>
     <string name="picker_banner_cloud_dismiss_button" msgid="2935903078288463882">"关闭"</string>
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"选择应用"</string>
-    <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"选择帐号"</string>
-    <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"更改帐号"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"选择账号"</string>
+    <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"更改账号"</string>
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"正在获取您的所有照片"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{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>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"安全保护"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"原生转码警报"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"原生转码进度"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"请稍后再试。问题解决后,您就能看到这些照片了。"</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"部分照片无法加载"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"知道了"</string>
 </resources>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index 2e84e71..fa75494 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"媒體"</string>
     <string name="storage_description" msgid="4081716890357580107">"本機儲存空間"</string>
-    <string name="app_label" msgid="9035307001052716210">"媒體儲存空間"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"媒體"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"媒體選擇器"</string>
     <string name="artist_label" msgid="8105600993099120273">"歌手"</string>
     <string name="unknown" msgid="2059049215682829375">"不明"</string>
     <string name="root_images" msgid="5861633549189045666">"相片"</string>
@@ -42,10 +41,13 @@
     <string name="picker_settings" msgid="6443463167344790260">"雲端媒體應用程式"</string>
     <string name="picker_settings_system_settings_menu_title" msgid="3055084757610063581">"雲端媒體應用程式"</string>
     <string name="picker_settings_title" msgid="5647700706470673258">"雲端媒體應用程式"</string>
-    <string name="picker_settings_description" msgid="2916686824777214585">"當應用程式或網站要求您選取相片或影片時,就可使用自己的雲端媒體"</string>
+    <string name="picker_settings_description" msgid="2916686824777214585">"當應用程式或網站要求你選取相片或影片時,就可使用自己的雲端媒體"</string>
     <string name="picker_settings_selection_message" msgid="245453573086488596">"從以下位置存取雲端媒體:"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"無"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"目前無法變更雲端媒體應用程式。"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"媒體選擇器"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"媒體選擇器"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"正在同步媒體…"</string>
     <string name="add" msgid="2894574044585549298">"新增"</string>
     <string name="deselect" msgid="4297825044827769490">"取消選取"</string>
     <string name="deselected" msgid="8488133193326208475">"已取消選取"</string>
@@ -58,20 +60,23 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"沒有相簿"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"查看所選項目"</string>
     <string name="picker_photos" msgid="7415035516411087392">"相片"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"相簿"</string>
     <string name="picker_preview" msgid="6257414886055861039">"預覽"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"切換至工作設定檔"</string>
     <string name="picker_personal_profile" msgid="639484258397758406">"切換至個人設定檔"</string>
-    <string name="picker_profile_admin_title" msgid="4172022376418293777">"管理員已禁止此操作"</string>
+    <string name="picker_profile_admin_title" msgid="4172022376418293777">"管理員禁止此操作"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"個人應用程式不得存取工作資料"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"工作應用程式不得存取個人資料"</string>
     <string name="picker_profile_work_paused_title" msgid="382212880704235925">"已暫停工作應用程式"</string>
     <string name="picker_profile_work_paused_msg" msgid="6321552322125246726">"如要開啟工作相片,請開啟工作應用程式,然後再試一次"</string>
-    <string name="picker_privacy_message" msgid="9132700451027116817">"此應用程式只能存取您選取的相片"</string>
+    <string name="picker_privacy_message" msgid="9132700451027116817">"此應用程式只能存取你選取的相片"</string>
     <string name="picker_header_permissions" msgid="675872774407768495">"選擇允許此應用程式存取的相片和影片"</string>
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> 個項目}other{<xliff:g id="COUNT_1">^1</xliff:g> 個項目}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"新增 (<xliff:g id="COUNT">^1</xliff:g> 個)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"允許 (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"全部禁止"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"相機"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"下載"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"我的最愛"</string>
@@ -90,13 +95,14 @@
     <string name="picker_pause_video" msgid="1092718225234326702">"暫停"</string>
     <string name="picker_error_snackbar" msgid="5970192792792369203">"無法播放影片"</string>
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"播放影片時發生問題"</string>
-    <string name="picker_error_dialog_body" msgid="2515738446802971453">"請檢查您的互聯網連線,然後再試一次"</string>
+    <string name="picker_error_dialog_body" msgid="2515738446802971453">"請檢查你的互聯網連線,然後再試一次"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"重試"</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="preloading_dialog_title" msgid="4974348221848532887">"正在準備你選取的媒體"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> 個項目已就緒,共 <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> 個"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"取消"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"現在已納入備份相片"</string>
-    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"您可從「<xliff:g id="APP_NAME">%1$s</xliff:g>」帳戶 <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> 選取相片"</string>
+    <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"你可從「<xliff:g id="APP_NAME">%1$s</xliff:g>」帳戶 <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> 選取相片"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"「<xliff:g id="APP_NAME">%1$s</xliff:g>」帳戶更新完成"</string>
     <string name="picker_banner_cloud_account_changed_desc" msgid="3433218869899792497">"現在亦會在此處納入 <xliff:g id="USER_ACCOUNT">%1$s</xliff:g> 的相片"</string>
     <string name="picker_banner_cloud_choose_app_title" msgid="3165966147547974251">"選擇雲端媒體應用程式"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"安全保護"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"原生轉碼警示"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"原生轉碼進度"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"請稍後再試。相片會在問題解決後顯示。"</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"部分相片無法載入"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"知道了"</string>
 </resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index f3263c9..63c3401 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"媒體"</string>
     <string name="storage_description" msgid="4081716890357580107">"本機儲存空間"</string>
-    <string name="app_label" msgid="9035307001052716210">"媒體儲存空間"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"媒體"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"媒體選擇器"</string>
     <string name="artist_label" msgid="8105600993099120273">"演出者"</string>
     <string name="unknown" msgid="2059049215682829375">"不明"</string>
     <string name="root_images" msgid="5861633549189045666">"圖片"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"從以下位置存取雲端媒體:"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"無"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"目前無法變更雲端媒體應用程式。"</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"媒體選擇器"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"媒體選擇器"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"正在同步媒體…"</string>
     <string name="add" msgid="2894574044585549298">"新增"</string>
     <string name="deselect" msgid="4297825044827769490">"取消選取"</string>
     <string name="deselected" msgid="8488133193326208475">"已取消選取"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"沒有相簿"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"查看所選項目"</string>
     <string name="picker_photos" msgid="7415035516411087392">"相片"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"相簿"</string>
     <string name="picker_preview" msgid="6257414886055861039">"預覽"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"切換至工作資料夾"</string>
@@ -72,6 +76,7 @@
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{<xliff:g id="COUNT_0">^1</xliff:g> 個項目}other{<xliff:g id="COUNT_1">^1</xliff:g> 個項目}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"新增 (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"允許 (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"全部禁止"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"相機"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"下載的內容"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"收藏的內容"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"播放影片時發生問題"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"請檢查網際網路連線,然後再試一次"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"重試"</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="preloading_dialog_title" msgid="4974348221848532887">"正在準備所選媒體"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"已備妥 <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> 個項目,共 <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> 個項目"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"取消"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"現在已納入備份相片"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"你可以從「<xliff:g id="APP_NAME">%1$s</xliff:g>」帳戶 <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> 選取相片"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"「<xliff:g id="APP_NAME">%1$s</xliff:g>」帳戶更新完成"</string>
@@ -151,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"安全防護"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"原生轉碼警示"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"原生轉碼進度"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"請稍後再試。問題解決後,你就可以存取相片。"</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"無法載入部分相片"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"我知道了"</string>
 </resources>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index ab427ed..c0a63ae 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -18,8 +18,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="uid_label" msgid="8421971615411294156">"Abezind"</string>
     <string name="storage_description" msgid="4081716890357580107">"Isitoreji sasendaweni"</string>
-    <string name="app_label" msgid="9035307001052716210">"Isitoreji Semidiya"</string>
-    <string name="picker_app_label" msgid="4254039089502164761">"Imidiya"</string>
+    <string name="picker_app_label" msgid="1195424381053599122">"Isikhethi semidiya"</string>
     <string name="artist_label" msgid="8105600993099120273">"Umculi"</string>
     <string name="unknown" msgid="2059049215682829375">"Akwaziwa"</string>
     <string name="root_images" msgid="5861633549189045666">"Izithombe"</string>
@@ -46,6 +45,9 @@
     <string name="picker_settings_selection_message" msgid="245453573086488596">"Finyelela imidiya yacloud ukusuka"</string>
     <string name="picker_settings_no_provider" msgid="2582311853680058223">"Lutho"</string>
     <string name="picker_settings_toast_error" msgid="697274445512467469">"Ayikwazanga ukushintsha i-app yemidiya ye-cloud manje."</string>
+    <string name="picker_sync_notification_channel" msgid="1867105708912627993">"Isikhethi semidiya"</string>
+    <string name="picker_sync_notification_title" msgid="1122713382122055246">"Isikhethi semidiya"</string>
+    <string name="picker_sync_notification_text" msgid="8204423917712309382">"Ivumelanisa imidiya…"</string>
     <string name="add" msgid="2894574044585549298">"Engeza"</string>
     <string name="deselect" msgid="4297825044827769490">"Susa ukukhetha"</string>
     <string name="deselected" msgid="8488133193326208475">"Okususwe ekukhethweni"</string>
@@ -58,6 +60,8 @@
     <string name="picker_albums_empty_message" msgid="8341079772950966815">"Awekho ama-albhamu"</string>
     <string name="picker_view_selected" msgid="2266031384396143883">"Ukubuka kukhethiwe"</string>
     <string name="picker_photos" msgid="7415035516411087392">"Izithombe"</string>
+    <!-- no translation found for picker_videos (2886971435439047097) -->
+    <skip />
     <string name="picker_albums" msgid="4822511902115299142">"Ama-albhamu"</string>
     <string name="picker_preview" msgid="6257414886055861039">"Hlola kuqala"</string>
     <string name="picker_work_profile" msgid="2083221066869141576">"Shintshela kokmsebenzi"</string>
@@ -65,13 +69,14 @@
     <string name="picker_profile_admin_title" msgid="4172022376418293777">"Kuvinjwe ngumphathi wakho"</string>
     <string name="picker_profile_admin_msg_from_personal" msgid="1941639895084555723">"Ukufinyelela idatha evela ku-app yomuntu siqu akuvunyelwe"</string>
     <string name="picker_profile_admin_msg_from_work" msgid="8048524337462790110">"Ukufinyelela idatha yomuntu siqu evela ku-app yomsebenzi akuvunyelwe"</string>
-    <string name="picker_profile_work_paused_title" msgid="382212880704235925">"Ama-app okusebenza aphunyuziwe"</string>
+    <string name="picker_profile_work_paused_title" msgid="382212880704235925">"Ama-app okusebenza amisiwe"</string>
     <string name="picker_profile_work_paused_msg" msgid="6321552322125246726">"Ukuze uvule izithombe zomsebenzi, vula ama-app wakho womsebenzi bese uzama futhi"</string>
     <string name="picker_privacy_message" msgid="9132700451027116817">"Le app ingafinyelela izithombe ozikhethayo kuphela"</string>
     <string name="picker_header_permissions" msgid="675872774407768495">"Khetha izithombe namavidiyo ovumela le app ukuthi iwafinyelele"</string>
     <string name="picker_album_item_count" msgid="4420723302534177596">"{count,plural, =1{into <xliff:g id="COUNT_0">^1</xliff:g>}one{izinto <xliff:g id="COUNT_1">^1</xliff:g>}other{izinto <xliff:g id="COUNT_1">^1</xliff:g>}}"</string>
     <string name="picker_add_button_multi_select" msgid="4005164092275518399">"Engeza (<xliff:g id="COUNT">^1</xliff:g>)"</string>
     <string name="picker_add_button_multi_select_permissions" msgid="5138751105800138838">"Vumela (<xliff:g id="COUNT">^1</xliff:g>)"</string>
+    <string name="picker_add_button_allow_none_option" msgid="9183772732922241035">"Ungavumeli lutho"</string>
     <string name="picker_category_camera" msgid="4857367052026843664">"Ikhamera"</string>
     <string name="picker_category_downloads" msgid="793866660287361900">"Okulandiwe"</string>
     <string name="picker_category_favorites" msgid="7008495397818966088">"Izintandokazi"</string>
@@ -92,9 +97,10 @@
     <string name="picker_error_dialog_title" msgid="4540095603788920965">"Inkinga yokudlala ividiyo"</string>
     <string name="picker_error_dialog_body" msgid="2515738446802971453">"Hlola ukuxhuma kwakho kwe-inthanethi uphinde uzame futhi"</string>
     <string name="picker_error_dialog_positive_action" msgid="749544129082109232">"Zama futhi"</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="preloading_dialog_title" msgid="4974348221848532887">"Ilungiselela imidiya yakho oyikhethile"</string>
     <string name="preloading_progress_message" msgid="4741327138031980582">"U-<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> wokungu-<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ulungile"</string>
+    <string name="preloading_cancel_button" msgid="824053521307342209">"Khansela"</string>
     <string name="picker_banner_cloud_first_time_available_title" msgid="5912973744275711595">"Izithombe ezenziwe isipele sezifakiwe manje"</string>
     <string name="picker_banner_cloud_first_time_available_desc" msgid="5570916598348187607">"Ungakhetha izithombe ezivela ku-akhawunti ye-<xliff:g id="APP_NAME">%1$s</xliff:g> ethi <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
     <string name="picker_banner_cloud_account_changed_title" msgid="4825058474378077327">"I-akhawunti ye-<xliff:g id="APP_NAME">%1$s</xliff:g> ibuyekeziwe"</string>
@@ -107,8 +113,7 @@
     <string name="picker_banner_cloud_choose_app_button" msgid="934085679890435479">"Khetha i-app"</string>
     <string name="picker_banner_cloud_choose_account_button" msgid="7979484877116991631">"Khetha i-akhawunti"</string>
     <string name="picker_banner_cloud_change_account_button" msgid="8361239765828471146">"Shintsha i-akhawunti"</string>
-    <!-- no translation found for picker_loading_photos_message (6449180084857178949) -->
-    <skip />
+    <string name="picker_loading_photos_message" msgid="6449180084857178949">"Ithola zonke izithombe zakho"</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>
     <string name="permission_write_video" msgid="103902551603700525">"{count,plural, =1{Vumela i-<xliff:g id="APP_NAME_0">^1</xliff:g> ukuguqula le vidiyo?}one{Vumela i-<xliff:g id="APP_NAME_1">^1</xliff:g> ukuguqula amavidiyo angu-<xliff:g id="COUNT">^2</xliff:g>?}other{Vumela i-<xliff:g id="APP_NAME_1">^1</xliff:g> ukuguqula amavidiyo angu-<xliff:g id="COUNT">^2</xliff:g>?}}"</string>
@@ -152,4 +157,7 @@
     <string name="safety_protection_icon_label" msgid="6714354052747723623">"Ukuvikeleka kokuphepha"</string>
     <string name="transcode_alert_channel" msgid="997332371757680478">"Izexwayiso Zokudlulisela Ikhodi Yomdabu"</string>
     <string name="transcode_progress_channel" msgid="6905136787933058387">"Inqubekela-phambili Yokudlulisela Ikhodi Yomdabu"</string>
+    <string name="dialog_error_message" msgid="5120432204743681606">"Zama futhi emuva kwesikhathi. Izithombe zakho zizotholakala uma inkinga isixazululiwe."</string>
+    <string name="dialog_error_title" msgid="636349284077820636">"Ayikwazi ukulayisha ezinye Izithombe"</string>
+    <string name="dialog_button_text" msgid="351366485240852280">"Ngiyezwa"</string>
 </resources>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 6f53ce1..c932084 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -78,4 +78,10 @@
     <!-- Photo Picker Banner button text color. -->
     <attr name="pickerBannerButtonTextColor" format="reference|color" />
 
+    <!-- Default thumbnail icon color for merged albums -->
+    <attr name="categoryDefaultThumbnailColor" format="reference|color"/>
+
+    <!-- Default thumbnail ellipse color for merged albums -->
+    <attr name="categoryDefaultThumbnailCircleColor" format="reference|color" />
+
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 34762e9..7681262 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -15,10 +15,10 @@
 -->
 
 <resources>
-    <dimen name="permission_dialog_width">320dp</dimen>
     <dimen name="permission_thumb_size">64dp</dimen>
     <dimen name="permission_thumb_margin">6dp</dimen>
     <dimen name="dialog_space">20dp</dimen>
+    <dimen name="button_touch_size">48dp</dimen>
 
     <!-- PhotoPicker -->
     <dimen name="picker_top_corner_radius">28dp</dimen>
@@ -53,7 +53,7 @@
 
     <dimen name="picker_photo_item_spacing">3dp</dimen>
 
-    <!-- Photo Picker recycler view bottom padding for profile button or bottom bar -->
+    <!-- Photo Picker recycler view bottom padding for progress bar -->
     <dimen name="picker_recycler_view_bottom_padding">78dp</dimen>
 
     <dimen name="picker_tab_text_size">14sp</dimen>
@@ -63,6 +63,12 @@
     <dimen name="picker_tab_min_width">88dp</dimen>
     <dimen name="picker_tab_horizontal_gap">4dp</dimen>
 
+    <dimen name="picker_tab_loading_message_text_size">11sp</dimen>
+
+    <dimen name="picker_progress_bar_margin_top">15dp</dimen>
+    <!-- Photo Picker recycler view top padding for profile button or bottom bar -->
+    <dimen name="picker_recycler_view_top_padding">31dp</dimen>
+
     <dimen name="picker_drag_margin_top">16dp</dimen>
     <dimen name="picker_drag_margin_bottom">16dp</dimen>
 
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 65521bd..748e7c5 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -21,12 +21,8 @@
     <!-- Label to show client applications a short description of storage location -->
     <string name="storage_description">Local storage</string>
 
-    <!-- TODO(b/246345209): This string is unused. Update app_label string to "Media" -->
-    <!-- and delete picker_app_label  -->
-    <string name="app_label">Media Storage</string>
-
     <!-- Label to show to user for this package and for Photo picker. -->
-    <string name="picker_app_label">Media</string>
+    <string name="picker_app_label">Media picker</string>
 
     <!-- Description line for music artists in the search/suggestion results -->
     <string name="artist_label">Artist</string>
@@ -107,6 +103,15 @@
     <!-- Error message displayed to the user when the user is not able to change cloud media app preference in Picker Settings. [CHAR LIMIT=50] -->
     <string name="picker_settings_toast_error">Could not change cloud media app at this time.</string>
 
+    <!-- PhotoPicker notification channel for sync updates [CHAR LIMIT=40] -->
+    <string name="picker_sync_notification_channel">Media picker</string>
+
+    <!-- PhotoPicker sync notification title [CHAR LIMIT=40] -->
+    <string name="picker_sync_notification_title">Media picker</string>
+
+    <!-- PhotoPicker sync notification text [CHAR LIMIT=60] -->
+    <string name="picker_sync_notification_text">Syncing media&#8230;</string>
+
     <!-- Add button for PhotoPicker. [CHAR LIMIT=30] -->
     <string name="add">Add</string>
 
@@ -138,12 +143,15 @@
     <!-- The message for empty message on Albums tab in PhotoPicker when the item count is zero. [CHAR LIMIT=NONE] -->
     <string name="picker_albums_empty_message">No albums</string>
 
-    <!-- PhotoPicker view selected action text. [CHAR LIMIT=80] -->
+    <!-- PhotoPicker view selected action text. [CHAR LIMIT=17] -->
     <string name="picker_view_selected">View selected</string>
 
-    <!-- The text of the photos tab for PhotoPicker. [CHAR LIMIT=30] -->
+    <!-- The text of the photos tab in PhotoPicker for 'Image/' mime type. [CHAR LIMIT=30] -->
     <string name="picker_photos">Photos</string>
 
+    <!-- The text of the photos tab in PhotoPicker for 'Video/' mime type. [CHAR LIMIT=30] -->
+    <string name="picker_videos">@string/root_videos</string>
+
     <!-- The text of the albums tab for PhotoPicker. [CHAR LIMIT=30] -->
     <string name="picker_albums">Albums</string>
 
@@ -188,6 +196,9 @@
     <!-- TODO(b/257208235): Update with finalized UX string. !-->
     <string name="picker_add_button_multi_select_permissions">Allow (<xliff:g id="count" example="42">^1</xliff:g>)</string>
 
+    <!-- Text shown on the add button for multi-select in Picker Choice when no photo is selected.[CHAR LIMIT=30]  -->
+    <string name="picker_add_button_allow_none_option">Allow none</string>
+
     <!-- Title for the category in the picker that offers items in Camera folder. [CHAR LIMIT=24] -->
     <string name="picker_category_camera">Camera</string>
     <!-- Title for the category in the picker that offers downloaded items. [CHAR LIMIT=24] -->
@@ -241,15 +252,16 @@
     <!-- Retriable error dialog positive action button text -->
     <string name="picker_error_dialog_positive_action">Retry</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>
 
+    <!-- Title of the preloading progress dialog -->
+    <string name="preloading_dialog_title">"Preparing your selected media"</string>
     <!-- A message for the Progress Dialog shown while preloading selected items before "closing" Photo Picker. [CHAR LIMIT=NONE] -->
     <string name="preloading_progress_message"><xliff:g id="number_preloaded">%1$d</xliff:g> of <xliff:g id="number_total">%2$d</xliff:g> ready</string>
 
+    <string name="preloading_cancel_button">Cancel</string>
+
     <!-- ========================= PHOTO PICKER CLOUD EDUCATION BANNERS ========================= -->
 
     <!-- Title for the banner notifying the user that the cloud media is now available in the picker [CHAR LIMIT=NONE] -->
@@ -288,6 +300,10 @@
     <!-- Change account button for banners [CHAR LIMIT=25] -->
     <string name="picker_banner_cloud_change_account_button">Change account</string>
 
+    <!-- A messaged displayed on top of the prgressbar when photos are being loaded. [CHAR LIMIT=80]-->
+    <string name="picker_loading_photos_message">Getting all your photos</string>
+
+
     <!-- ========================= BEGIN AUTO-GENERATED BY gen_strings.py ========================= -->
 
     <!-- ========================= WRITE STRINGS ========================= -->
@@ -498,4 +514,13 @@
 
     <!-- Transcode progress channel name. -->
     <string name="transcode_progress_channel">Native Transcode Progress</string>
+
+    <!-- Dialog error message-->
+    <string name="dialog_error_message">Try again later. Your photos will be available once the issue is resolved.</string>
+
+    <!-- Dialog error title-->
+    <string name="dialog_error_title">Can\'t load some Photos</string>
+
+    <!-- Error dialog OK button text-->
+    <string name="dialog_button_text">Got it</string>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index d499105..55a1f9d 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -14,7 +14,8 @@
      limitations under the License.
 -->
 
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+           xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
 
     <style name="PickerDialogTheme"
            parent="@android:style/Theme.DeviceDefault.Light.Dialog.Alert">
@@ -89,6 +90,7 @@
 
     <style name="PickerDefaultTheme" parent="@android:style/Theme.DeviceDefault.DayNight">
         <!-- System | Widget section -->
+        <item name="actionOverflowButtonStyle">@style/OverflowButtonStyle</item>
         <item name="android:backgroundDimEnabled">true</item>
         <item name="android:navigationBarColor">@color/picker_background_color</item>
         <item name="android:statusBarColor">@android:color/transparent</item>
@@ -103,7 +105,7 @@
         <item name="android:listPreferredItemPaddingEnd">@dimen/picker_settings_list_item_padding_end</item>
     </style>
 
-    <style name="PickerMaterialTheme" parent="@style/Theme.MaterialComponents.DayNight.NoActionBar">
+    <style name="PickerMaterialTheme" parent="@style/Theme.Material3.DayNight.NoActionBar">
         <item name="materialAlertDialogTheme">@style/ProfileDialogTheme</item>
         <item name="pickerDragBarColor">#DADCE0</item>
         <item name="pickerHighlightColor">?android:attr/colorAccent</item>
@@ -127,6 +129,8 @@
         <item name="pickerBannerPrimaryTextColor">?android:attr/textColorSecondary</item>
         <item name="pickerBannerSecondaryTextColor">?android:attr/textColorPrimary</item>
         <item name="pickerBannerButtonTextColor">?android:attr/colorAccent</item>
+        <item name="categoryDefaultThumbnailColor">?attr/colorOnSurfaceVariant</item>
+        <item name="categoryDefaultThumbnailCircleColor">?attr/colorSurfaceVariant</item>
     </style>
 
     <style name="PickerBannerButtonTheme"
@@ -138,4 +142,23 @@
         <item name="android:textColor">?attr/pickerBannerButtonTextColor</item>
     </style>
 
+    <style name="OverflowButtonStyle" parent="Widget.AppCompat.ActionButton.Overflow">
+        <item name="android:minWidth">@dimen/button_touch_size</item>
+    </style>
+
+    <style name="SelectedMediaPreloaderDialogTheme"
+           parent="@style/ThemeOverlay.MaterialComponents.MaterialAlertDialog.Centered">
+        <item name="android:textColor">?attr/colorOnSurfaceVariant</item>
+        <item name="materialAlertDialogTitleTextStyle">@style/AlertDialogTitleStyle</item>
+    </style>
+
+    <style name="ProgressDialogCancelButtonStyle"
+           parent="@style/Widget.MaterialComponents.Button.TextButton">
+        <item name="android:textColor">?attr/colorOnSurface</item>
+    </style>
+
+    <style name="AlertDialogTitleStyle"
+           parent="@style/MaterialAlertDialog.MaterialComponents.Title.Text.CenterStacked">
+        <item name="android:textColor">?attr/colorOnSurface</item>
+    </style>
 </resources>
diff --git a/src/com/android/providers/media/ConfigStore.java b/src/com/android/providers/media/ConfigStore.java
index 20a5484..828d620 100644
--- a/src/com/android/providers/media/ConfigStore.java
+++ b/src/com/android/providers/media/ConfigStore.java
@@ -35,6 +35,7 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.providers.media.util.StringUtils;
 
+import java.io.PrintWriter;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -47,23 +48,27 @@
  * always have permissions for accessing the {@link android.provider.DeviceConfig}).
  */
 public interface ConfigStore {
+
+    // TODO(b/288066342): Remove and replace after new constant definition in
+    //  {@link android.provider.DeviceConfig}.
+    String NAMESPACE_MEDIAPROVIDER = "mediaprovider";
     boolean DEFAULT_TAKE_OVER_GET_CONTENT = false;
     boolean DEFAULT_USER_SELECT_FOR_APP = true;
     boolean DEFAULT_STABILISE_VOLUME_INTERNAL = false;
     boolean DEFAULT_STABILIZE_VOLUME_EXTERNAL = false;
+    boolean DEFAULT_STABILIZE_VOLUME_PUBLIC = false;
 
     boolean DEFAULT_TRANSCODE_ENABLED = true;
     boolean DEFAULT_TRANSCODE_OPT_OUT_STRATEGY_ENABLED = false;
     int DEFAULT_TRANSCODE_MAX_DURATION = 60 * 1000; // 1 minute
 
-    int DEFAULT_PICKER_SYNC_DELAY = 5000; // 5 seconds
-
     boolean DEFAULT_PICKER_GET_CONTENT_PRELOAD = true;
     boolean DEFAULT_PICKER_PICK_IMAGES_PRELOAD = true;
     boolean DEFAULT_PICKER_PICK_IMAGES_RESPECT_PRELOAD_ARG = false;
 
-    boolean DEFAULT_CLOUD_MEDIA_IN_PHOTO_PICKER_ENABLED = false;
+    boolean DEFAULT_CLOUD_MEDIA_IN_PHOTO_PICKER_ENABLED = true;
     boolean DEFAULT_ENFORCE_CLOUD_PROVIDER_ALLOWLIST = true;
+    boolean DEFAULT_PICKER_CHOICE_MANAGED_SELECTION_ENABLED = true;
 
     /**
      * @return if the Cloud-Media-in-Photo-Picker enabled (e.g. platform will recognize and
@@ -74,6 +79,14 @@
     }
 
     /**
+     * @return if the Picker-Choice_Managed_selection is enabled.
+     */
+    default boolean isPickerChoiceManagedSelectionEnabled() {
+        return DEFAULT_PICKER_CHOICE_MANAGED_SELECTION_ENABLED;
+    }
+
+
+    /**
      * @return package name of the pre-configured "system default"
      *         {@link android.provider.CloudMediaProvider}.
      * @see #isCloudMediaInPhotoPickerEnabled()
@@ -104,14 +117,6 @@
     }
 
     /**
-     * @return a delay (in milliseconds) before executing PhotoPicker media sync on media events
-     *         like inserts/updates/deletes to artificially throttle the burst notifications.
-     */
-    default int getPickerSyncDelayMs() {
-        return DEFAULT_PICKER_SYNC_DELAY;
-    }
-
-    /**
      * @return if {@link com.android.providers.media.photopicker.PhotoPickerActivity} should preload
      *         selected media items before "returning"
      *         ({@link com.android.providers.media.photopicker.PhotoPickerActivity#setResultAndFinishSelf()})
@@ -180,6 +185,13 @@
     }
 
     /**
+     * @return if stable URI are enabled for public volumes.
+     */
+    default boolean isStableUrisForPublicVolumeEnabled() {
+        return DEFAULT_STABILIZE_VOLUME_PUBLIC;
+    }
+
+    /**
      * @return if transcoding is enabled.
      */
     default boolean isTranscodeEnabled() {
@@ -212,6 +224,33 @@
     void addOnChangeListener(@NonNull Executor executor, @NonNull Runnable listener);
 
     /**
+     * Print the {@link ConfigStore} state into the given stream.
+     */
+    default void dump(PrintWriter writer) {
+        writer.println("Config store state:");
+        writer.println("  isCloudMediaInPhotoPickerEnabled=" + isCloudMediaInPhotoPickerEnabled());
+        writer.println("  defaultCloudProviderPackage=" + getDefaultCloudProviderPackage());
+        writer.println("  allowedCloudProviderPackages=" + getAllowedCloudProviderPackages());
+        writer.println("  shouldEnforceCloudProviderAllowlist="
+                + shouldEnforceCloudProviderAllowlist());
+        writer.println("  shouldPickerPreloadForGetContent=" + shouldPickerPreloadForGetContent());
+        writer.println("  shouldPickerPreloadForPickImages=" + shouldPickerPreloadForPickImages());
+        writer.println("  shouldPickerRespectPreloadArgumentForPickImages="
+                + shouldPickerRespectPreloadArgumentForPickImages());
+        writer.println("  isGetContentTakeOverEnabled=" + isGetContentTakeOverEnabled());
+        writer.println("  isUserSelectForAppEnabled=" + isUserSelectForAppEnabled());
+        writer.println("  isStableUrisForInternalVolumeEnabled="
+                + isStableUrisForInternalVolumeEnabled());
+        writer.println("  isStableUrisForExternalVolumeEnabled="
+                + isStableUrisForExternalVolumeEnabled());
+        writer.println("  isTranscodeEnabled=" + isTranscodeEnabled());
+        writer.println("  shouldTranscodeDefault=" + shouldTranscodeDefault());
+        writer.println("  transcodeMaxDurationMs=" + getTranscodeMaxDurationMs());
+        writer.println("  transcodeCompatManifest=" + getTranscodeCompatManifest());
+        writer.println("  transcodeCompatStale=" + getTranscodeCompatStale());
+    }
+
+    /**
      * Implementation of the {@link ConfigStore} that reads "real" configs from
      * {@link android.provider.DeviceConfig}. Meant to be used by the "production" code.
      */
@@ -220,8 +259,11 @@
         private static final String KEY_USER_SELECT_FOR_APP = "user_select_for_app";
 
         @VisibleForTesting
-        public static final String KEY_STABILISE_VOLUME_INTERNAL = "stablise_volume_internal";
-        private static final String KEY_STABILIZE_VOLUME_EXTERNAL = "stabilize_volume_external";
+        public static final String KEY_STABILIZE_VOLUME_INTERNAL = "stabilize_volume_internal";
+        @VisibleForTesting
+        public static final String KEY_STABILIZE_VOLUME_EXTERNAL = "stabilize_volume_external";
+        @VisibleForTesting
+        public static final String KEY_STABILIZE_VOLUME_PUBLIC = "stabilize_volume_public";
 
         private static final String KEY_TRANSCODE_ENABLED = "transcode_enabled";
         private static final String KEY_TRANSCODE_OPT_OUT_STRATEGY_ENABLED = "transcode_default";
@@ -232,7 +274,7 @@
         private static final String SYSPROP_TRANSCODE_MAX_DURATION =
             "persist.sys.fuse.transcode_max_file_duration_ms";
         private static final int TRANSCODE_MAX_DURATION_INVALID = 0;
-        private static final String KEY_PICKER_SYNC_DELAY = "default_sync_delay_ms";
+
         private static final String KEY_PICKER_GET_CONTENT_PRELOAD =
                 "picker_get_content_preload_selected";
         private static final String KEY_PICKER_PICK_IMAGES_PRELOAD =
@@ -241,6 +283,8 @@
                 "picker_pick_images_respect_preload_selected_arg";
 
         private static final String KEY_CLOUD_MEDIA_FEATURE_ENABLED = "cloud_media_feature_enabled";
+        private static final String KEY_PICKER_CHOICE_MANAGED_SELECTION_ENABLED =
+                "picker_choice_managed_selection_enabled";
         private static final String KEY_CLOUD_MEDIA_PROVIDER_ALLOWLIST = "allowed_cloud_providers";
         private static final String KEY_CLOUD_MEDIA_ENFORCE_PROVIDER_ALLOWLIST =
                 "cloud_media_enforce_provider_allowlist";
@@ -256,8 +300,27 @@
 
         @Override
         public boolean isCloudMediaInPhotoPickerEnabled() {
-            return getBooleanDeviceConfig(KEY_CLOUD_MEDIA_FEATURE_ENABLED,
-                    DEFAULT_CLOUD_MEDIA_IN_PHOTO_PICKER_ENABLED);
+            Boolean isEnabled =
+                    getBooleanDeviceConfig(
+                            NAMESPACE_MEDIAPROVIDER,
+                            KEY_CLOUD_MEDIA_FEATURE_ENABLED,
+                            DEFAULT_CLOUD_MEDIA_IN_PHOTO_PICKER_ENABLED);
+
+            List<String> allowList =
+                    getStringArrayDeviceConfig(
+                            NAMESPACE_MEDIAPROVIDER, KEY_CLOUD_MEDIA_PROVIDER_ALLOWLIST);
+
+            // Only consider the feature enabled when the enabled flag is on AND when the allowlist
+            // of permitted cloud media providers is not empty.
+            return isEnabled && !allowList.isEmpty();
+        }
+
+        @Override
+        public boolean isPickerChoiceManagedSelectionEnabled() {
+            return getBooleanDeviceConfig(
+                            NAMESPACE_MEDIAPROVIDER,
+                            KEY_PICKER_CHOICE_MANAGED_SELECTION_ENABLED,
+                            DEFAULT_PICKER_CHOICE_MANAGED_SELECTION_ENABLED);
         }
 
         @Nullable
@@ -281,7 +344,8 @@
         @Override
         public List<String> getAllowedCloudProviderPackages() {
             final List<String> allowlist =
-                    getStringArrayDeviceConfig(KEY_CLOUD_MEDIA_PROVIDER_ALLOWLIST);
+                    getStringArrayDeviceConfig(NAMESPACE_MEDIAPROVIDER,
+                            KEY_CLOUD_MEDIA_PROVIDER_ALLOWLIST);
 
             // BACKWARD COMPATIBILITY WORKAROUND.
             // See javadoc to maybeExtractPackageNameFromCloudProviderAuthority() below for more
@@ -299,16 +363,13 @@
 
         @Override
         public boolean shouldEnforceCloudProviderAllowlist() {
-            return getBooleanDeviceConfig(KEY_CLOUD_MEDIA_ENFORCE_PROVIDER_ALLOWLIST,
+            return getBooleanDeviceConfig(
+                    NAMESPACE_MEDIAPROVIDER,
+                    KEY_CLOUD_MEDIA_ENFORCE_PROVIDER_ALLOWLIST,
                     DEFAULT_ENFORCE_CLOUD_PROVIDER_ALLOWLIST);
         }
 
         @Override
-        public int getPickerSyncDelayMs() {
-            return getIntDeviceConfig(KEY_PICKER_SYNC_DELAY, DEFAULT_PICKER_SYNC_DELAY);
-        }
-
-        @Override
         public boolean shouldPickerPreloadForGetContent() {
             return getBooleanDeviceConfig(KEY_PICKER_GET_CONTENT_PRELOAD,
                     DEFAULT_PICKER_GET_CONTENT_PRELOAD);
@@ -338,14 +399,20 @@
 
         @Override
         public boolean isStableUrisForInternalVolumeEnabled() {
-            return getBooleanDeviceConfig(
-                    KEY_STABILISE_VOLUME_INTERNAL, DEFAULT_STABILISE_VOLUME_INTERNAL);
+            return getBooleanDeviceConfig(NAMESPACE_MEDIAPROVIDER, KEY_STABILIZE_VOLUME_INTERNAL,
+                    DEFAULT_STABILISE_VOLUME_INTERNAL);
         }
 
         @Override
         public boolean isStableUrisForExternalVolumeEnabled() {
-            return getBooleanDeviceConfig(
-                    KEY_STABILIZE_VOLUME_EXTERNAL, DEFAULT_STABILIZE_VOLUME_EXTERNAL);
+            return getBooleanDeviceConfig(NAMESPACE_MEDIAPROVIDER, KEY_STABILIZE_VOLUME_EXTERNAL,
+                    DEFAULT_STABILIZE_VOLUME_EXTERNAL);
+        }
+
+        @Override
+        public boolean isStableUrisForPublicVolumeEnabled() {
+            return getBooleanDeviceConfig(NAMESPACE_MEDIAPROVIDER, KEY_STABILIZE_VOLUME_PUBLIC,
+                    DEFAULT_STABILIZE_VOLUME_PUBLIC);
         }
 
         @Override
@@ -400,6 +467,8 @@
             // that make changes to this package independent of reboot
             DeviceConfig.addOnPropertiesChangedListener(
                     NAMESPACE_STORAGE_NATIVE_BOOT, executor, unused -> listener.run());
+            DeviceConfig.addOnPropertiesChangedListener(
+                    NAMESPACE_MEDIAPROVIDER, executor, unused -> listener.run());
         }
 
         private static boolean getBooleanDeviceConfig(@NonNull String key, boolean defaultValue) {
@@ -410,6 +479,15 @@
                     DeviceConfig.getBoolean(NAMESPACE_STORAGE_NATIVE_BOOT, key, defaultValue));
         }
 
+        private static boolean getBooleanDeviceConfig(@NonNull String namespace,
+                @NonNull String key, boolean defaultValue) {
+            if (!sCanReadDeviceConfig) {
+                return defaultValue;
+            }
+            return withCleanCallingIdentity(
+                    () -> DeviceConfig.getBoolean(namespace, key, defaultValue));
+        }
+
         private static int getIntDeviceConfig(@NonNull String key, int defaultValue) {
             if (!sCanReadDeviceConfig) {
                 return defaultValue;
@@ -426,6 +504,15 @@
                     DeviceConfig.getString(NAMESPACE_STORAGE_NATIVE_BOOT, key, null));
         }
 
+        private static String getStringDeviceConfig(@NonNull String namespace,
+                @NonNull String key) {
+            if (!sCanReadDeviceConfig) {
+                return null;
+            }
+            return withCleanCallingIdentity(() ->
+                    DeviceConfig.getString(namespace, key, null));
+        }
+
         private static List<String> getStringArrayDeviceConfig(@NonNull String key) {
             final String items = getStringDeviceConfig(key);
             if (StringUtils.isNullOrEmpty(items)) {
@@ -434,6 +521,15 @@
             return Arrays.asList(items.split(","));
         }
 
+        private static List<String> getStringArrayDeviceConfig(@NonNull String namespace,
+                @NonNull String key) {
+            final String items = getStringDeviceConfig(namespace, key);
+            if (StringUtils.isNullOrEmpty(items)) {
+                return Collections.emptyList();
+            }
+            return Arrays.asList(items.split(","));
+        }
+
         private static <T> T withCleanCallingIdentity(@NonNull Supplier<T> action) {
             final long callingIdentity = Binder.clearCallingIdentity();
             try {
diff --git a/src/com/android/providers/media/DatabaseBackupAndRecovery.java b/src/com/android/providers/media/DatabaseBackupAndRecovery.java
index cef55ed..2a99e7d 100644
--- a/src/com/android/providers/media/DatabaseBackupAndRecovery.java
+++ b/src/com/android/providers/media/DatabaseBackupAndRecovery.java
@@ -16,6 +16,11 @@
 
 package com.android.providers.media;
 
+import static com.android.providers.media.DatabaseHelper.DATA_MEDIA_XATTR_DIRECTORY_PATH;
+import static com.android.providers.media.DatabaseHelper.EXTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX;
+import static com.android.providers.media.DatabaseHelper.EXTERNAL_DB_SESSION_ID_XATTR_KEY_PREFIX;
+import static com.android.providers.media.DatabaseHelper.INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX;
+import static com.android.providers.media.DatabaseHelper.INTERNAL_DB_SESSION_ID_XATTR_KEY_PREFIX;
 import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__VOLUME__EXTERNAL_PRIMARY;
 import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__VOLUME__INTERNAL;
 import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__VOLUME__PUBLIC;
@@ -25,12 +30,15 @@
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.os.CancellationSignal;
+import android.os.Environment;
 import android.os.ParcelFileDescriptor;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.provider.MediaStore;
+import android.system.ErrnoException;
 import android.system.Os;
+import android.system.OsConstants;
 import android.util.Log;
 import android.util.Pair;
 
@@ -46,13 +54,17 @@
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
 
 /**
  * To ensure that the ids of MediaStore database uris are stable and reliable.
@@ -70,12 +82,9 @@
             "/data/media/" + UserHandle.myUserId() + "/.transforms/recovery/leveldb-ownership";
 
     /**
-     * Path which stores backup of external primary volume.
-     * Lower file system path is used as upper file system does not support xattrs.
+     * Every LevelDB table name starts with this prefix.
      */
-    private static final String EXTERNAL_PRIMARY_VOLUME_BACKUP_PATH =
-            "/data/media/" + UserHandle.myUserId()
-                    + "/.transforms/recovery/leveldb-external_primary";
+    private static final String LEVEL_DB_PREFIX = "leveldb-";
 
     /**
      * Frequency at which next value of owner id is backed up in the external storage.
@@ -116,15 +125,11 @@
             MediaStore.Files.FileColumns._USER_ID,
             MediaStore.Files.FileColumns.DATE_EXPIRES,
             MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME,
-            MediaStore.Files.FileColumns.GENERATION_MODIFIED
+            MediaStore.Files.FileColumns.GENERATION_MODIFIED,
+            MediaStore.Files.FileColumns.VOLUME_NAME
     };
 
     /**
-     * Wait time of 5 seconds in millis.
-     */
-    private static final long WAIT_TIME_5_SECONDS_IN_MILLIS = 5000;
-
-    /**
      * Wait time of 10 seconds in millis.
      */
     private static final long WAIT_TIME_10_SECONDS_IN_MILLIS = 10000;
@@ -146,10 +151,9 @@
     private AtomicInteger mNextOwnerIdBackup;
     private final ConfigStore mConfigStore;
     private final VolumeCache mVolumeCache;
+    private Set<String> mSetupCompletePublicVolumes = ConcurrentHashMap.newKeySet();
 
-    private AtomicBoolean mIsBackupSetupComplete = new AtomicBoolean(false);
-
-    private Map<String, String> mOwnerIdRelationMap;
+    private static Map<String, String> sOwnerIdRelationMap;
 
     protected DatabaseBackupAndRecovery(ConfigStore configStore, VolumeCache volumeCache) {
         mConfigStore = configStore;
@@ -171,20 +175,11 @@
                         "persist.sys.fuse.backup.external_volume_backup",
                         /* defaultValue */ false);
             default:
-                return false;
-        }
-    }
-
-    protected void onConfigPropertyChangeListener() {
-        if ((mConfigStore.isStableUrisForInternalVolumeEnabled()
-                || mConfigStore.isStableUrisForExternalVolumeEnabled())
-                && mVolumeCache.getExternalVolumeNames().contains(
-                MediaStore.VOLUME_EXTERNAL_PRIMARY)) {
-            Log.i(TAG,
-                    "On device config change, found stable uri support enabled. Attempting backup"
-                            + " and recovery setup.");
-            setupVolumeDbBackupAndRecovery(MediaStore.VOLUME_EXTERNAL_PRIMARY,
-                    new File(EXTERNAL_PRIMARY_ROOT_PATH));
+                // public volume
+                return isStableUrisEnabled(MediaStore.VOLUME_EXTERNAL_PRIMARY)
+                        && mConfigStore.isStableUrisForPublicVolumeEnabled()
+                        || SystemProperties.getBoolean("persist.sys.fuse.backup.public_db_backup",
+                        /* defaultValue */ false);
         }
     }
 
@@ -195,10 +190,9 @@
      * volume on Media mount signal of EXTERNAL_PRIMARY.
      */
     protected synchronized void setupVolumeDbBackupAndRecovery(String volumeName, File volumePath) {
-        // We are setting up leveldb instance only for internal volume as of now. Since internal
-        // volume does not have any fuse daemon thread, leveldb instance is created by fuse
-        // daemon thread of EXTERNAL_PRIMARY.
-        if (!MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)) {
+        // Since internal volume does not have any fuse daemon thread, leveldb instance
+        // for internal volume is created by fuse daemon thread of EXTERNAL_PRIMARY.
+        if (MediaStore.VOLUME_INTERNAL.equalsIgnoreCase(volumeName)) {
             // Set backup only for external primary for now.
             return;
         }
@@ -208,22 +202,37 @@
             return;
         }
 
-        if (mIsBackupSetupComplete.get()) {
+        if (mSetupCompletePublicVolumes.contains(volumeName)) {
             // Return if setup is already done
             return;
         }
 
+        final long startTime = SystemClock.elapsedRealtime();
         try {
             if (!new File(RECOVERY_DIRECTORY_PATH).exists()) {
                 new File(RECOVERY_DIRECTORY_PATH).mkdirs();
             }
-            FuseDaemon fuseDaemon = getFuseDaemonForFileWithWait(volumePath,
-                    WAIT_TIME_5_SECONDS_IN_MILLIS);
-            fuseDaemon.setupVolumeDbBackup();
-            mIsBackupSetupComplete = new AtomicBoolean(true);
+            FuseDaemon fuseDaemon = getFuseDaemonForFileWithWait(new File(
+                    DatabaseBackupAndRecovery.EXTERNAL_PRIMARY_ROOT_PATH));
+            Log.d(TAG, "Received db backup Fuse Daemon for: " + volumeName);
+            if (isStableUrisEnabled(volumeName)) {
+                if (MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)) {
+                    // Setup internal and external volumes
+                    fuseDaemon.setupVolumeDbBackup();
+                } else {
+                    // Setup public volume
+                    fuseDaemon.setupPublicVolumeDbBackup(volumeName);
+                }
+                mSetupCompletePublicVolumes.add(volumeName);
+            }
         } catch (IOException e) {
             Log.e(TAG, "Failure in setting up backup and recovery for volume: " + volumeName, e);
+            return;
+        } finally {
+            Log.i(TAG, "Backup and recovery setup time taken in milliseconds:" + (
+                    SystemClock.elapsedRealtime() - startTime));
         }
+        Log.i(TAG, "Successfully set up backup and recovery for volume: " + volumeName);
     }
 
     /**
@@ -231,9 +240,19 @@
      */
     public void backupDatabases(DatabaseHelper internalDatabaseHelper,
             DatabaseHelper externalDatabaseHelper, CancellationSignal signal) {
+        setupVolumeDbBackupAndRecovery(MediaStore.VOLUME_EXTERNAL_PRIMARY,
+          new File(EXTERNAL_PRIMARY_ROOT_PATH));
         Log.i(TAG, "Triggering database backup");
         backupInternalDatabase(internalDatabaseHelper, signal);
-        backupExternalDatabase(externalDatabaseHelper, signal);
+        backupExternalDatabase(externalDatabaseHelper, MediaStore.VOLUME_EXTERNAL_PRIMARY, signal);
+
+        for (MediaVolume mediaVolume : mVolumeCache.getExternalVolumes()) {
+            if (mediaVolume.isPublicVolume()) {
+                setupVolumeDbBackupAndRecovery(mediaVolume.getName(),
+                        new File(EXTERNAL_PRIMARY_ROOT_PATH));
+                backupExternalDatabase(externalDatabaseHelper, mediaVolume.getName(), signal);
+            }
+        }
     }
 
     protected Optional<BackupIdRow> readDataFromBackup(String volumeName, String filePath) {
@@ -241,9 +260,14 @@
             return Optional.empty();
         }
 
-        final String fuseDaemonFilePath = getFuseDaemonFilePath(filePath);
         try {
-            final String data = getFuseDaemonForPath(fuseDaemonFilePath).readBackedUpData(filePath);
+            final String data = getFuseDaemonForPath(EXTERNAL_PRIMARY_ROOT_PATH)
+                    .readBackedUpData(filePath);
+            if (data == null || data.isEmpty()) {
+                Log.w(TAG, "No backup found for path: " + filePath);
+                return Optional.empty();
+            }
+
             return Optional.of(BackupIdRow.deserialize(data));
         } catch (Exception e) {
             Log.e(TAG, "Failure in getting backed up data for filePath: " + filePath, e);
@@ -251,16 +275,15 @@
         }
     }
 
-    protected void backupInternalDatabase(DatabaseHelper internalDbHelper,
+    protected synchronized void backupInternalDatabase(DatabaseHelper internalDbHelper,
             CancellationSignal signal) {
         if (!isStableUrisEnabled(MediaStore.VOLUME_INTERNAL)
                 || internalDbHelper.isDatabaseRecovering()) {
             return;
         }
 
-        if (!mIsBackupSetupComplete.get()) {
-            setupVolumeDbBackupAndRecovery(MediaStore.VOLUME_EXTERNAL,
-                    new File(EXTERNAL_PRIMARY_ROOT_PATH));
+        if (!mSetupCompletePublicVolumes.contains(MediaStore.VOLUME_EXTERNAL_PRIMARY)) {
+            return;
         }
 
         FuseDaemon fuseDaemon;
@@ -291,63 +314,60 @@
         });
     }
 
-    protected void backupExternalDatabase(DatabaseHelper externalDbHelper,
-            CancellationSignal signal) {
-        if (!isStableUrisEnabled(MediaStore.VOLUME_EXTERNAL_PRIMARY)
+    protected synchronized void backupExternalDatabase(DatabaseHelper externalDbHelper,
+            String volumeName, CancellationSignal signal) {
+        if (!isStableUrisEnabled(volumeName)
                 || externalDbHelper.isDatabaseRecovering()) {
             return;
         }
 
-        if (!mIsBackupSetupComplete.get()) {
-            setupVolumeDbBackupAndRecovery(MediaStore.VOLUME_EXTERNAL,
-                    new File(EXTERNAL_PRIMARY_ROOT_PATH));
+        if (!mSetupCompletePublicVolumes.contains(volumeName)) {
+            return;
         }
 
         FuseDaemon fuseDaemon;
         try {
-            fuseDaemon = getFuseDaemonForFileWithWait(new File(EXTERNAL_PRIMARY_ROOT_PATH),
-                    WAIT_TIME_5_SECONDS_IN_MILLIS);
+            fuseDaemon = getFuseDaemonForFileWithWait(new File(EXTERNAL_PRIMARY_ROOT_PATH));
         } catch (FileNotFoundException e) {
             Log.e(TAG,
                     "Fuse Daemon not found for primary external storage, skipping backing up of "
-                            + "external database.",
+                            + volumeName,
                     e);
             return;
         }
 
-        // Read last backed up generation number
-        Optional<Long> lastBackedUpGenNum = getXattrOfLongValue(
-                EXTERNAL_PRIMARY_VOLUME_BACKUP_PATH, LAST_BACKEDUP_GENERATION_XATTR_KEY);
-        long lastBackedGenerationNumber = lastBackedUpGenNum.isPresent()
-                ? lastBackedUpGenNum.get() : 0;
-        if (lastBackedGenerationNumber > 0) {
-            Log.i(TAG, "Last backed up generation number is " + lastBackedGenerationNumber);
-        }
-        final String generationClause = MediaStore.Files.FileColumns.GENERATION_MODIFIED + " > "
+        final String backupPath = RECOVERY_DIRECTORY_PATH + "/" + LEVEL_DB_PREFIX + volumeName;
+        long lastBackedGenerationNumber = getLastBackedGenerationNumber(backupPath);
+
+        final String generationClause = MediaStore.Files.FileColumns.GENERATION_MODIFIED + " >= "
                 + lastBackedGenerationNumber;
         final String volumeClause = MediaStore.Files.FileColumns.VOLUME_NAME + " = '"
-                + MediaStore.VOLUME_EXTERNAL_PRIMARY + "'";
+                + volumeName + "'";
         final String selectionClause = generationClause + " AND " + volumeClause;
 
         externalDbHelper.runWithTransaction((db) -> {
             long maxGeneration = lastBackedGenerationNumber;
+            Log.d(TAG, "Started to back up " + volumeName
+                    + ", maxGeneration:" + maxGeneration);
             try (Cursor c = db.query(true, "files", QUERY_COLUMNS, selectionClause, null, null,
-                    null, null, null, signal)) {
+                    null, MediaStore.MediaColumns._ID + " ASC", null, signal)) {
                 while (c.moveToNext()) {
                     if (signal != null && signal.isCanceled()) {
+                        Log.i(TAG, "Received a cancellation signal during the DB "
+                                + "backup process");
                         break;
                     }
                     backupDataValues(fuseDaemon, c);
                     maxGeneration = Math.max(maxGeneration, c.getLong(9));
                 }
-                setXattr(EXTERNAL_PRIMARY_VOLUME_BACKUP_PATH, LAST_BACKEDUP_GENERATION_XATTR_KEY,
-                        String.valueOf(maxGeneration));
+                setXattr(backupPath, LAST_BACKEDUP_GENERATION_XATTR_KEY,
+                        String.valueOf(maxGeneration - 1));
                 Log.d(TAG, String.format(Locale.ROOT,
-                        "Backed up %d rows of external database to external storage on idle "
+                        "Backed up %d rows of " + volumeName + " to external storage on idle "
                                 + "maintenance.",
                         c.getCount()));
             } catch (Exception e) {
-                Log.e(TAG, "Failure in backing up external database to external storage.", e);
+                Log.e(TAG, "Failure in backing up " + volumeName + " to external storage.", e);
                 return null;
             }
             return null;
@@ -364,10 +384,11 @@
         final int userId = c.getInt(6);
         final String dateExpires = c.getString(7);
         final String ownerPackageName = c.getString(8);
+        final String volumeName = c.getString(10);
         BackupIdRow backupIdRow = createBackupIdRow(fuseDaemon, id, mediaType,
                 isFavorite, isPending, isTrashed, userId, dateExpires,
                 ownerPackageName);
-        fuseDaemon.backupVolumeDbData(data, BackupIdRow.serialize(backupIdRow));
+        fuseDaemon.backupVolumeDbData(volumeName, data, BackupIdRow.serialize(backupIdRow));
     }
 
     protected void deleteBackupForVolume(String volumeName) {
@@ -414,6 +435,19 @@
         }
     }
 
+    private long getLastBackedGenerationNumber(String backupPath) {
+        // Read last backed up generation number
+        Optional<Long> lastBackedUpGenNum = getXattrOfLongValue(
+                backupPath, LAST_BACKEDUP_GENERATION_XATTR_KEY);
+        long lastBackedGenerationNumber = lastBackedUpGenNum.isPresent()
+                ? lastBackedUpGenNum.get() : 0;
+        if (lastBackedGenerationNumber > 0) {
+            Log.i(TAG, "Last backed up generation number for " + backupPath + " is "
+                    + lastBackedGenerationNumber);
+        }
+        return lastBackedGenerationNumber;
+    }
+
     @NonNull
     private FuseDaemon getFuseDaemonForPath(@NonNull String path)
             throws FileNotFoundException {
@@ -434,21 +468,16 @@
             return;
         }
 
-        // For all internal file paths, redirect to external primary fuse daemon.
-        final String fuseDaemonFilePath = getFuseDaemonFilePath(insertedRow.getPath());
         try {
-            FuseDaemon fuseDaemon = getFuseDaemonForPath(fuseDaemonFilePath);
+            FuseDaemon fuseDaemon = getFuseDaemonForPath(EXTERNAL_PRIMARY_ROOT_PATH);
             final BackupIdRow value = createBackupIdRow(fuseDaemon, insertedRow);
-            fuseDaemon.backupVolumeDbData(insertedRow.getPath(), BackupIdRow.serialize(value));
+            fuseDaemon.backupVolumeDbData(insertedRow.getVolumeName(), insertedRow.getPath(),
+                    BackupIdRow.serialize(value));
         } catch (Exception e) {
             Log.e(TAG, "Failure in backing up data to external storage", e);
         }
     }
 
-    private String getFuseDaemonFilePath(String filePath) {
-        return filePath.startsWith("/storage") ? filePath : EXTERNAL_PRIMARY_ROOT_PATH;
-    }
-
     private BackupIdRow createBackupIdRow(FuseDaemon fuseDaemon, FileRow insertedRow)
             throws IOException {
         return createBackupIdRow(fuseDaemon, insertedRow.getId(), insertedRow.getMediaType(),
@@ -503,7 +532,7 @@
 
         int nextOwnerId = getAndIncrementNextOwnerId();
         fuseDaemon.createOwnerIdRelation(String.valueOf(nextOwnerId), ownerPackageIdentifier);
-        Log.i(TAG, "Created relation b/w " + nextOwnerId + " and " + ownerPackageIdentifier);
+        Log.v(TAG, "Created relation b/w " + nextOwnerId + " and " + ownerPackageIdentifier);
         return nextOwnerId;
     }
 
@@ -576,10 +605,9 @@
             return;
         }
 
-        // For all internal file paths, redirect to external primary fuse daemon.
-        String fuseDaemonFilePath = getFuseDaemonFilePath(deletedFilePath);
         try {
-            getFuseDaemonForPath(fuseDaemonFilePath).deleteDbBackup(deletedFilePath);
+            getFuseDaemonForPath(EXTERNAL_PRIMARY_ROOT_PATH).deleteDbBackup(
+                    deletedFilePath);
         } catch (IOException e) {
             Log.w(TAG, "Failure in deleting backup data for key: " + deletedFilePath, e);
         }
@@ -588,7 +616,7 @@
     protected boolean isBackupUpdateAllowed(DatabaseHelper databaseHelper, String volumeName) {
         // Backup only if stable uris is enabled, db is not recovering and backup setup is complete.
         return isStableUrisEnabled(volumeName) && !databaseHelper.isDatabaseRecovering()
-                && mIsBackupSetupComplete.get();
+                && mSetupCompletePublicVolumes.contains(volumeName);
     }
 
 
@@ -614,10 +642,10 @@
         }
 
         final String updatedFilePath = updatedRow.getPath();
-        // For all internal file paths, redirect to external primary fuse daemon.
-        final String fuseDaemonFilePath = getFuseDaemonFilePath(updatedFilePath);
         try {
-            getFuseDaemonForPath(fuseDaemonFilePath).backupVolumeDbData(updatedFilePath,
+            getFuseDaemonForPath(EXTERNAL_PRIMARY_ROOT_PATH).backupVolumeDbData(
+                    updatedRow.getVolumeName(),
+                    updatedFilePath,
                     BackupIdRow.serialize(BackupIdRow.newBuilder(updatedRow.getId()).setIsDirty(
                             true).build()));
         } catch (IOException e) {
@@ -629,7 +657,7 @@
     /**
      * Reads value corresponding to given key from xattr on given path.
      */
-    public static Optional<String> getXattr(String path, String key) {
+    static Optional<String> getXattr(String path, String key) {
         try {
             return Optional.of(Arrays.toString(Os.getxattr(path, key)));
         } catch (Exception e) {
@@ -642,7 +670,7 @@
     /**
      * Reads long value corresponding to given key from xattr on given path.
      */
-    public static Optional<Long> getXattrOfLongValue(String path, String key) {
+    static Optional<Long> getXattrOfLongValue(String path, String key) {
         try {
             return Optional.of(Long.parseLong(new String(Os.getxattr(path, key))));
         } catch (Exception e) {
@@ -655,7 +683,7 @@
     /**
      * Reads integer value corresponding to given key from xattr on given path.
      */
-    public static Optional<Integer> getXattrOfIntegerValue(String path, String key) {
+    static Optional<Integer> getXattrOfIntegerValue(String path, String key) {
         try {
             return Optional.of(Integer.parseInt(new String(Os.getxattr(path, key))));
         } catch (Exception e) {
@@ -668,7 +696,7 @@
     /**
      * Sets key and value as xattr on given path.
      */
-    public static boolean setXattr(String path, String key, String value) {
+    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
@@ -683,6 +711,44 @@
         }
     }
 
+    /**
+     * Deletes xattr with given key on given path. Becomes a no-op when xattr is not present.
+     */
+    static boolean removeXattr(String path, String key) {
+        try (ParcelFileDescriptor pfd = ParcelFileDescriptor.open(new File(path),
+                ParcelFileDescriptor.MODE_READ_ONLY)) {
+            Os.removexattr(path, key);
+            Os.fsync(pfd.getFileDescriptor());
+            Log.d(TAG, String.format("xattr key:%s removed on path: %s.", key, path));
+            return true;
+        } catch (Exception e) {
+            if (e instanceof ErrnoException) {
+                ErrnoException exception = (ErrnoException) e;
+                if (exception.errno == OsConstants.ENODATA) {
+                    Log.w(TAG, String.format(Locale.ROOT,
+                            "xattr:%s is not removed as it is not found on path: %s.", key, path));
+                    return true;
+                }
+            }
+
+            Log.e(TAG, String.format(Locale.ROOT, "Failed to remove xattr:%s for path: %s.", key,
+                    path), e);
+            return false;
+        }
+    }
+
+    /**
+     * Lists xattrs of given path.
+     */
+    static List<String> listXattr(String path) {
+        try {
+            return Arrays.asList(Os.listxattr(path));
+        } catch (Exception e) {
+            Log.e(TAG, "Exception in reading xattrs on path: " + path, e);
+            return new ArrayList<>();
+        }
+    }
+
     protected void insertDataInDatabase(SQLiteDatabase db, BackupIdRow row, String filePath,
             String volumeName) {
         final ContentValues values = createValuesFromFileRow(row, filePath, volumeName);
@@ -722,41 +788,60 @@
         return values;
     }
 
-    private Pair<String, Integer> getOwnerPackageNameAndUidPair(int ownerPackageId) {
-        if (mOwnerIdRelationMap == null) {
+    protected Pair<String, Integer> getOwnerPackageNameAndUidPair(int ownerPackageId) {
+        if (sOwnerIdRelationMap == null) {
             try {
-                mOwnerIdRelationMap = getFuseDaemonForPath(
-                        EXTERNAL_PRIMARY_ROOT_PATH).readOwnerIdRelations();
-                Log.i(TAG, "Cached owner id map");
+                sOwnerIdRelationMap = readOwnerIdRelationsFromLevelDb();
+                Log.v(TAG, "Cached owner id map");
             } catch (IOException e) {
                 Log.e(TAG, "Failure in reading owner details for owner id:" + ownerPackageId, e);
                 return Pair.create(null, null);
             }
         }
 
-        if (mOwnerIdRelationMap.containsKey(String.valueOf(ownerPackageId))) {
-            return getPackageNameAndUserId(mOwnerIdRelationMap.get(String.valueOf(ownerPackageId)));
+        if (sOwnerIdRelationMap.containsKey(String.valueOf(ownerPackageId))) {
+            return getPackageNameAndUserId(sOwnerIdRelationMap.get(String.valueOf(ownerPackageId)));
         }
+
         return Pair.create(null, null);
     }
 
-    protected void recoverData(SQLiteDatabase db, String volumeName) {
-        if (!isBackupPresent()) {
-            return;
+    protected Map<String, String> readOwnerIdRelationsFromLevelDb() throws IOException {
+        return getFuseDaemonForPath(EXTERNAL_PRIMARY_ROOT_PATH).readOwnerIdRelations();
+    }
+
+    protected String readOwnerPackageName(String ownerId) throws IOException {
+        Map<String, String> ownerIdRelationMap = readOwnerIdRelationsFromLevelDb();
+        if (ownerIdRelationMap.containsKey(String.valueOf(ownerId))) {
+            return getPackageNameAndUserId(ownerIdRelationMap.get(ownerId)).first;
         }
 
+        return null;
+    }
+
+    protected void recoverData(SQLiteDatabase db, String volumeName) {
+        if (!MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)
+                && !MediaStore.VOLUME_INTERNAL.equalsIgnoreCase(volumeName)) {
+            // todo: implement for public volume
+            return;
+        }
         final long startTime = SystemClock.elapsedRealtime();
         final String fuseFilePath = getFuseFilePathFromVolumeName(volumeName);
         // Wait for external primary to be attached as we use same thread for internal volume.
         // Maximum wait for 10s
         try {
-            getFuseDaemonForFileWithWait(new File(fuseFilePath), WAIT_TIME_10_SECONDS_IN_MILLIS);
+            getFuseDaemonForFileWithWait(new File(fuseFilePath));
         } catch (FileNotFoundException e) {
             Log.e(TAG, "Could not recover data as fuse daemon could not serve requests.", e);
             return;
         }
 
-        setupVolumeDbBackupAndRecovery(volumeName, new File(EXTERNAL_PRIMARY_ROOT_PATH));
+        if (!isBackupPresent()) {
+            Log.w(TAG, "Backup is not present for " + volumeName);
+            return;
+        }
+        Log.d(TAG, "Backup is present for " + volumeName);
+
         long rowsRecovered = 0;
         long dirtyRowsCount = 0;
         String[] backedUpFilePaths;
@@ -765,10 +850,12 @@
         while (true) {
             backedUpFilePaths = readBackedUpFilePaths(volumeName, lastReadValue,
                     LEVEL_DB_READ_LIMIT);
-            if (backedUpFilePaths.length <= 0) {
+            if (backedUpFilePaths.length == 0) {
                 break;
             }
 
+            // Reset cached owner id relation map
+            sOwnerIdRelationMap = null;
             for (String filePath : backedUpFilePaths) {
                 Optional<BackupIdRow> fileRow = readDataFromBackup(volumeName, filePath);
                 if (fileRow.isPresent()) {
@@ -795,8 +882,9 @@
                 volumeName));
         if (MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)) {
             // Resetting generation number
-            setXattr(EXTERNAL_PRIMARY_VOLUME_BACKUP_PATH, LAST_BACKEDUP_GENERATION_XATTR_KEY,
-                    String.valueOf(0));
+            setXattr(RECOVERY_DIRECTORY_PATH + "/" + LEVEL_DB_PREFIX
+                            + MediaStore.VOLUME_EXTERNAL_PRIMARY,
+                    LAST_BACKEDUP_GENERATION_XATTR_KEY, String.valueOf(0));
         }
         Log.i(TAG, String.format(Locale.ROOT, "Recovery time: %d ms", recoveryTime));
     }
@@ -805,9 +893,11 @@
         return new File(RECOVERY_DIRECTORY_PATH).exists();
     }
 
-    protected FuseDaemon getFuseDaemonForFileWithWait(File fuseFilePath, long waitTime)
+    protected FuseDaemon getFuseDaemonForFileWithWait(File fuseFilePath)
             throws FileNotFoundException {
-        return MediaProvider.getFuseDaemonForFileWithWait(fuseFilePath, mVolumeCache, waitTime);
+        pollForExternalStorageMountedState();
+        return MediaProvider.getFuseDaemonForFileWithWait(fuseFilePath, mVolumeCache,
+                WAIT_TIME_10_SECONDS_IN_MILLIS);
     }
 
     private int getVolumeNameForStatsLog(String volumeName) {
@@ -863,12 +953,11 @@
                     null, null)) {
                 if (c.moveToFirst()) {
                     backupDataValues(fuseDaemon, c);
-                    Log.v(TAG, "Updated backed up row in leveldb");
                     String newPath = c.getString(1);
                     if (oldRow.getPath() != null && !oldRow.getPath().equalsIgnoreCase(newPath)) {
                         // If file path has changed, update leveldb backup to delete old path.
                         deleteFromDbBackup(helper, oldRow);
-                        Log.v(TAG, "Deleted backup of old file path.");
+                        Log.v(TAG, "Deleted backup of old file path: " + oldRow.getPath());
                     }
                 }
             } catch (Exception e) {
@@ -877,4 +966,80 @@
             return null;
         });
     }
+
+    /**
+     * Removes database recovery data for given user id. This is done when a user is removed.
+     */
+    protected void removeRecoveryDataForUserId(int removedUserId) {
+        String removeduserIdString = String.valueOf(removedUserId);
+        removeXattr(DATA_MEDIA_XATTR_DIRECTORY_PATH,
+                INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX.concat(
+                        removeduserIdString));
+        removeXattr(DATA_MEDIA_XATTR_DIRECTORY_PATH,
+                EXTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX.concat(
+                        removeduserIdString));
+        removeXattr(DATA_MEDIA_XATTR_DIRECTORY_PATH,
+                INTERNAL_DB_SESSION_ID_XATTR_KEY_PREFIX.concat(removeduserIdString));
+        removeXattr(DATA_MEDIA_XATTR_DIRECTORY_PATH,
+                EXTERNAL_DB_SESSION_ID_XATTR_KEY_PREFIX.concat(removeduserIdString));
+        Log.v(TAG, "Removed recovery data for user id: " + removedUserId);
+    }
+
+    /**
+     * Removes database recovery data for obsolete user id. It accepts list of valid/active users
+     * and removes the recovery data for ones not present in this list.
+     * This is done during an idle maintenance.
+     */
+    protected void removeRecoveryDataExceptValidUsers(List<String> validUsers) {
+        List<String> xattrList = listXattr(DATA_MEDIA_XATTR_DIRECTORY_PATH);
+        Log.i(TAG, "Xattr list is " + xattrList);
+        if (xattrList.isEmpty()) {
+            return;
+        }
+
+        Log.i(TAG, "Valid users list is " + validUsers);
+        List<String> invalidUsers = getInvalidUsersList(xattrList, validUsers);
+        Log.i(TAG, "Invalid users list is " + invalidUsers);
+        for (String userIdToBeRemoved : invalidUsers) {
+            if (userIdToBeRemoved != null && !userIdToBeRemoved.trim().isEmpty()) {
+                removeRecoveryDataForUserId(Integer.parseInt(userIdToBeRemoved));
+            }
+        }
+    }
+
+    protected static List<String> getInvalidUsersList(List<String> recoveryData,
+            List<String> validUsers) {
+        Set<String> presentUserIdsAsXattr = new HashSet<>();
+        for (String xattr : recoveryData) {
+            if (xattr.startsWith(INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX)) {
+                presentUserIdsAsXattr.add(
+                        xattr.substring(INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX.length()));
+            } else if (xattr.startsWith(EXTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX)) {
+                presentUserIdsAsXattr.add(
+                        xattr.substring(EXTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX.length()));
+            } else if (xattr.startsWith(INTERNAL_DB_SESSION_ID_XATTR_KEY_PREFIX)) {
+                presentUserIdsAsXattr.add(
+                        xattr.substring(INTERNAL_DB_SESSION_ID_XATTR_KEY_PREFIX.length()));
+            } else if (xattr.startsWith(EXTERNAL_DB_SESSION_ID_XATTR_KEY_PREFIX)) {
+                presentUserIdsAsXattr.add(
+                        xattr.substring(EXTERNAL_DB_SESSION_ID_XATTR_KEY_PREFIX.length()));
+            }
+        }
+        // Remove valid users
+        validUsers.forEach(presentUserIdsAsXattr::remove);
+        return presentUserIdsAsXattr.stream().collect(Collectors.toList());
+    }
+
+    private static void pollForExternalStorageMountedState() {
+        final File target = Environment.getExternalStorageDirectory();
+        for (int i = 0; i < WAIT_TIME_10_SECONDS_IN_MILLIS / 100; i++) {
+            if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState(target))) {
+                return;
+            }
+            Log.v(TAG, "Waiting for external storage...");
+            SystemClock.sleep(100);
+        }
+        throw new RuntimeException("Timed out while waiting for ExternalStorageState "
+                + "to be MEDIA_MOUNTED");
+    }
 }
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index 69e54b2..0fb7ff3 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -112,28 +112,52 @@
     public static final String TEST_CLEAN_DB = "test_clean";
 
     /**
+     * Prefix of key name of xattr used to set next row id for internal DB.
+     */
+    static final String INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX = "user.intdbnextrowid";
+
+    /**
      * 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()));
+    static final String INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY =
+            INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX.concat(
+                    String.valueOf(UserHandle.myUserId()));
+
+    /**
+     * Prefix of key name of xattr used to set next row id for external DB.
+     */
+    static final String EXTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX = "user.extdbnextrowid";
 
     /**
      * 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()));
+    static final String EXTERNAL_DB_NEXT_ROW_ID_XATTR_KEY =
+            EXTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX.concat(
+                    String.valueOf(UserHandle.myUserId()));
+
+    /**
+     * Prefix of key name of xattr used to set session id for internal DB.
+     */
+    static final String INTERNAL_DB_SESSION_ID_XATTR_KEY_PREFIX = "user.intdbsessionid";
 
     /**
      * 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()));
+    static final String INTERNAL_DB_SESSION_ID_XATTR_KEY =
+            INTERNAL_DB_SESSION_ID_XATTR_KEY_PREFIX.concat(
+                    String.valueOf(UserHandle.myUserId()));
+
+    /**
+     * Prefix of key name of xattr used to set session id for external DB.
+     */
+    static final String EXTERNAL_DB_SESSION_ID_XATTR_KEY_PREFIX = "user.extdbsessionid";
 
     /**
      * 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()));
+    static final String EXTERNAL_DB_SESSION_ID_XATTR_KEY =
+            EXTERNAL_DB_SESSION_ID_XATTR_KEY_PREFIX.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(
@@ -148,7 +172,7 @@
      * For devices with adoptable storage support, opting for adoptable storage will not delete
      * /data/media/0 directory.
      */
-    private static final String DATA_MEDIA_XATTR_DIRECTORY_PATH = "/data/media/0";
+    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";
@@ -314,8 +338,8 @@
         // Recreate all views to apply this filter
         final SQLiteDatabase db = super.getWritableDatabase();
         mSchemaLock.writeLock().lock();
+        db.beginTransaction();
         try {
-            db.beginTransaction();
             createLatestViews(db);
             db.setTransactionSuccessful();
         } finally {
@@ -562,13 +586,6 @@
                     getExternalStorageDbXattrPath(), getSessionIdXattrKeyForDatabase());
             if (!lastUsedSessionIdFromExternalStoragePathXattr.isPresent()) {
                 // First time scenario will have no session id at /data/media/0.
-                // Trigger database backup to external storage because
-                // StableUrisIdleMaintenanceService will be attempted to run only once in 7days.
-                // Any rollback before that will not recover DB rows.
-                if (isInternal()) {
-                    BackgroundThread.getExecutor().execute(
-                            () -> mDatabaseBackupAndRecovery.backupInternalDatabase(this, null));
-                }
                 // Set next row id in External Storage to handle rollback in future.
                 backupNextRowId(NEXT_ROW_ID_DEFAULT_BILLION_VALUE);
                 updateSessionIdInDatabaseAndExternalStorage(db);
@@ -592,10 +609,15 @@
             // Recover data from backup
             // Ensure we do not back up in case of recovery.
             mIsRecovering.set(true);
-            mDatabaseBackupAndRecovery.recoverData(db, volumeName);
-            updateNextRowIdInDatabaseAndExternalStorage(db);
-            mIsRecovering.set(false);
-            updateSessionIdInDatabaseAndExternalStorage(db);
+            try {
+                mDatabaseBackupAndRecovery.recoverData(db, volumeName);
+            } catch (Exception exception) {
+                Log.e(TAG, "Error in recovering data", exception);
+            } finally {
+                updateNextRowIdInDatabaseAndExternalStorage(db);
+                mIsRecovering.set(false);
+                updateSessionIdInDatabaseAndExternalStorage(db);
+            }
         }
     }
 
@@ -644,6 +666,10 @@
                     "%s database inconsistent: isLastUsedDatabaseSession:%b, "
                             + "nextRowIdOptionalPresent:%b", mName, isLastUsedDatabaseSession,
                     nextRowIdFromXattrOptional.isPresent()));
+
+            // This could be a rollback, clear all media grants
+            clearMediaGrantsTable(db);
+
             // 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);
@@ -651,6 +677,15 @@
         }
     }
 
+    private void clearMediaGrantsTable(SQLiteDatabase db) {
+        mSchemaLock.writeLock().lock();
+        try {
+            updateAddMediaGrantsTable(db);
+        } finally {
+            mSchemaLock.writeLock().unlock();
+        }
+    }
+
     @GuardedBy("sRecoveryLock")
     private boolean isLastUsedDatabaseSession(SQLiteDatabase db) {
         Optional<String> lastUsedSessionIdFromDatabasePathXattr = getXattr(db.getPath(),
diff --git a/src/com/android/providers/media/LocalUriMatcher.java b/src/com/android/providers/media/LocalUriMatcher.java
index 6a9174f..888a619 100644
--- a/src/com/android/providers/media/LocalUriMatcher.java
+++ b/src/com/android/providers/media/LocalUriMatcher.java
@@ -78,6 +78,8 @@
     static final int PICKER_INTERNAL_ALBUMS_ALL = 904;
     static final int PICKER_INTERNAL_ALBUMS_LOCAL = 905;
 
+    public static final int MEDIA_GRANTS = 1000;
+
     // MediaProvider Command Line Interface
     static final int CLI = 100_000;
 
@@ -169,6 +171,7 @@
         mHidden.addURI(auth, "picker_internal/media/local", PICKER_INTERNAL_MEDIA_LOCAL);
         mHidden.addURI(auth, "picker_internal/albums/all", PICKER_INTERNAL_ALBUMS_ALL);
         mHidden.addURI(auth, "picker_internal/albums/local", PICKER_INTERNAL_ALBUMS_LOCAL);
+        mHidden.addURI(auth, "media_grants", MEDIA_GRANTS);
         mHidden.addURI(auth, "*", VOLUMES_ID);
         mHidden.addURI(auth, null, VOLUMES);
 
diff --git a/src/com/android/providers/media/MediaGrants.java b/src/com/android/providers/media/MediaGrants.java
index b08bb63..d654ca0 100644
--- a/src/com/android/providers/media/MediaGrants.java
+++ b/src/com/android/providers/media/MediaGrants.java
@@ -16,10 +16,16 @@
 
 package com.android.providers.media;
 
+import static android.provider.MediaStore.MediaColumns.DATA;
+
 import static com.android.providers.media.LocalUriMatcher.PICKER_ID;
+import static com.android.providers.media.util.DatabaseUtils.replaceMatchAnyChar;
 
 import android.content.ContentUris;
 import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteConstraintException;
+import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
 import android.provider.MediaStore;
@@ -30,8 +36,11 @@
 
 import com.android.providers.media.photopicker.PickerSyncController;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
+import java.util.stream.Collectors;
 
 /**
  * Manager class for the {@code media_grants} table in the {@link
@@ -39,7 +48,7 @@
  *
  * <p>Manages media grants for files in the {@code files} table based on package name.
  */
-class MediaGrants {
+public class MediaGrants {
     public static final String TAG = "MediaGrants";
     public static final String MEDIA_GRANTS_TABLE = "media_grants";
     public static final String FILE_ID_COLUMN = "file_id";
@@ -47,6 +56,40 @@
     public static final String OWNER_PACKAGE_NAME_COLUMN =
             MediaStore.MediaColumns.OWNER_PACKAGE_NAME;
 
+    private static final String CREATE_TEMPORARY_TABLE_QUERY = "CREATE TEMPORARY TABLE ";
+    private static final String MEDIA_GRANTS_AND_FILES_JOIN_TABLE_NAME = "media_grants LEFT JOIN "
+            + "files ON media_grants.file_id = files._id";
+
+    private static final String WHERE_MEDIA_GRANTS_PACKAGE_NAME_IN =
+            "media_grants." + MediaGrants.OWNER_PACKAGE_NAME_COLUMN + " IN ";
+
+    private static final String WHERE_MEDIA_GRANTS_USER_ID =
+            "media_grants." + MediaGrants.PACKAGE_USER_ID_COLUMN + " = ? ";
+
+    private static final String WHERE_ITEM_IS_NOT_TRASHED =
+            "files." + MediaStore.Files.FileColumns.IS_TRASHED + " = ? ";
+
+    private static final String WHERE_ITEM_IS_NOT_PENDING =
+            "files." + MediaStore.Files.FileColumns.IS_PENDING + " = ? ";
+
+    private static final String WHERE_MEDIA_TYPE =
+            "files." + MediaStore.Files.FileColumns.MEDIA_TYPE + " IN ";
+
+    private static final String WHERE_MIME_TYPE =
+            "files." + MediaStore.Files.FileColumns.MIME_TYPE + " LIKE ? ";
+
+    private static final String WHERE_VOLUME_NAME_IN =
+            "files." + MediaStore.Files.FileColumns.VOLUME_NAME + " IN ";
+
+    private static final String TEMP_TABLE_NAME_FOR_DELETION =
+            "temp_table_for_media_grants_deletion";
+
+    private static final String TEMP_TABLE_FOR_DELETION_FILE_ID_COLUMN_NAME =
+            "temp_table_for_media_grants_deletion.file_id";
+
+    private static final String ARG_VALUE_FOR_FALSE = "0";
+
+    private static final int VISUAL_MEDIA_TYPE_COUNT = 2;
     private SQLiteQueryBuilder mQueryBuilder = new SQLiteQueryBuilder();
     private DatabaseHelper mExternalDatabase;
     private LocalUriMatcher mUriMatcher;
@@ -87,7 +130,15 @@
                         values.put(FILE_ID_COLUMN, id);
                         values.put(PACKAGE_USER_ID_COLUMN, packageUserId);
 
-                        mQueryBuilder.insert(db, values);
+                        try {
+                            mQueryBuilder.insert(db, values);
+                        } catch (SQLiteConstraintException exception) {
+                            // no-op
+                            // this may happen due to the presence of a foreign key between the
+                            // media_grants and files table. An SQLiteConstraintException
+                            // exception my occur if: while inserting the grant for a file, the
+                            // file itself is deleted. In this situation no operation is required.
+                        }
                     }
 
                     Log.d(
@@ -101,6 +152,119 @@
     }
 
     /**
+     * Returns the cursor for file data of items for which the passed package has READ_GRANTS.
+     *
+     * @param packageNames  the package name that has access.
+     * @param packageUserId the user_id of the package
+     */
+    Cursor getMediaGrantsForPackages(String[] packageNames, int packageUserId,
+            String[] mimeTypes, String[] availableVolumes)
+            throws IllegalArgumentException {
+        Objects.requireNonNull(packageNames);
+        return mExternalDatabase.runWithoutTransaction((db) -> {
+            final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
+            queryBuilder.setDistinct(true);
+            queryBuilder.setTables(MEDIA_GRANTS_AND_FILES_JOIN_TABLE_NAME);
+            String[] selectionArgs = buildSelectionArg(queryBuilder,
+                    QueryFilterBuilder.newInstance()
+                            .setPackageNameSelection(packageNames)
+                            .setUserIdSelection(packageUserId)
+                            .setIsNotTrashedSelection(true)
+                            .setIsNotPendingSelection(true)
+                            .setIsOnlyVisualMediaType(true)
+                            .setMimeTypeSelection(mimeTypes)
+                            .setAvailableVolumes(availableVolumes)
+                            .build());
+
+            return queryBuilder.query(db,
+                    new String[]{DATA, FILE_ID_COLUMN}, null, selectionArgs, null, null, null, null,
+                    null);
+        });
+    }
+
+    int removeMediaGrantsForPackage(@NonNull String[] packages, @NonNull List<Uri> uris,
+            int packageUserId) {
+        Objects.requireNonNull(packages);
+        Objects.requireNonNull(uris);
+        if (packages.length == 0) {
+            throw new IllegalArgumentException(
+                    "Removing grants requires a non empty package name.");
+        }
+
+        return mExternalDatabase.runWithTransaction(
+                (db) -> {
+                    // create a temporary table to be used as a selection criteria for local ids.
+                    createTempTableWithLocalIdsAsColumn(uris, db);
+
+                    // Create query builder and add selection args.
+                    final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
+                    queryBuilder.setDistinct(true);
+                    queryBuilder.setTables(MEDIA_GRANTS_TABLE);
+                    String[] selectionArgs = buildSelectionArg(queryBuilder,
+                            QueryFilterBuilder.newInstance()
+                                    .setPackageNameSelection(packages)
+                                    .setUserIdSelection(packageUserId)
+                                    .setUriSelection(uris)
+                                    .build());
+                    // execute query.
+                    int grantsRemoved = queryBuilder.delete(db, null, selectionArgs);
+                    Log.d(
+                            TAG,
+                            String.format(
+                                    "Removed %s media_grants for %s user for %s.",
+                                    grantsRemoved,
+                                    String.valueOf(packageUserId),
+                                    Arrays.toString(packages)));
+                    // Drop the temporary table.
+                    deleteTempTableCreatedForLocalIdSelection(db);
+                    return grantsRemoved;
+                });
+    }
+
+    private static void createTempTableWithLocalIdsAsColumn(@NonNull List<Uri> uris,
+            @NonNull SQLiteDatabase db) {
+
+        // create a temporary table and insert the ids from received uris.
+        db.execSQL(String.format(CREATE_TEMPORARY_TABLE_QUERY + "%s (%s INTEGER)",
+                TEMP_TABLE_NAME_FOR_DELETION, FILE_ID_COLUMN));
+
+        final SQLiteQueryBuilder queryBuilderTempTable = new SQLiteQueryBuilder();
+        queryBuilderTempTable.setTables(TEMP_TABLE_NAME_FOR_DELETION);
+
+        List<List<Uri>> listOfSelectionArgsForId = splitArrayList(uris,
+                /* number of ids per query */ 50);
+
+        StringBuilder sb = new StringBuilder();
+        List<Uri> selectionArgForIdSelection;
+        for (int itr = 0; itr < listOfSelectionArgsForId.size(); itr++) {
+            selectionArgForIdSelection = listOfSelectionArgsForId.get(itr);
+            if (itr == 0 || selectionArgForIdSelection.size() != listOfSelectionArgsForId.get(
+                    itr - 1).size()) {
+                sb.setLength(0);
+                for (int i = 0; i < selectionArgForIdSelection.size() - 1; i++) {
+                    sb.append("(?)").append(",");
+                }
+                sb.append("(?)");
+            }
+            db.execSQL("INSERT INTO " + TEMP_TABLE_NAME_FOR_DELETION + " VALUES " + sb.toString(),
+                    selectionArgForIdSelection.stream().map(
+                            ContentUris::parseId).collect(Collectors.toList()).stream().toArray());
+        }
+    }
+
+    private static <T> List<List<T>> splitArrayList(List<T> list, int chunkSize) {
+        List<List<T>> subLists = new ArrayList<>();
+        for (int i = 0; i < list.size(); i += chunkSize) {
+            subLists.add(list.subList(i, Math.min(i + chunkSize, list.size())));
+        }
+        return subLists;
+    }
+
+    private static void deleteTempTableCreatedForLocalIdSelection(SQLiteDatabase db) {
+        db.execSQL("DROP TABLE " + TEMP_TABLE_NAME_FOR_DELETION);
+    }
+
+    /**
      * Removes any existing media grants for the given package from the external database. This will
      * not alter the files or file metadata themselves.
      *
@@ -111,32 +275,37 @@
      *
      * <p>The action is performed for only specific {@code user}.</p>
      *
-     * @param packageName   the package name to clear media grants for.
+     * @param packages      the package(s) name to clear media grants for.
      * @param reason        a logged reason why the grants are being cleared.
      * @param user          the user for which the grants need to be modified.
      *
      * @return              the number of grants removed.
      */
-    int removeAllMediaGrantsForPackage(String packageName, String reason,
-            @NonNull Integer user)
+    int removeAllMediaGrantsForPackages(String[] packages, String reason, @NonNull Integer user)
             throws IllegalArgumentException {
-        Objects.requireNonNull(packageName);
-        if (TextUtils.isEmpty(packageName)) {
+        Objects.requireNonNull(packages);
+        if (packages.length == 0) {
             throw new IllegalArgumentException(
                     "Removing grants requires a non empty package name.");
         }
+
+        final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
+        queryBuilder.setDistinct(true);
+        queryBuilder.setTables(MEDIA_GRANTS_TABLE);
+        String[] selectionArgs = buildSelectionArg(queryBuilder, QueryFilterBuilder.newInstance()
+                .setPackageNameSelection(packages)
+                .setUserIdSelection(user)
+                .build());
         return mExternalDatabase.runWithTransaction(
                 (db) -> {
-                    int grantsRemoved =
-                            mQueryBuilder.delete(
-                                    db, String.format(
-                                            "%s = ? AND %s = ?", OWNER_PACKAGE_NAME_COLUMN,
-                                            PACKAGE_USER_ID_COLUMN),
-                                    new String[]{packageName, String.valueOf(user)});
-                    Log.d(TAG,
-                            String.format("Removed %s media_grants for %s user for %s. Reason: %s",
-                                    grantsRemoved, String.valueOf(user),
-                                    packageName,
+                    int grantsRemoved = queryBuilder.delete(db, null, selectionArgs);
+                    Log.d(
+                            TAG,
+                            String.format(
+                                    "Removed %s media_grants for %s user for %s. Reason: %s",
+                                    grantsRemoved,
+                                    String.valueOf(user),
+                                    Arrays.toString(packages),
                                     reason));
                     return grantsRemoved;
                 });
@@ -188,7 +357,204 @@
 
         return isPickerUri(uri)
                 && PickerUriResolver.unwrapProviderUri(uri)
-                        .getHost()
-                        .equals(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY);
+                .getHost()
+                .equals(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY);
+    }
+
+    /**
+     * Add required selection arguments like comparisons and WHERE checks to the
+     * {@link SQLiteQueryBuilder} qb.
+     *
+     * @param qb           query builder on which the conditions/filters needs to be applied.
+     * @param queryFilter  representing the types of selection arguments to be applied.
+     * @return array of selection args used to replace placeholders in query builder conditions.
+     */
+    private String[] buildSelectionArg(SQLiteQueryBuilder qb, MediaGrantsQueryFilter queryFilter) {
+        List<String> selectArgs = new ArrayList<>();
+        // Append where clause for package names.
+        if (queryFilter.mPackageNames != null && queryFilter.mPackageNames.length > 0) {
+            // Append the where clause for package name selection to the query builder.
+            qb.appendWhereStandalone(
+                    WHERE_MEDIA_GRANTS_PACKAGE_NAME_IN + buildPlaceholderForWhereClause(
+                            queryFilter.mPackageNames.length));
+
+            // Add package names to selection args.
+            selectArgs.addAll(Arrays.asList(queryFilter.mPackageNames));
+        }
+
+        // Append Where clause for Uris
+        if (queryFilter.mUris != null && !queryFilter.mUris.isEmpty()) {
+            // Append the where clause for local id selection to the query builder.
+            // this query would look like this example query:
+            // WHERE EXISTS (SELECT 1 from temp_table_for_media_grants_deletion WHERE
+            // temp_table_for_media_grants_deletion.file_id = media_grants.file_id)
+            qb.appendWhereStandalone(String.format("EXISTS (SELECT %s from %s WHERE %s = %s)",
+                    TEMP_TABLE_FOR_DELETION_FILE_ID_COLUMN_NAME,
+                    TEMP_TABLE_NAME_FOR_DELETION,
+                    TEMP_TABLE_FOR_DELETION_FILE_ID_COLUMN_NAME,
+                    MediaGrants.MEDIA_GRANTS_TABLE + "." + MediaGrants.FILE_ID_COLUMN));
+        }
+
+        // Append where clause for userID.
+        if (queryFilter.mUserId != null) {
+            qb.appendWhereStandalone(WHERE_MEDIA_GRANTS_USER_ID);
+            selectArgs.add(String.valueOf(queryFilter.mUserId));
+        }
+
+        if (queryFilter.mIsNotTrashed) {
+            qb.appendWhereStandalone(WHERE_ITEM_IS_NOT_TRASHED);
+            selectArgs.add(ARG_VALUE_FOR_FALSE);
+        }
+
+        if (queryFilter.mIsNotPending) {
+            qb.appendWhereStandalone(WHERE_ITEM_IS_NOT_PENDING);
+            selectArgs.add(ARG_VALUE_FOR_FALSE);
+        }
+
+        if (queryFilter.mIsOnlyVisualMediaType) {
+            qb.appendWhereStandalone(WHERE_MEDIA_TYPE + buildPlaceholderForWhereClause(
+                    VISUAL_MEDIA_TYPE_COUNT));
+            selectArgs.add(String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE));
+            selectArgs.add(String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO));
+        }
+
+        if (queryFilter.mAvailableVolumes != null && queryFilter.mAvailableVolumes.length > 0) {
+            qb.appendWhereStandalone(
+                    WHERE_VOLUME_NAME_IN + buildPlaceholderForWhereClause(
+                            queryFilter.mAvailableVolumes.length));
+            selectArgs.addAll(Arrays.asList(queryFilter.mAvailableVolumes));
+        }
+
+        addMimeTypesToQueryBuilderAndSelectionArgs(qb, selectArgs, queryFilter.mMimeTypeSelection);
+
+        return selectArgs.toArray(new String[selectArgs.size()]);
+    }
+
+    private void addMimeTypesToQueryBuilderAndSelectionArgs(SQLiteQueryBuilder qb,
+            List<String> selectionArgs, String[] mimeTypes) {
+        if (mimeTypes == null) {
+            return;
+        }
+
+        mimeTypes = replaceMatchAnyChar(mimeTypes);
+        ArrayList<String> whereMimeTypes = new ArrayList<>();
+        for (String mimeType : mimeTypes) {
+            if (!TextUtils.isEmpty(mimeType)) {
+                whereMimeTypes.add(WHERE_MIME_TYPE);
+                selectionArgs.add(mimeType);
+            }
+        }
+
+        if (whereMimeTypes.isEmpty()) {
+            return;
+        }
+        qb.appendWhereStandalone(TextUtils.join(" OR ", whereMimeTypes));
+    }
+
+    private String buildPlaceholderForWhereClause(int numberOfItemsInSelection) {
+        StringBuilder placeholder = new StringBuilder("(");
+        for (int itr = 0; itr < numberOfItemsInSelection; itr++) {
+            placeholder.append("?,");
+        }
+        placeholder.deleteCharAt(placeholder.length() - 1);
+        placeholder.append(")");
+        return placeholder.toString();
+    }
+
+    static final class MediaGrantsQueryFilter {
+
+        private final List<Uri> mUris;
+        private final String[] mPackageNames;
+        private final Integer mUserId;
+
+        private final boolean mIsNotTrashed;
+
+        private final boolean mIsNotPending;
+
+        private final boolean mIsOnlyVisualMediaType;
+        private final String[] mMimeTypeSelection;
+
+        private final String[] mAvailableVolumes;
+
+        MediaGrantsQueryFilter(QueryFilterBuilder builder) {
+            this.mUris = builder.mUris;
+            this.mPackageNames = builder.mPackageNames;
+            this.mUserId = builder.mUserId;
+            this.mIsNotTrashed = builder.mIsNotTrashed;
+            this.mIsNotPending = builder.mIsNotPending;
+            this.mMimeTypeSelection = builder.mMimeTypeSelection;
+            this.mIsOnlyVisualMediaType = builder.mIsOnlyVisualMediaType;
+            this.mAvailableVolumes = builder.mAvailableVolumes;
+        }
+    }
+
+    // Static class Builder
+    static class QueryFilterBuilder {
+
+        private List<Uri> mUris;
+        private String[] mPackageNames;
+        private int mUserId;
+
+        private boolean mIsNotTrashed;
+
+        private boolean mIsNotPending;
+
+        private boolean mIsOnlyVisualMediaType;
+        private String[] mMimeTypeSelection;
+
+        private String[] mAvailableVolumes;
+
+        public static QueryFilterBuilder newInstance() {
+            return new QueryFilterBuilder();
+        }
+
+        private QueryFilterBuilder() {}
+
+        // Setter methods
+        public QueryFilterBuilder setUriSelection(List<Uri> uris) {
+            this.mUris = uris;
+            return this;
+        }
+
+        public QueryFilterBuilder setPackageNameSelection(String[] packageNames) {
+            this.mPackageNames = packageNames;
+            return this;
+        }
+
+        public QueryFilterBuilder setUserIdSelection(int userId) {
+            this.mUserId = userId;
+            return this;
+        }
+
+        public QueryFilterBuilder setIsNotTrashedSelection(boolean isNotTrashed) {
+            this.mIsNotTrashed = isNotTrashed;
+            return this;
+        }
+
+        public QueryFilterBuilder setIsNotPendingSelection(boolean isNotPending) {
+            this.mIsNotPending = isNotPending;
+            return this;
+        }
+
+        public QueryFilterBuilder setIsOnlyVisualMediaType(boolean isOnlyVisualMediaType) {
+            this.mIsOnlyVisualMediaType = isOnlyVisualMediaType;
+            return this;
+        }
+
+        public QueryFilterBuilder setMimeTypeSelection(String[] mimeTypeSelection) {
+            this.mMimeTypeSelection = mimeTypeSelection;
+            return this;
+        }
+
+        public QueryFilterBuilder setAvailableVolumes(String[] availableVolumes) {
+            this.mAvailableVolumes = availableVolumes;
+            return this;
+        }
+
+        // build method to deal with outer class
+        // to return outer instance
+        public MediaGrantsQueryFilter build() {
+            return new MediaGrantsQueryFilter(this);
+        }
     }
 }
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index a93f530..69a9d02 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -37,6 +37,7 @@
 import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT;
 import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_NONE;
 import static android.provider.MediaStore.GET_BACKUP_FILES;
+import static android.provider.MediaStore.GET_OWNER_PACKAGE_NAME;
 import static android.provider.MediaStore.MATCH_DEFAULT;
 import static android.provider.MediaStore.MATCH_EXCLUDE;
 import static android.provider.MediaStore.MATCH_INCLUDE;
@@ -51,7 +52,7 @@
 import static android.provider.MediaStore.QUERY_ARG_MATCH_TRASHED;
 import static android.provider.MediaStore.QUERY_ARG_REDACTED_URI;
 import static android.provider.MediaStore.QUERY_ARG_RELATED_URI;
-import static android.provider.MediaStore.READ_BACKED_UP_FILE_PATHS;
+import static android.provider.MediaStore.READ_BACKUP;
 import static android.provider.MediaStore.getVolumeName;
 import static android.system.OsConstants.F_GETFL;
 
@@ -60,7 +61,6 @@
 import static com.android.providers.media.AccessChecker.getWhereForUserSelectedAccess;
 import static com.android.providers.media.AccessChecker.hasAccessToCollection;
 import static com.android.providers.media.AccessChecker.hasUserSelectedAccess;
-import static com.android.providers.media.DatabaseBackupAndRecovery.LEVEL_DB_READ_LIMIT;
 import static com.android.providers.media.DatabaseHelper.EXTERNAL_DATABASE_NAME;
 import static com.android.providers.media.DatabaseHelper.INTERNAL_DATABASE_NAME;
 import static com.android.providers.media.LocalCallingIdentity.APPOP_REQUEST_INSTALL_PACKAGES_FOR_SHARED_UID;
@@ -107,6 +107,7 @@
 import static com.android.providers.media.LocalUriMatcher.IMAGES_MEDIA_ID_THUMBNAIL;
 import static com.android.providers.media.LocalUriMatcher.IMAGES_THUMBNAILS;
 import static com.android.providers.media.LocalUriMatcher.IMAGES_THUMBNAILS_ID;
+import static com.android.providers.media.LocalUriMatcher.MEDIA_GRANTS;
 import static com.android.providers.media.LocalUriMatcher.MEDIA_SCANNER;
 import static com.android.providers.media.LocalUriMatcher.PICKER_ID;
 import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_ALBUMS_ALL;
@@ -122,6 +123,7 @@
 import static com.android.providers.media.LocalUriMatcher.VOLUMES;
 import static com.android.providers.media.LocalUriMatcher.VOLUMES_ID;
 import static com.android.providers.media.PickerUriResolver.getMediaUri;
+import static com.android.providers.media.photopicker.data.ItemsProvider.EXTRA_MIME_TYPE_SELECTION;
 import static com.android.providers.media.scan.MediaScanner.REASON_DEMAND;
 import static com.android.providers.media.scan.MediaScanner.REASON_IDLE;
 import static com.android.providers.media.util.DatabaseUtils.bindList;
@@ -164,6 +166,7 @@
 
 import android.Manifest;
 import android.annotation.IntDef;
+import android.app.ActivityOptions;
 import android.app.AppOpsManager;
 import android.app.AppOpsManager.OnOpActiveChangedListener;
 import android.app.AppOpsManager.OnOpChangedListener;
@@ -248,6 +251,7 @@
 import android.provider.MediaStore.Images.ImageColumns;
 import android.provider.MediaStore.MediaColumns;
 import android.provider.MediaStore.Video;
+import android.provider.Settings;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.OsConstants;
@@ -284,10 +288,13 @@
 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.PickerSyncRequestExtras;
+import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
 import com.android.providers.media.playlist.Playlist;
 import com.android.providers.media.scan.MediaScanner;
 import com.android.providers.media.scan.MediaScanner.ScanReason;
 import com.android.providers.media.scan.ModernMediaScanner;
+import com.android.providers.media.stableuris.dao.BackupIdRow;
 import com.android.providers.media.util.CachedSupplier;
 import com.android.providers.media.util.DatabaseUtils;
 import com.android.providers.media.util.FileUtils;
@@ -309,6 +316,8 @@
 import com.google.common.base.Strings;
 import com.google.common.hash.Hashing;
 
+import org.jetbrains.annotations.NotNull;
+
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
@@ -488,6 +497,14 @@
      */
     private static final String DOWNLOADS_PROVIDER_AUTHORITY = "downloads";
 
+    private static final String DEFAULT_FOLDER_CREATED_KEY_PREFIX = "created_default_folders_";
+
+    /**
+     * This value should match android.os.Trace.MAX_SECTION_NAME_LEN , not accessible from this
+     * class
+     */
+    private static final int MAX_SECTION_NAME_LEN = 127;
+
     @GuardedBy("mPendingOpenInfo")
     private final Map<Integer, PendingOpenInfo> mPendingOpenInfo = new ArrayMap<>();
 
@@ -649,19 +666,22 @@
         Context context = getContext();
         PackageManager packageManager = context.getPackageManager();
         try {
-            int uid = packageManager.getPackageUidAsUser(packageName,
-                    PackageManager.PackageInfoFlags.of(0), userId);
-            if (!LocalCallingIdentity.fromExternal(context, mUserCache, uid)
-                    .checkCallingPermissionUserSelected()) {
-                // Revoke media grants if permission state is not "Select flow".
-                mMediaGrants.removeAllMediaGrantsForPackage(
-                        packageName,
-                        /*reason=*/ "Mode changed: " + op,
-                        userId);
+            int uid =
+                    packageManager.getPackageUidAsUser(
+                            packageName, PackageManager.PackageInfoFlags.of(0), userId);
+            LocalCallingIdentity lci = LocalCallingIdentity.fromExternal(context, mUserCache, uid);
+            if (!lci.checkCallingPermissionUserSelected()) {
+                String[] packages = lci.getSharedPackageNamesArray();
+                mMediaGrants.removeAllMediaGrantsForPackages(
+                        packages, /* reason= */ "Mode changed: " + op, userId);
             }
         } catch (NameNotFoundException e) {
-            Log.d(TAG, "Unable to resolve uid. Ignoring the AppOp change for "
-                    + packageName + ", User : " + userId);
+            Log.d(
+                    TAG,
+                    "Unable to resolve uid. Ignoring the AppOp change for "
+                            + packageName
+                            + ", User : "
+                            + userId);
         }
     }
 
@@ -804,6 +824,11 @@
                      * isMediaSharedWithParent is true.On removal of such user profile,
                      * the owner's MediaProvider would need to clean any media files stored
                      * by the removed user profile.
+                     * We also remove the default folder key for the cloned user (just removed)
+                     * from user 0's SharedPreferences. Usually, the next clone user would be
+                     * created with a different key (as user-id would be incremented), however, if
+                     * device is restarted, the next clone-user can use the user-id previously
+                     * assigned, causing stale entries in user 0's SharedPreferences
                      */
                     UserHandle userToBeRemoved  = intent.getParcelableExtra(Intent.EXTRA_USER);
                     if(userToBeRemoved.getIdentifier() != sUserId){
@@ -812,6 +837,43 @@
                                     new String[]{String.valueOf(userToBeRemoved.getIdentifier())});
                             return null ;
                         });
+                        String userToBeRemovedVolId = null;
+                        synchronized (mAttachedVolumes) {
+                          for (MediaVolume volume : mAttachedVolumes) {
+                              if (userToBeRemoved.equals(volume.getUser())) {
+                                  userToBeRemovedVolId = volume.getId();
+                                  break;
+                              }
+                          }
+                        }
+                        //The clone user volume may be unmounted at this time (userToBeRemovedVolId
+                        // will be null then), we construct the volId of unmounted vol from userId.
+                        String key = DEFAULT_FOLDER_CREATED_KEY_PREFIX
+                                + getPrimaryVolumeId(userToBeRemovedVolId, userToBeRemoved);
+                        final SharedPreferences prefs = PreferenceManager
+                                .getDefaultSharedPreferences(getContext());
+                        if (prefs.getInt(key, /* default */ 0) == 1) {
+                            SharedPreferences.Editor editor = prefs.edit();
+                            editor.remove(key);
+                            editor.commit();
+                        }
+                    }
+
+                    boolean isDeviceInDemoMode = false;
+                    try {
+                        isDeviceInDemoMode = Settings.Global.getInt(
+                                getContext().getContentResolver(), Settings.Global.DEVICE_DEMO_MODE)
+                                > 0;
+                    } catch (Settings.SettingNotFoundException e) {
+                        Log.w(TAG, "Exception in reading DEVICE_DEMO_MODE setting", e);
+                    }
+
+                    Log.i(TAG, "isDeviceInDemoMode: " + isDeviceInDemoMode);
+                    // Only allow default system user 0 to update xattrs on /data/media/0 and
+                    // only on retail demo devices
+                    if (sUserId == UserHandle.SYSTEM.getIdentifier() && isDeviceInDemoMode) {
+                        mDatabaseBackupAndRecovery.removeRecoveryDataForUserId(
+                                userToBeRemoved.getIdentifier());
                     }
                     break;
             }
@@ -831,17 +893,33 @@
         }
     }
 
-    private void updateQuotaTypeForUri(@NonNull Uri uri, int mediaType,
-            @NonNull String volumeName) {
+    protected void updateQuotaTypeForUri(@NonNull FileRow row) {
+        final String volumeName = row.getVolumeName();
+        final String path = row.getPath();
+
         // Quota type is only updated for external primary volume
         if (!MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)) {
             return;
         }
 
+        int mediaType = row.getMediaType();
         Trace.beginSection("MP.updateQuotaTypeForUri");
         File file;
         try {
-            file = queryForDataFile(uri, null);
+            if (path != null) {
+                file = new File(path);
+            } else {
+                // This can happen in case of renames, where the path isn't
+                // part of the 'new' FileRow data. Fall back to querying
+                // the path directly.
+                final Uri uri = MediaStore.Files.getContentUri(row.getVolumeName(),
+                        row.getId());
+                if (uri == null) {
+                    // Row could have been deleted
+                    return;
+                }
+                file = queryForDataFile(uri, null);
+            }
             if (!file.exists()) {
                 // This can happen if an item is inserted in MediaStore before it is created
                 return;
@@ -857,7 +935,7 @@
             updateQuotaTypeForFileInternal(file, mediaType);
         } catch (FileNotFoundException | IllegalArgumentException e) {
             // Ignore
-            Log.w(TAG, "Failed to update quota for uri: " + uri, e);
+            Log.w(TAG, "Failed to update quota", e);
         } finally {
             Trace.endSection();
         }
@@ -913,8 +991,7 @@
                     // Update the quota type on the filesystem
                     Uri fileUri = MediaStore.Files.getContentUri(insertedRow.getVolumeName(),
                             insertedRow.getId());
-                    updateQuotaTypeForUri(fileUri, insertedRow.getMediaType(),
-                            insertedRow.getVolumeName());
+                    updateQuotaTypeForUri(insertedRow);
                 }
 
                 // Tell our SAF provider so it knows when views are no longer empty
@@ -923,7 +1000,7 @@
 
                 if (mExternalDbFacade.onFileInserted(insertedRow.getMediaType(),
                         insertedRow.isPending())) {
-                    mPickerSyncController.notifyMediaEvent();
+                    mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ true);
                 }
 
                 mDatabaseBackupAndRecovery.backupVolumeDbData(helper, insertedRow);
@@ -953,7 +1030,7 @@
             helper.postBackground(() -> {
                 if (helper.isExternal()) {
                     // Update the quota type on the filesystem
-                    updateQuotaTypeForUri(fileUri, newRow.getMediaType(), oldRow.getVolumeName());
+                    updateQuotaTypeForUri(newRow);
                 }
 
                 if (mExternalDbFacade.onFileUpdated(oldRow.getId(),
@@ -962,7 +1039,7 @@
                         oldRow.isPending(), newRow.isPending(),
                         oldRow.isFavorite(), newRow.isFavorite(),
                         oldRow.getSpecialFormat(), newRow.getSpecialFormat())) {
-                    mPickerSyncController.notifyMediaEvent();
+                    mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ true);
                 }
 
                 mDatabaseBackupAndRecovery.updateBackup(helper, oldRow, newRow);
@@ -1022,7 +1099,7 @@
 
                 if (mExternalDbFacade.onFileDeleted(deletedRow.getId(),
                         deletedRow.getMediaType())) {
-                    mPickerSyncController.notifyMediaEvent();
+                    mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ true);
                 }
 
                 mDatabaseBackupAndRecovery.deleteFromDbBackup(helper, deletedRow);
@@ -1128,11 +1205,12 @@
         if (volumeName.equals(MediaStore.VOLUME_EXTERNAL_PRIMARY)) {
             // For the primary volume, we use the ID, because we may be handling
             // the primary volume for multiple users
-            key = "created_default_folders_" + volume.getId();
+            key = DEFAULT_FOLDER_CREATED_KEY_PREFIX
+                    + getPrimaryVolumeId(volume.getId(), volume.getUser());
         } else {
             // For others, like public volumes, just use the name, because the id
             // might not change when re-formatted
-            key = "created_default_folders_" + volumeName;
+            key = DEFAULT_FOLDER_CREATED_KEY_PREFIX + volumeName;
         }
 
         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
@@ -1152,6 +1230,24 @@
     }
 
     /**
+     * Returns the volume id for Primary External Volumes.
+     * If volId is supplied, it is returned as-is, in case it is not, user-id is used to
+     * construct the id for Primary External Volume.
+     *
+     * @param volId the id of the Volume in consideration.
+     * @param userId userId for which primary volume id needs to be determined.
+     * @return the primary volume id.
+     */
+    private String getPrimaryVolumeId(String volId, UserHandle userId) {
+        if (volId == null) {
+            // The construction is based upon system/vold/model/EmulatedVolume.cpp
+            // Should be kept in sync with the same.
+            return "emulated;" + userId.getIdentifier();
+        }
+        return volId;
+    }
+
+    /**
      * Ensure that any thumbnail collections on the given storage volume can be
      * used with the given {@link DatabaseHelper}. If the
      * {@link DatabaseHelper#getOrCreateUuid} doesn't match the UUID found on
@@ -1251,12 +1347,15 @@
                 mProjectionHelper, Metrics::logSchemaChange, mFilesListener,
                 MIGRATION_LISTENER, mIdGenerator, true, mDatabaseBackupAndRecovery);
         mExternalDbFacade = new ExternalDbFacade(getContext(), mExternalDatabase, mVolumeCache);
-        mPickerDbFacade = new PickerDbFacade(context);
 
         mMediaGrants = new MediaGrants(mExternalDatabase);
 
-        mPickerSyncController = new PickerSyncController(context, mPickerDbFacade, mConfigStore);
-        mPickerDataLayer = new PickerDataLayer(context, mPickerDbFacade, mPickerSyncController);
+        PickerSyncLockManager pickerSyncLockManager = new PickerSyncLockManager();
+        mPickerDbFacade = new PickerDbFacade(context, pickerSyncLockManager);
+        mPickerSyncController = PickerSyncController.initialize(context, mPickerDbFacade,
+                mConfigStore, pickerSyncLockManager);
+        mPickerDataLayer = PickerDataLayer.create(context, mPickerDbFacade, mPickerSyncController,
+                mConfigStore);
         mPickerUriResolver = new PickerUriResolver(context, mPickerDbFacade, mProjectionHelper);
 
         if (SdkLevel.isAtLeastS()) {
@@ -1265,9 +1364,6 @@
             mTranscodeHelper = new TranscodeHelperNoOp();
         }
 
-        // Create dir for redacted and picker URI paths.
-        buildPrimaryVolumeFile(uidToUserId(MY_UID), getRedactedRelativePath()).mkdirs();
-
         final IntentFilter packageFilter = new IntentFilter();
         packageFilter.setPriority(10);
         packageFilter.addDataScheme("package");
@@ -1300,9 +1396,9 @@
         }
 
         updateVolumes();
-        attachVolume(MediaVolume.fromInternal(), /* validate */ false);
+        attachVolume(MediaVolume.fromInternal(), /* validate */ false, /* volumeState */ null);
         for (MediaVolume volume : mVolumeCache.getExternalVolumes()) {
-            attachVolume(volume, /* validate */ false);
+            attachVolume(volume, /* validate */ false, /* volumeState */ null);
         }
 
         // Watch for performance-sensitive activity
@@ -1366,11 +1462,6 @@
         mConfigStore.addOnChangeListener(
                 BackgroundThread.getExecutor(), this::storageNativeBootPropertyChangeListener);
 
-        // media_grants are cleared on device reboot, and onCreate is a good signal for this.
-        ForegroundThread.getExecutor().execute(() -> {
-            mMediaGrants.removeAllMediaGrants();
-        });
-
         PulledMetrics.initialize(context);
         return true;
     }
@@ -1380,6 +1471,8 @@
         boolean isGetContentTakeoverEnabled;
         if (SdkLevel.isAtLeastT()) {
             isGetContentTakeoverEnabled = true;
+        } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
+            isGetContentTakeoverEnabled = true;
         } else {
             isGetContentTakeoverEnabled = mConfigStore.isGetContentTakeOverEnabled();
         }
@@ -1387,8 +1480,6 @@
 
         setComponentEnabledSetting("PhotoPickerUserSelectActivity",
                 mConfigStore.isUserSelectForAppEnabled());
-
-        mDatabaseBackupAndRecovery.onConfigPropertyChangeListener();
     }
 
     public DatabaseBackupAndRecovery getDatabaseBackupAndRecovery() {
@@ -1458,10 +1549,25 @@
         }
 
         // Second, is the app pending, probably from a backup/restore operation?
-        for (SessionInfo si : pm.getPackageInstaller().getAllSessions()) {
-            if (Objects.equals(packageName, si.getAppPackageName())) {
+        // Cloned app installations do not have a linked install session, so skipping the check in
+        // case the user-id is a clone profile.
+        if (!isAppCloneUserForFuse(userId)) {
+            if (sUserId != userId) {
+                // Skip the package check and ensure media provider doesn't crash
+                // Returning true since we are unsure what caused the cross-user entries to be in
+                // the database and want to avoid deleting data that might be required.
+                Log.e(TAG, "Skip pruning cross-user entries stored in database for package: "
+                        + packageName + " userId: " + userId + " processUserId: " + sUserId);
                 return true;
             }
+            for (SessionInfo si : pm.getPackageInstaller().getAllSessions()) {
+                if (Objects.equals(packageName, si.getAppPackageName())) {
+                    return true;
+                }
+            }
+        } else {
+            Log.e(TAG, "Cross-user entries found in database for package " + packageName
+                    + " userId: " + userId + " processUserId: " + sUserId);
         }
 
         // I've never met this package in my life
@@ -1480,8 +1586,8 @@
 
             try {
                 MediaService.onScanVolume(getContext(), volume, REASON_IDLE);
-            } catch (IOException e) {
-                Log.w(TAG, e);
+            } catch (IOException | IllegalArgumentException e) {
+                Log.w(TAG, "Failure in " + volume.getName() + " volume scan", e);
             }
 
             // Ensure that our thumbnails are valid
@@ -1543,11 +1649,13 @@
 
         // removing calling userId
         userIds.remove(String.valueOf(sUserId));
+
+        List<String> validUserProfiles = mUserManager.getEnabledProfiles().stream()
+                .map(userHandle -> String.valueOf(userHandle.getIdentifier())).collect(
+                        Collectors.toList());
         // removing all the valid/existing user, remaining userIds would be users who would have
         // been removed
-        userIds.removeAll(mUserManager.getEnabledProfiles().stream()
-                .map(userHandle -> String.valueOf(userHandle.getIdentifier())).collect(
-                        Collectors.toList()));
+        userIds.removeAll(validUserProfiles);
 
         // Cleaning media files of users who have been removed
         mExternalDatabase.runWithTransaction((db) -> {
@@ -1558,6 +1666,25 @@
             });
             return null ;
         });
+
+        boolean isDeviceInDemoMode = false;
+        try {
+            isDeviceInDemoMode = Settings.Global.getInt(getContext().getContentResolver(),
+                    Settings.Global.DEVICE_DEMO_MODE) > 0;
+        } catch (Settings.SettingNotFoundException e) {
+            Log.w(TAG, "Exception in reading DEVICE_DEMO_MODE setting", e);
+        }
+
+        Log.i(TAG, "isDeviceInDemoMode: " + isDeviceInDemoMode);
+        // Only allow default system user 0 to update xattrs on /data/media/0 and only when
+        // device is in retail mode
+        if (sUserId == UserHandle.SYSTEM.getIdentifier() && isDeviceInDemoMode) {
+            List<String> validUsers = mUserManager.getUserHandles(/* excludeDying */ true).stream()
+                    .map(userHandle -> String.valueOf(userHandle.getIdentifier())).collect(
+                            Collectors.toList());
+            Log.i(TAG, "Active user ids are:" + validUsers);
+            mDatabaseBackupAndRecovery.removeRecoveryDataExceptValidUsers(validUsers);
+        }
     }
 
     private void pruneStalePackages(CancellationSignal signal) {
@@ -1873,6 +2000,10 @@
         mExternalDatabase.runWithTransaction((db) -> {
             final int userId = uid / PER_USER_RANGE;
             onPackageOrphaned(db, packageName, userId);
+
+            if (SdkLevel.isAtLeastU()) {
+                removeAllMediaGrantsForUid(uid, userId, packageName);
+            }
             return null;
         });
     }
@@ -1888,9 +2019,42 @@
         // Orphan rest of entries.
         orphanEntries(db, packageName, userId);
         mDatabaseBackupAndRecovery.removeOwnerIdToPackageRelation(packageName, userId);
-        // TODO(b/260685885): Add e2e tests to ensure these are cleared when a package is removed.
-        mMediaGrants.removeAllMediaGrantsForPackage(packageName, /* reason */ "Package orphaned",
-                userId);
+
+    }
+
+    /**
+     * Removes all media_grants for all packages with the given UID. (i.e. shared packages.)
+     *
+     * @param uid the package uid. (will use this to query all shared packages that use this uid)
+     * @param userId the user id, since packages can be installed by multiple users.
+     * @param additionalPackageName An optional additional package name in the event that the
+     *     package has been removed at won't be returned by the PackageManager APIs.
+     */
+    private void removeAllMediaGrantsForUid(
+            int uid, int userId, @Nullable String additionalPackageName) {
+
+        String[] packages;
+        try {
+            LocalCallingIdentity lci =
+                    LocalCallingIdentity.fromExternal(getContext(), mUserCache, uid);
+            packages = lci.getSharedPackageNamesArray();
+        } catch (IllegalArgumentException notFound) {
+            // If there are no packages found, this means the specified UID has no packages
+            // remaining on the system.
+            packages = new String[]{};
+        }
+        if (additionalPackageName != null) {
+            // Include the passed additional package in the list LocalCallingIdentity returns.
+            List<String> packageList = new ArrayList<>();
+            packageList.addAll(Arrays.asList(packages));
+            packageList.add(additionalPackageName);
+            packages = packageList.toArray(new String[packageList.size()]);
+        }
+
+        // TODO(b/260685885): Add e2e tests to ensure these are cleared when a package
+        // is removed.
+        mMediaGrants.removeAllMediaGrantsForPackages(
+                packages, /* reason */ "Package orphaned", userId);
     }
 
     private void deleteAndroidMediaEntries(SQLiteDatabase db, String packageName, int userId) {
@@ -2449,15 +2613,15 @@
     }
 
     @Override
-    public Uri canonicalize(Uri uri) {
-        final boolean allowHidden = isCallingPackageAllowedHidden();
-        final int match = matchUri(uri, allowHidden);
-
+    public Uri canonicalize(@NonNull Uri uri) {
         // Skip when we have nothing to canonicalize
         if ("1".equals(uri.getQueryParameter(CANONICAL))) {
             return uri;
         }
 
+        final boolean allowHidden = mCallingIdentity.get().hasPermission(PERMISSION_IS_SELF);
+        final int match = matchUri(uri, allowHidden);
+
         try (Cursor c = queryForSingleItem(uri, null, null, null, null)) {
             switch (match) {
                 case AUDIO_MEDIA_ID: {
@@ -2490,14 +2654,13 @@
     }
 
     @Override
-    public Uri uncanonicalize(Uri uri) {
-        final boolean allowHidden = isCallingPackageAllowedHidden();
-        final int match = matchUri(uri, allowHidden);
-
+    public Uri uncanonicalize(@NonNull Uri uri) {
         // Skip when we have nothing to uncanonicalize
         if (!"1".equals(uri.getQueryParameter(CANONICAL))) {
             return uri;
         }
+        final boolean allowHidden = mCallingIdentity.get().hasPermission(PERMISSION_IS_SELF);
+        final int match = matchUri(uri, allowHidden);
 
         // Extract values and then clear to avoid recursive lookups
         final String title = uri.getQueryParameter(AudioColumns.TITLE);
@@ -2561,6 +2724,14 @@
         return uri;
     }
 
+    private static String safeTraceSectionNameWithUri(String operation, Uri uri) {
+        String sectionName = "MP." + operation + " [" + uri + "]";
+        if (sectionName.length() > MAX_SECTION_NAME_LEN) {
+            return sectionName.substring(0, MAX_SECTION_NAME_LEN);
+        }
+        return sectionName;
+    }
+
     /**
      * @return where clause to exclude database rows where
      * <ul>
@@ -3446,20 +3617,21 @@
     }
 
     @Override
-    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
-            String sortOrder) {
+    public Cursor query(@NonNull Uri uri, String[] projection, String selection,
+                        String[] selectionArgs, String sortOrder) {
         return query(uri, projection,
                 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, sortOrder), null);
     }
 
     @Override
-    public Cursor query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal) {
+    public Cursor query(@NonNull Uri uri, String[] projection, Bundle queryArgs,
+                        CancellationSignal signal) {
         return query(uri, projection, queryArgs, signal, /* forSelf */ false);
     }
 
     private Cursor query(Uri uri, String[] projection, Bundle queryArgs,
             CancellationSignal signal, boolean forSelf) {
-        Trace.beginSection("MP.query [" + uri + ']');
+        Trace.beginSection(safeTraceSectionNameWithUri("query", uri));
         try {
             return queryInternal(uri, projection, queryArgs, signal, forSelf);
         } catch (FallbackException e) {
@@ -3501,6 +3673,10 @@
         final boolean allowHidden = isCallingPackageAllowedHidden();
         final int table = matchUri(uri, allowHidden);
 
+        if (table == MEDIA_GRANTS) {
+            return getReadGrantedMediaForPackage(queryArgs);
+        }
+
         // handle MEDIA_SCANNER before calling getDatabaseForUri()
         if (table == MEDIA_SCANNER) {
             // create a cursor to return volume currently being scanned by the media scanner
@@ -3651,6 +3827,31 @@
         return c;
     }
 
+    @NotNull
+    private Cursor getReadGrantedMediaForPackage(Bundle extras) {
+        final int caller = Binder.getCallingUid();
+        int userId;
+        String[] packageNames;
+        if (!checkPermissionSelf(caller)) {
+            // All other callers are unauthorized.
+            throw new SecurityException(
+                    getSecurityExceptionMessage("read media grants"));
+        }
+        final PackageManager pm = getContext().getPackageManager();
+        final int packageUid = extras.getInt(Intent.EXTRA_UID);
+        packageNames = pm.getPackagesForUid(packageUid);
+        // Get the userId from packageUid as the initiator could be a cloned app, which
+        // accesses Media via MP of its parent user and Binder's callingUid reflects
+        // the latter.
+        userId = uidToUserId(packageUid);
+        String[] mimeTypes = extras.getStringArray(EXTRA_MIME_TYPE_SELECTION);
+        // Available volumes, to filter out any external storage that may be removed but the grants
+        // persisted.
+        String[] availableVolumes = mVolumeCache.getExternalVolumeNames().toArray(new String[0]);
+        return mMediaGrants.getMediaGrantsForPackages(packageNames, userId, mimeTypes,
+                availableVolumes);
+    }
+
     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     private Set<String> getQueryablePackages(String[] packageNames) {
         final boolean[] canPackageBeQueried;
@@ -4162,6 +4363,20 @@
             // DATA column.
             File volumePath;
             UserHandle userHandle = mCallingIdentity.get().getUser();
+            Integer userIdFromPathObject = values.getAsInteger(FileColumns._USER_ID);
+            int userIdFromPath = (userIdFromPathObject == null ? userHandle.getIdentifier() :
+                    userIdFromPathObject);
+            // In case if the _user_id column is set, and is different from the userHandle
+            // determined from mCallingIdentity, we prefer the former, as it comes from the original
+            // path provided to MP process.
+            // Normally this does not create any issues, but when cloned profile is active, an app
+            // in root user can try to create an image file in lower file system, by specifying
+            // the file directory as /storage/emulated/<cloneUserId>/DCIM. For such cases, we
+            // would want <cloneUserId> to be used to determine path in MP entry.
+            if (userHandle.getIdentifier() != userIdFromPath
+                    && isAppCloneUserPair(userHandle.getIdentifier(), userIdFromPath)) {
+                userHandle = UserHandle.of(userIdFromPath);
+            }
             if (currentPath != null) {
                 int userId = FileUtils.extractUserId(currentPath);
                 if (userId != -1) {
@@ -4974,7 +5189,7 @@
     @Nullable
     public Uri insert(@NonNull Uri uri, @Nullable ContentValues values,
             @Nullable Bundle extras) {
-        Trace.beginSection("MP.insert [" + uri + ']');
+        Trace.beginSection(safeTraceSectionNameWithUri("insert", uri));
         try {
             try {
                 return insertInternal(uri, values, extras);
@@ -5008,7 +5223,6 @@
         final boolean allowHidden = isCallingPackageAllowedHidden();
         final int match = matchUri(uri, allowHidden);
 
-        final int targetSdkVersion = getCallingPackageTargetSdkVersion();
         final String resolvedVolumeName = resolveVolumeName(uri);
 
         // handle MEDIA_SCANNER before calling getDatabaseForUri()
@@ -5027,7 +5241,8 @@
             MediaVolume volume = null;
             try {
                 volume = getVolume(name);
-                Uri attachedVolume = attachVolume(volume, /* validate */ true);
+                Uri attachedVolume = attachVolume(volume, /* validate */ true, /* volumeState */
+                        null);
                 if (mMediaScannerVolume != null && mMediaScannerVolume.equals(name)) {
                     final DatabaseHelper helper = getDatabaseForUri(
                             MediaStore.Files.getContentUri(mMediaScannerVolume));
@@ -6027,7 +6242,7 @@
 
     @Override
     public int delete(@NonNull Uri uri, @Nullable Bundle extras) {
-        Trace.beginSection("MP.delete [" + uri + ']');
+        Trace.beginSection(safeTraceSectionNameWithUri("delete", uri));
         try {
             return deleteInternal(uri, extras);
         } catch (FallbackException e) {
@@ -6086,8 +6301,6 @@
 
         int count = 0;
 
-        final int targetSdkVersion = getCallingPackageTargetSdkVersion();
-
         // handle MEDIA_SCANNER before calling getDatabaseForUri()
         if (match == MEDIA_SCANNER) {
             if (mMediaScannerVolume == null) {
@@ -6383,427 +6596,668 @@
     private Bundle callInternal(String method, String arg, Bundle extras) {
         switch (method) {
             case MediaStore.RESOLVE_PLAYLIST_MEMBERS_CALL: {
-                final LocalCallingIdentity token = clearLocalCallingIdentity();
-                final CallingIdentity providerToken = clearCallingIdentity();
-                try {
-                    final Uri playlistUri = extras.getParcelable(MediaStore.EXTRA_URI);
-                    resolvePlaylistMembers(playlistUri);
-                } finally {
-                    restoreCallingIdentity(providerToken);
-                    restoreLocalCallingIdentity(token);
-                }
-                return null;
+                return getResultForResolvePlaylistMembers(extras);
             }
             case MediaStore.RUN_IDLE_MAINTENANCE_CALL: {
-                // Protect ourselves from random apps by requiring a generic
-                // permission held by common debugging components, such as shell
-                getContext().enforceCallingOrSelfPermission(
-                        android.Manifest.permission.DUMP, TAG);
-                final LocalCallingIdentity token = clearLocalCallingIdentity();
-                final CallingIdentity providerToken = clearCallingIdentity();
-                try {
-                    onIdleMaintenance(new CancellationSignal());
-                } finally {
-                    restoreCallingIdentity(providerToken);
-                    restoreLocalCallingIdentity(token);
-                }
-                return null;
+                return getResultForRunIdleMaintenance();
             }
             case MediaStore.WAIT_FOR_IDLE_CALL: {
-                // TODO(b/195009139): Remove after overriding wait for idle in test to sync picker
-                // Syncing the picker while waiting for idle fixes tests with the picker db
-                // flag enabled because the picker db is in a consistent state with the external
-                // db after the sync
-                syncAllMedia();
-                ForegroundThread.waitForIdle();
-                final CountDownLatch latch = new CountDownLatch(1);
-                BackgroundThread.getExecutor().execute(latch::countDown);
-                try {
-                    latch.await(30, TimeUnit.SECONDS);
-                } catch (InterruptedException e) {
-                    throw new IllegalStateException(e);
-                }
-                return null;
+                return getResultForWaitForIdle();
             }
             case MediaStore.SCAN_FILE_CALL: {
-                final LocalCallingIdentity token = clearLocalCallingIdentity();
-                final CallingIdentity providerToken = clearCallingIdentity();
-
-                final String filePath = arg;
-                final Uri uri;
-                try {
-                    File file;
-                    try {
-                        file = FileUtils.getCanonicalFile(filePath);
-                    } catch (IOException e) {
-                        file = null;
-                    }
-
-                    uri = file != null ? scanFile(file, REASON_DEMAND) : null;
-                } finally {
-                    restoreCallingIdentity(providerToken);
-                    restoreLocalCallingIdentity(token);
-                }
-
-                // TODO(b/262244882): maybe enforceCallingPermissionInternal(uri, ...)
-
-                final Bundle res = new Bundle();
-                res.putParcelable(Intent.EXTRA_STREAM, uri);
-                return res;
+                return getResultForScanFile(arg);
             }
             case MediaStore.SCAN_VOLUME_CALL: {
-                final int userId = uidToUserId(Binder.getCallingUid());
-                final LocalCallingIdentity token = clearLocalCallingIdentity();
-                final CallingIdentity providerToken = clearCallingIdentity();
-
-                final String volumeName = arg;
-                try {
-                    final MediaVolume volume = mVolumeCache.findVolume(volumeName,
-                            UserHandle.of(userId));
-                    MediaService.onScanVolume(getContext(), volume, REASON_DEMAND);
-                } catch (FileNotFoundException e) {
-                    Log.w(TAG, "Failed to find volume " + volumeName, e);
-                } catch (IOException e) {
-                    throw new RuntimeException(e);
-                } finally {
-                    restoreCallingIdentity(providerToken);
-                    restoreLocalCallingIdentity(token);
-                }
-                return Bundle.EMPTY;
+                return getResultForScanVolume(arg);
             }
             case MediaStore.GET_VERSION_CALL: {
-                final String volumeName = extras.getString(Intent.EXTRA_TEXT);
-
-                final DatabaseHelper helper;
-                try {
-                    helper = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName));
-                } catch (VolumeNotFoundException e) {
-                    throw e.rethrowAsIllegalArgumentException();
-                }
-
-                final String version = helper.runWithoutTransaction((db) ->
-                        db.getVersion() + ":" + DatabaseHelper.getOrCreateUuid(db));
-
-                final Bundle res = new Bundle();
-                res.putString(Intent.EXTRA_TEXT, version);
-                return res;
+                return getResultForGetVersion(extras);
             }
             case MediaStore.GET_GENERATION_CALL: {
-                final String volumeName = extras.getString(Intent.EXTRA_TEXT);
-
-                final DatabaseHelper helper;
-                try {
-                    helper = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName));
-                } catch (VolumeNotFoundException e) {
-                    throw e.rethrowAsIllegalArgumentException();
-                }
-
-                final long generation = helper.runWithoutTransaction(DatabaseHelper::getGeneration);
-
-                final Bundle res = new Bundle();
-                res.putLong(Intent.EXTRA_INDEX, generation);
-                return res;
+                return getResultForGetGeneration(extras);
             }
             case MediaStore.GET_DOCUMENT_URI_CALL: {
-                final Uri mediaUri = extras.getParcelable(MediaStore.EXTRA_URI);
-                enforceCallingPermission(mediaUri, extras, false);
-
-                final Uri fileUri;
-                final LocalCallingIdentity token = clearLocalCallingIdentity();
-                try {
-                    fileUri = Uri.fromFile(queryForDataFile(mediaUri, null));
-                } catch (FileNotFoundException e) {
-                    throw new IllegalArgumentException(e);
-                } finally {
-                    restoreLocalCallingIdentity(token);
-                }
-
-                try (ContentProviderClient client = getContext().getContentResolver()
-                        .acquireUnstableContentProviderClient(
-                                getExternalStorageProviderAuthority())) {
-                    extras.putParcelable(MediaStore.EXTRA_URI, fileUri);
-                    return client.call(method, null, extras);
-                } catch (RemoteException e) {
-                    throw new IllegalStateException(e);
-                }
+                return getResultForGetDocumentUri(method, extras);
             }
             case MediaStore.GET_MEDIA_URI_CALL: {
-                final Uri documentUri = extras.getParcelable(MediaStore.EXTRA_URI);
-                getContext().enforceCallingUriPermission(documentUri,
-                        Intent.FLAG_GRANT_READ_URI_PERMISSION, TAG);
-
-                final int callingPid = mCallingIdentity.get().pid;
-                final int callingUid = mCallingIdentity.get().uid;
-                final String callingPackage = getCallingPackage();
-                final CallingIdentity token = clearCallingIdentity();
-                final String authority = documentUri.getAuthority();
-
-                if (!authority.equals(MediaDocumentsProvider.AUTHORITY) &&
-                        !authority.equals(DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) {
-                    throw new IllegalArgumentException("Provider for this Uri is not supported.");
-                }
-
-                try (ContentProviderClient client = getContext().getContentResolver()
-                        .acquireUnstableContentProviderClient(authority)) {
-                    final Bundle clientRes = client.call(method, null, extras);
-                    final Uri fileUri = clientRes.getParcelable(MediaStore.EXTRA_URI);
-                    final Bundle res = new Bundle();
-                    final Uri mediaStoreUri = fileUri.getAuthority().equals(MediaStore.AUTHORITY) ?
-                            fileUri : queryForMediaUri(new File(fileUri.getPath()), null);
-                    copyUriPermissionGrants(documentUri, mediaStoreUri, callingPid,
-                            callingUid, callingPackage);
-                    res.putParcelable(MediaStore.EXTRA_URI, mediaStoreUri);
-                    return res;
-                } catch (FileNotFoundException e) {
-                    throw new IllegalArgumentException(e);
-                } catch (RemoteException e) {
-                    throw new IllegalStateException(e);
-                } finally {
-                    restoreCallingIdentity(token);
-                }
+                return getResultForGetMediaUri(method, extras);
             }
             case MediaStore.GET_REDACTED_MEDIA_URI_CALL: {
-                final Uri uri = extras.getParcelable(MediaStore.EXTRA_URI);
-                // NOTE: It is ok to update the DB and return a redacted URI for the cases when
-                // the user code only has read access, hence we don't check for write permission.
-                enforceCallingPermission(uri, Bundle.EMPTY, false);
-                final LocalCallingIdentity token = clearLocalCallingIdentity();
-                try {
-                    final Bundle res = new Bundle();
-                    res.putParcelable(MediaStore.EXTRA_URI, getRedactedUri(uri));
-                    return res;
-                } finally {
-                    restoreLocalCallingIdentity(token);
-                }
+                return getResultForGetRedactedMediaUri(extras);
             }
             case MediaStore.GET_REDACTED_MEDIA_URI_LIST_CALL: {
-                final List<Uri> uris = extras.getParcelableArrayList(MediaStore.EXTRA_URI_LIST);
-                // NOTE: It is ok to update the DB and return a redacted URI for the cases when
-                // the user code only has read access, hence we don't check for write permission.
-                enforceCallingPermission(uris, false);
-                final LocalCallingIdentity token = clearLocalCallingIdentity();
-                try {
-                    final Bundle res = new Bundle();
-                    res.putParcelableArrayList(MediaStore.EXTRA_URI_LIST,
-                            (ArrayList<? extends Parcelable>) getRedactedUri(uris));
-                    return res;
-                } finally {
-                    restoreLocalCallingIdentity(token);
-                }
+                return getResultForGetRedactedMediaUriList(extras);
             }
             case MediaStore.GRANT_MEDIA_READ_FOR_PACKAGE_CALL: {
-                final int caller = Binder.getCallingUid();
-                int userId;
-                final List<Uri> uris;
-                String packageName;
-                if (checkPermissionSelf(caller)) {
-                    // If the caller is MediaProvider the accepted parameters are EXTRA_URI_LIST
-                    // and EXTRA_UID.
-                    if (!extras.containsKey(
-                            MediaStore.EXTRA_URI_LIST)
-                                    && !extras.containsKey(Intent.EXTRA_UID)) {
-                        throw new IllegalArgumentException(
-                                "Missing required extras arguments: EXTRA_URI_LIST or"
-                                    + " EXTRA_UID");
-                    }
-                    uris = extras.getParcelableArrayList(MediaStore.EXTRA_URI_LIST);
-                    final PackageManager pm = getContext().getPackageManager();
-                    final int packageUid = extras.getInt(Intent.EXTRA_UID);
-                    packageName = pm.getNameForUid(packageUid);
-                    // Get the userId from packageUid as the initiator could be a cloned app, which
-                    // accesses Media via MP of its parent user and Binder's callingUid reflects
-                    // the latter.
-                    userId = uidToUserId(packageUid);
-                    if (packageName.contains(":")) {
-                        // Check if the package name includes the package uid. This is expected
-                        // for packages that are referencing a shared user. PackageManager will
-                        // return a string such as <packagename>:<uid> in this instance.
-                        packageName = packageName.split(":")[0];
-                    }
-                } else if (checkPermissionShell(caller)) {
-                    // If the caller is the shell, the accepted parameters are EXTRA_URI (as string)
-                    // and EXTRA_PACKAGE_NAME (as string).
-                    if (!extras.containsKey(MediaStore.EXTRA_URI)
-                                    && !extras.containsKey(Intent.EXTRA_PACKAGE_NAME)) {
-                        throw new IllegalArgumentException(
-                                "Missing required extras arguments: EXTRA_URI or"
-                                    + " EXTRA_PACKAGE_NAME");
-                    }
-                    packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME);
-                    uris = List.of(Uri.parse(extras.getString(MediaStore.EXTRA_URI)));
-                    userId = uidToUserId(caller);
-                } else {
-                    // All other callers are unauthorized.
-                    throw new SecurityException("Create media grants not allowed. "
-                                + " Calling app ID:" + UserHandle.getAppId(Binder.getCallingUid())
-                                + " Calling UID:" + Binder.getCallingUid()
-                                + " Media Provider app ID:" + UserHandle.getAppId(MY_UID)
-                                + " Media Provider UID:" + MY_UID);
-                }
-
-                mMediaGrants.addMediaGrantsForPackage(packageName, uris, userId);
-                return null;
+                return getResultForGrantMediaReadForPackage(extras);
+            }
+            case MediaStore.REVOKE_READ_GRANT_FOR_PACKAGE_CALL: {
+                return getResultForRevokeReadGrantForPackage(extras);
             }
             case MediaStore.CREATE_WRITE_REQUEST_CALL:
             case MediaStore.CREATE_FAVORITE_REQUEST_CALL:
             case MediaStore.CREATE_TRASH_REQUEST_CALL:
             case MediaStore.CREATE_DELETE_REQUEST_CALL: {
-                final PendingIntent pi = createRequest(method, extras);
-                final Bundle res = new Bundle();
-                res.putParcelable(MediaStore.EXTRA_RESULT, pi);
-                return res;
+                return getResultForCreateOperationsRequest(method, extras);
             }
             case MediaStore.IS_SYSTEM_GALLERY_CALL:
-                final LocalCallingIdentity token = clearLocalCallingIdentity();
-                try {
-                    String packageName = arg;
-                    int uid = extras.getInt(MediaStore.EXTRA_IS_SYSTEM_GALLERY_UID);
-                    boolean isSystemGallery = PermissionUtils.checkWriteImagesOrVideoAppOps(
-                            getContext(), uid, packageName, getContext().getAttributionTag());
-                    Bundle res = new Bundle();
-                    res.putBoolean(MediaStore.EXTRA_IS_SYSTEM_GALLERY_RESPONSE, isSystemGallery);
-                    return res;
-                } finally {
-                    restoreLocalCallingIdentity(token);
-                }
+                return getResultForIsSystemGallery(arg, extras);
+            case MediaStore.PICKER_MEDIA_INIT_CALL: {
+                return getResultForPickerMediaInit(extras);
+            }
             case MediaStore.GET_CLOUD_PROVIDER_CALL: {
-                // TODO(b/245746037): replace UID check with Permission(MANAGE_CLOUD_MEDIA_PROVIDER)
-                // PhotoPickerSettingsActivity will run as either the primary or the managed user.
-                // Since the activity shows both personal and work tabs, it will have to make get
-                // cloud provider IPC call to both instances of Media Provider - one running as
-                // primary profile and the other as managed profile. Hence, UID check will not be
-                // feasible here.
-                if (!checkPermissionSelf(Binder.getCallingUid())) {
-                    throw new SecurityException("Get cloud provider not allowed. "
-                            + " Calling app ID:" + UserHandle.getAppId(Binder.getCallingUid())
-                            + " Calling UID:" + Binder.getCallingUid()
-                            + " Media Provider app ID:" + UserHandle.getAppId(MY_UID)
-                            + " Media Provider UID:" + MY_UID);
-                }
-                final Bundle bundle = new Bundle();
-                bundle.putString(MediaStore.GET_CLOUD_PROVIDER_RESULT,
-                        mPickerSyncController.getCloudProvider());
-                return bundle;
+                return getResultForGetCloudProvider();
             }
             case MediaStore.SET_CLOUD_PROVIDER_CALL: {
-                // TODO(b/267327327): Add permission check before updating cloud provider. Also
-                //  validate the new cloud provider before setting it by using
-                //  PickerSyncController#setCloudProvider instead of
-                //  PickerSyncController#forceSetCloudProvider.
-                final String cloudProvider = extras.getString(MediaStore.EXTRA_CLOUD_PROVIDER);
-                Log.i(TAG, "Request received to set cloud provider to " + cloudProvider);
-                mPickerSyncController.forceSetCloudProvider(cloudProvider);
-                Log.i(TAG, "Completed request to set cloud provider to " + cloudProvider);
-
-                // Cannot start sync here yet because currently sync and other picker related
-                // queries like SET_CLOUD_PROVIDER_CALL and GET_CLOUD_PROVIDER use the same lock.
-                // If we start sync here and then user tries to return to the Picker or change the
-                // provider again, Picker will ANR and crash.
-                return new Bundle();
+                return getResultForSetCloudProvider(extras);
             }
             case MediaStore.SYNC_PROVIDERS_CALL: {
-                syncAllMedia();
-                return new Bundle();
+                return getResultForSyncProviders();
             }
             case MediaStore.IS_SUPPORTED_CLOUD_PROVIDER_CALL: {
-                final boolean isSupported = mPickerSyncController.isProviderSupported(arg,
-                        Binder.getCallingUid());
-
-                Bundle bundle = new Bundle();
-                bundle.putBoolean(MediaStore.EXTRA_CLOUD_PROVIDER_RESULT, isSupported);
-                return bundle;
+                return getResultForIsSupportedCloudProvider(arg);
             }
             case MediaStore.IS_CURRENT_CLOUD_PROVIDER_CALL: {
-                final boolean isEnabled = mPickerSyncController.isProviderEnabled(arg,
-                        Binder.getCallingUid());
-
-                Bundle bundle = new Bundle();
-                bundle.putBoolean(MediaStore.EXTRA_CLOUD_PROVIDER_RESULT, isEnabled);
-                return bundle;
+                return getResultForIsCurrentCloudProviderCall(arg);
             }
             case MediaStore.NOTIFY_CLOUD_MEDIA_CHANGED_EVENT_CALL: {
-                final boolean notifyCloudEventResult;
-                if (mPickerSyncController.isProviderEnabled(arg, Binder.getCallingUid())) {
-                    mPickerSyncController.notifyMediaEvent();
-                    notifyCloudEventResult = true;
-                } else {
-                    notifyCloudEventResult = false;
-                }
-
-                Bundle bundle = new Bundle();
-                bundle.putBoolean(MediaStore.EXTRA_CLOUD_PROVIDER_RESULT,
-                        notifyCloudEventResult);
-                return bundle;
+                return getResultForNotifyCloudMediaChangedEvent(arg);
             }
             case MediaStore.USES_FUSE_PASSTHROUGH: {
-                boolean isEnabled = false;
-                try {
-                    FuseDaemon daemon = getFuseDaemonForFile(new File(arg), mVolumeCache);
-                    if (daemon != null) {
-                        isEnabled = daemon.usesFusePassthrough();
-                    }
-                } catch (FileNotFoundException e) {
-                }
-
-                Bundle bundle = new Bundle();
-                bundle.putBoolean(MediaStore.USES_FUSE_PASSTHROUGH_RESULT, isEnabled);
-                return bundle;
+                return getResultForUsesFusePassThrough(arg);
             }
             case MediaStore.RUN_IDLE_MAINTENANCE_FOR_STABLE_URIS: {
-                backupDatabases(null);
+                return getResultForIdleMaintenanceForStableUris();
+            }
+            case READ_BACKUP: {
+                return getResultForReadBackup(arg, extras);
+            }
+            case GET_OWNER_PACKAGE_NAME: {
+                return getResultForGetOwnerPackageName(arg);
+            }
+            case MediaStore.DELETE_BACKED_UP_FILE_PATHS: {
+                return getResultForDeleteBackedUpFilePaths(arg);
+            }
+            case MediaStore.GET_BACKUP_FILES: {
+                return getResultForGetBackupFiles();
+            }
+            case MediaStore.GET_RECOVERY_DATA: {
+                return getResultForGetRecoveryData();
+            }
+            case MediaStore.REMOVE_RECOVERY_DATA: {
+                removeRecoveryData();
                 return new Bundle();
             }
-            case MediaStore.READ_BACKED_UP_FILE_PATHS: {
-                getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
-                        "Permission missing to call READ_BACKED_UP_FILE_PATHS by "
-                                + "uid:" + Binder.getCallingUid());
-                List<String> cumulatedValues = new ArrayList<String>();
-                String[] backedUpFilePaths;
-                String lastReadValue = "";
-                while (true) {
-                    backedUpFilePaths = mDatabaseBackupAndRecovery.readBackedUpFilePaths(arg,
-                            lastReadValue, LEVEL_DB_READ_LIMIT);
-                    if (backedUpFilePaths.length <= 0) {
-                        break;
-                    }
-                    cumulatedValues.addAll(Arrays.asList(backedUpFilePaths));
-                    if (backedUpFilePaths.length < LEVEL_DB_READ_LIMIT) {
-                        break;
-                    }
-                    lastReadValue = backedUpFilePaths[backedUpFilePaths.length - 1];
-                }
-
-                Bundle bundle = new Bundle();
-                Object[] values =  cumulatedValues.toArray();
-                String[] resultArray =  Arrays.copyOf(values, values.length, String[].class);
-                bundle.putStringArray(READ_BACKED_UP_FILE_PATHS, resultArray);
-                return bundle;
-            }
-            case MediaStore.DELETE_BACKED_UP_FILE_PATHS:
-                getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
-                        "Permission missing to call DELETE_BACKED_UP_FILE_PATHS by "
-                                + "uid:" + Binder.getCallingUid());
-                mDatabaseBackupAndRecovery.deleteBackupForVolume(arg);
-                return new Bundle();
-            case MediaStore.GET_BACKUP_FILES:
-                getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
-                        "Permission missing to call GET_BACKUP_FILES by "
-                                + "uid:" + Binder.getCallingUid());
-                List<File> backupFiles = mDatabaseBackupAndRecovery.getBackupFiles();
-                List<String> fileNames = new ArrayList<>();
-                for (File file : backupFiles) {
-                    fileNames.add(file.getName());
-                }
-                Bundle bundle = new Bundle();
-                Object[] values = fileNames.toArray();
-                String[] resultArray = Arrays.copyOf(values, values.length, String[].class);
-                bundle.putStringArray(GET_BACKUP_FILES, resultArray);
-                return bundle;
             default:
                 throw new UnsupportedOperationException("Unsupported call: " + method);
         }
     }
 
+    @Nullable
+    private Bundle getResultForRevokeReadGrantForPackage(Bundle extras) {
+        final int caller = Binder.getCallingUid();
+        int userId;
+        final List<Uri> uris;
+        String[] packageNames;
+        if (checkPermissionSelf(caller)) {
+            final PackageManager pm = getContext().getPackageManager();
+            final int packageUid = extras.getInt(Intent.EXTRA_UID);
+            packageNames = pm.getPackagesForUid(packageUid);
+            // Get the userId from packageUid as the initiator could be a cloned app, which
+            // accesses Media via MP of its parent user and Binder's callingUid reflects
+            // the latter.
+            userId = uidToUserId(packageUid);
+            uris = extras.getParcelableArrayList(MediaStore.EXTRA_URI_LIST);
+        } else if (checkPermissionShell(caller)) {
+            // If the caller is the shell, the accepted parameter is EXTRA_PACKAGE_NAME
+            // (as string).
+            if (!extras.containsKey(Intent.EXTRA_PACKAGE_NAME)) {
+                throw new IllegalArgumentException(
+                        "Missing required extras arguments: EXTRA_URI or"
+                                + " EXTRA_PACKAGE_NAME");
+            }
+            packageNames = new String[]{extras.getString(Intent.EXTRA_PACKAGE_NAME)};
+            uris = List.of(Uri.parse(extras.getString(MediaStore.EXTRA_URI)));
+            // Caller is always shell which may not have the desired userId. Hence, use
+            // UserId from the MediaProvider process itself.
+            userId = UserHandle.myUserId();
+        } else {
+            // All other callers are unauthorized.
+            throw new SecurityException(
+                    getSecurityExceptionMessage("read media grants"));
+        }
+
+        mMediaGrants.removeMediaGrantsForPackage(packageNames, uris, userId);
+        return null;
+    }
+
+    @Nullable
+    private Bundle getResultForResolvePlaylistMembers(Bundle extras) {
+        final LocalCallingIdentity token = clearLocalCallingIdentity();
+        final CallingIdentity providerToken = clearCallingIdentity();
+        try {
+            final Uri playlistUri = extras.getParcelable(MediaStore.EXTRA_URI);
+            resolvePlaylistMembers(playlistUri);
+        } finally {
+            restoreCallingIdentity(providerToken);
+            restoreLocalCallingIdentity(token);
+        }
+        return null;
+    }
+
+    @Nullable
+    private Bundle getResultForRunIdleMaintenance() {
+        // Protect ourselves from random apps by requiring a generic
+        // permission held by common debugging components, such as shell
+        getContext().enforceCallingOrSelfPermission(
+                Manifest.permission.DUMP, TAG);
+        final LocalCallingIdentity token = clearLocalCallingIdentity();
+        final CallingIdentity providerToken = clearCallingIdentity();
+        try {
+            onIdleMaintenance(new CancellationSignal());
+        } finally {
+            restoreCallingIdentity(providerToken);
+            restoreLocalCallingIdentity(token);
+        }
+        return null;
+    }
+
+    @Nullable
+    private Bundle getResultForWaitForIdle() {
+        // TODO(b/195009139): Remove after overriding wait for idle in test to sync picker
+        // Syncing the picker while waiting for idle fixes tests with the picker db
+        // flag enabled because the picker db is in a consistent state with the external
+        // db after the sync
+        syncAllMedia();
+        ForegroundThread.waitForIdle();
+        final CountDownLatch latch = new CountDownLatch(1);
+        BackgroundThread.getExecutor().execute(latch::countDown);
+        try {
+            latch.await(30, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            throw new IllegalStateException(e);
+        }
+        return null;
+    }
+
+    @NotNull
+    private Bundle getResultForScanFile(String arg) {
+        final LocalCallingIdentity token = clearLocalCallingIdentity();
+        final CallingIdentity providerToken = clearCallingIdentity();
+
+        final String filePath = arg;
+        final Uri uri;
+        try {
+            File file;
+            try {
+                file = FileUtils.getCanonicalFile(filePath);
+            } catch (IOException e) {
+                file = null;
+            }
+
+            uri = file != null ? scanFile(file, REASON_DEMAND) : null;
+        } finally {
+            restoreCallingIdentity(providerToken);
+            restoreLocalCallingIdentity(token);
+        }
+
+        // TODO(b/262244882): maybe enforceCallingPermissionInternal(uri, ...)
+
+        final Bundle res = new Bundle();
+        res.putParcelable(Intent.EXTRA_STREAM, uri);
+        return res;
+    }
+
+    private Bundle getResultForScanVolume(String arg) {
+        final int userId = uidToUserId(Binder.getCallingUid());
+        final LocalCallingIdentity token = clearLocalCallingIdentity();
+        final CallingIdentity providerToken = clearCallingIdentity();
+
+        final String volumeName = arg;
+        try {
+            final MediaVolume volume = mVolumeCache.findVolume(volumeName,
+                    UserHandle.of(userId));
+            MediaService.onScanVolume(getContext(), volume, REASON_DEMAND);
+        } catch (FileNotFoundException e) {
+            Log.w(TAG, "Failed to find volume " + volumeName, e);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        } finally {
+            restoreCallingIdentity(providerToken);
+            restoreLocalCallingIdentity(token);
+        }
+        return Bundle.EMPTY;
+    }
+
+    @NotNull
+    private Bundle getResultForGetVersion(Bundle extras) {
+        final String volumeName = extras.getString(Intent.EXTRA_TEXT);
+
+        final DatabaseHelper helper;
+        try {
+            helper = getDatabaseForUri(Files.getContentUri(volumeName));
+        } catch (VolumeNotFoundException e) {
+            throw e.rethrowAsIllegalArgumentException();
+        }
+
+        final String version = helper.runWithoutTransaction((db) ->
+                db.getVersion() + ":" + DatabaseHelper.getOrCreateUuid(db));
+
+        final Bundle res = new Bundle();
+        res.putString(Intent.EXTRA_TEXT, version);
+        return res;
+    }
+
+    @NotNull
+    private Bundle getResultForGetGeneration(Bundle extras) {
+        final String volumeName = extras.getString(Intent.EXTRA_TEXT);
+
+        final DatabaseHelper helper;
+        try {
+            helper = getDatabaseForUri(Files.getContentUri(volumeName));
+        } catch (VolumeNotFoundException e) {
+            throw e.rethrowAsIllegalArgumentException();
+        }
+
+        final long generation = helper.runWithoutTransaction(DatabaseHelper::getGeneration);
+
+        final Bundle res = new Bundle();
+        res.putLong(Intent.EXTRA_INDEX, generation);
+        return res;
+    }
+
+    private Bundle getResultForGetDocumentUri(String method, Bundle extras) {
+        final Uri mediaUri = extras.getParcelable(MediaStore.EXTRA_URI);
+        enforceCallingPermission(mediaUri, extras, false);
+
+        final Uri fileUri;
+        final LocalCallingIdentity token = clearLocalCallingIdentity();
+        try {
+            fileUri = Uri.fromFile(queryForDataFile(mediaUri, null));
+        } catch (FileNotFoundException e) {
+            throw new IllegalArgumentException(e);
+        } finally {
+            restoreLocalCallingIdentity(token);
+        }
+
+        try (ContentProviderClient client = getContext().getContentResolver()
+                .acquireUnstableContentProviderClient(
+                        getExternalStorageProviderAuthority())) {
+            extras.putParcelable(MediaStore.EXTRA_URI, fileUri);
+            return client.call(method, null, extras);
+        } catch (RemoteException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    @NotNull
+    private Bundle getResultForGetMediaUri(String method, Bundle extras) {
+        final Uri documentUri = extras.getParcelable(MediaStore.EXTRA_URI);
+        getContext().enforceCallingUriPermission(documentUri,
+                Intent.FLAG_GRANT_READ_URI_PERMISSION, TAG);
+
+        final int callingPid = mCallingIdentity.get().pid;
+        final int callingUid = mCallingIdentity.get().uid;
+        final String callingPackage = getCallingPackage();
+        final CallingIdentity token = clearCallingIdentity();
+        final String authority = documentUri.getAuthority();
+
+        if (!authority.equals(MediaDocumentsProvider.AUTHORITY)
+                && !authority.equals(DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) {
+            throw new IllegalArgumentException("Provider for this Uri is not supported.");
+        }
+
+        try (ContentProviderClient client = getContext().getContentResolver()
+                .acquireUnstableContentProviderClient(authority)) {
+            final Bundle clientRes = client.call(method, null, extras);
+            final Uri fileUri = clientRes.getParcelable(MediaStore.EXTRA_URI);
+            final Bundle res = new Bundle();
+            final Uri mediaStoreUri = fileUri.getAuthority().equals(MediaStore.AUTHORITY)
+                    ? fileUri : queryForMediaUri(new File(fileUri.getPath()), null);
+            copyUriPermissionGrants(documentUri, mediaStoreUri, callingPid,
+                    callingUid, callingPackage);
+            res.putParcelable(MediaStore.EXTRA_URI, mediaStoreUri);
+            return res;
+        } catch (FileNotFoundException e) {
+            throw new IllegalArgumentException(e);
+        } catch (RemoteException e) {
+            throw new IllegalStateException(e);
+        } finally {
+            restoreCallingIdentity(token);
+        }
+    }
+
+    @NotNull
+    private Bundle getResultForGetRedactedMediaUri(Bundle extras) {
+        final Uri uri = extras.getParcelable(MediaStore.EXTRA_URI);
+        // NOTE: It is ok to update the DB and return a redacted URI for the cases when
+        // the user code only has read access, hence we don't check for write permission.
+        enforceCallingPermission(uri, Bundle.EMPTY, false);
+        final LocalCallingIdentity token = clearLocalCallingIdentity();
+        try {
+            final Bundle res = new Bundle();
+            res.putParcelable(MediaStore.EXTRA_URI, getRedactedUri(uri));
+            return res;
+        } finally {
+            restoreLocalCallingIdentity(token);
+        }
+    }
+
+    @NotNull
+    private Bundle getResultForGetRedactedMediaUriList(Bundle extras) {
+        final List<Uri> uris = extras.getParcelableArrayList(MediaStore.EXTRA_URI_LIST);
+        // NOTE: It is ok to update the DB and return a redacted URI for the cases when
+        // the user code only has read access, hence we don't check for write permission.
+        enforceCallingPermission(uris, false);
+        final LocalCallingIdentity token = clearLocalCallingIdentity();
+        try {
+            final Bundle res = new Bundle();
+            res.putParcelableArrayList(MediaStore.EXTRA_URI_LIST,
+                    (ArrayList<? extends Parcelable>) getRedactedUri(uris));
+            return res;
+        } finally {
+            restoreLocalCallingIdentity(token);
+        }
+    }
+
+    @Nullable
+    private Bundle getResultForGrantMediaReadForPackage(Bundle extras) {
+        final int caller = Binder.getCallingUid();
+        int userId;
+        final List<Uri> uris;
+        String packageName;
+        if (checkPermissionSelf(caller)) {
+            // If the caller is MediaProvider the accepted parameters are EXTRA_URI_LIST
+            // and EXTRA_UID.
+            if (!extras.containsKey(MediaStore.EXTRA_URI_LIST)
+                    && !extras.containsKey(Intent.EXTRA_UID)) {
+                throw new IllegalArgumentException(
+                        "Missing required extras arguments: EXTRA_URI_LIST or" + " EXTRA_UID");
+            }
+            uris = extras.getParcelableArrayList(MediaStore.EXTRA_URI_LIST);
+            final PackageManager pm = getContext().getPackageManager();
+            final int packageUid = extras.getInt(Intent.EXTRA_UID);
+            final String[] packages = pm.getPackagesForUid(packageUid);
+            if (packages == null || packages.length == 0) {
+                throw new IllegalArgumentException(
+                        String.format(
+                                "Could not find packages for media_grants with uid: %d",
+                                packageUid));
+            }
+            // Use the first package in the returned list for grants. In the case this
+            // uid has multiple shared packages, the eventual queries to check for file
+            // access will use all of the packages in this list, so just one is needed
+            // to create the grants.
+            packageName = packages[0];
+            // Get the userId from packageUid as the initiator could be a cloned app, which
+            // accesses Media via MP of its parent user and Binder's callingUid reflects
+            // the latter.
+            userId = uidToUserId(packageUid);
+        } else if (checkPermissionShell(caller)) {
+            // If the caller is the shell, the accepted parameters are EXTRA_URI (as string)
+            // and EXTRA_PACKAGE_NAME (as string).
+            if (!extras.containsKey(MediaStore.EXTRA_URI)
+                    && !extras.containsKey(Intent.EXTRA_PACKAGE_NAME)) {
+                throw new IllegalArgumentException(
+                        "Missing required extras arguments: EXTRA_URI or" + " EXTRA_PACKAGE_NAME");
+            }
+            packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME);
+            uris = List.of(Uri.parse(extras.getString(MediaStore.EXTRA_URI)));
+            // Caller is always shell which may not have the desired userId. Hence, use
+            // UserId from the MediaProvider process itself.
+            userId = UserHandle.myUserId();
+        } else {
+            // All other callers are unauthorized.
+
+            throw new SecurityException(getSecurityExceptionMessage("Create media grants"));
+        }
+
+        mMediaGrants.addMediaGrantsForPackage(packageName, uris, userId);
+        return null;
+    }
+
+    @NotNull
+    private Bundle getResultForCreateOperationsRequest(String method, Bundle extras) {
+        final PendingIntent pi = createRequest(method, extras);
+        final Bundle res = new Bundle();
+        res.putParcelable(MediaStore.EXTRA_RESULT, pi);
+        return res;
+    }
+
+    @NotNull
+    private Bundle getResultForIsSystemGallery(String arg, Bundle extras) {
+        final LocalCallingIdentity token = clearLocalCallingIdentity();
+        try {
+            String packageName = arg;
+            int uid = extras.getInt(MediaStore.EXTRA_IS_SYSTEM_GALLERY_UID);
+            boolean isSystemGallery = PermissionUtils.checkWriteImagesOrVideoAppOps(
+                    getContext(), uid, packageName, getContext().getAttributionTag());
+            Bundle res = new Bundle();
+            res.putBoolean(MediaStore.EXTRA_IS_SYSTEM_GALLERY_RESPONSE, isSystemGallery);
+            return res;
+        } finally {
+            restoreLocalCallingIdentity(token);
+        }
+    }
+
+    @Nullable
+    private Bundle getResultForPickerMediaInit(Bundle extras) {
+        Log.i(TAG, "Received media init query for extras: " + extras);
+        if (!checkPermissionShell(Binder.getCallingUid())
+                && !checkPermissionSelf(Binder.getCallingUid())) {
+            throw new SecurityException(
+                    getSecurityExceptionMessage("Picker media init"));
+        }
+        mPickerDataLayer.initMediaData(PickerSyncRequestExtras.fromBundle(extras));
+        return null;
+    }
+
+    @NotNull
+    private Bundle getResultForGetCloudProvider() {
+        if (!checkPermissionShell(Binder.getCallingUid())
+                && !checkPermissionSelf(Binder.getCallingUid())) {
+            throw new SecurityException(
+                    getSecurityExceptionMessage("Get cloud provider"));
+        }
+        final Bundle bundle = new Bundle();
+        bundle.putString(MediaStore.GET_CLOUD_PROVIDER_RESULT,
+                mPickerSyncController.getCloudProvider());
+        return bundle;
+    }
+
+    @NotNull
+    private Bundle getResultForSetCloudProvider(Bundle extras) {
+        final String cloudProvider = extras.getString(MediaStore.EXTRA_CLOUD_PROVIDER);
+        Log.i(TAG, "Request received to set cloud provider to " + cloudProvider);
+        boolean isUpdateSuccessful = false;
+        if (checkPermissionSelf(Binder.getCallingUid())) {
+            isUpdateSuccessful = mPickerSyncController.setCloudProvider(cloudProvider);
+        } else if (checkPermissionShell(Binder.getCallingUid())) {
+            isUpdateSuccessful =
+                    mPickerSyncController.forceSetCloudProvider(cloudProvider);
+        } else {
+            throw new SecurityException(
+                    getSecurityExceptionMessage("Set cloud provider"));
+        }
+
+        if (isUpdateSuccessful) {
+            Log.i(TAG, "Completed request to set cloud provider to " + cloudProvider);
+        }
+        final Bundle bundle = new Bundle();
+        bundle.putBoolean(MediaStore.SET_CLOUD_PROVIDER_RESULT, isUpdateSuccessful);
+        return bundle;
+    }
+
+    @NotNull
+    private Bundle getResultForSyncProviders() {
+        syncAllMedia();
+        return new Bundle();
+    }
+
+    @NotNull
+    private Bundle getResultForIsSupportedCloudProvider(String arg) {
+        final boolean isSupported = mPickerSyncController.isProviderSupported(arg,
+                Binder.getCallingUid());
+
+        Bundle bundle = new Bundle();
+        bundle.putBoolean(MediaStore.EXTRA_CLOUD_PROVIDER_RESULT, isSupported);
+        return bundle;
+    }
+
+    @NotNull
+    private Bundle getResultForIsCurrentCloudProviderCall(String arg) {
+        Bundle bundle = new Bundle();
+        boolean isEnabled = false;
+
+        if (mConfigStore.isCloudMediaInPhotoPickerEnabled()) {
+            isEnabled =
+                    mPickerSyncController.isProviderEnabled(
+                            arg, Binder.getCallingUid());
+        }
+
+        bundle.putBoolean(MediaStore.EXTRA_CLOUD_PROVIDER_RESULT, isEnabled);
+        return bundle;
+    }
+
+    @NotNull
+    private Bundle getResultForNotifyCloudMediaChangedEvent(String arg) {
+        final boolean notifyCloudEventResult;
+        if (mPickerSyncController.isProviderEnabled(arg, Binder.getCallingUid())) {
+            mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ false);
+            notifyCloudEventResult = true;
+        } else {
+            notifyCloudEventResult = false;
+        }
+
+        Bundle bundle = new Bundle();
+        bundle.putBoolean(MediaStore.EXTRA_CLOUD_PROVIDER_RESULT,
+                notifyCloudEventResult);
+        return bundle;
+    }
+
+    @NotNull
+    private Bundle getResultForUsesFusePassThrough(String arg) {
+        boolean isEnabled = false;
+        try {
+            FuseDaemon daemon = getFuseDaemonForFile(new File(arg), mVolumeCache);
+            if (daemon != null) {
+                isEnabled = daemon.usesFusePassthrough();
+            }
+        } catch (FileNotFoundException e) {
+        }
+
+        Bundle bundle = new Bundle();
+        bundle.putBoolean(MediaStore.USES_FUSE_PASSTHROUGH_RESULT, isEnabled);
+        return bundle;
+    }
+
+    @NotNull
+    private Bundle getResultForIdleMaintenanceForStableUris() {
+        backupDatabases(null);
+        return new Bundle();
+    }
+
+    @NotNull
+    private Bundle getResultForReadBackup(String arg, Bundle extras) {
+        getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
+                "Permission missing to call READ_BACKUP by uid:" + Binder.getCallingUid());
+        Bundle bundle = new Bundle();
+        Optional<BackupIdRow> backupIdRowOptional =
+                mDatabaseBackupAndRecovery.readDataFromBackup(arg, extras.getString(
+                        FileColumns.DATA));
+        String data = null;
+        try {
+            data = backupIdRowOptional.isPresent() ? BackupIdRow.serialize(
+                    backupIdRowOptional.get()) : null;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        bundle.putString(READ_BACKUP, data);
+        return bundle;
+    }
+
+    @NotNull
+    private Bundle getResultForGetOwnerPackageName(String arg) {
+        getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
+                "Permission missing to call GET_OWNER_PACKAGE_NAME by "
+                        + "uid:" + Binder.getCallingUid());
+        try {
+            String ownerPackageName = mDatabaseBackupAndRecovery.readOwnerPackageName(arg);
+            Bundle result = new Bundle();
+            result.putString(GET_OWNER_PACKAGE_NAME, ownerPackageName);
+            return result;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @NotNull
+    private Bundle getResultForDeleteBackedUpFilePaths(String arg) {
+        getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
+                "Permission missing to call DELETE_BACKED_UP_FILE_PATHS by "
+                        + "uid:" + Binder.getCallingUid());
+        mDatabaseBackupAndRecovery.deleteBackupForVolume(arg);
+        return new Bundle();
+    }
+
+    @NotNull
+    private Bundle getResultForGetBackupFiles() {
+        getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
+                "Permission missing to call GET_BACKUP_FILES by "
+                        + "uid:" + Binder.getCallingUid());
+        List<File> backupFiles = mDatabaseBackupAndRecovery.getBackupFiles();
+        List<String> fileNames = new ArrayList<>();
+        for (File file : backupFiles) {
+            fileNames.add(file.getName());
+        }
+        Bundle bundle = new Bundle();
+        Object[] values = fileNames.toArray();
+        String[] resultArray = Arrays.copyOf(values, values.length, String[].class);
+        bundle.putStringArray(GET_BACKUP_FILES, resultArray);
+        return bundle;
+    }
+
+    @NotNull
+    private Bundle getResultForGetRecoveryData() {
+        getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
+                "Permission missing to call GET_RECOVERY_DATA by "
+                        + "uid:" + Binder.getCallingUid());
+
+        String[] xattrs = null;
+        try {
+           xattrs = Os.listxattr("/data/media/0");
+        } catch (ErrnoException e) {
+            Log.w(TAG, "Error in getting xattr list ", e);
+        }
+
+        Bundle bundle = new Bundle();
+        bundle.putStringArray(MediaStore.GET_RECOVERY_DATA, xattrs);
+        return bundle;
+    }
+
+    private void removeRecoveryData() {
+        getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
+                "Permission missing to call REMOVE_RECOVERY_DATA by "
+                        + "uid:" + Binder.getCallingUid());
+
+        List<String> validUsers = mUserManager.getUserHandles(/* excludeDying */ true).stream()
+                .map(userHandle -> String.valueOf(userHandle.getIdentifier())).collect(
+                        Collectors.toList());
+        Log.i(TAG, "Active user ids are:" + validUsers);
+        mDatabaseBackupAndRecovery.removeRecoveryDataExceptValidUsers(validUsers);
+    }
+
+    private String getSecurityExceptionMessage(String method) {
+        int callingUid = Binder.getCallingUid();
+        return String.format("%s not allowed. Calling app ID: %d, Calling UID %d. "
+                        + "Media Provider app ID: %d, Media Provider UID: %d.",
+                method,
+                UserHandle.getAppId(callingUid),
+                callingUid,
+                UserHandle.getAppId(MY_UID),
+                MY_UID);
+    }
+
     public void backupDatabases(CancellationSignal signal) {
         mDatabaseBackupAndRecovery.backupDatabases(mInternalDatabase, mExternalDatabase, signal);
     }
@@ -6980,8 +7434,13 @@
         final Context context = getContext();
         final Intent intent = new Intent(method, null, context, PermissionActivity.class);
         intent.putExtras(extras);
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            options.setPendingIntentCreatorBackgroundActivityStartMode(
+                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+        }
         return PendingIntent.getActivity(context, PermissionActivity.REQUEST_CODE, intent,
-                FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE);
+                FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE, options.toBundle());
     }
 
     /**
@@ -7260,7 +7719,7 @@
     @Override
     public int update(@NonNull Uri uri, @Nullable ContentValues values,
             @Nullable Bundle extras) {
-        Trace.beginSection("MP.update [" + uri + ']');
+        Trace.beginSection(safeTraceSectionNameWithUri("update", uri));
         try {
             return updateInternal(uri, values, extras);
         } catch (FallbackException e) {
@@ -7310,7 +7769,6 @@
 
         int count;
 
-        final int targetSdkVersion = getCallingPackageTargetSdkVersion();
         final boolean allowHidden = isCallingPackageAllowedHidden();
         final int match = matchUri(uri, allowHidden);
         final DatabaseHelper helper = getDatabaseForUri(uri);
@@ -7583,7 +8041,7 @@
             final String probePath = initialValues.getAsString(MediaColumns.DATA);
             final String probeVolume = extractVolumeName(probePath);
             final String probeOwner = extractPathOwnerPackageName(probePath);
-            if (Objects.equals(beforePath, probePath)) {
+            if (StringUtils.equalIgnoreCase(beforePath, probePath)) {
                 Log.d(TAG, "Identical paths " + beforePath + "; not moving");
             } else if (!Objects.equals(beforeVolume, probeVolume)) {
                 throw new IllegalArgumentException("Changing volume from " + beforePath + " to "
@@ -9688,12 +10146,21 @@
         values.put(MediaColumns.MIME_TYPE, mimeType);
         values.put(FileColumns.IS_PENDING, 1);
 
+        int userIdFromPath = FileUtils.extractUserId(path);
+
         if (useData) {
             values.put(FileColumns.DATA, path);
         } else {
             values.put(FileColumns.VOLUME_NAME, extractVolumeName(path));
             values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path));
             values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path));
+            // In some cases when clone profile is active, this userId can be used to determine
+            // the path to be saved in MP database.
+            // We do this only if the path contains a valid user-id and any such value set is
+            // only a hint, the actual userId set will be determined later.
+            if (userIdFromPath != -1) {
+                values.put(FileColumns._USER_ID, userIdFromPath);
+            }
         }
         return insert(uri, values, Bundle.EMPTY);
     }
@@ -10565,7 +11032,8 @@
         return MediaStore.AUTHORITY_URI.buildUpon().appendPath(volumeName).build();
     }
 
-    public Uri attachVolume(MediaVolume volume, boolean validate) {
+    public Uri attachVolume(MediaVolume volume, boolean validate, String volumeState) {
+        Log.v(TAG, "attachVolume() called for " + volume.getName() + " with state:" + volumeState);
         if (mCallingIdentity.get().pid != android.os.Process.myPid()) {
             throw new SecurityException(
                     "Opening and closing databases not allowed.");
@@ -10590,9 +11058,6 @@
             mAttachedVolumes.add(volume);
         }
 
-        mDatabaseBackupAndRecovery.setupVolumeDbBackupAndRecovery(volume.getName(),
-                volume.getPath());
-
         final ContentResolver resolver = getContext().getContentResolver();
         final Uri uri = getBaseContentUri(volumeName);
         // TODO(b/182396009) we probably also want to notify clone profile (and vice versa)
@@ -10605,8 +11070,7 @@
 
             ForegroundThread.getExecutor().execute(() -> {
                 mExternalDatabase.runWithTransaction((db) -> {
-                    ensureDefaultFolders(volume, db);
-                    ensureThumbnailsValid(volume, db);
+                    ensureNecessaryFolders(volume, db);
                     return null;
                 });
 
@@ -10616,6 +11080,12 @@
                 MediaDocumentsProvider.onMediaStoreReady(getContext());
             });
         }
+
+        if (Environment.MEDIA_MOUNTED.equalsIgnoreCase(volumeState)) {
+            mDatabaseBackupAndRecovery.setupVolumeDbBackupAndRecovery(volume.getName(),
+                    volume.getPath());
+        }
+
         return uri;
     }
 
@@ -10668,6 +11138,22 @@
         if (LOGV) Log.v(TAG, "Detached volume: " + volumeName);
     }
 
+    private void ensureNecessaryFolders(MediaVolume volume, SQLiteDatabase db) {
+        ensureDefaultFolders(volume, db);
+        ensureThumbnailsValid(volume, db);
+
+        // Create redacted directories
+        if (MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volume.getName())) {
+            // Create dir for redacted and picker URI paths.
+            File redactedRelativePath = buildPrimaryVolumeFile(uidToUserId(MY_UID),
+                    getRedactedRelativePath());
+            if (!redactedRelativePath.exists() && !redactedRelativePath.mkdirs()) {
+                // We should always be able to create these directories from MediaProvider
+                Log.wtf(TAG, "Couldn't create redacted path for " + UserHandle.myUserId());
+            }
+        }
+    }
+
     @GuardedBy("mAttachedVolumes")
     private final ArraySet<MediaVolume> mAttachedVolumes = new ArraySet<>();
     @GuardedBy("mCustomCollators")
@@ -10942,6 +11428,15 @@
         mTranscodeHelper.dump(writer);
         writer.println();
 
+        mConfigStore.dump(writer);
+        writer.println();
+
+        mPickerDbFacade.dump(writer);
+        writer.println();
+
+        mPickerSyncController.dump(writer);
+        writer.println();
+
         dumpAccessLogs(writer);
         writer.println();
 
diff --git a/src/com/android/providers/media/MediaProviderShellCommand.java b/src/com/android/providers/media/MediaProviderShellCommand.java
index ddeef00..3886160 100644
--- a/src/com/android/providers/media/MediaProviderShellCommand.java
+++ b/src/com/android/providers/media/MediaProviderShellCommand.java
@@ -34,6 +34,7 @@
 import com.android.providers.media.photopicker.PickerSyncController;
 import com.android.providers.media.photopicker.data.CloudProviderInfo;
 import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
+import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
 
 import java.io.OutputStream;
 import java.io.PrintWriter;
@@ -187,7 +188,12 @@
 
         // TODO(b/242550131): add PickerSyncController's API to make it possible to reset just one
         //  provider's library at a time (i.e. either CMP or local).
-        mPickerSyncController.resetAllMedia();
+        try {
+            mPickerSyncController.resetAllMedia();
+        } catch (UnableToAcquireLockException e) {
+            pw.print("Could not reset all media" + e.getMessage());
+            return 1;
+        }
 
         pw.println("Done.");
         return 0;
diff --git a/src/com/android/providers/media/MediaService.java b/src/com/android/providers/media/MediaService.java
index 690114d..37f9b02 100644
--- a/src/com/android/providers/media/MediaService.java
+++ b/src/com/android/providers/media/MediaService.java
@@ -186,7 +186,7 @@
         try (ContentProviderClient cpc = context.getContentResolver()
                 .acquireContentProviderClient(MediaStore.AUTHORITY)) {
             final MediaProvider provider = ((MediaProvider) cpc.getLocalContentProvider());
-            provider.attachVolume(volume, /* validate */ true);
+            provider.attachVolume(volume, /* validate */ true, /* volumeState */ null);
 
             final ContentResolver resolver = ContentResolver.wrap(cpc.getLocalContentProvider());
 
diff --git a/src/com/android/providers/media/PermissionActivity.java b/src/com/android/providers/media/PermissionActivity.java
index d54841b..ad925ca 100644
--- a/src/com/android/providers/media/PermissionActivity.java
+++ b/src/com/android/providers/media/PermissionActivity.java
@@ -243,10 +243,6 @@
             Log.w(TAG, "Couldn't find message element");
         }
 
-        final WindowManager.LayoutParams params = actionDialog.getWindow().getAttributes();
-        params.width = getResources().getDimensionPixelSize(R.dimen.permission_dialog_width);
-        actionDialog.getWindow().setAttributes(params);
-
         // Hunt around to find the title of our newly created dialog so we can
         // adjust accessibility focus once descriptions have been loaded
         titleView = (TextView) findViewByPredicate(actionDialog.getWindow().getDecorView(),
@@ -640,7 +636,7 @@
     private @Nullable CharSequence resolveTitleText() {
         final String resName = "permission_" + verb + "_" + data;
         final int resId = getResources().getIdentifier(resName, "string",
-                getResources().getResourcePackageName(R.string.app_label));
+                getResources().getResourcePackageName(R.string.picker_app_label));
         if (resId != 0) {
             final int count = uris.size();
             final CharSequence text = StringUtils.getICUFormatString(getResources(), count, resId);
@@ -658,7 +654,7 @@
     private @Nullable CharSequence resolveProgressMessageText() {
         final String resName = "permission_progress_" + verb + "_" + data;
         final int resId = getResources().getIdentifier(resName, "string",
-                getResources().getResourcePackageName(R.string.app_label));
+                getResources().getResourcePackageName(R.string.picker_app_label));
         if (resId != 0) {
             final int count = uris.size();
             final CharSequence text = StringUtils.getICUFormatString(getResources(), count, resId);
diff --git a/src/com/android/providers/media/PickerUriResolver.java b/src/com/android/providers/media/PickerUriResolver.java
index 3625b5d..b56b694 100644
--- a/src/com/android/providers/media/PickerUriResolver.java
+++ b/src/com/android/providers/media/PickerUriResolver.java
@@ -44,7 +44,7 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.providers.media.photopicker.data.PickerDbFacade;
 import com.android.providers.media.photopicker.data.model.UserId;
-import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger;
+import com.android.providers.media.photopicker.metrics.NonUiEventLogger;
 
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -70,6 +70,10 @@
     public static final Uri PICKER_INTERNAL_URI = MediaStore.AUTHORITY_URI.buildUpon().
             appendPath(PICKER_INTERNAL_SEGMENT).build();
 
+    public static final String REFRESH_PICKER_UI_PATH = "refresh_ui";
+    public static final Uri REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI =
+            PICKER_INTERNAL_URI.buildUpon().appendPath(REFRESH_PICKER_UI_PATH).build();
+
     public static final String MEDIA_PATH = "media";
     public static final String ALBUM_PATH = "albums";
 
@@ -314,8 +318,9 @@
 
         for (String column : projection) {
             if (!mAllValidProjectionColumns.contains(column)) {
-                final PhotoPickerUiEventLogger logger = new PhotoPickerUiEventLogger();
-                logger.logPickerQueriedWithUnknownColumn(callingUid, callingPackageName);
+                final String callingPackageAndColumn = callingPackageName + ":" + column;
+                NonUiEventLogger.logPickerQueriedWithUnknownColumn(
+                        callingUid, callingPackageAndColumn);
             }
         }
     }
diff --git a/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java b/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java
index 84f4205..a28420c 100644
--- a/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java
+++ b/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java
@@ -107,7 +107,7 @@
         switch(vol.getState()) {
             case Environment.MEDIA_MOUNTED:
                 MediaVolume volume = MediaVolume.fromStorageVolume(vol);
-                mediaProvider.attachVolume(volume, /* validate */ false);
+                mediaProvider.attachVolume(volume, /* validate */ false, Environment.MEDIA_MOUNTED);
                 MediaService.queueVolumeScan(mediaProvider.getContext(), volume, REASON_MOUNTED);
                 break;
             case Environment.MEDIA_UNMOUNTED:
diff --git a/src/com/android/providers/media/fuse/FuseDaemon.java b/src/com/android/providers/media/fuse/FuseDaemon.java
index 34836af..fa910c1 100644
--- a/src/com/android/providers/media/fuse/FuseDaemon.java
+++ b/src/com/android/providers/media/fuse/FuseDaemon.java
@@ -36,8 +36,8 @@
  */
 public final class FuseDaemon extends Thread {
     public static final String TAG = "FuseDaemonThread";
-    private static final int POLL_INTERVAL_MS = 1000;
-    private static final int POLL_COUNT = 5;
+    private static final int POLL_INTERVAL_MS = 100;
+    private static final int POLL_COUNT = 50;
 
     private final Object mLock = new Object();
     private final MediaProvider mMediaProvider;
@@ -219,6 +219,18 @@
     }
 
     /**
+     * Sets up public volume's database backup to external storage to recover during a rollback.
+     */
+    public void setupPublicVolumeDbBackup(String volumeName) throws IOException {
+        synchronized (mLock) {
+            if (mPtr == 0) {
+                throw new IOException("FUSE daemon unavailable");
+            }
+            native_setup_public_volume_db_backup(mPtr, volumeName);
+        }
+    }
+
+    /**
      * Deletes entry for given key from external storage.
      */
     public void deleteDbBackup(String key) throws IOException {
@@ -231,14 +243,14 @@
     }
 
     /**
-     * Backs up given key-value pair in external storage.
+     * Backs up given key-value pair in external storage for provided volume.
      */
-    public void backupVolumeDbData(String key, String value) throws IOException {
+    public void backupVolumeDbData(String volumeName, String key, String value) throws IOException {
         synchronized (mLock) {
             if (mPtr == 0) {
                 throw new IOException("FUSE daemon unavailable");
             }
-            native_backup_volume_db_data(mPtr, key, value);
+            native_backup_volume_db_data(mPtr, volumeName, key, value);
         }
     }
 
@@ -333,8 +345,10 @@
     private native FdAccessResult native_check_fd_access(long daemon, int fd, int uid);
     private native void native_initialize_device_id(long daemon, String path);
     private native void native_setup_volume_db_backup(long daemon);
+    private native void native_setup_public_volume_db_backup(long daemon, String volumeName);
     private native void native_delete_db_backup(long daemon, String key);
-    private native void native_backup_volume_db_data(long daemon, String key, String value);
+    private native void native_backup_volume_db_data(long daemon, String volumeName, String key,
+            String value);
     private native String[] native_read_backed_up_file_paths(long daemon, String volumeName,
             String lastReadValue, int limit);
     private native String native_read_backed_up_data(long daemon, String key);
diff --git a/src/com/android/providers/media/photopicker/DataLoaderThread.java b/src/com/android/providers/media/photopicker/DataLoaderThread.java
new file mode 100644
index 0000000..1101b6a
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/DataLoaderThread.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2023 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;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import com.android.modules.utils.HandlerExecutor;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Thread for asynchronous event processing. This thread is configured as
+ * {@link android.os.Process#THREAD_PRIORITY_FOREGROUND}, which means more CPU
+ * resources will be dedicated to it, and it will be treated like "a user
+ * interface that the user is interacting with."
+ * <p>
+ * This thread is best suited for UI related tasks that the user is actively waiting for.
+ * (like data loading on grid, banner initialization etc.)
+ *
+ */
+public class DataLoaderThread extends HandlerThread {
+    private static DataLoaderThread sInstance;
+    private static Handler sHandler;
+    private static HandlerExecutor sHandlerExecutor;
+
+    // Token for cancelling tasks in handler's queue. Can be used with Handler#postDelayed.
+    public static Object TOKEN = new Object();
+
+    public DataLoaderThread() {
+        super("DataLoaderThread", android.os.Process.THREAD_PRIORITY_FOREGROUND);
+    }
+
+    private static void ensureThreadLocked() {
+        if (sInstance == null) {
+            sInstance = new DataLoaderThread();
+            sInstance.start();
+            sHandler = new Handler(sInstance.getLooper());
+            sHandlerExecutor = new HandlerExecutor(sHandler);
+        }
+    }
+
+    /**
+     * Return singleton instance of DataLoaderThread.
+     */
+    public static DataLoaderThread get() {
+        synchronized (DataLoaderThread.class) {
+            ensureThreadLocked();
+            return sInstance;
+        }
+    }
+
+    /**
+     * Return singleton handler of DataLoaderThread.
+     */
+    public static Handler getHandler() {
+        synchronized (DataLoaderThread.class) {
+            ensureThreadLocked();
+            return sHandler;
+        }
+    }
+
+    /**
+     * Return singleton executor of DataLoaderThread.
+     */
+    public static Executor getExecutor() {
+        synchronized (DataLoaderThread.class) {
+            ensureThreadLocked();
+            return sHandlerExecutor;
+        }
+    }
+
+    /**
+     * Wait for thread to be idle.
+     */
+    public static void waitForIdle() {
+        final CountDownLatch latch = new CountDownLatch(1);
+        getExecutor().execute(() -> {
+            latch.countDown();
+        });
+        try {
+            latch.await(30, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/DialogUtils.java b/src/com/android/providers/media/photopicker/DialogUtils.java
new file mode 100644
index 0000000..9f94aef
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/DialogUtils.java
@@ -0,0 +1,57 @@
+/*
+ * 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.photopicker;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.android.providers.media.R;
+
+/**
+ * Dialog box to display custom alert or error messages
+ */
+public class DialogUtils extends AppCompatActivity {
+    /**
+     * Custom dialog box with single button to display title and single error message
+     */
+    public static void showDialog(Context context, String title, String message) {
+        View customView =
+                LayoutInflater.from(context).inflate(R.layout.error_dialog, null);
+
+        TextView dialogTitle = customView.findViewById(R.id.title);
+        TextView dialogMessage = customView.findViewById(R.id.message);
+        Button gotItButton = customView.findViewById(R.id.okButton);
+        dialogTitle.setText(title);
+        dialogMessage.setText(message);
+
+        AlertDialog.Builder builder = new AlertDialog.Builder(context);
+        builder.setView(customView);
+        builder.setCancelable(false); // Prevent dismiss when clicking outside
+        final AlertDialog dialog = builder.create();
+
+        gotItButton.setOnClickListener(v -> {
+            dialog.dismiss(); // Close the dialog
+        });
+        dialog.show();
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/NotificationContentObserver.java b/src/com/android/providers/media/photopicker/NotificationContentObserver.java
new file mode 100644
index 0000000..c205002
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/NotificationContentObserver.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2023 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;
+
+import static com.android.providers.media.PickerUriResolver.PICKER_INTERNAL_URI;
+
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * {@link ContentObserver} to listen to notification on database update
+ * (for e.g. cloud sync completion of a batch).
+ *
+ * <p> This observer listens to below uris:
+ * <ul>
+ * <li>content://media/picker_internal/update</li>
+ * <li>content://media/picker_internal/update/media</li>
+ * <li>content://media/picker_internal/update/album_content/ALBUM_ID</li>
+ * </ul>
+ *
+ * <p> The notification received will contain date_taken_ms
+ * {@link android.provider.CloudMediaProviderContract.MediaColumns#DATE_TAKEN_MILLIS} or
+ * {@link android.provider.CloudMediaProviderContract.AlbumColumns#DATE_TAKEN_MILLIS}.
+ * In case of album content, it will also contain
+ * {@link android.provider.CloudMediaProviderContract#EXTRA_ALBUM_ID}
+ */
+public class NotificationContentObserver extends ContentObserver {
+    private static final String TAG = "NotificationContentObserver";
+
+    /**
+     * Callback triggered upon receiving notification.
+     */
+    public interface ContentObserverCallback{
+        /**
+         * Callers must implement this to handle the notification received.
+         *
+         * @param dateTakenMs date_taken_ms of the update
+         * @param albumId album_id in case of album_content update. Null in case of media update
+         */
+        void onNotificationReceived(String dateTakenMs, String albumId);
+    }
+
+    // Key: Collection of preference keys, Value: onChange callback for keys
+    private final Map<List<String>, ContentObserverCallback> mUrisToCallback = new HashMap<>();
+
+    public static final String UPDATE = "update";
+    public static final String MEDIA = "media";
+    public static final String ALBUM_CONTENT = "album_content";
+
+    private final List<String> mKeys;
+    private final List<Uri> mUris;
+
+    private static final Uri URI_UPDATE = PICKER_INTERNAL_URI.buildUpon()
+            .appendPath(UPDATE).build();
+
+    private static final Uri URI_UPDATE_MEDIA = URI_UPDATE.buildUpon()
+            .appendPath(MEDIA).build();
+
+    private static final Uri URI_UPDATE_ALBUM_CONTENT = URI_UPDATE.buildUpon()
+            .appendPath(ALBUM_CONTENT).build();
+
+    public static final String REGEX_MEDIA = URI_UPDATE_MEDIA + "/[0-9]*$";
+    public static final Pattern PATTERN_MEDIA = Pattern.compile(REGEX_MEDIA);
+    public static final String REGEX_ALBUM_CONTENT = URI_UPDATE_ALBUM_CONTENT + "/[0-9]*/[0-9]*$";
+    public static final Pattern PATTERN_ALBUM_CONTENT = Pattern.compile(REGEX_ALBUM_CONTENT);
+
+    /**
+     * Creates a content observer.
+     *
+     * @param handler The handler to run {@link #onChange} on, or null if none.
+     */
+    public NotificationContentObserver(Handler handler) {
+        super(handler);
+        mKeys = Arrays.asList(MEDIA, ALBUM_CONTENT);
+        mUris = Arrays.asList(URI_UPDATE_MEDIA, URI_UPDATE_ALBUM_CONTENT);
+    }
+
+    /**
+     * Registers {@link ContentObserver} instance of this class to the resolver for {@link #mUris}.
+     */
+    public void register(ContentResolver contentResolver) {
+        for (Uri uri : mUris) {
+            contentResolver.registerContentObserver(uri, /* notifyForDescendants */ true,
+                    /* observer */ this);
+        }
+    }
+
+    /**
+     * Unregisters ContentObserver
+     */
+    public void unregister(ContentResolver contentResolver) {
+        contentResolver.unregisterContentObserver(this);
+    }
+
+    /**
+     * {@link ContentObserverCallback} is added to {@link ContentObserver} to handle the
+     * onNotificationReceived event triggered by the key collection of {@code keysToObserve}.
+     *
+     * <p> Note: Observer can observe the keys present in {@link #mKeys}.
+     *
+     * @param observerCallback A callback which is used to handle the onNotificationReceived event
+     *                         triggered by the key collection of {@code keysToObserve}.
+     */
+    public void registerKeysToObserverCallback(List<String> keysToObserve,
+            ContentObserverCallback observerCallback) {
+        boolean hasValidKey = false;
+        for (String key : keysToObserve) {
+            if (!mKeys.contains(key)) {
+                Log.w(TAG, "NotificationContentObserver can not observer the key: " + key
+                        + ". Please pass valid keys from " + mKeys);
+                continue;
+            }
+            hasValidKey = true;
+        }
+        if (hasValidKey) {
+            mUrisToCallback.put(keysToObserve, observerCallback);
+        }
+    }
+
+    @Override
+    public final void onChange(boolean selfChange, Uri uri) {
+        String albumId = null;
+        String key = null;
+
+        if (PATTERN_MEDIA.matcher(uri.toString()).find()) {
+            key = MEDIA;
+        } else if (PATTERN_ALBUM_CONTENT.matcher(uri.toString()).find()) {
+            key = ALBUM_CONTENT;
+            albumId = uri.getPathSegments().get(3);
+        } else {
+            Log.w(TAG, "NotificationContentObserver cannot parse uri: " + uri
+                    + " . Please send correct uri path.");
+            return;
+        }
+
+        String dateTakenMs = uri.getLastPathSegment();
+
+        for (List<String> keys : mUrisToCallback.keySet()) {
+            if (keys.contains(key)) {
+                mUrisToCallback.get(keys).onNotificationReceived(dateTakenMs, albumId);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    public Map<List<String>, ContentObserverCallback> getUrisToCallback() {
+        return mUrisToCallback;
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
index 7f748c6..48ee7fb 100644
--- a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
+++ b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
@@ -21,7 +21,6 @@
 import static android.provider.MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP;
 import static android.provider.MediaStore.grantMediaReadForPackage;
 
-import static com.android.providers.media.MediaApplication.getConfigStore;
 import static com.android.providers.media.photopicker.PhotoPickerSettingsActivity.EXTRA_CURRENT_USER_ID;
 import static com.android.providers.media.photopicker.data.PickerResult.getPickerResponseIntent;
 import static com.android.providers.media.photopicker.data.PickerResult.getPickerUrisForItems;
@@ -47,6 +46,7 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.UserHandle;
+import android.provider.MediaStore;
 import android.util.Log;
 import android.util.TypedValue;
 import android.view.Menu;
@@ -66,6 +66,8 @@
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.appcompat.widget.Toolbar;
 import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
 import androidx.lifecycle.ViewModel;
 import androidx.lifecycle.ViewModelProvider;
 
@@ -74,6 +76,7 @@
 import com.android.providers.media.photopicker.data.PickerResult;
 import com.android.providers.media.photopicker.data.Selection;
 import com.android.providers.media.photopicker.data.UserIdManager;
+import com.android.providers.media.photopicker.data.model.Item;
 import com.android.providers.media.photopicker.data.model.UserId;
 import com.android.providers.media.photopicker.ui.TabContainerFragment;
 import com.android.providers.media.photopicker.util.LayoutModeUtils;
@@ -116,6 +119,10 @@
     private Toolbar mToolbar;
     private CrossProfileListeners mCrossProfileListeners;
 
+    @NonNull
+    private final MutableLiveData<Boolean> mIsItemPhotoGridViewChanged =
+            new MutableLiveData<>(false);
+
     @ColorInt
     private int mDefaultBackgroundColor;
 
@@ -123,7 +130,6 @@
     private int mToolBarIconColor;
 
     private int mToolbarHeight = 0;
-    private boolean mIsAccessibilityEnabled;
     private boolean mShouldLogCancelledResult = true;
 
     @Override
@@ -147,7 +153,6 @@
         super.onCreate(savedInstanceState);
 
         setContentView(R.layout.activity_photo_picker);
-
         mToolbar = findViewById(R.id.toolbar);
         setSupportActionBar(mToolbar);
         getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@@ -173,7 +178,6 @@
             return;
         }
         mSelection = mPickerViewModel.getSelection();
-
         mDragBar = findViewById(R.id.drag_bar);
         mPrivacyText = findViewById(R.id.privacy_text);
         mBottomBar = findViewById(R.id.picker_bottom_bar);
@@ -181,16 +185,7 @@
 
         mTabLayout = findViewById(R.id.tab_layout);
 
-        final AccessibilityManager am = getSystemService(AccessibilityManager.class);
-        mIsAccessibilityEnabled = am.isEnabled();
-        am.addAccessibilityStateChangeListener(enabled -> mIsAccessibilityEnabled = enabled);
-
         initBottomSheetBehavior();
-        restoreState(savedInstanceState);
-
-        final String intentAction = intent != null ? intent.getAction() : null;
-        // Call this after state is restored, to use the correct LOGGER_INSTANCE_ID_ARG
-        mPickerViewModel.logPickerOpened(Binder.getCallingUid(), getCallingPackage(), intentAction);
 
         // Save the fragment container layout so that we can adjust the padding based on preview or
         // non-preview mode.
@@ -202,6 +197,16 @@
         if (mPreloaderInstanceHolder.preloader != null) {
             subscribeToSelectedMediaPreloader(mPreloaderInstanceHolder.preloader);
         }
+
+        observeRefreshUiNotificationLiveData();
+        // Restore state operation should always be kept at the end of this method.
+        restoreState(savedInstanceState);
+        // Call this after state is restored, to use the correct LOGGER_INSTANCE_ID_ARG
+        if (savedInstanceState == null) {
+            final String intentAction = intent != null ? intent.getAction() : null;
+            mPickerViewModel.logPickerOpened(Binder.getCallingUid(), getCallingPackage(),
+                    intentAction);
+        }
     }
 
     @Override
@@ -239,13 +244,33 @@
         return super.dispatchTouchEvent(event);
     }
 
+    /**
+     * This method is called on action bar home button clicks if
+     * {@link androidx.appcompat.app.ActionBar#setDisplayHomeAsUpEnabled(boolean)} is set
+     * {@code true}.
+     */
     @Override
     public boolean onSupportNavigateUp() {
-        onBackPressed();
+        int backStackEntryCount = getSupportFragmentManager().getBackStackEntryCount();
+        mPickerViewModel.logActionBarHomeButtonClick(backStackEntryCount);
+        super.onBackPressed();
         return true;
     }
 
     @Override
+    public void onBackPressed() {
+        int backStackEntryCount = getSupportFragmentManager().getBackStackEntryCount();
+        mPickerViewModel.logBackGestureWithStackCount(backStackEntryCount);
+        super.onBackPressed();
+    }
+
+    @Override
+    public boolean onMenuOpened(int featureId, Menu menu) {
+        mPickerViewModel.logMenuOpened();
+        return super.onMenuOpened(featureId, menu);
+    }
+
+    @Override
     public void setTitle(CharSequence title) {
         super.setTitle(title);
         getSupportActionBar().setTitle(title);
@@ -312,19 +337,6 @@
         startActivity(intent);
     }
 
-    @Override
-    public void onRestart() {
-        super.onRestart();
-
-        // TODO(b/262001857): For each profile, conditionally reset PhotoPicker when cloud provider
-        //  app or account has changed. Currently, we'll reset picker each time it restarts when
-        //  settings page is enabled to avoid the scenario where cloud provider app or account has
-        //  changed but picker continues to show stale data from old provider app and account.
-        if (shouldShowSettingsScreen()) {
-            reset(/* switchToPersonalProfile */ false);
-        }
-    }
-
     /**
      * @return {@code true} if the intent was re-routed to the DocumentsUI (and this
      *  {@code PhotoPickerActivity} is {@link #isFinishing()} now). {@code false} - otherwise.
@@ -425,7 +437,10 @@
             @Override
             public void onStateChanged(@NonNull View bottomSheet, int newState) {
                 if (newState == BottomSheetBehavior.STATE_HIDDEN) {
+                    mPickerViewModel.logSwipeDownExit();
                     finish();
+                } else if (newState == BottomSheetBehavior.STATE_EXPANDED) {
+                    mPickerViewModel.logExpandToFullScreen();
                 }
                 saveBottomSheetState();
             }
@@ -469,7 +484,7 @@
     }
 
     private void initStateForBottomSheet() {
-        if (!mIsAccessibilityEnabled && !mSelection.canSelectMultiple()
+        if (!isAccessibilityEnabled() && !mSelection.canSelectMultiple()
                 && !isOrientationLandscape()) {
             final int peekHeight = getBottomSheetPeekHeight(this);
             mBottomSheetBehavior.setPeekHeight(peekHeight);
@@ -480,6 +495,15 @@
         }
     }
 
+    /**
+     * Warning: This method is visible for espresso tests, we are not customizing anything here.
+     * Allowing ourselves to control the accessibility state helps us mock it for these tests.
+     */
+    @VisibleForTesting
+    protected boolean isAccessibilityEnabled() {
+        return getSystemService(AccessibilityManager.class).isEnabled();
+    }
+
     private static int getBottomSheetPeekHeight(Context context) {
         final WindowManager windowManager = context.getSystemService(WindowManager.class);
         final Rect displayBounds = windowManager.getCurrentWindowMetrics().getBounds();
@@ -515,13 +539,19 @@
         return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
     }
 
+    public LiveData<Boolean> isItemPhotoGridViewChanged() {
+        return mIsItemPhotoGridViewChanged;
+    }
+
     public void setResultAndFinishSelf() {
         logPickerSelectionConfirmed(mSelection.getSelectedItems().size());
-
         if (shouldPreloadSelectedItems()) {
-            final var uris = PickerResult.getPickerUrisForItems(mSelection.getSelectedItems());
+            final var uris = PickerResult.getPickerUrisForItems(
+                    mSelection.getSelectedItems());
+            mPickerViewModel.logPreloadingStarted(uris.size());
             mPreloaderInstanceHolder.preloader =
                     SelectedMediaPreloader.preload(/* activity */ this, uris);
+            deSelectUnavailableMedia(mPreloaderInstanceHolder.preloader);
             subscribeToSelectedMediaPreloader(mPreloaderInstanceHolder.preloader);
         } else {
             setResultAndFinishSelfInternal();
@@ -546,11 +576,30 @@
         // The permission controller will pass the requesting package's UID here
         final Bundle extras = getIntent().getExtras();
         final int uid = extras.getInt(Intent.EXTRA_UID);
-        final List<Uri> uris = getPickerUrisForItems(mSelection.getSelectedItems());
-        ForegroundThread.getExecutor().execute(() -> {
-            // Handle grants in another thread to not block the UI.
-            grantMediaReadForPackage(getApplicationContext(), uid, uris);
-        });
+        final List<Uri> uris = getPickerUrisForItems(mSelection.getSelectedItemsWithoutGrants());
+        if (!uris.isEmpty()) {
+            ForegroundThread.getExecutor().execute(() -> {
+                // Handle grants in another thread to not block the UI.
+                grantMediaReadForPackage(getApplicationContext(), uid, uris);
+                mPickerViewModel.logPickerChoiceAddedGrantsCount(uris.size(), extras);
+            });
+        }
+
+        // Revoke READ_GRANT for items that were pre-granted but now in the current session user has
+        // deselected them.
+        if (mPickerViewModel.isManagedSelectionEnabled()) {
+            final List<Uri> urisForItemsWhoseGrantsNeedsToBeRevoked = getPickerUrisForItems(
+                    mSelection.getPreGrantedItemsToBeRevoked());
+            if (!urisForItemsWhoseGrantsNeedsToBeRevoked.isEmpty()) {
+                ForegroundThread.getExecutor().execute(() -> {
+                    // Handle grants in another thread to not block the UI.
+                    MediaStore.revokeMediaReadForPackages(getApplicationContext(), uid,
+                            urisForItemsWhoseGrantsNeedsToBeRevoked);
+                    mPickerViewModel.logPickerChoiceRevokedGrantsCount(
+                            urisForItemsWhoseGrantsNeedsToBeRevoked.size(), extras);
+                });
+            }
+        }
     }
 
     private void setResultForPickImagesOrGetContentAction() {
@@ -568,7 +617,7 @@
 
         final boolean isGetContent = isGetContentAction();
         final boolean isPickImages = isPickImagesAction();
-        final ConfigStore cs = getConfigStore();
+        final ConfigStore cs = mPickerViewModel.getConfigStore();
 
         if (getIntent().hasExtra(EXTRA_PRELOAD_SELECTED)) {
             if (Build.isDebuggable()
@@ -593,11 +642,44 @@
                 /* lifecycleOwner */ PhotoPickerActivity.this,
                 isFinished -> {
                     if (isFinished) {
+                        mPickerViewModel.logPreloadingFinished();
                         setResultAndFinishSelfInternal();
                     }
                 });
     }
 
+    // This method is responsible for deselecting all  unavailable items from selection list
+    // when user tries selecting unavailable could only media (not cached) while offline
+    private void deSelectUnavailableMedia(@NonNull SelectedMediaPreloader preloader) {
+        preloader.getUnavailableMediaIndexes().observe(
+                /* lifecycleOwner */ PhotoPickerActivity.this,
+                unavailableMediaIndexes -> {
+                    if (unavailableMediaIndexes.size() > 0) {
+                        // To notify the fragment to uncheck the unavailable items at UI those are
+                        // no longer available in the selection list.
+                        mIsItemPhotoGridViewChanged.postValue(true);
+
+                        // Checking if preloading was intentionally be cancelled by the user
+                        if (unavailableMediaIndexes.get(unavailableMediaIndexes.size() - 1) != -1) {
+                            // Displaying  error dialog with an error message when the user tries
+                            // to add unavailable cloud only media (not cached) while offline.
+                            DialogUtils.showDialog(this,
+                                    getResources().getString(R.string.dialog_error_title),
+                                    getResources().getString(R.string.dialog_error_message));
+                            mPickerViewModel.logPreloadingFailed(unavailableMediaIndexes.size());
+                        } else {
+                            unavailableMediaIndexes.remove(
+                                    unavailableMediaIndexes.size() - 1);
+                            mPickerViewModel.logPreloadingCancelled(unavailableMediaIndexes.size());
+                        }
+                        List<Item> selectedItems = mSelection.getSelectedItems();
+                        for (var mediaIndex : unavailableMediaIndexes) {
+                            mSelection.removeSelectedItem(selectedItems.get(mediaIndex));
+                        }
+                    }
+                });
+    }
+
     /**
      * NOTE: this may wrongly return {@code false} if called before {@link PickerViewModel} had a
      * chance to fetch the authority and the account of the current
@@ -825,10 +907,33 @@
 
     /**
      * Reset to Photo Picker initial launch state (Photos grid tab) in personal profile mode.
-     * @param switchToPersonalProfile is true then set personal profile as current profile.
      */
-    private void reset(boolean switchToPersonalProfile) {
-        mPickerViewModel.reset(switchToPersonalProfile);
+    private void resetToPersonalProfile() {
+        // Clear all the fragments in the FragmentManager
+        final FragmentManager fragmentManager = getSupportFragmentManager();
+        fragmentManager.popBackStackImmediate(/* name */ null,
+                FragmentManager.POP_BACK_STACK_INCLUSIVE);
+
+        // Reset all content to the personal profile
+        mPickerViewModel.resetToPersonalProfile();
+
+        // Set up the fragments same as the initial launch state
+        setupInitialLaunchState();
+    }
+
+    /**
+     * Reset to Photo Picker initial launch state (Photos grid tab) in the current profile mode.
+     */
+    private void resetInCurrentProfile() {
+        // Clear all the fragments in the FragmentManager
+        final FragmentManager fragmentManager = getSupportFragmentManager();
+        fragmentManager.popBackStackImmediate(/* name */ null,
+                FragmentManager.POP_BACK_STACK_INCLUSIVE);
+
+        // Reset all content in the current profile
+        mPickerViewModel.resetAllContentInCurrentProfile();
+
+        // Set up the fragments same as the initial launch state
         setupInitialLaunchState();
     }
 
@@ -958,14 +1063,9 @@
         }
 
         private void switchToPersonalProfileInitialLaunchState() {
-            final FragmentManager fragmentManager = getSupportFragmentManager();
-            // Clear all back stacks in FragmentManager
-            fragmentManager.popBackStackImmediate(/* name */ null,
-                    FragmentManager.POP_BACK_STACK_INCLUSIVE);
-
             // We reset the state of the PhotoPicker as we do not want to make any
             // assumptions on the state of the PhotoPicker when it was in Work Profile mode.
-            reset(/* switchToPersonalProfile */ true);
+            resetToPersonalProfile();
         }
     }
 
@@ -979,4 +1079,17 @@
         @Nullable
         SelectedMediaPreloader preloader;
     }
+
+    /**
+     * Reset the Picker view model content when launched with cloud features and notified to
+     * refresh the UI.
+     */
+    private void observeRefreshUiNotificationLiveData() {
+        mPickerViewModel.shouldRefreshUiLiveData()
+                .observe(this, shouldRefresh -> {
+                    if (shouldRefresh && !mPickerViewModel.shouldShowOnlyLocalFeatures()) {
+                        resetInCurrentProfile();
+                    }
+                });
+    }
 }
diff --git a/src/com/android/providers/media/photopicker/PhotoPickerProvider.java b/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
index c71f600..c7336fd 100644
--- a/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
+++ b/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
@@ -46,7 +46,6 @@
 import com.android.providers.media.photopicker.data.CloudProviderQueryExtras;
 import com.android.providers.media.photopicker.data.ExternalDbFacade;
 
-
 import java.io.FileNotFoundException;
 
 /**
@@ -71,7 +70,7 @@
                 CloudProviderQueryExtras.fromCloudMediaBundle(extras);
 
         return mDbFacade.queryMedia(queryExtras.getGeneration(), queryExtras.getAlbumId(),
-                queryExtras.getMimeTypes());
+                queryExtras.getMimeTypes(), queryExtras.getPageSize(), queryExtras.getPageToken());
     }
 
     @Override
diff --git a/src/com/android/providers/media/photopicker/PickerDataLayer.java b/src/com/android/providers/media/photopicker/PickerDataLayer.java
index 4f0bb57..49b3d4b 100644
--- a/src/com/android/providers/media/photopicker/PickerDataLayer.java
+++ b/src/com/android/providers/media/photopicker/PickerDataLayer.java
@@ -22,9 +22,12 @@
 import static android.provider.CloudMediaProviderContract.METHOD_GET_MEDIA_COLLECTION_INFO;
 import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT;
 import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME;
+import static android.provider.MediaStore.MY_UID;
 
 import static com.android.providers.media.PickerUriResolver.getAlbumUri;
 import static com.android.providers.media.PickerUriResolver.getMediaCollectionInfoUri;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.IMMEDIATE_ALBUM_SYNC_WORK_NAME;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.IMMEDIATE_LOCAL_SYNC_WORK_NAME;
 
 import static java.util.Objects.requireNonNull;
 
@@ -41,15 +44,33 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.work.Configuration;
+import androidx.work.WorkManager;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.InstanceId;
+import com.android.providers.media.ConfigStore;
 import com.android.providers.media.photopicker.data.CloudProviderQueryExtras;
 import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.data.PickerSyncRequestExtras;
+import com.android.providers.media.photopicker.metrics.NonUiEventLogger;
+import com.android.providers.media.photopicker.sync.PickerSyncManager;
+import com.android.providers.media.photopicker.sync.SyncTracker;
+import com.android.providers.media.photopicker.sync.SyncTrackerRegistry;
+import com.android.providers.media.util.ForegroundThread;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 /**
  * Fetches data for the picker UI from the db and cloud/local providers
@@ -58,20 +79,62 @@
     private static final String TAG = "PickerDataLayer";
     private static final boolean DEBUG = false;
     private static final boolean DEBUG_DUMP_CURSORS = false;
+    private static final long CLOUD_SYNC_TIMEOUT_MILLIS = 500L;
 
     public static final String QUERY_ARG_LOCAL_ONLY = "android:query-arg-local-only";
 
-    private final Context mContext;
-    private final PickerDbFacade mDbFacade;
-    private final PickerSyncController mSyncController;
-    private final String mLocalProvider;
+    public static final String QUERY_DATE_TAKEN_BEFORE_MS = "android:query-date-taken-before-ms";
 
-    public PickerDataLayer(Context context, PickerDbFacade dbFacade,
-            PickerSyncController syncController) {
-        mContext = context;
-        mDbFacade = dbFacade;
-        mSyncController = syncController;
-        mLocalProvider = dbFacade.getLocalProvider();
+    public static final String QUERY_LOCAL_ID_SELECTION = "android:query-local-id-selection";
+
+    public static final String QUERY_ROW_ID = "android:query-row-id";
+
+    // Thread pool size should be at least equal to the number of unique work requests in
+    // {@link PickerSyncManager} to ensure that any request type is not blocked on other request
+    // types. It is advisable to use unique work requests because in case the number of queued
+    // requests grows, they should not block other work requests.
+    private static final int WORK_MANAGER_THREAD_POOL_SIZE = 6;
+    @Nullable
+    private static volatile Executor sWorkManagerExecutor;
+
+    @NonNull
+    private final Context mContext;
+    @NonNull
+    private final PickerDbFacade mDbFacade;
+    @NonNull
+    private final PickerSyncController mSyncController;
+    @NonNull
+    private final PickerSyncManager mSyncManager;
+    @NonNull
+    private final String mLocalProvider;
+    @NonNull
+    private final ConfigStore mConfigStore;
+
+    @VisibleForTesting
+    public PickerDataLayer(@NonNull Context context, @NonNull PickerDbFacade dbFacade,
+            @NonNull PickerSyncController syncController, @NonNull ConfigStore configStore,
+            @NonNull PickerSyncManager syncManager) {
+        mContext = requireNonNull(context);
+        mDbFacade = requireNonNull(dbFacade);
+        mSyncController = requireNonNull(syncController);
+        mLocalProvider = requireNonNull(dbFacade.getLocalProvider());
+        mConfigStore = requireNonNull(configStore);
+        mSyncManager = syncManager;
+
+        // Add a subscriber to config store changes to monitor the allowlist.
+        mConfigStore.addOnChangeListener(
+                ForegroundThread.getExecutor(),
+                this::validateCurrentCloudProviderOnAllowlistChange);
+    }
+
+    /**
+     * Create a new instance of PickerDataLayer.
+     */
+    public static PickerDataLayer create(@NonNull Context context, @NonNull PickerDbFacade dbFacade,
+            @NonNull PickerSyncController syncController, @NonNull ConfigStore configStore) {
+        PickerSyncManager syncManager = new PickerSyncManager(
+                getWorkManager(context), context, configStore, /* schedulePeriodicSyncs */ true);
+        return new PickerDataLayer(context, dbFacade, syncController, configStore, syncManager);
     }
 
     /**
@@ -112,15 +175,24 @@
             // Use media table for all media except albums. Merged categories like,
             // favorites and video are tagged in the media table and are not a part of
             // album_media.
-            if (TextUtils.isEmpty(albumId) || isMergedAlbum(queryExtras)) {
+            if (TextUtils.isEmpty(albumId) || queryExtras.isMergedAlbum()) {
                 // Refresh the 'media' table
-                syncAllMedia(isLocalOnly);
+                if (shouldSyncBeforePickerQuery()) {
+                    syncAllMedia(isLocalOnly);
+                } else {
+                    // Wait for local sync to finish indefinitely
+                    waitForSync(SyncTrackerRegistry.getLocalSyncTracker(),
+                            IMMEDIATE_LOCAL_SYNC_WORK_NAME);
+                    Log.i(TAG, "Local sync is complete");
 
-                if (!isLocalOnly && TextUtils.isEmpty(albumId)) {
-                    // TODO(b/257887919): Build proper UI and remove this.
-                    // Notify that the picker is launched in case there's any pending UI
-                    // notification
-                    mSyncController.notifyPickerLaunch();
+                    // Wait for on cloud sync with timeout
+                    if (!isLocalOnly) {
+                        boolean syncIsComplete = waitForSyncWithTimeout(
+                                SyncTrackerRegistry.getCloudSyncTracker(),
+                                CLOUD_SYNC_TIMEOUT_MILLIS);
+                        Log.i(TAG, "Finished waiting for cloud sync.  Is cloud sync complete: "
+                                + syncIsComplete);
+                    }
                 }
 
                 // Fetch all merged and deduped cloud and local media from 'media' table
@@ -138,7 +210,13 @@
                 // The album type here can only be local or cloud because merged categories like,
                 // Favorites and Videos would hit the first condition.
                 // Refresh the 'album_media' table
-                mSyncController.syncAlbumMedia(albumId, isLocal(albumAuthority));
+                if (shouldSyncBeforePickerQuery()) {
+                    mSyncController.syncAlbumMedia(albumId, isLocal(albumAuthority));
+                } else {
+                    waitForSync(SyncTrackerRegistry.getAlbumSyncTracker(isLocal(albumAuthority)),
+                            IMMEDIATE_ALBUM_SYNC_WORK_NAME);
+                    Log.i(TAG, "Album sync is complete");
+                }
 
                 // Fetch album specific media for local or cloud from 'album_media' table
                 result = mDbFacade.queryAlbumMediaForUi(
@@ -162,20 +240,77 @@
 
     private void syncAllMedia(boolean isLocalOnly) {
         if (isLocalOnly) {
-            mSyncController.syncAllMediaFromLocalProvider();
+            mSyncController.syncAllMediaFromLocalProvider(/* cancellationSignal= */ null);
         } else {
             mSyncController.syncAllMedia();
         }
     }
 
     /**
-     * Checks if the query is for a merged album type.
-     * Some albums are not cloud only, they are merged from files on devices and the cloudprovider.
+     * Will try it's best to wait for the existing sync requests to complete. It may not wait for
+     * new sync requests received after this method starts running.
      */
-    private boolean isMergedAlbum(CloudProviderQueryExtras queryExtras) {
-        final boolean isFavorite = queryExtras.isFavorite();
-        final boolean isVideo = queryExtras.isVideo();
-        return isFavorite || isVideo;
+    private void waitForSync(@NonNull SyncTracker syncTracker, String uniqueWorkName) {
+        try {
+            final CompletableFuture<Void> completableFuture =
+                    CompletableFuture.allOf(
+                            syncTracker.pendingSyncFutures().toArray(new CompletableFuture[0]));
+
+            waitForSync(completableFuture, uniqueWorkName, /* retryCount */ 30);
+        } catch (ExecutionException | InterruptedException e) {
+            Log.w(TAG, "Could not wait for the sync to finish: " + e);
+        }
+    }
+
+    /**
+     * Wait for sync tracked by the input future to complete. In case the future takes an unusually
+     * long time to complete, check the relevant unique work status from Work Manager.
+     */
+    @VisibleForTesting
+    public int waitForSync(@NonNull CompletableFuture<Void> completableFuture,
+            @NonNull String uniqueWorkName,
+            int retryCount) throws ExecutionException, InterruptedException {
+        for (; retryCount > 0; retryCount--) {
+            try {
+                completableFuture.get(/* timeout */ 3, TimeUnit.SECONDS);
+                return retryCount;
+            } catch (TimeoutException e) {
+                if (mSyncManager.isUniqueWorkPending(uniqueWorkName)) {
+                    Log.i(TAG, "Waiting for the sync again."
+                            + " Unique work name: " + uniqueWorkName
+                            + " Retry count: " + retryCount);
+                } else {
+                    Log.e(TAG, "Either immediate unique work is complete and the sync futures "
+                            + "were not cleared, or a proactive sync might be blocking the query. "
+                            + "Unblocking the query now for " + uniqueWorkName);
+                    return retryCount;
+                }
+            }
+        }
+
+        if (retryCount == 0) {
+            Log.e(TAG, "Retry count exhausted, could not wait for sync anymore.");
+        }
+        return retryCount;
+    }
+
+    /**
+     * Will wait for the existing sync requests to complete till the provided timeout. It may
+     * not wait for new sync requests received after this method starts running.
+     */
+    private boolean waitForSyncWithTimeout(
+            @NonNull SyncTracker syncTracker,
+            @Nullable Long timeoutInMillis) {
+        try {
+            final CompletableFuture<Void> completableFuture =
+                    CompletableFuture.allOf(
+                            syncTracker.pendingSyncFutures().toArray(new CompletableFuture[0]));
+            completableFuture.get(timeoutInMillis, TimeUnit.MILLISECONDS);
+            return true;
+        } catch (ExecutionException | InterruptedException | TimeoutException e) {
+            Log.w(TAG, "Could not wait for the sync with timeout to finish: " + e);
+            return false;
+        }
     }
 
     /**
@@ -209,9 +344,11 @@
             final boolean isLocalOnly = queryArgs.getBoolean(QUERY_ARG_LOCAL_ONLY, false);
             // Refresh the 'media' table so that 'merged' albums (Favorites and Videos) are
             // up-to-date
-            syncAllMedia(isLocalOnly);
+            if (shouldSyncBeforePickerQuery()) {
+                syncAllMedia(isLocalOnly);
+            }
 
-            final String cloudProvider = mDbFacade.getCloudProvider();
+            final String cloudProvider = mSyncController.getCloudProvider();
             final CloudProviderQueryExtras queryExtras =
                     CloudProviderQueryExtras.fromMediaStoreBundle(queryArgs);
             final Bundle cloudMediaArgs = queryExtras.toCloudMediaBundle();
@@ -221,7 +358,8 @@
             cursorExtra.putString(MediaStore.EXTRA_LOCAL_PROVIDER, mLocalProvider);
 
             // Favorites and Videos are merged albums.
-            final Cursor mergedAlbums = mDbFacade.getMergedAlbums(queryExtras.toQueryFilter());
+            final Cursor mergedAlbums = mDbFacade.getMergedAlbums(queryExtras.toQueryFilter(),
+                    cloudProvider);
             if (mergedAlbums != null) {
                 cursors.add(mergedAlbums);
             }
@@ -293,7 +431,12 @@
         final Bundle accountBundle = mContext.getContentResolver()
                 .call(getMediaCollectionInfoUri(cloudProvider), METHOD_GET_MEDIA_COLLECTION_INFO,
                         /* arg */ null, /* extras */ null);
-
+        if (accountBundle == null) {
+            Log.e(TAG,
+                    "Media collection info received is null. Failed to fetch Cloud account "
+                            + "information.");
+            return null;
+        }
         final String accountName = accountBundle.getString(ACCOUNT_NAME);
         if (accountName == null) {
             return null;
@@ -318,12 +461,22 @@
     }
 
     private Cursor queryProviderAlbumsInternal(@NonNull String authority, Bundle queryArgs) {
+        final InstanceId instanceId = NonUiEventLogger.generateInstanceId();
+        int numberOfAlbumsFetched = -1;
+        NonUiEventLogger.logPickerGetAlbumsStart(instanceId, MY_UID, authority);
         try {
-            return mContext.getContentResolver().query(getAlbumUri(authority),
+            final Cursor res = mContext.getContentResolver().query(getAlbumUri(authority),
                     /* projection */ null, queryArgs, /* cancellationSignal */ null);
+            if (res != null) {
+                numberOfAlbumsFetched = res.getCount();
+            }
+            return res;
         } catch (Exception e) {
             Log.w(TAG, "Failed to fetch cloud albums for: " + authority, e);
             return null;
+        } finally {
+            NonUiEventLogger.logPickerGetAlbumsEnd(instanceId, MY_UID, authority,
+                    numberOfAlbumsFetched);
         }
     }
 
@@ -344,6 +497,68 @@
         return sb.toString();
     }
 
+    /**
+     * Triggers a sync operation based on the parameters.
+     */
+    public void initMediaData(@NonNull PickerSyncRequestExtras syncRequestExtras) {
+        if (syncRequestExtras.shouldSyncMediaData()) {
+            // Sync media data
+            Log.i(TAG, "Init data request for the main photo grid i.e. media data."
+                    + " Should sync with local provider only: "
+                    + syncRequestExtras.shouldSyncLocalOnlyData());
+
+            mSyncManager.syncMediaImmediately(syncRequestExtras.shouldSyncLocalOnlyData());
+        } else {
+            // Sync album media data
+            Log.i(TAG, String.format("Init data request for album content of: %s"
+                            + " Should sync with local provider only: %b",
+                    syncRequestExtras.getAlbumId(),
+                    syncRequestExtras.shouldSyncLocalOnlyData()));
+
+            validateAlbumMediaSyncArgs(syncRequestExtras);
+
+            // We don't need to sync in case of merged albums
+            if (!syncRequestExtras.shouldSyncMergedAlbum()) {
+                mSyncManager.syncAlbumMediaForProviderImmediately(
+                        syncRequestExtras.getAlbumId(),
+                        syncRequestExtras.getAlbumAuthority());
+            }
+        }
+    }
+
+    private void validateAlbumMediaSyncArgs(PickerSyncRequestExtras syncRequestExtras) {
+        if (!syncRequestExtras.shouldSyncMediaData()) {
+            Objects.requireNonNull(syncRequestExtras.getAlbumId(),
+                    "Album Id can't be null for an album sync request.");
+            Objects.requireNonNull(syncRequestExtras.getAlbumAuthority(),
+                    "Album authority can't be null for an album sync request.");
+        }
+        if (!syncRequestExtras.shouldSyncMediaData()
+                && !syncRequestExtras.shouldSyncMergedAlbum()
+                && syncRequestExtras.shouldSyncLocalOnlyData()
+                && !isLocal(syncRequestExtras.getAlbumAuthority())) {
+            throw new IllegalStateException(
+                    "Can't exclude cloud contents in cloud album "
+                            + syncRequestExtras.getAlbumAuthority());
+        }
+    }
+
+
+    /**
+     * Handles notification about media events like inserts/updates/deletes received from cloud or
+     * local providers.
+     * @param localOnly - whether the media event is coming from the local provider
+     */
+    public void handleMediaEventNotification(Boolean localOnly) {
+        try {
+            mSyncManager.syncMediaProactively(localOnly);
+        } catch (RuntimeException e) {
+            // Catch any unchecked exceptions so that critical paths in MP that call this method are
+            // not affected by Picker related issues.
+            Log.e(TAG, "Could not handle media event notification ", e);
+        }
+    }
+
     public static class AccountInfo {
         public final String accountName;
         public final Intent accountConfigurationIntent;
@@ -365,6 +580,7 @@
 
         @NonNull static final Map<String, Integer> COLUMN_NAME_TO_INDEX_MAP;
         static final int AUTHORITY_COLUMN_INDEX;
+
         static {
             final Map<String, Integer> map = new HashMap<>();
             for (int columnIndex = 0; columnIndex < ALL_PROJECTION.length; columnIndex++) {
@@ -449,5 +665,90 @@
             // is stored in the cursor.
             return mAuthority;
         }
+
+        @Override
+        public int getType(int columnIndex) {
+            // 1. Get value from the underlying cursor.
+            final int cursorColumnIndex = mColumnIndexToCursorColumnIndexArray[columnIndex];
+            final int cursorValue = cursorColumnIndex != -1
+                    ? getWrappedCursor().getType(cursorColumnIndex) : Cursor.FIELD_TYPE_NULL;
+
+            // 2a. If this is NOT the AUTHORITY column: just return the value.
+            if (columnIndex != AUTHORITY_COLUMN_INDEX) {
+                return cursorValue;
+            }
+
+            // 2b. If this IS the AUTHORITY column: "override" whatever value (which may be 0)
+            // is stored in the cursor.
+            return Cursor.FIELD_TYPE_STRING;
+        }
+    }
+
+    /**
+     * Initialize the {@link WorkManager} if it is not initialized already.
+     *
+     * @return a {@link WorkManager} object that can be used to run work requests.
+     */
+    @NonNull
+    private static WorkManager getWorkManager(Context mContext) {
+        if (!WorkManager.isInitialized()) {
+            Log.i(TAG, "Work manager not initialised. Attempting to initialise.");
+            WorkManager.initialize(mContext, getWorkManagerConfiguration());
+        }
+        return WorkManager.getInstance(mContext);
+    }
+
+    @NonNull
+    private static Configuration getWorkManagerConfiguration() {
+        ensureWorkManagerExecutor();
+        return new Configuration.Builder()
+                .setMinimumLoggingLevel(Log.INFO)
+                .setExecutor(sWorkManagerExecutor)
+                .build();
+    }
+
+    private static void ensureWorkManagerExecutor() {
+        if (sWorkManagerExecutor == null) {
+            synchronized (PickerDataLayer.class) {
+                if (sWorkManagerExecutor == null) {
+                    sWorkManagerExecutor = Executors
+                            .newFixedThreadPool(WORK_MANAGER_THREAD_POOL_SIZE);
+                }
+            }
+        }
+    }
+
+    /**
+     * For cloud feature enabled scenarios, sync request is sent from the
+     * MediaStore.PICKER_MEDIA_INIT_CALL method call once when a fresh grid needs to be filled
+     * populated data. This is because UI paginated queries are supported when cloud feature
+     * enabled. This avoids triggering a sync for the same dataset for each paged query received
+     * from the UI.
+     */
+    private boolean shouldSyncBeforePickerQuery() {
+        return !mConfigStore.isCloudMediaInPhotoPickerEnabled();
+    }
+
+    /**
+     * Checks the current allowed list of Cloud Provider packages, and ensures that the currently
+     * set provider is a member of the allowlist. In the event the current Cloud Provider is not on
+     * the list, the current Cloud Provider is removed.
+     */
+    private void validateCurrentCloudProviderOnAllowlistChange() {
+
+        List<String> currentAllowlist = mConfigStore.getAllowedCloudProviderPackages();
+        String currentCloudProvider = mSyncController.getCurrentCloudProviderInfo().packageName;
+
+        if (!currentAllowlist.contains(currentCloudProvider)) {
+            Log.d(
+                    TAG,
+                    String.format(
+                            "Cloud provider allowlist was changed, and the current cloud provider"
+                                    + " is no longer on the allowlist."
+                                    + " Allowlist: %s"
+                                    + " Current Provider: %s",
+                            currentAllowlist.toString(), currentCloudProvider));
+            mSyncController.notifyPackageRemoval(currentCloudProvider);
+        }
     }
 }
diff --git a/src/com/android/providers/media/photopicker/PickerSyncController.java b/src/com/android/providers/media/photopicker/PickerSyncController.java
index 05c38a9..eb7df4f 100644
--- a/src/com/android/providers/media/photopicker/PickerSyncController.java
+++ b/src/com/android/providers/media/photopicker/PickerSyncController.java
@@ -19,49 +19,59 @@
 import static android.content.ContentResolver.EXTRA_HONORED_ARGS;
 import static android.provider.CloudMediaProviderContract.EXTRA_ALBUM_ID;
 import static android.provider.CloudMediaProviderContract.EXTRA_MEDIA_COLLECTION_ID;
+import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_SIZE;
 import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_TOKEN;
 import static android.provider.CloudMediaProviderContract.EXTRA_SYNC_GENERATION;
 import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION;
 import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.MEDIA_COLLECTION_ID;
+import static android.provider.MediaStore.MY_UID;
 
+import static com.android.providers.media.PickerUriResolver.PICKER_INTERNAL_URI;
+import static com.android.providers.media.PickerUriResolver.REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI;
 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 static com.android.providers.media.photopicker.NotificationContentObserver.ALBUM_CONTENT;
+import static com.android.providers.media.photopicker.NotificationContentObserver.MEDIA;
+import static com.android.providers.media.photopicker.NotificationContentObserver.UPDATE;
+import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString;
 
 import android.annotation.IntDef;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.CancellationSignal;
 import android.os.Handler;
-import android.os.Process;
 import android.os.Trace;
 import android.os.storage.StorageManager;
 import android.provider.CloudMediaProvider;
 import android.provider.CloudMediaProviderContract;
+import android.provider.CloudMediaProviderContract.MediaColumns;
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
-import android.widget.Toast;
 
-import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.internal.logging.InstanceId;
 import com.android.modules.utils.BackgroundThread;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.providers.media.ConfigStore;
-import com.android.providers.media.R;
 import com.android.providers.media.photopicker.data.CloudProviderInfo;
 import com.android.providers.media.photopicker.data.PickerDbFacade;
-import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger;
+import com.android.providers.media.photopicker.metrics.NonUiEventLogger;
+import com.android.providers.media.photopicker.sync.CloseableReentrantLock;
+import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
 import com.android.providers.media.photopicker.util.CloudProviderUtils;
 import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException;
-import com.android.providers.media.util.ForegroundThread;
+import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
 
+import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -82,11 +92,14 @@
     private static final boolean DEBUG = false;
 
     private static final String PREFS_KEY_CLOUD_PROVIDER_AUTHORITY = "cloud_provider_authority";
-    private static final String PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATION =
-            "cloud_provider_pending_notification";
     private static final String PREFS_KEY_CLOUD_PREFIX = "cloud_provider:";
     private static final String PREFS_KEY_LOCAL_PREFIX = "local_provider:";
 
+    private static final String PREFS_KEY_RESUME = "resume";
+    private static final String PREFS_KEY_OPERATION_MEDIA_ADD_PREFIX = "media_add:";
+    private static final String PREFS_KEY_OPERATION_MEDIA_REMOVE_PREFIX = "media_remove:";
+    private static final String PREFS_KEY_OPERATION_ALBUM_ADD_PREFIX = "album_add:";
+
     private static final String PICKER_USER_PREFS_FILE_NAME = "picker_user_prefs";
     public static final String PICKER_SYNC_PREFS_FILE_NAME = "picker_sync_prefs";
     public static final String LOCAL_PICKER_PROVIDER_AUTHORITY =
@@ -94,10 +107,22 @@
 
     private static final String PREFS_VALUE_CLOUD_PROVIDER_UNSET = "-";
 
+    private static final int OPERATION_ADD_MEDIA = 1;
+    private static final int OPERATION_ADD_ALBUM = 2;
+    private static final int OPERATION_REMOVE_MEDIA = 3;
+
+    @IntDef(
+            flag = false,
+            value = {OPERATION_ADD_MEDIA, OPERATION_ADD_ALBUM, OPERATION_REMOVE_MEDIA})
+    @Retention(RetentionPolicy.SOURCE)
+    private @interface OperationType {}
+
     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;
     private static final int SYNC_TYPE_MEDIA_RESET = 3;
+    private static final int SYNC_TYPE_MEDIA_FULL_WITH_RESET = 4;
+    public static final int PAGE_SIZE = 1000;
     @NonNull
     private static final Handler sBgThreadHandler = BackgroundThread.getHandler();
     @IntDef(flag = false, prefix = { "SYNC_TYPE_" }, value = {
@@ -105,35 +130,87 @@
             SYNC_TYPE_MEDIA_INCREMENTAL,
             SYNC_TYPE_MEDIA_FULL,
             SYNC_TYPE_MEDIA_RESET,
+            SYNC_TYPE_MEDIA_FULL_WITH_RESET,
     })
     @Retention(RetentionPolicy.SOURCE)
     private @interface SyncType {}
 
+    private static final long DEFAULT_GENERATION = -1;
     private final Context mContext;
     private final ConfigStore mConfigStore;
     private final PickerDbFacade mDbFacade;
     private final SharedPreferences mSyncPrefs;
     private final SharedPreferences mUserPrefs;
+    private final PickerSyncLockManager mPickerSyncLockManager;
     private final String mLocalProvider;
-    private final long mSyncDelayMs;
-    private final Runnable mSyncAllMediaCallback;
 
-    private final PhotoPickerUiEventLogger mLogger;
-    private final Object mCloudSyncLock = new Object();
-    // TODO(b/278562157): If there is a dependency on the sync process, always acquire the
-    //  {@link mCloudSyncLock} before {@link mCloudProviderLock} to avoid deadlock.
-    private final Object mCloudProviderLock = new Object();
-    @GuardedBy("mCloudProviderLock")
     private CloudProviderInfo mCloudProviderInfo;
+    @Nullable
+    private static PickerSyncController sInstance;
 
-    public PickerSyncController(@NonNull Context context, @NonNull PickerDbFacade dbFacade,
-            @NonNull ConfigStore configStore) {
-        this(context, dbFacade, configStore, LOCAL_PICKER_PROVIDER_AUTHORITY);
+    /**
+     * Initialize {@link PickerSyncController} object.{@link PickerSyncController} should only be
+     * initialized from {@link com.android.providers.media.MediaProvider#onCreate}.
+     *
+     * @param context the app context of type {@link Context}
+     * @param dbFacade instance of {@link PickerDbFacade} that will be used for DB queries.
+     * @param configStore {@link ConfigStore} that returns the sync config of the device.
+     * @return an instance of {@link PickerSyncController}
+     */
+    @NonNull
+    public static PickerSyncController initialize(@NonNull Context context,
+            @NonNull PickerDbFacade dbFacade, @NonNull ConfigStore configStore, @NonNull
+            PickerSyncLockManager pickerSyncLockManager) {
+        return initialize(context, dbFacade, configStore, pickerSyncLockManager,
+                LOCAL_PICKER_PROVIDER_AUTHORITY);
     }
 
+    /**
+     * Initialize {@link PickerSyncController} object.{@link PickerSyncController} should only be
+     * initialized from {@link com.android.providers.media.MediaProvider#onCreate}.
+     *
+     * @param context the app context of type {@link Context}
+     * @param dbFacade instance of {@link PickerDbFacade} that will be used for DB queries.
+     * @param configStore {@link ConfigStore} that returns the sync config of the device.
+     * @param localProvider is the name of the local provider that is responsible for providing the
+     *                      local media items.
+     * @return an instance of {@link PickerSyncController}
+     */
+    @NonNull
     @VisibleForTesting
-    public PickerSyncController(@NonNull Context context, @NonNull PickerDbFacade dbFacade,
-            @NonNull ConfigStore configStore, @NonNull String localProvider) {
+    public static PickerSyncController initialize(@NonNull Context context,
+            @NonNull PickerDbFacade dbFacade, @NonNull ConfigStore configStore,
+            @NonNull PickerSyncLockManager pickerSyncLockManager, @NonNull String localProvider) {
+        sInstance = new PickerSyncController(context, dbFacade, configStore, pickerSyncLockManager,
+                localProvider);
+        return sInstance;
+    }
+
+    /**
+     * This method is available for injecting a mock instance from tests. PickerSyncController is
+     * used in Worker classes. They cannot directly be injected with a mock controller instance.
+     */
+    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    public static void setInstance(PickerSyncController controller) {
+        sInstance = controller;
+    }
+
+    /**
+     * Returns PickerSyncController instance if it is initialized else throws an exception.
+     * @return a PickerSyncController object.
+     * @throws IllegalStateException when the PickerSyncController is not initialized.
+     */
+    @NonNull
+    public static PickerSyncController getInstanceOrThrow() throws IllegalStateException {
+        if (sInstance == null) {
+            throw new IllegalStateException("PickerSyncController is not initialised.");
+        }
+        return sInstance;
+    }
+
+    private PickerSyncController(@NonNull Context context, @NonNull PickerDbFacade dbFacade,
+            @NonNull ConfigStore configStore, @NonNull PickerSyncLockManager pickerSyncLockManager,
+            @NonNull String localProvider) {
         mContext = context;
         mConfigStore = configStore;
         mSyncPrefs = mContext.getSharedPreferences(PICKER_SYNC_PREFS_FILE_NAME,
@@ -141,16 +218,22 @@
         mUserPrefs = mContext.getSharedPreferences(PICKER_USER_PREFS_FILE_NAME,
                 Context.MODE_PRIVATE);
         mDbFacade = dbFacade;
+        mPickerSyncLockManager = pickerSyncLockManager;
         mLocalProvider = localProvider;
-        mSyncAllMediaCallback = this::syncAllMedia;
-        mLogger = new PhotoPickerUiEventLogger();
-        mSyncDelayMs = configStore.getPickerSyncDelayMs();
 
+        // Listen to the device config, and try to enable cloud features when the config changes.
+        mConfigStore.addOnChangeListener(BackgroundThread.getExecutor(), this::initCloudProvider);
         initCloudProvider();
     }
 
+    @NonNull
+    public PickerSyncLockManager getPickerSyncLockManager() {
+        return mPickerSyncLockManager;
+    }
+
     private void initCloudProvider() {
-        synchronized (mCloudProviderLock) {
+        try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
             if (!mConfigStore.isCloudMediaInPhotoPickerEnabled()) {
                 Log.d(TAG, "Cloud-Media-in-Photo-Picker feature is disabled during " + TAG
                         + " construction.");
@@ -194,62 +277,52 @@
 
         Trace.beginSection(traceSectionName("syncAllMedia"));
         try {
-            syncAllMediaFromLocalProvider();
-            syncAllMediaFromCloudProvider();
+            syncAllMediaFromLocalProvider(/*CancellationSignal=*/ null);
+            syncAllMediaFromCloudProvider(/*CancellationSignal=*/ null);
         } finally {
             Trace.endSection();
         }
     }
 
-
     /**
      * Syncs the local media
      */
-    public void syncAllMediaFromLocalProvider() {
+    public void syncAllMediaFromLocalProvider(@Nullable CancellationSignal cancellationSignal) {
         // Picker sync and special format update can execute concurrently and run into a deadlock.
         // Acquiring a lock before execution of each flow to avoid this.
         sIdleMaintenanceSyncLock.lock();
         try {
-            syncAllMediaFromProvider(mLocalProvider, /* isLocal */ true, /* retryOnFailure */ true);
+            final InstanceId instanceId = NonUiEventLogger.generateInstanceId();
+            syncAllMediaFromProvider(mLocalProvider, /* isLocal */ true, /* retryOnFailure */ true,
+                    /* enablePagedSync= */ true, instanceId, cancellationSignal);
         } finally {
             sIdleMaintenanceSyncLock.unlock();
         }
     }
 
-    private void syncAllMediaFromCloudProvider() {
-        synchronized (mCloudSyncLock) {
-            final String cloudProvider = getCloudProvider();
+    /**
+     * Syncs the cloud media
+     */
+    public void syncAllMediaFromCloudProvider(@Nullable CancellationSignal cancellationSignal) {
 
-            // Disable cloud queries in the database. If any cloud related queries come through
-            // while cloud sync is in progress, all cloud items will be ignored and local items will
-            // be returned.
-            mDbFacade.setCloudProvider(null);
+        try (CloseableReentrantLock ignored =
+                     mPickerSyncLockManager.tryLock(PickerSyncLockManager.CLOUD_SYNC_LOCK)) {
+            final String cloudProvider = getCloudProviderWithTimeout();
 
             // Trigger a sync.
-            final boolean isSyncCommitted = syncAllMediaFromProvider(cloudProvider,
-                    /* isLocal */ false, /* retryOnFailure */ true);
+            final InstanceId instanceId = NonUiEventLogger.generateInstanceId();
+            final boolean didSyncFinish = syncAllMediaFromProvider(cloudProvider,
+                    /* isLocal= */ false, /* retryOnFailure= */ true, /* enablePagedSync= */ true,
+                    instanceId, cancellationSignal);
 
-            // Check if sync was committed i.e. the latest collection info was persisted.
-            if (!isSyncCommitted) {
-                Log.e(TAG, "Failed to sync with cloud provider - " + cloudProvider
-                        + ". The cloud provider may have changed during the sync");
-                return;
+            // Check if sync was completed successfully.
+            if (!didSyncFinish) {
+                Log.e(TAG, "Failed to fully complete sync with cloud provider - " + cloudProvider
+                        + ". The cloud provider may have changed during the sync, or only a"
+                        + " partial sync was completed.");
             }
-
-            // Reset the album_media table every time we sync all media
-            // TODO(258765155): do we really need to reset for both providers?
-            resetAlbumMedia();
-
-            // Re-enable cloud queries in the database for the latest cloud provider.
-            synchronized (mCloudProviderLock) {
-                if (Objects.equals(mCloudProviderInfo.authority, cloudProvider)) {
-                    mDbFacade.setCloudProvider(cloudProvider);
-                } else {
-                    Log.e(TAG, "Failed to sync with cloud provider - " + cloudProvider
-                            + ". The cloud provider has changed to "
-                            + mCloudProviderInfo.authority);
-                }
-            }
+        } catch (UnableToAcquireLockException e) {
+            Log.e(TAG, "Could not sync with the cloud provider", e);
         }
     }
 
@@ -259,27 +332,37 @@
      */
     public void syncAlbumMedia(String albumId, boolean isLocal) {
         if (isLocal) {
-            syncAlbumMediaFromLocalProvider(albumId);
+            executeSyncAlbumReset(getLocalProvider(), isLocal, albumId);
+            syncAlbumMediaFromLocalProvider(albumId, /* cancellationSignal=*/ null);
         } else {
-            syncAlbumMediaFromCloudProvider(albumId);
+            try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                    .tryLock(PickerSyncLockManager.CLOUD_ALBUM_SYNC_LOCK)) {
+                executeSyncAlbumReset(getCloudProviderWithTimeout(), isLocal, albumId);
+            } catch (UnableToAcquireLockException e) {
+                Log.e(TAG, "Unable to reset cloud album media " + albumId, e);
+                // Continue to attempt cloud album sync. This may show deleted album media on
+                // the album view.
+            }
+            syncAlbumMediaFromCloudProvider(albumId, /*cancellationSignal=*/ null);
         }
     }
 
-    private void syncAlbumMediaFromLocalProvider(@NonNull String albumId) {
-        syncAlbumMediaFromProvider(mLocalProvider, /* isLocal */ true, albumId);
+    /** Syncs album media from the local provider. */
+    public void syncAlbumMediaFromLocalProvider(
+            @NonNull String albumId, @Nullable CancellationSignal cancellationSignal) {
+        syncAlbumMediaFromProvider(mLocalProvider, /* isLocal */ true, albumId,
+                /* enablePagedSync= */ true, cancellationSignal);
     }
 
-    private void syncAlbumMediaFromCloudProvider(@NonNull String albumId) {
-        synchronized (mCloudSyncLock) {
-            syncAlbumMediaFromProvider(getCloudProvider(), /* isLocal */ false, albumId);
-        }
-    }
-
-    private void resetAlbumMedia() {
-        executeSyncAlbumReset(mLocalProvider, /* isLocal */ true, /* albumId */ null);
-
-        synchronized (mCloudSyncLock) {
-            executeSyncAlbumReset(getCloudProvider(), /* isLocal */ false, /* albumId */ null);
+    /** Syncs album media from the currently enabled cloud {@link CloudMediaProvider}. */
+    public void syncAlbumMediaFromCloudProvider(
+            @NonNull String albumId, @Nullable CancellationSignal cancellationSignal) {
+        try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                .tryLock(PickerSyncLockManager.CLOUD_ALBUM_SYNC_LOCK)) {
+            syncAlbumMediaFromProvider(getCloudProviderWithTimeout(), /* isLocal */ false, albumId,
+                    /* enablePagedSync= */ true, cancellationSignal);
+        } catch (UnableToAcquireLockException e) {
+            Log.e(TAG, "Unable to sync cloud album media " + albumId, e);
         }
     }
 
@@ -287,14 +370,20 @@
      * Resets media library previously synced from the current {@link CloudMediaProvider} as well
      * as the {@link #mLocalProvider local provider}.
      */
-    public void resetAllMedia() {
+    public void resetAllMedia() throws UnableToAcquireLockException {
+        // No need to acquire cloud lock for local reset.
         resetAllMedia(mLocalProvider, /* isLocal */ true);
-        synchronized (mCloudSyncLock) {
+
+        try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                .lock(PickerSyncLockManager.CLOUD_SYNC_LOCK)) {
+
+            // This does not fall in any sync path. Try to acquire the lock indefinitely.
             resetAllMedia(getCloudProvider(), /* isLocal */ false);
         }
     }
 
-    private boolean resetAllMedia(@Nullable String authority, boolean isLocal) {
+    private boolean resetAllMedia(@Nullable String authority, boolean isLocal)
+            throws UnableToAcquireLockException {
         Trace.beginSection(traceSectionName("resetAllMedia", isLocal));
         try {
             executeSyncReset(authority, isLocal);
@@ -373,13 +462,8 @@
             Log.v(TAG, "Thread=" + Thread.currentThread() + "; Stacktrace:", new Throwable());
         }
 
-        if (!mConfigStore.isCloudMediaInPhotoPickerEnabled()) {
-            Log.w(TAG, "Ignoring a request to set the CloudMediaProvider (" + authority + ") "
-                    + "since the Cloud-Media-in-Photo-Picker feature is disabled");
-            return false;
-        }
-
-        synchronized (mCloudProviderLock) {
+        try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
             if (Objects.equals(mCloudProviderInfo.authority, authority)) {
                 Log.w(TAG, "Cloud provider already set: " + authority);
                 return true;
@@ -388,7 +472,8 @@
 
         final CloudProviderInfo newProviderInfo = getCloudProviderInfo(authority, ignoreAllowList);
         if (authority == null || !newProviderInfo.isEmpty()) {
-            synchronized (mCloudProviderLock) {
+            try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                    .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
                 // Disable cloud provider queries on the db until next sync
                 // This will temporarily *clear* the cloud provider on the db facade and prevent
                 // any queries from seeing cloud media until a sync where the cloud provider will be
@@ -399,7 +484,7 @@
                 persistCloudProviderInfo(newProviderInfo, /* shouldUnset */ true);
 
                 // TODO(b/242897322): Log from PickerViewModel using its InstanceId when relevant
-                mLogger.logPickerCloudProviderChanged(newProviderInfo.uid,
+                NonUiEventLogger.logPickerCloudProviderChanged(newProviderInfo.uid,
                         newProviderInfo.packageName);
                 Log.i(TAG, "Cloud provider changed successfully. Old: "
                         + oldAuthority + ". New: " + newProviderInfo.authority);
@@ -419,7 +504,8 @@
      */
     @NonNull
     public CloudProviderInfo getCurrentCloudProviderInfo() {
-        synchronized (mCloudProviderLock) {
+        try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
             return mCloudProviderInfo;
         }
     }
@@ -430,19 +516,38 @@
      *         disabled by the user.
      */
     private void setCurrentCloudProviderInfo(@NonNull CloudProviderInfo cloudProviderInfo) {
-        synchronized (mCloudProviderLock) {
+        try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
             mCloudProviderInfo = cloudProviderInfo;
         }
     }
 
     /**
+     * This should not be used in picker sync paths because we should not wait on a lock
+     * indefinitely during the picker sync process.
+     * Use {@link this#getCloudProviderWithTimeout()} instead.
      * @return {@link android.content.pm.ProviderInfo#authority authority} of the current
      *         {@link CloudMediaProvider} or {@code null} if the {@link CloudMediaProvider}
      *         integration is not enabled.
      */
     @Nullable
     public String getCloudProvider() {
-        synchronized (mCloudProviderLock) {
+        try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
+            return mCloudProviderInfo.authority;
+        }
+    }
+
+    /**
+     * @return {@link android.content.pm.ProviderInfo#authority authority} of the current
+     *         {@link CloudMediaProvider} or {@code null} if the {@link CloudMediaProvider}
+     *         integration is not enabled. This operation acquires a lock internally with a timeout.
+     * @throws UnableToAcquireLockException if the lock was not acquired within the given timeout.
+     */
+    @Nullable
+    public String getCloudProviderWithTimeout() throws UnableToAcquireLockException {
+        try (CloseableReentrantLock ignored  = mPickerSyncLockManager
+                .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
             return mCloudProviderInfo.authority;
         }
     }
@@ -460,7 +565,8 @@
             return true;
         }
 
-        synchronized (mCloudProviderLock) {
+        try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
             if (!mCloudProviderInfo.isEmpty()
                     && Objects.equals(mCloudProviderInfo.authority, authority)) {
                 return true;
@@ -471,11 +577,12 @@
     }
 
     public boolean isProviderEnabled(String authority, int uid) {
-        if (uid == Process.myUid() && mLocalProvider.equals(authority)) {
+        if (uid == MY_UID && mLocalProvider.equals(authority)) {
             return true;
         }
 
-        synchronized (mCloudProviderLock) {
+        try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
             if (!mCloudProviderInfo.isEmpty() && uid == mCloudProviderInfo.uid
                     && Objects.equals(mCloudProviderInfo.authority, authority)) {
                 return true;
@@ -486,7 +593,7 @@
     }
 
     public boolean isProviderSupported(String authority, int uid) {
-        if (uid == Process.myUid() && mLocalProvider.equals(authority)) {
+        if (uid == MY_UID && mLocalProvider.equals(authority)) {
             return true;
         }
 
@@ -505,22 +612,11 @@
     }
 
     /**
-     * Notifies about media events like inserts/updates/deletes from cloud and local providers and
-     * syncs the changes in the background.
-     *
-     * There is a delay before executing the background sync to artificially throttle the burst
-     * notifications.
-     */
-    public void notifyMediaEvent() {
-        sBgThreadHandler.removeCallbacks(mSyncAllMediaCallback);
-        sBgThreadHandler.postDelayed(mSyncAllMediaCallback, mSyncDelayMs);
-    }
-
-    /**
      * Notifies about package removal
      */
     public void notifyPackageRemoval(String packageName) {
-        synchronized (mCloudProviderLock) {
+        try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
             if (mCloudProviderInfo.matches(packageName)) {
                 Log.i(TAG, "Package " + packageName
                         + " is the current cloud provider and got removed");
@@ -530,7 +626,8 @@
     }
 
     private void resetCloudProvider() {
-        synchronized (mCloudProviderLock) {
+        try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
             setCloudProvider(/* authority */ null);
 
             /**
@@ -543,60 +640,37 @@
         }
     }
 
-    // TODO(b/257887919): Build proper UI and remove this.
     /**
-     * Notifies about picker UI launched
+     * Syncs album media.
+     *
+     * @param enablePagedSync Set to true if the data from the provider may be synced in batches.
+     *                         If true, {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE
+     *                         is passed during query to the provider.
      */
-    public void notifyPickerLaunch() {
-        final String authority = getCloudProvider();
+    private void syncAlbumMediaFromProvider(String authority, boolean isLocal, String albumId,
+            boolean enablePagedSync, @Nullable CancellationSignal cancellationSignal) {
+        final InstanceId instanceId = NonUiEventLogger.generateInstanceId();
+        NonUiEventLogger.logPickerAlbumMediaSyncStart(instanceId, MY_UID, authority);
 
-        final boolean hasPendingNotification = mUserPrefs.getBoolean(
-                PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATION, /* defaultValue */ false);
-
-        if (!hasPendingNotification || (authority == 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();
-            final String appName = CloudProviderUtils.getProviderLabel(pm, authority);
-
-            final String message = mContext.getResources().getString(R.string.picker_cloud_sync,
-                    appName);
-            Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
-        });
-
-        // Clear the notification
-        updateBooleanUserPref(PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATION, false);
-    }
-
-    private void updateBooleanUserPref(String key, boolean value) {
-        final SharedPreferences.Editor editor = mUserPrefs.edit();
-        editor.putBoolean(key, value);
-        editor.apply();
-    }
-
-    private void syncAlbumMediaFromProvider(String authority, boolean isLocal, String albumId) {
         final Bundle queryArgs = new Bundle();
         queryArgs.putString(EXTRA_ALBUM_ID, albumId);
+        if (enablePagedSync) {
+            queryArgs.putInt(EXTRA_PAGE_SIZE, PAGE_SIZE);
+        }
 
         Trace.beginSection(traceSectionName("syncAlbumMediaFromProvider", isLocal));
         try {
-            executeSyncAlbumReset(authority, isLocal, albumId);
-
             if (authority != null) {
-                executeSyncAddAlbum(authority, isLocal, albumId, queryArgs);
+                executeSyncAddAlbum(
+                        authority, isLocal, albumId, queryArgs, instanceId, cancellationSignal);
             }
-        } catch (RuntimeException e) {
+        } catch (RuntimeException | UnableToAcquireLockException e) {
             // Unlike syncAllMediaFromProvider, we don't retry here because any errors would have
             // occurred in fetching all the album_media since incremental sync is not supported.
             // A full sync is therefore unlikely to resolve any issue
             Log.e(TAG, "Failed to sync album media", e);
+        } catch (RequestObsoleteException e) {
+            Log.e(TAG, "Failed to sync all album media because authority has changed: ", e);
         } finally {
             Trace.endSection();
         }
@@ -604,9 +678,18 @@
 
     /**
      * Returns true if the sync was successful and the latest collection info was persisted.
+     *
+     * @param enablePagedSync Set to true if the data from the provider may be synced in batches.
+     *                         If true, {@link CloudMediaProviderContract#EXTRA_PAGE_SIZE} is passed
+     *                         during query to the provider.
      */
-    private boolean syncAllMediaFromProvider(@Nullable String authority, boolean isLocal,
-            boolean retryOnFailure) {
+    private boolean syncAllMediaFromProvider(
+            @Nullable String authority,
+            boolean isLocal,
+            boolean retryOnFailure,
+            boolean enablePagedSync,
+            InstanceId instanceId,
+            @Nullable CancellationSignal cancellationSignal) {
         Log.d(TAG, "syncAllMediaFromProvider() " + (isLocal ? "LOCAL" : "CLOUD")
                 + ", auth=" + authority
                 + ", retry=" + retryOnFailure);
@@ -617,52 +700,96 @@
         Trace.beginSection(traceSectionName("syncAllMediaFromProvider", isLocal));
         try {
             final SyncRequestParams params = getSyncRequestParams(authority, isLocal);
-
             switch (params.syncType) {
                 case SYNC_TYPE_MEDIA_RESET:
                     // Can only happen when |authority| has been set to null and we need to clean up
+                    disablePickerCloudMediaQueries(isLocal);
                     return resetAllMedia(authority, isLocal);
-                case SYNC_TYPE_MEDIA_FULL:
+                case SYNC_TYPE_MEDIA_FULL_WITH_RESET:
+                    disablePickerCloudMediaQueries(isLocal);
                     if (!resetAllMedia(authority, isLocal)) {
                         return false;
                     }
+                    enablePickerCloudMediaQueries(authority, isLocal);
 
+                    // Cache collection id with default generation id to prevent DB reset if full
+                    // sync resumes the next time sync is triggered.
+                    cacheMediaCollectionInfo(
+                            authority, isLocal,
+                            getDefaultGenerationCollectionInfo(params.latestMediaCollectionInfo));
+                    // Fall through to run full sync
+                case SYNC_TYPE_MEDIA_FULL:
+                    NonUiEventLogger.logPickerFullSyncStart(instanceId, MY_UID, authority);
+
+                    // Send UI refresh notification for any active picker sessions, as the
+                    // UI data might be stale if a full sync needs to be run.
+                    sendPickerUiRefreshNotification();
+
+                    final Bundle fullSyncQueryArgs = new Bundle();
+                    if (enablePagedSync) {
+                        fullSyncQueryArgs.putInt(EXTRA_PAGE_SIZE, params.mPageSize);
+                    }
                     // Pass a mutable empty bundle intentionally because it might be populated with
                     // the next page token as part of a query to a cloud provider supporting
                     // pagination
                     executeSyncAdd(authority, isLocal, params.getMediaCollectionId(),
-                            /* isIncrementalSync */ false, /* queryArgs */ new Bundle());
+                            /* isIncrementalSync */ false, fullSyncQueryArgs,
+                            instanceId, cancellationSignal);
 
                     // Commit sync position
                     return cacheMediaCollectionInfo(
                             authority, isLocal, params.latestMediaCollectionInfo);
                 case SYNC_TYPE_MEDIA_INCREMENTAL:
+                    enablePickerCloudMediaQueries(authority, isLocal);
+                    NonUiEventLogger.logPickerIncrementalSyncStart(instanceId, MY_UID, authority);
                     final Bundle queryArgs = new Bundle();
                     queryArgs.putLong(EXTRA_SYNC_GENERATION, params.syncGeneration);
+                    if (enablePagedSync) {
+                        queryArgs.putInt(EXTRA_PAGE_SIZE, params.mPageSize);
+                    }
 
-                    executeSyncAdd(authority, isLocal, params.getMediaCollectionId(),
-                            /* isIncrementalSync */ true, queryArgs);
-                    executeSyncRemove(authority, isLocal, params.getMediaCollectionId(), queryArgs);
+                    executeSyncAdd(
+                            authority,
+                            isLocal,
+                            params.getMediaCollectionId(),
+                            /* isIncrementalSync */ true,
+                            queryArgs,
+                            instanceId,
+                            cancellationSignal);
+                    executeSyncRemove(authority, isLocal, params.getMediaCollectionId(), queryArgs,
+                            instanceId, cancellationSignal);
 
                     // Commit sync position
                     return cacheMediaCollectionInfo(
                             authority, isLocal, params.latestMediaCollectionInfo);
                 case SYNC_TYPE_NONE:
+                    enablePickerCloudMediaQueries(authority, isLocal);
                     return true;
                 default:
                     throw new IllegalArgumentException("Unexpected sync type: " + params.syncType);
             }
         } catch (RequestObsoleteException e) {
             Log.e(TAG, "Failed to sync all media because authority has changed: ", e);
-        } catch (RuntimeException e) {
-            // Reset all media for the cloud provider in case it never succeeds
-            resetAllMedia(authority, isLocal);
-
-            // Attempt a full sync. If this fails, the db table would have been reset,
-            // flushing all old content and leaving the picker UI empty.
+        } catch (IllegalStateException e) {
+            // If we're in an illegal state, reset and start a full sync again.
+            Log.e(TAG, "Failed to sync all media. Reset media and retry: " + retryOnFailure, e);
+            try {
+                resetAllMedia(authority, isLocal);
+                if (retryOnFailure) {
+                    return syncAllMediaFromProvider(authority, isLocal, /* retryOnFailure */ false,
+                            enablePagedSync, instanceId, cancellationSignal);
+                }
+            } catch (UnableToAcquireLockException ex) {
+                Log.e(TAG, "Could not reset media", e);
+            }
+        } catch (RuntimeException | UnableToAcquireLockException e) {
+            // Retry the failed operation to see if it was an intermittent problem. If this fails,
+            // the database will be in a partial state until the sync resumes from this point
+            // on next run.
             Log.e(TAG, "Failed to sync all media. Reset media and retry: " + retryOnFailure, e);
             if (retryOnFailure) {
-                return syncAllMediaFromProvider(authority, isLocal, /* retryOnFailure */ false);
+                return syncAllMediaFromProvider(authority, isLocal, /* retryOnFailure */ false,
+                        enablePagedSync, instanceId, cancellationSignal);
             }
         } finally {
             Trace.endSection();
@@ -670,6 +797,33 @@
         return false;
     }
 
+    /**
+     * Disable cloud media queries from Picker database. After disabling cloud media queries, when a
+     * media query will run on Picker database, only local media items will be returned.
+     */
+    private void disablePickerCloudMediaQueries(boolean isLocal)
+            throws UnableToAcquireLockException {
+        if (!isLocal) {
+            mDbFacade.setCloudProviderWithTimeout(null);
+        }
+    }
+
+    /**
+     * Enable cloud media queries from Picker database. After enabling cloud media queries, when a
+     * media query will run on Picker database, both local and cloud media items will be returned.
+     */
+    private void enablePickerCloudMediaQueries(String authority, boolean isLocal)
+            throws UnableToAcquireLockException {
+        if (!isLocal) {
+            try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                    .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
+                if (Objects.equals(mCloudProviderInfo.authority, authority)) {
+                    mDbFacade.setCloudProviderWithTimeout(authority);
+                }
+            }
+        }
+    }
+
     private void executeSyncReset(String authority, boolean isLocal) {
         Log.i(TAG, "Executing SyncReset. isLocal: " + isLocal + ". authority: " + authority);
 
@@ -703,8 +857,29 @@
         }
     }
 
-    private void executeSyncAdd(String authority, boolean isLocal,
-            String expectedMediaCollectionId, boolean isIncrementalSync, Bundle queryArgs) {
+    /**
+     * Queries the provider and adds media to the picker database.
+     *
+     * @param authority Provider's authority
+     * @param isLocal Whether this is the local provider or not
+     * @param expectedMediaCollectionId The MediaCollectionId from the last sync point.
+     * @param isIncrementalSync If true, {@link CloudMediaProviderContract#EXTRA_SYNC_GENERATION}
+     *     should be honoured by the provider.
+     * @param queryArgs Query arguments to pass in query.
+     * @param instanceId Metrics related Picker session instance Id.
+     * @param cancellationSignal CancellationSignal used to abort the sync.
+     * @throws RequestObsoleteException When the sync is interrupted due to the provider
+     *     changing.
+     */
+    private void executeSyncAdd(
+            String authority,
+            boolean isLocal,
+            String expectedMediaCollectionId,
+            boolean isIncrementalSync,
+            Bundle queryArgs,
+            InstanceId instanceId,
+            @Nullable CancellationSignal cancellationSignal)
+            throws RequestObsoleteException, UnableToAcquireLockException {
         final Uri uri = getMediaUri(authority);
         final List<String> expectedHonoredArgs = new ArrayList<>();
         if (isIncrementalSync) {
@@ -713,47 +888,120 @@
 
         Log.i(TAG, "Executing SyncAdd. isLocal: " + isLocal + ". authority: " + authority);
 
+        String resumeKey =
+                getPrefsKey(isLocal, PREFS_KEY_OPERATION_MEDIA_ADD_PREFIX + PREFS_KEY_RESUME);
+
         Trace.beginSection(traceSectionName("executeSyncAdd", isLocal));
-        try (PickerDbFacade.DbWriteOperation operation =
-                     mDbFacade.beginAddMediaOperation(authority)) {
-            executePagedSync(uri, expectedMediaCollectionId, expectedHonoredArgs, queryArgs,
-                    operation);
+        try {
+            int syncedItems = executePagedSync(
+                    uri,
+                    expectedMediaCollectionId,
+                    expectedHonoredArgs,
+                    queryArgs,
+                    resumeKey,
+                    OPERATION_ADD_MEDIA,
+                    authority,
+                    isLocal,
+                    cancellationSignal);
+            NonUiEventLogger.logPickerAddMediaSyncCompletion(instanceId, MY_UID, authority,
+                    syncedItems);
         } finally {
             Trace.endSection();
         }
     }
 
-    private void executeSyncAddAlbum(String authority, boolean isLocal,
-            String albumId, Bundle queryArgs) {
+    /**
+     * Queries the provider to sync media from the given albumId into the picker database.
+     *
+     * @param authority Provider's authority
+     * @param isLocal Whether this is the local provider or not
+     * @param albumId the Id of the album to sync
+     * @param queryArgs Query arguments to pass in query.
+     * @param instanceId Metrics related Picker session instance Id.
+     * @param cancellationSignal CancellationSignal used to abort the sync.
+     * @throws RequestObsoleteException When the sync is interrupted due to the provider
+     *     changing.
+     */
+    private void executeSyncAddAlbum(
+            String authority,
+            boolean isLocal,
+            String albumId,
+            Bundle queryArgs,
+            InstanceId instanceId,
+            @Nullable CancellationSignal cancellationSignal)
+            throws RequestObsoleteException, UnableToAcquireLockException {
         final Uri uri = getMediaUri(authority);
 
         Log.i(TAG, "Executing SyncAddAlbum. "
                 + "isLocal: " + isLocal + ". authority: " + authority + ". albumId: " + albumId);
+        String resumeKey =
+                getPrefsKey(isLocal, PREFS_KEY_OPERATION_ALBUM_ADD_PREFIX + PREFS_KEY_RESUME);
 
         Trace.beginSection(traceSectionName("executeSyncAddAlbum", isLocal));
-        try (PickerDbFacade.DbWriteOperation operation =
-                     mDbFacade.beginAddAlbumMediaOperation(authority, albumId)) {
+        try {
 
             // We don't need to validate the mediaCollectionId for album_media sync since it's
             // always a full sync
-            executePagedSync(uri, /* mediaCollectionId */ null, Arrays.asList(EXTRA_ALBUM_ID),
-                    queryArgs, operation);
+            int syncedItems =
+                    executePagedSync(
+                            uri, /* mediaCollectionId */
+                            null,
+                            List.of(EXTRA_ALBUM_ID),
+                            queryArgs,
+                            resumeKey,
+                            OPERATION_ADD_ALBUM,
+                            authority,
+                            isLocal,
+                            albumId,
+                            /*cancellationSignal=*/ cancellationSignal);
+            NonUiEventLogger.logPickerAddAlbumMediaSyncCompletion(instanceId, MY_UID, authority,
+                    syncedItems);
         } finally {
             Trace.endSection();
         }
     }
 
-    private void executeSyncRemove(String authority, boolean isLocal,
-            String mediaCollectionId, Bundle queryArgs) {
+    /**
+     * Queries the provider and syncs removed media with the picker database.
+     *
+     * @param authority Provider's authority
+     * @param isLocal Whether this is the local provider or not
+     * @param mediaCollectionId The last synced media collection id
+     * @param queryArgs Query arguments to pass in query.
+     * @param instanceId Metrics related Picker session instance Id.
+     * @param cancellationSignal CancellationSignal used to abort the sync.
+     * @throws RequestObsoleteException When the sync is interrupted due to the provider
+     *     changing.
+     */
+    private void executeSyncRemove(
+            String authority,
+            boolean isLocal,
+            String mediaCollectionId,
+            Bundle queryArgs,
+            InstanceId instanceId,
+            @Nullable CancellationSignal cancellationSignal)
+            throws RequestObsoleteException, UnableToAcquireLockException {
         final Uri uri = getDeletedMediaUri(authority);
 
         Log.i(TAG, "Executing SyncRemove. isLocal: " + isLocal + ". authority: " + authority);
+        String resumeKey =
+                getPrefsKey(isLocal, PREFS_KEY_OPERATION_MEDIA_REMOVE_PREFIX + PREFS_KEY_RESUME);
 
         Trace.beginSection(traceSectionName("executeSyncRemove", isLocal));
-        try (PickerDbFacade.DbWriteOperation operation =
-                     mDbFacade.beginRemoveMediaOperation(authority)) {
-            executePagedSync(uri, mediaCollectionId, Arrays.asList(EXTRA_SYNC_GENERATION),
-                    queryArgs, operation);
+        try {
+            int syncedItems =
+                    executePagedSync(
+                            uri,
+                            mediaCollectionId,
+                            List.of(EXTRA_SYNC_GENERATION),
+                            queryArgs,
+                            resumeKey,
+                            OPERATION_REMOVE_MEDIA,
+                            authority,
+                            isLocal,
+                            cancellationSignal);
+            NonUiEventLogger.logPickerRemoveMediaSyncCompletion(instanceId, MY_UID, authority,
+                    syncedItems);
         } finally {
             Trace.endSection();
         }
@@ -763,7 +1011,8 @@
      * Persist cloud provider info and send a sync request to the background thread.
      */
     private void persistCloudProviderInfo(@NonNull CloudProviderInfo info, boolean shouldUnset) {
-        synchronized (mCloudProviderLock) {
+        try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                .lock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
             setCurrentCloudProviderInfo(info);
 
             final String authority = info.authority;
@@ -779,9 +1028,6 @@
                 editor.remove(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY);
             }
 
-            editor.putBoolean(
-                    PREFS_KEY_CLOUD_PROVIDER_PENDING_NOTIFICATION, isCloudProviderInfoNotEmpty);
-
             editor.apply();
 
             if (SdkLevel.isAtLeastT()) {
@@ -798,7 +1044,22 @@
 
             Log.d(TAG, "Updated cloud provider to: " + authority);
 
-            resetCachedMediaCollectionInfo(info.authority, /* isLocal */ false);
+            try {
+                resetCachedMediaCollectionInfo(info.authority, /* isLocal */ false);
+            } catch (UnableToAcquireLockException e) {
+                Log.wtf(TAG, "CLOUD_PROVIDER_LOCK is already held by this thread.");
+            }
+
+            sendPickerUiRefreshNotification();
+        }
+    }
+
+    private void sendPickerUiRefreshNotification() {
+        ContentResolver contentResolver = mContext.getContentResolver();
+        if (contentResolver != null) {
+            contentResolver.notifyChange(REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI, null);
+        } else {
+            Log.d(TAG, "Couldn't notify the Picker UI to refresh");
         }
     }
 
@@ -816,7 +1077,7 @@
      * Commit the latest media collection info when a sync operation is completed.
      */
     private boolean cacheMediaCollectionInfo(@Nullable String authority, boolean isLocal,
-            @Nullable Bundle bundle) {
+            @Nullable Bundle bundle) throws UnableToAcquireLockException {
         if (authority == null) {
             Log.d(TAG, "Ignoring cache media info for null authority with bundle: " + bundle);
             return true;
@@ -829,7 +1090,8 @@
                 cacheMediaCollectionInfoInternal(isLocal, bundle);
                 return true;
             } else {
-                synchronized (mCloudProviderLock) {
+                try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                        .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
                     // Check if the media collection info belongs to the current cloud provider
                     // authority.
                     if (Objects.equals(authority, mCloudProviderInfo.authority)) {
@@ -854,6 +1116,14 @@
         if (bundle == null) {
             editor.remove(getPrefsKey(isLocal, MEDIA_COLLECTION_ID));
             editor.remove(getPrefsKey(isLocal, LAST_MEDIA_SYNC_GENERATION));
+            // Clear any resume keys for page tokens.
+            editor.remove(
+                    getPrefsKey(isLocal, PREFS_KEY_OPERATION_MEDIA_ADD_PREFIX + PREFS_KEY_RESUME));
+            editor.remove(
+                    getPrefsKey(isLocal, PREFS_KEY_OPERATION_ALBUM_ADD_PREFIX + PREFS_KEY_RESUME));
+            editor.remove(
+                    getPrefsKey(
+                            isLocal, PREFS_KEY_OPERATION_MEDIA_REMOVE_PREFIX + PREFS_KEY_RESUME));
         } else {
             final String collectionId = bundle.getString(MEDIA_COLLECTION_ID);
             final long generation = bundle.getLong(LAST_MEDIA_SYNC_GENERATION);
@@ -864,7 +1134,47 @@
         editor.apply();
     }
 
-    private boolean resetCachedMediaCollectionInfo(@Nullable String authority, boolean isLocal) {
+    /**
+     * Adds the given token to the saved sync preferences.
+     *
+     * @param token The token to remember. A null value will clear the preference.
+     * @param resumeKey The operation's key in sync preferences.
+     */
+    private void rememberNextPageToken(@Nullable String token, String resumeKey)
+            throws UnableToAcquireLockException {
+
+        try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
+            final SharedPreferences.Editor editor = mSyncPrefs.edit();
+            if (token == null) {
+                Log.d(TAG, String.format("Clearing next page token for key: %s", resumeKey));
+                editor.remove(resumeKey);
+            } else {
+                Log.d(
+                        TAG,
+                        String.format("Saving next page token: %s for key: %s", token, resumeKey));
+                editor.putString(resumeKey, token);
+            }
+            editor.apply();
+        }
+    }
+
+    /**
+     * Fetches the next page token given a resume key. Returns null if no NextPage token was saved.
+     *
+     * @param resumeKey The operation's resume key.
+     * @return The PageToken to resume from, or {@code null} if there is no operation to resume.
+     */
+    @Nullable
+    private String getPageTokenFromResumeKey(String resumeKey) throws UnableToAcquireLockException {
+        try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
+            return mSyncPrefs.getString(resumeKey, /* defValue= */ null);
+        }
+    }
+
+    private boolean resetCachedMediaCollectionInfo(@Nullable String authority, boolean isLocal)
+            throws UnableToAcquireLockException {
         return cacheMediaCollectionInfo(authority, isLocal, /* bundle */ null);
     }
 
@@ -874,7 +1184,7 @@
         final String collectionId = mSyncPrefs.getString(
                 getPrefsKey(isLocal, MEDIA_COLLECTION_ID), /* default */ null);
         final long generation = mSyncPrefs.getLong(
-                getPrefsKey(isLocal, LAST_MEDIA_SYNC_GENERATION), /* default */ -1);
+                getPrefsKey(isLocal, LAST_MEDIA_SYNC_GENERATION), DEFAULT_GENERATION);
 
         bundle.putString(MEDIA_COLLECTION_ID, collectionId);
         bundle.putLong(LAST_MEDIA_SYNC_GENERATION, generation);
@@ -882,20 +1192,37 @@
         return bundle;
     }
 
+    @NonNull
     private Bundle getLatestMediaCollectionInfo(String authority) {
-        return mContext.getContentResolver().call(getMediaCollectionInfoUri(authority),
-                CloudMediaProviderContract.METHOD_GET_MEDIA_COLLECTION_INFO, /* arg */ null,
-                /* extras */ null);
+        final InstanceId instanceId = NonUiEventLogger.generateInstanceId();
+        NonUiEventLogger.logPickerGetMediaCollectionInfoStart(instanceId, MY_UID, authority);
+        try {
+            Bundle result = mContext.getContentResolver().call(getMediaCollectionInfoUri(authority),
+                    CloudMediaProviderContract.METHOD_GET_MEDIA_COLLECTION_INFO, /* arg */ null,
+                    /* extras */ null);
+            return (result == null) ? (new Bundle()) : result;
+        } finally {
+            NonUiEventLogger.logPickerGetMediaCollectionInfoEnd(instanceId, MY_UID, authority);
+        }
+    }
+
+    private Bundle getDefaultGenerationCollectionInfo(@NonNull Bundle latestCollectionInfo) {
+        final Bundle bundle = new Bundle();
+        final String collectionId = latestCollectionInfo.getString(MEDIA_COLLECTION_ID);
+        bundle.putString(MEDIA_COLLECTION_ID, collectionId);
+        bundle.putLong(LAST_MEDIA_SYNC_GENERATION, DEFAULT_GENERATION);
+        return bundle;
     }
 
     @NonNull
     private SyncRequestParams getSyncRequestParams(@Nullable String authority,
-            boolean isLocal) throws RequestObsoleteException {
+            boolean isLocal) throws RequestObsoleteException, UnableToAcquireLockException {
         if (isLocal) {
             return getSyncRequestParamsInternal(authority, isLocal);
         } else {
             // Ensure that we are fetching sync request params for the current cloud provider.
-            synchronized (mCloudProviderLock) {
+            try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                    .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
                 if (Objects.equals(mCloudProviderInfo.authority, authority)) {
                     return getSyncRequestParamsInternal(authority, isLocal);
                 } else {
@@ -907,7 +1234,6 @@
         }
     }
 
-
     @NonNull
     private SyncRequestParams getSyncRequestParamsInternal(@Nullable String authority,
             boolean isLocal) {
@@ -943,6 +1269,8 @@
             }
 
             if (!Objects.equals(latestCollectionId, cachedCollectionId)) {
+                result = SyncRequestParams.forFullMediaWithReset(latestMediaCollectionInfo);
+            } else if (cachedGeneration == DEFAULT_GENERATION) {
                 result = SyncRequestParams.forFullMedia(latestMediaCollectionInfo);
             } else if (cachedGeneration == latestGeneration) {
                 result = SyncRequestParams.forNone();
@@ -964,42 +1292,290 @@
                 /* cancellationSignal */ null);
     }
 
-    private void executePagedSync(Uri uri, String expectedMediaCollectionId,
-            List<String> expectedHonoredArgs, Bundle queryArgs,
-            PickerDbFacade.DbWriteOperation dbWriteOperation) {
+    /**
+     * Creates a matching {@link PickerDbFacade.DbWriteOperation} for the given
+     * {@link OperationType}.
+     *
+     * @param op {@link OperationType} Which type of paged operation to begin.
+     * @param authority The authority string of the sync provider.
+     * @param albumId An {@link Nullable} AlbumId for album related operations.
+     * @throws IllegalArgumentException When an unexpected op type is encountered.
+     */
+    private PickerDbFacade.DbWriteOperation beginPagedOperation(
+            @OperationType int op, String authority, @Nullable String albumId)
+            throws IllegalArgumentException {
+        switch (op) {
+            case OPERATION_ADD_MEDIA:
+                return mDbFacade.beginAddMediaOperation(authority);
+            case OPERATION_ADD_ALBUM:
+                Objects.requireNonNull(
+                        albumId, "Cannot begin an AddAlbum operation without albumId");
+                return mDbFacade.beginAddAlbumMediaOperation(authority, albumId);
+            case OPERATION_REMOVE_MEDIA:
+                return mDbFacade.beginRemoveMediaOperation(authority);
+            default:
+                throw new IllegalArgumentException(
+                        "Cannot begin a paged operation without an expected operation type.");
+        }
+    }
+
+    /**
+     * Executes a page-by-page sync from the provider.
+     *
+     * @param uri The uri to query for a cursor.
+     * @param expectedMediaCollectionId The expected media collection id.
+     * @param expectedHonoredArgs The arguments that are expected to be present in cursors fetched
+     *     from the provider.
+     * @param queryArgs Any query arguments that are to be passed to the provider when fetching the
+     *     cursor.
+     * @param resumeKey The resumable operation key. This is used to check for previously failed
+     *     operations so they can be resumed at the last successful page, and also to save progress
+     *     between pages.
+     * @param op The DbWriteOperation type. {@link OperationType}
+     * @param authority The authority string of the provider to sync with.
+     * @param cancellationSignal CancellationSignal used to abort the sync.
+     * @throws RequestObsoleteException When the sync is interrupted due to the provider
+     *     changing.
+     * @return the total number of rows synced.
+     */
+    private int executePagedSync(
+            Uri uri,
+            String expectedMediaCollectionId,
+            List<String> expectedHonoredArgs,
+            Bundle queryArgs,
+            @Nullable String resumeKey,
+            @OperationType int op,
+            String authority,
+            Boolean isLocal,
+            @Nullable CancellationSignal cancellationSignal)
+            throws RequestObsoleteException, UnableToAcquireLockException {
+        return executePagedSync(
+                uri,
+                expectedMediaCollectionId,
+                expectedHonoredArgs,
+                queryArgs,
+                resumeKey,
+                op,
+                authority,
+                isLocal,
+                /* albumId=*/ null,
+                cancellationSignal);
+    }
+
+    /**
+     * Executes a page-by-page sync from the provider.
+     *
+     * @param uri The uri to query for a cursor.
+     * @param expectedMediaCollectionId The expected media collection id.
+     * @param expectedHonoredArgs The arguments that are expected to be present in cursors fetched
+     *     from the provider.
+     * @param queryArgs Any query arguments that are to be passed to the provider when fetching the
+     *     cursor.
+     * @param resumeKey The resumable operation key. This is used to check for previously failed
+     *     operations so they can be resumed at the last successful page, and also to save progress
+     *     between pages.
+     * @param op The DbWriteOperation type. {@link OperationType}
+     * @param authority The authority string of the provider to sync with.
+     * @param albumId A {@link Nullable} albumId for album related operations.
+     * @param cancellationSignal CancellationSignal used to abort the sync.
+     * @throws RequestObsoleteException When the sync is interrupted due to the provider
+     *     changing.
+     * @return the total number of rows synced.
+     */
+    private int executePagedSync(
+            Uri uri,
+            String expectedMediaCollectionId,
+            List<String> expectedHonoredArgs,
+            Bundle queryArgs,
+            @Nullable String resumeKey,
+            @OperationType int op,
+            String authority,
+            Boolean isLocal,
+            @Nullable String albumId,
+            @Nullable CancellationSignal cancellationSignal)
+            throws RequestObsoleteException, UnableToAcquireLockException {
         Trace.beginSection(traceSectionName("executePagedSync"));
+
         try {
-            int cursorCount = 0;
             int totalRowcount = 0;
             // Set to check the uniqueness of tokens across pages.
             Set<String> tokens = new ArraySet<>();
 
-            String nextPageToken = null;
+            String nextPageToken = getPageTokenFromResumeKey(resumeKey);
+            if (nextPageToken != null) {
+                Log.i(
+                        TAG,
+                        String.format(
+                                "Resumable operation found for %s, resuming with page token %s",
+                                resumeKey, nextPageToken));
+            }
+
             do {
+                // At the top of each loop check to see if we've received a CancellationSignal
+                // to stop the paged sync.
+                if (cancellationSignal != null && cancellationSignal.isCanceled()) {
+                    throw new RequestObsoleteException(
+                            "Aborting sync: cancellationSignal was received");
+                }
+
+                String updateDateTakenMs = null;
                 if (nextPageToken != null) {
                     queryArgs.putString(EXTRA_PAGE_TOKEN, nextPageToken);
                 }
 
                 try (Cursor cursor = query(uri, queryArgs)) {
-                    nextPageToken = validateCursor(cursor, expectedMediaCollectionId,
-                            expectedHonoredArgs, tokens);
+                    nextPageToken =
+                            validateCursor(
+                                    cursor, expectedMediaCollectionId, expectedHonoredArgs, tokens);
 
-                    int writeCount = dbWriteOperation.execute(cursor);
+                    try (PickerDbFacade.DbWriteOperation operation =
+                            beginPagedOperation(op, authority, albumId)) {
+                        int writeCount = operation.execute(cursor);
 
-                    totalRowcount += writeCount;
-                    cursorCount += cursor.getCount();
+                        if (!isLocal) {
+                            // Ensure the cloud provider hasn't change out from underneath the
+                            // running sync. If it has, we need to stop syncing.
+                            String currentCloudProvider = getCloudProviderWithTimeout();
+                            if (TextUtils.isEmpty(currentCloudProvider)
+                                    || !currentCloudProvider.equals(authority)) {
+
+                                throw new RequestObsoleteException(
+                                        String.format(
+                                                "Aborting sync: the CloudProvider seems to have"
+                                                        + " changed mid-sync. Old: %s Current: %s",
+                                                authority, currentCloudProvider));
+                            }
+                        }
+
+                        operation.setSuccess();
+                        totalRowcount += writeCount;
+
+                        if (cursor.getCount() > 0) {
+                            // Before the cursor is closed pull the date taken ms for the first row.
+                            updateDateTakenMs = getFirstDateTakenMsInCursor(cursor);
+
+                            // If the cursor count is not null and the date taken field is not
+                            // present in the cursor, fallback on the operation to provide the date
+                            // taken.
+                            if (updateDateTakenMs == null) {
+                                updateDateTakenMs = getFirstDateTakenMsFromOperation(operation);
+                            }
+                        }
+                    }
+                } catch (IllegalArgumentException ex) {
+                    Log.e(TAG, String.format("Failed to open DbWriteOperation for op: %d", op), ex);
+                    return -1;
                 }
+
+                // Keep track of the next page token in case this operation crashes and is
+                // later resumed.
+                rememberNextPageToken(nextPageToken, resumeKey);
+
+                // Emit notification that new data has arrived in the database.
+                if (updateDateTakenMs != null) {
+                    Uri notification = buildNotificationUri(op, albumId, updateDateTakenMs);
+
+                    if (notification != null) {
+                        mContext.getContentResolver()
+                                .notifyChange(/* itemUri= */ notification, /* observer= */ null);
+                    }
+                }
+
             } while (nextPageToken != null);
 
-            dbWriteOperation.setSuccess();
-            Log.i(TAG, "Paged sync successful. QueryArgs: " + queryArgs + ". Result count: "
-                    + totalRowcount + ". Cursor count: " + cursorCount);
+            Log.i(
+                    TAG,
+                    "Paged sync successful. QueryArgs: "
+                            + queryArgs
+                            + " Total Rows: "
+                            + totalRowcount);
+            return totalRowcount;
         } finally {
             Trace.endSection();
         }
     }
 
     /**
+     * Extracts the {@link MediaColumns.DATE_TAKEN_MILLIS} from the first row in the cursor.
+     *
+     * @param cursor The cursor to read from.
+     * @return Either the column value if it exists, or {@code null} if it doesn't.
+     */
+    @Nullable
+    private String getFirstDateTakenMsInCursor(Cursor cursor) {
+        if (cursor.moveToFirst()) {
+            return getCursorString(cursor, MediaColumns.DATE_TAKEN_MILLIS);
+        }
+        return null;
+    }
+
+    /**
+     * Extracts the first row's date taken from the operation. Note that all functions may not
+     * implement this method.
+     */
+    private String getFirstDateTakenMsFromOperation(PickerDbFacade.DbWriteOperation op) {
+        final long firstDateTakenMillis = op.getFirstDateTakenMillis();
+
+        return firstDateTakenMillis == Long.MIN_VALUE
+                ? null
+                : Long.toString(firstDateTakenMillis);
+    }
+
+    /**
+     * Assembles a ContentObserver notification uri for the given operation.
+     *
+     * @param op {@link OperationType} the operation to notify has completed.
+     * @param albumId An optional album id if this is an album based operation.
+     * @param dateTakenMs The notification data; the {@link MediaColumns.DATE_TAKEN_MILLIS} of the
+     *     first row updated.
+     * @return the assembled notification uri.
+     */
+    @Nullable
+    private Uri buildNotificationUri(
+            @NonNull @OperationType int op,
+            @Nullable String albumId,
+            @Nullable String dateTakenMs) {
+
+        Objects.requireNonNull(
+                dateTakenMs, "Cannot notify subscribers without a date taken timestamp.");
+
+        // base: content://media/picker_internal/
+        Uri.Builder builder = PICKER_INTERNAL_URI.buildUpon().appendPath(UPDATE);
+
+        switch (op) {
+            case OPERATION_ADD_MEDIA:
+                // content://media/picker_internal/update/media
+                builder.appendPath(MEDIA);
+                break;
+            case OPERATION_ADD_ALBUM:
+                // content://media/picker_internal/update/album_content/${albumId}
+                builder.appendPath(ALBUM_CONTENT);
+                builder.appendPath(albumId);
+                break;
+            case OPERATION_REMOVE_MEDIA:
+                if (albumId != null) {
+                    // content://media/picker_internal/update/album_content/${albumId}
+                    builder.appendPath(ALBUM_CONTENT);
+                    builder.appendPath(albumId);
+                } else {
+                    // content://media/picker_internal/update/media
+                    builder.appendPath(MEDIA);
+                }
+                break;
+            default:
+                Log.w(
+                        TAG,
+                        String.format(
+                                "Requested operation (%d) is not supported for notifications.",
+                                op));
+                return null;
+        }
+
+        builder.appendPath(dateTakenMs);
+        return builder.build();
+    }
+
+    /**
      * Get the default {@link CloudProviderInfo} at {@link PickerSyncController} construction
      */
     @VisibleForTesting
@@ -1094,16 +1670,21 @@
         final long syncGeneration;
         // Only valid for SYNC_TYPE_[INCREMENTAL|FULL]
         final Bundle latestMediaCollectionInfo;
+        // Only valid for sync triggered by opening photopicker activity.
+        // Not valid for proactive syncs.
+        final int mPageSize;
 
         SyncRequestParams(@SyncType int syncType) {
-            this(syncType, /* syncGeneration */ 0, /* latestMediaCollectionInfo */ null);
+            this(syncType, /* syncGeneration */ 0, /* latestMediaCollectionInfo */ null,
+                    /*pageSize */ PAGE_SIZE);
         }
 
         SyncRequestParams(@SyncType int syncType, long syncGeneration,
-                Bundle latestMediaCollectionInfo) {
+                Bundle latestMediaCollectionInfo, int pageSize) {
             this.syncType = syncType;
             this.syncGeneration = syncGeneration;
             this.latestMediaCollectionInfo = latestMediaCollectionInfo;
+            this.mPageSize = pageSize;
         }
 
         String getMediaCollectionId() {
@@ -1118,20 +1699,26 @@
             return SYNC_REQUEST_MEDIA_RESET;
         }
 
-        static SyncRequestParams forFullMedia(Bundle latestMediaCollectionInfo) {
+        static SyncRequestParams forFullMediaWithReset(@NonNull Bundle latestMediaCollectionInfo) {
+            return new SyncRequestParams(SYNC_TYPE_MEDIA_FULL_WITH_RESET, /* generation */ 0,
+                    latestMediaCollectionInfo, /*pageSize */ PAGE_SIZE);
+        }
+
+        static SyncRequestParams forFullMedia(@NonNull Bundle latestMediaCollectionInfo) {
             return new SyncRequestParams(SYNC_TYPE_MEDIA_FULL, /* generation */ 0,
-                    latestMediaCollectionInfo);
+                    latestMediaCollectionInfo, /*pageSize */ PAGE_SIZE);
         }
 
         static SyncRequestParams forIncremental(long generation, Bundle latestMediaCollectionInfo) {
             return new SyncRequestParams(SYNC_TYPE_MEDIA_INCREMENTAL, generation,
-                    latestMediaCollectionInfo);
+                    latestMediaCollectionInfo, /*pageSize */ PAGE_SIZE);
         }
 
         @Override
         public String toString() {
             return "SyncRequestParams{type=" + syncTypeToString(syncType)
-                    + ", gen=" + syncGeneration + ", latest=" + latestMediaCollectionInfo + '}';
+                    + ", gen=" + syncGeneration + ", latest=" + latestMediaCollectionInfo
+                    + ", pageSize=" + mPageSize + '}';
         }
     }
 
@@ -1145,6 +1732,8 @@
                 return "MEDIA_FULL";
             case SYNC_TYPE_MEDIA_RESET:
                 return "MEDIA_RESET";
+            case SYNC_TYPE_MEDIA_FULL_WITH_RESET:
+                return "MEDIA_FULL_WITH_RESET";
             default:
                 return "Unknown";
         }
@@ -1153,4 +1742,23 @@
     private static boolean isCloudProviderUnset(@Nullable String lastProviderAuthority) {
         return Objects.equals(lastProviderAuthority, PREFS_VALUE_CLOUD_PROVIDER_UNSET);
     }
+
+    /**
+     * Print the {@link PickerSyncController} state into the given stream.
+     */
+    public void dump(PrintWriter writer) {
+        writer.println("Picker sync controller state:");
+
+        writer.println("  mLocalProvider=" + getLocalProvider());
+        writer.println("  mCloudProviderInfo=" + getCurrentCloudProviderInfo());
+        writer.println("  allAvailableCloudProviders="
+                + CloudProviderUtils.getAllAvailableCloudProviders(mContext, mConfigStore));
+
+        writer.println("  cachedAuthority="
+                + mUserPrefs.getString(PREFS_KEY_CLOUD_PROVIDER_AUTHORITY, /* defValue */ null));
+        writer.println("  cachedLocalMediaCollectionInfo="
+                + getCachedMediaCollectionInfo(/* isLocal */ true));
+        writer.println("  cachedCloudMediaCollectionInfo="
+                + getCachedMediaCollectionInfo(/* isLocal */ false));
+    }
 }
diff --git a/src/com/android/providers/media/photopicker/SelectedMediaPreloader.java b/src/com/android/providers/media/photopicker/SelectedMediaPreloader.java
index 661345b..deefc1b 100644
--- a/src/com/android/providers/media/photopicker/SelectedMediaPreloader.java
+++ b/src/com/android/providers/media/photopicker/SelectedMediaPreloader.java
@@ -27,9 +27,11 @@
 import android.app.ProgressDialog;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.DialogInterface;
 import android.net.Uri;
 import android.os.Looper;
 import android.util.Log;
+import android.widget.Button;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -42,12 +44,18 @@
 import com.android.providers.media.R;
 
 import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * Responsible for "preloading" selected media items including showing the appropriate UI
@@ -56,9 +64,10 @@
  * @see #preload(Context, List)
  */
 class SelectedMediaPreloader {
+    private static final long TIMEOUT_IN_SECONDS = 4L;
     private static final String TRACE_SECTION_NAME = "preload-selected-media";
     private static final String TAG = "SelectedMediaPreloader";
-    private static final boolean DEBUG = false;
+    private static final boolean DEBUG = true;
 
     @Nullable
     private static volatile Executor sExecutor;
@@ -66,6 +75,7 @@
     @NonNull
     private final List<Uri> mItems;
     private final int mCount;
+    private boolean mIsPreloadingCancelled = false;
     @NonNull
     private final AtomicInteger mFinishedCount = new AtomicInteger(0);
     @NonNull
@@ -73,7 +83,14 @@
     @NonNull
     private final MutableLiveData<Boolean> mIsFinishedLiveData = new MutableLiveData<>(false);
     @NonNull
+    private static final MutableLiveData<Boolean> mIsPreloadingCancelledLiveData =
+            new MutableLiveData<>(false);
+    @NonNull
+    private final MutableLiveData<List<Integer>> mUnavailableMediaIndexes =
+            new MutableLiveData<>(new ArrayList<>());
+    @NonNull
     private final ContentResolver mContentResolver;
+    private List<Integer> mSuccessfullyPreloadedMediaIndexes = new ArrayList<>();
 
     /**
      * Creates, start and eventually returns a new {@link SelectedMediaPreloader} instance.
@@ -92,6 +109,7 @@
         // Make a copy of the list.
         final List<Uri> items = new ArrayList<>(requireNonNull(selectedMedia));
         final int count = items.size();
+        mIsPreloadingCancelledLiveData.setValue(false);
 
         Log.d(TAG, "preload() " + count + " items");
         if (DEBUG) {
@@ -107,24 +125,24 @@
 
         Trace.beginAsyncSection(TRACE_SECTION_NAME, /* cookie */ preloader.hashCode());
 
-        final var dialog = createProgressDialog(activity, items);
+        final var dialog = createProgressDialog(activity, items, context);
 
         preloader.mIsFinishedLiveData.observeForever(new Observer<>() {
             @Override
             public void onChanged(Boolean isFinished) {
                 if (isFinished) {
                     preloader.mIsFinishedLiveData.removeObserver(this);
-                    dialog.dismiss();
-
                     Trace.endAsyncSection(TRACE_SECTION_NAME, /* cookie */ preloader.hashCode());
                 }
             }
         });
+
         preloader.mFinishedCountLiveData.observeForever(new Observer<>() {
             @Override
             public void onChanged(Integer finishedCount) {
                 if (finishedCount == count) {
                     preloader.mFinishedCountLiveData.removeObserver(this);
+                    dialog.dismiss();
                 }
                 // "X of Y ready"
                 final String message = context.getString(
@@ -133,9 +151,31 @@
             }
         });
 
+        mIsPreloadingCancelledLiveData.observeForever(new Observer<>() {
+            @Override
+            public void onChanged(Boolean isPreloadingCancelled) {
+                if (isPreloadingCancelled) {
+                    preloader.mIsPreloadingCancelled = true;
+                    mIsPreloadingCancelledLiveData.removeObserver(this);
+                    List<Integer> unsuccessfullyPreloadedMediaIndexes = new ArrayList<>();
+                    for (int index = 0; index < preloader.mItems.size(); index++) {
+                        if (!preloader.mSuccessfullyPreloadedMediaIndexes.contains(index)) {
+                            unsuccessfullyPreloadedMediaIndexes.add(index);
+                        }
+                    }
+                    // this extra "-1" element indicates that preloading has been cancelled by
+                    // the user
+                    unsuccessfullyPreloadedMediaIndexes.add(-1);
+                    preloader.mUnavailableMediaIndexes.setValue(
+                            unsuccessfullyPreloadedMediaIndexes);
+                    preloader.mIsFinishedLiveData.setValue(false);
+                    preloader.mFinishedCountLiveData.setValue(preloader.mItems.size());
+                }
+            }
+        });
+
         ensureExecutor();
         preloader.start(sExecutor);
-
         return preloader;
     }
 
@@ -155,27 +195,49 @@
         return mIsFinishedLiveData;
     }
 
+    @NonNull
+    LiveData<List<Integer>> getUnavailableMediaIndexes() {
+        return mUnavailableMediaIndexes;
+    }
+
     /**
      * This method is intentionally {@code private}: clients should use static
      * {@link #preload(Context, List)} method.
      */
     @UiThread
     private void start(@NonNull Executor executor) {
-        for (var item : mItems) {
+        List<Integer> unavailableMediaIndexes = new ArrayList<>();
+        for (int index = 0; index < mItems.size(); index++) {
+            int currIndex = index;
             // Off-loading to an Executor (presumable backed up by a thread pool)
             executor.execute(new Runnable() {
                 @Override
                 public void run() {
-                    openFileDescriptor(item);
+                    boolean isOpenedSuccessfully = false;
+                    if (!mIsPreloadingCancelled) {
+                        isOpenedSuccessfully = openFileDescriptor(mItems.get(currIndex));
+                    }
+
+                    if (!isOpenedSuccessfully) {
+                        unavailableMediaIndexes.add(currIndex);
+                    } else {
+                        mSuccessfullyPreloadedMediaIndexes.add(currIndex);
+                    }
 
                     final int preloadedCount = mFinishedCount.incrementAndGet();
                     if (DEBUG) {
                         Log.d(TAG, "Preloaded " + preloadedCount + " (of " + mCount + ") items");
                     }
-                    if (preloadedCount == mCount) {
+
+                    if (preloadedCount == mCount && !mIsPreloadingCancelled) {
                         // Don't need to "synchronize" here: mCount is our final value for
                         // preloadedCount, it won't be changing anymore.
-                        mIsFinishedLiveData.postValue(true);
+                        if (unavailableMediaIndexes.size() == 0) {
+                            mIsFinishedLiveData.postValue(true);
+                        } else {
+                            mUnavailableMediaIndexes.postValue(unavailableMediaIndexes);
+                            mIsFinishedLiveData.postValue(false);
+                        }
                     }
 
                     // In order to prevent race conditions where we may "post" a lower value after
@@ -190,7 +252,8 @@
     }
 
     @Nullable
-    private void openFileDescriptor(@NonNull Uri uri) {
+    private Boolean openFileDescriptor(@NonNull Uri uri) {
+        AtomicReference<Boolean> isOpenedSuccessfully = new AtomicReference<>(true);
         long start = 0;
         if (DEBUG) {
             Log.d(TAG, "openFileDescriptor() START, " + Thread.currentThread() + ", " + uri);
@@ -198,10 +261,24 @@
         }
 
         Trace.beginSection("Preloader.openFd");
+
+        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
+            try {
+                mContentResolver.openAssetFileDescriptor(uri, "r").close();
+            } catch (FileNotFoundException e) {
+                isOpenedSuccessfully.set(false);
+                Log.w(TAG, "Could not open FileDescriptor for " + uri, e);
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to preload media file ", e);
+            }
+        });
+
         try {
-            mContentResolver.openAssetFileDescriptor(uri, "r");
-        } catch (FileNotFoundException e) {
-            Log.w(TAG, "Could not open FileDescriptor for " + uri, e);
+            future.get(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+        } catch (TimeoutException e) {
+            return isOpenedSuccessfully.get();
+        } catch (InterruptedException | ExecutionException e) {
+            Log.w(TAG, "Could not preload the media item ", e);
         } finally {
             Trace.endSection();
 
@@ -211,22 +288,43 @@
                         + ", " + uri);
             }
         }
+
+        return isOpenedSuccessfully.get();
     }
 
     @NonNull
     private static AlertDialog createProgressDialog(
-            @NonNull Activity activity, @NonNull List<Uri> selectedMedia) {
-        return ProgressDialog.show(activity,
-                /* tile */ "Preparing your selected media",
-                /* message */ "0 of " + selectedMedia.size() + " ready.",
-                /* indeterminate */ true);
+            @NonNull Activity activity, @NonNull List<Uri> selectedMedia, Context context) {
+        ProgressDialog dialog = new ProgressDialog(activity,
+                R.style.SelectedMediaPreloaderDialogTheme);
+        dialog.setTitle(/* title */ context.getString(R.string.preloading_dialog_title));
+        dialog.setMessage(/* message */ context.getString(
+                R.string.preloading_progress_message, 0, selectedMedia.size()));
+        dialog.setIndeterminate(/* indeterminate */ true);
+        dialog.setCancelable(false);
+
+        dialog.setButton(DialogInterface.BUTTON_NEGATIVE,
+                context.getString(R.string.preloading_cancel_button), (dialog1, which) -> {
+                mIsPreloadingCancelledLiveData.setValue(true);
+            });
+        dialog.create();
+
+        Button cancelButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
+        if (cancelButton != null) {
+            cancelButton.setTextAppearance(R.style.ProgressDialogCancelButtonStyle);
+            cancelButton.setAllCaps(false);
+        }
+
+        dialog.show();
+
+        return dialog;
     }
 
     private static void ensureExecutor() {
         if (sExecutor == null) {
             synchronized (SelectedMediaPreloader.class) {
                 if (sExecutor == null) {
-                    final ThreadFactory threadFactory = new ThreadFactory() {
+                    sExecutor = Executors.newFixedThreadPool(2, new ThreadFactory() {
 
                         final AtomicInteger mCount = new AtomicInteger(1);
 
@@ -250,8 +348,7 @@
                                 }
                             };
                         }
-                    };
-                    sExecutor = Executors.newCachedThreadPool(threadFactory);
+                    });
                 }
             }
         }
diff --git a/src/com/android/providers/media/photopicker/TEST_MAPPING b/src/com/android/providers/media/photopicker/TEST_MAPPING
new file mode 100644
index 0000000..53545d8
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/TEST_MAPPING
@@ -0,0 +1,22 @@
+{
+  "mainline-presubmit": [
+    {
+      "name": "CtsPhotoPickerTest[com.google.android.mediaprovider.apex]",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.LargeTest"
+        }
+      ]
+    }
+  ],
+  "presubmit": [
+    {
+      "name": "CtsPhotoPickerTest",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.LargeTest"
+        }
+      ]
+    }
+  ]
+}
diff --git a/src/com/android/providers/media/photopicker/data/CloudProviderQueryExtras.java b/src/com/android/providers/media/photopicker/data/CloudProviderQueryExtras.java
index f05d3a4..4dcd6f7 100644
--- a/src/com/android/providers/media/photopicker/data/CloudProviderQueryExtras.java
+++ b/src/com/android/providers/media/photopicker/data/CloudProviderQueryExtras.java
@@ -17,8 +17,13 @@
 
 import static android.content.ContentResolver.QUERY_ARG_LIMIT;
 
+import static com.android.providers.media.photopicker.PickerDataLayer.QUERY_DATE_TAKEN_BEFORE_MS;
+import static com.android.providers.media.photopicker.PickerDataLayer.QUERY_LOCAL_ID_SELECTION;
+import static com.android.providers.media.photopicker.PickerDataLayer.QUERY_ROW_ID;
 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.BOOLEAN_DEFAULT;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.INT_DEFAULT;
 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LIMIT_DEFAULT;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LIST_DEFAULT;
 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LONG_DEFAULT;
 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_ARRAY_DEFAULT;
 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_DEFAULT;
@@ -31,6 +36,8 @@
 
 import com.android.providers.media.photopicker.PickerDataLayer;
 
+import java.util.List;
+
 /**
  * Represents the {@link CloudMediaProviderContract} extra filters from a {@link Bundle}.
  */
@@ -44,6 +51,13 @@
     private final boolean mIsFavorite;
     private final boolean mIsVideo;
     private final boolean mIsLocalOnly;
+    private final int mPageSize;
+    private final long mDateTakenBeforeMs;
+    private final int mRowId;
+
+    private final List<Integer> mLocalIdSelection;
+
+    private String mPageToken;
 
     private CloudProviderQueryExtras() {
         mAlbumId = STRING_DEFAULT;
@@ -55,11 +69,17 @@
         mIsFavorite = BOOLEAN_DEFAULT;
         mIsVideo = BOOLEAN_DEFAULT;
         mIsLocalOnly = BOOLEAN_DEFAULT;
+        mPageSize = INT_DEFAULT;
+        mDateTakenBeforeMs = Long.MIN_VALUE;
+        mRowId = INT_DEFAULT;
+        mLocalIdSelection = LIST_DEFAULT;
+        mPageToken = STRING_DEFAULT;
     }
 
     private CloudProviderQueryExtras(String albumId, String albumAuthority, String[] mimeTypes,
             long sizeBytes, long generation, int limit, boolean isFavorite, boolean isVideo,
-            boolean isLocalOnly) {
+            boolean isLocalOnly, int pageSize, long dateTakenBeforeMs, int rowId,
+            List<Integer> localIdSelection, String pageToken) {
         mAlbumId = albumId;
         mAlbumAuthority = albumAuthority;
         mMimeTypes = mimeTypes;
@@ -69,6 +89,11 @@
         mIsFavorite = isFavorite;
         mIsVideo = isVideo;
         mIsLocalOnly = isLocalOnly;
+        mPageSize = pageSize;
+        mDateTakenBeforeMs = dateTakenBeforeMs;
+        mRowId = rowId;
+        mLocalIdSelection = localIdSelection;
+        mPageToken = pageToken;
     }
 
     /**
@@ -88,14 +113,21 @@
         final long generation = LONG_DEFAULT;
         final int limit = bundle.getInt(QUERY_ARG_LIMIT, LIMIT_DEFAULT);
 
-        final boolean isFavorite = AlbumColumns.ALBUM_ID_FAVORITES.equals(albumId);
-        final boolean isVideo = AlbumColumns.ALBUM_ID_VIDEOS.equals(albumId);
+        final boolean isFavorite = isFavorite(albumId);
+        final boolean isVideo = isVideo(albumId);
 
         final boolean isLocalOnly = bundle.getBoolean(PickerDataLayer.QUERY_ARG_LOCAL_ONLY,
                 BOOLEAN_DEFAULT);
+        final int pageSize = INT_DEFAULT;
+        final long dateTakenBeforeMs = bundle.getLong(QUERY_DATE_TAKEN_BEFORE_MS, Long.MIN_VALUE);
+        final int rowId = bundle.getInt(QUERY_ROW_ID, INT_DEFAULT);
+        final List<Integer> localIdSelection = bundle.getIntegerArrayList(QUERY_LOCAL_ID_SELECTION);
+        final String pageToken = bundle.getString(
+                CloudMediaProviderContract.EXTRA_PAGE_TOKEN, STRING_DEFAULT);
 
         return new CloudProviderQueryExtras(albumId, albumAuthority, mimeTypes, sizeBytes,
-                generation, limit, isFavorite, isVideo, isLocalOnly);
+                generation, limit, isFavorite, isVideo, isLocalOnly, pageSize, dateTakenBeforeMs,
+                rowId, localIdSelection, pageToken);
     }
 
     public static CloudProviderQueryExtras fromCloudMediaBundle(Bundle bundle) {
@@ -117,9 +149,17 @@
         final boolean isFavorite = BOOLEAN_DEFAULT;
         final boolean isVideo = BOOLEAN_DEFAULT;
         final boolean isLocalOnly = BOOLEAN_DEFAULT;
+        final long dateTakenBeforeMs = bundle.getLong(QUERY_DATE_TAKEN_BEFORE_MS, Long.MIN_VALUE);
+        final int rowId = bundle.getInt(QUERY_ROW_ID, INT_DEFAULT);
+
+        final int pageSize = bundle.getInt(CloudMediaProviderContract.EXTRA_PAGE_SIZE, INT_DEFAULT);
+        final List<Integer> localIdSelection = bundle.getIntegerArrayList(QUERY_LOCAL_ID_SELECTION);
+        final String pageToken = bundle.getString(
+                CloudMediaProviderContract.EXTRA_PAGE_TOKEN, STRING_DEFAULT);
 
         return new CloudProviderQueryExtras(albumId, albumAuthority, mimeTypes, sizeBytes,
-                generation, limit, isFavorite, isVideo, isLocalOnly);
+                generation, limit, isFavorite, isVideo, isLocalOnly, pageSize, dateTakenBeforeMs,
+                rowId, localIdSelection, pageToken);
     }
 
     public PickerDbFacade.QueryFilter toQueryFilter() {
@@ -130,6 +170,11 @@
         qfb.setIsVideo(mIsVideo);
         qfb.setAlbumId(mAlbumId);
         qfb.setIsLocalOnly(mIsLocalOnly);
+        qfb.setDateTakenBeforeMs(mDateTakenBeforeMs);
+        qfb.setId(mRowId);
+        qfb.setLocalIdSelection(mLocalIdSelection);
+        qfb.setPageSize(mPageSize);
+        qfb.setPageToken(mPageToken);
         return qfb.build();
     }
 
@@ -142,6 +187,28 @@
         return extras;
     }
 
+    /**
+     * Checks if the query is for a merged album type.
+     */
+    public boolean isMergedAlbum() {
+        return mIsFavorite || mIsVideo;
+    }
+
+    private static boolean isFavorite(String albumId) {
+        return AlbumColumns.ALBUM_ID_FAVORITES.equals(albumId);
+    }
+
+    private static boolean isVideo(String albumId) {
+        return AlbumColumns.ALBUM_ID_VIDEOS.equals(albumId);
+    }
+
+    /**
+     * Checks if the given albumID belongs to a merged album type.
+     */
+    public static boolean isMergedAlbum(String albumId) {
+        return isFavorite(albumId) || isVideo(albumId);
+    }
+
     public String getAlbumId() {
         return mAlbumId;
     }
@@ -173,4 +240,12 @@
     public boolean isLocalOnly() {
         return mIsLocalOnly;
     }
+
+    public int getPageSize() {
+        return mPageSize;
+    }
+
+    public String getPageToken() {
+        return mPageToken;
+    }
 }
diff --git a/src/com/android/providers/media/photopicker/data/ExternalDbFacade.java b/src/com/android/providers/media/photopicker/data/ExternalDbFacade.java
index d4e7fb1..c4361ba 100644
--- a/src/com/android/providers/media/photopicker/data/ExternalDbFacade.java
+++ b/src/com/android/providers/media/photopicker/data/ExternalDbFacade.java
@@ -23,9 +23,12 @@
 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS;
 import static android.provider.CloudMediaProviderContract.EXTRA_ALBUM_ID;
 import static android.provider.CloudMediaProviderContract.EXTRA_MEDIA_COLLECTION_ID;
+import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_SIZE;
+import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_TOKEN;
 import static android.provider.CloudMediaProviderContract.EXTRA_SYNC_GENERATION;
 import static android.provider.CloudMediaProviderContract.MediaCollectionInfo;
 
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.INT_DEFAULT;
 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LONG_DEFAULT;
 import static com.android.providers.media.photopicker.data.PickerDbFacade.addMimeTypesToQueryBuilderAndSelectionArgs;
 import static com.android.providers.media.photopicker.util.CursorUtils.getCursorLong;
@@ -117,6 +120,13 @@
     private static final String WHERE_RELATIVE_PATH = MediaStore.MediaColumns.RELATIVE_PATH
             + " LIKE ?";
 
+    private static final String WHERE_DATE_TAKEN_MILLIS_BEFORE =
+            String.format("(%s < CAST(? AS INT) OR (%s = CAST(? AS INT) AND %s < CAST(? AS INT)))",
+                    CloudMediaProviderContract.MediaColumns.DATE_TAKEN_MILLIS,
+                    CloudMediaProviderContract.MediaColumns.DATE_TAKEN_MILLIS,
+                    MediaColumns._ID);
+
+
     /* Include any directory named exactly {@link Environment.DIRECTORY_SCREENSHOTS}
      * and its child directories. */
     private static final String WHERE_RELATIVE_PATH_IS_SCREENSHOT_DIR =
@@ -275,7 +285,8 @@
                     /* having */ null, /* orderBy */ null);
          });
 
-        cursor.setExtras(getCursorExtras(generation, /* albumId */ null));
+        cursor.setExtras(getCursorExtras(generation, /* albumId */ null, /*pageSize*/ -1,
+                /*pageToken*/ null));
         return cursor;
     }
 
@@ -283,27 +294,85 @@
      * Returns all items from the files table where {@link MediaColumns#GENERATION_MODIFIED}
      * is greater than {@code generation}.
      */
-    public Cursor queryMedia(long generation, String albumId, String[] mimeTypes) {
+    public Cursor queryMedia(long generation, String albumId, String[] mimeTypes, int pageSize,
+            String pageToken) {
         final List<String> selectionArgs = new ArrayList<>();
-        final String orderBy = CloudMediaProviderContract.MediaColumns.DATE_TAKEN_MILLIS + " DESC";
+        final String orderBy = getOrderByClause();
+
+        Log.d(TAG, "Token received for queryMedia = " + pageToken);
 
         final Cursor cursor = mDatabaseHelper.runWithTransaction(db -> {
-                SQLiteQueryBuilder qb = createMediaQueryBuilder();
-                qb.appendWhereStandalone(WHERE_GREATER_GENERATION);
-                selectionArgs.add(String.valueOf(generation));
+            SQLiteQueryBuilder qb = createMediaQueryBuilder();
+            qb.appendWhereStandalone(WHERE_GREATER_GENERATION);
+            selectionArgs.add(String.valueOf(generation));
 
-                selectionArgs.addAll(appendWhere(qb, albumId, mimeTypes));
+            if (pageToken != null) {
+                String[] lastMedia = parsePageToken(pageToken);
+                if (lastMedia != null) {
+                    qb.appendWhereStandalone(getDateTakenWhereClause());
+                    addSelectionArgsForWhereClause(lastMedia, selectionArgs);
+                }
+            }
 
-                return qb.query(db, PROJECTION_MEDIA_COLUMNS, /* select */ null,
-                        selectionArgs.toArray(new String[selectionArgs.size()]), /* groupBy */ null,
-                        /* having */ null, orderBy);
-            });
+            selectionArgs.addAll(appendWhere(qb, albumId, mimeTypes));
 
-        cursor.setExtras(getCursorExtras(generation, albumId));
+            return qb.query(db, PROJECTION_MEDIA_COLUMNS, /* select */ null,
+                    selectionArgs.toArray(new String[selectionArgs.size()]), /* groupBy */ null,
+                    /* having */ null, orderBy, String.valueOf(pageSize));
+        });
+
+        String nextPageToken = null;
+        if (cursor.getCount() > 0 && pageSize != INT_DEFAULT) {
+            nextPageToken = setPageToken(cursor);
+
+        }
+        cursor.setExtras(getCursorExtras(generation, albumId, pageSize, nextPageToken));
         return cursor;
     }
 
-    private Bundle getCursorExtras(long generation, String albumId) {
+    private static void addSelectionArgsForWhereClause(String[] lastMedia,
+            List<String> selectionArgs) {
+        selectionArgs.add(lastMedia[0]);
+        selectionArgs.add(lastMedia[0]);
+        selectionArgs.add(lastMedia[1]);
+    }
+
+    private static String[] parsePageToken(String pageToken) {
+        String[] lastMedia = pageToken.split("\\|");
+
+        if (lastMedia.length != 2) {
+            Log.w(TAG, "Error parsing token in queryMedia.");
+            return null;
+        }
+        return lastMedia;
+    }
+
+    private static String getDateTakenWhereClause() {
+        return CloudMediaProviderContract.MediaColumns.DATE_TAKEN_MILLIS + " IS NOT NULL AND "
+                + WHERE_DATE_TAKEN_MILLIS_BEFORE;
+    }
+
+    private static String getOrderByClause() {
+        return CloudMediaProviderContract.MediaColumns.DATE_TAKEN_MILLIS + " DESC,"
+                + CloudMediaProviderContract.MediaColumns.ID + " DESC";
+    }
+
+
+    private String setPageToken(Cursor mediaList) {
+        String token = null;
+        if (mediaList.moveToLast()) {
+            String timeTakenMillis = getCursorString(mediaList,
+                    CloudMediaProviderContract.MediaColumns.DATE_TAKEN_MILLIS);
+            String lastItemRowId = getCursorString(mediaList,
+                    CloudMediaProviderContract.MediaColumns.ID);
+            token = timeTakenMillis + "|" + lastItemRowId;
+            mediaList.moveToFirst();
+        }
+        return token;
+    }
+
+    private Bundle getCursorExtras(long generation, String albumId, int pageSize,
+            String pageToken) {
         final Bundle bundle = new Bundle();
         final ArrayList<String> honoredArgs = new ArrayList<>();
 
@@ -314,7 +383,18 @@
             honoredArgs.add(EXTRA_ALBUM_ID);
         }
 
+        if (pageSize > INT_DEFAULT) {
+            honoredArgs.add(EXTRA_PAGE_SIZE);
+        }
+
+        if (pageToken != null) {
+            honoredArgs.add(EXTRA_PAGE_TOKEN);
+        }
+
         bundle.putString(EXTRA_MEDIA_COLLECTION_ID, getMediaCollectionId());
+        if (pageToken != null) {
+            bundle.putString(EXTRA_PAGE_TOKEN, pageToken);
+        }
         bundle.putStringArrayList(EXTRA_HONORED_ARGS, honoredArgs);
 
         return bundle;
@@ -472,6 +552,10 @@
         qb.appendWhereStandalone(WHERE_NOT_TRASHED);
         qb.appendWhereStandalone(WHERE_NOT_PENDING);
 
+        // the file is corrupted if both datetaken and takenmodified are null.
+        // hence exclude those files.
+        qb.appendWhereStandalone(getDateTakenOrDateModifiedNonNull());
+
         String[] volumes = getVolumeList();
         if (volumes.length > 0) {
             qb.appendWhereStandalone(buildWhereVolumeIn(volumes));
@@ -480,6 +564,11 @@
         return qb;
     }
 
+    private CharSequence getDateTakenOrDateModifiedNonNull() {
+        return MediaColumns.DATE_TAKEN + " IS NOT NULL OR "
+                + MediaColumns.DATE_MODIFIED + " IS NOT NULL";
+    }
+
     private String buildWhereVolumeIn(String[] volumes) {
         return String.format(WHERE_VOLUME_IN_PREFIX, bindList((Object[]) volumes));
     }
diff --git a/src/com/android/providers/media/photopicker/data/ItemsProvider.java b/src/com/android/providers/media/photopicker/data/ItemsProvider.java
index 84a5356..6e58395 100644
--- a/src/com/android/providers/media/photopicker/data/ItemsProvider.java
+++ b/src/com/android/providers/media/photopicker/data/ItemsProvider.java
@@ -18,21 +18,27 @@
 
 import static android.content.ContentResolver.QUERY_ARG_LIMIT;
 import static android.database.DatabaseUtils.dumpCursorToString;
-import static android.widget.Toast.LENGTH_LONG;
+import static android.provider.MediaStore.AUTHORITY;
+import static android.provider.MediaStore.MediaColumns.DATA;
 
+import static com.android.providers.media.MediaGrants.FILE_ID_COLUMN;
 import static com.android.providers.media.PickerUriResolver.PICKER_INTERNAL_URI;
+import static com.android.providers.media.photopicker.PickerDataLayer.QUERY_DATE_TAKEN_BEFORE_MS;
+import static com.android.providers.media.photopicker.PickerDataLayer.QUERY_LOCAL_ID_SELECTION;
+import static com.android.providers.media.photopicker.PickerDataLayer.QUERY_ROW_ID;
+import static com.android.providers.media.photopicker.util.CloudProviderUtils.sendInitPhotoPickerDataNotification;
+import static com.android.providers.media.util.FileUtils.getContentUriForPath;
 
 import android.content.ContentProvider;
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
+import android.os.CancellationSignal;
 import android.os.RemoteException;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -40,32 +46,32 @@
 import android.provider.MediaStore;
 import android.text.TextUtils;
 import android.util.Log;
-import android.widget.Toast;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.modules.utils.build.SdkLevel;
 import com.android.providers.media.PickerUriResolver;
-import com.android.providers.media.photopicker.PickerSyncController;
 import com.android.providers.media.photopicker.data.model.Category;
 import com.android.providers.media.photopicker.data.model.UserId;
 
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
 
 /**
  * Provides image and video items from {@link MediaStore} collection to the Photo Picker.
  */
 public class ItemsProvider {
     private static final String TAG = ItemsProvider.class.getSimpleName();
-    private static final boolean DEBUG = false;
+    private static final boolean DEBUG = true;
     private static final boolean DEBUG_DUMP_CURSORS = false;
 
     private final Context mContext;
 
     public ItemsProvider(Context context) {
         mContext = context;
-        ensureNotificationHandler(context);
     }
 
     private static final Uri URI_MEDIA_ALL;
@@ -73,6 +79,10 @@
     private static final Uri URI_ALBUMS_ALL;
     private static final Uri URI_ALBUMS_LOCAL;
 
+    private static final String MEDIA_GRANTS_URI_PATH = "content://media/media_grants";
+    public static final String EXTRA_MIME_TYPE_SELECTION = "media_grant_mime_type_selection";
+
+
     static {
         final Uri media = PICKER_INTERNAL_URI.buildUpon()
                 .appendPath(PickerUriResolver.MEDIA_PATH).build();
@@ -92,9 +102,10 @@
      * <p>
      * By default, the returned {@link Cursor} sorts by latest date taken.
      *
-     * @param category the category of items to return. May be cloud, local or merged albums like
-     * favorites or videos.
-     * @param limit the limit of number of items to return.
+     * @param category  the category of items to return. May be cloud, local or merged albums like
+     *                  favorites or videos.
+     * @param pagingParameters parameters to represent the page for which the items need to be
+     *                            returned.
      * @param mimeTypes the mime type of item. {@code null} returns all images/videos that are
      *                 scanned by {@link MediaStore}.
      * @param userId the {@link UserId} of the user to get items as.
@@ -106,21 +117,15 @@
      * contains {@link android.provider.CloudMediaProviderContract.MediaColumns}
      */
     @Nullable
-    public Cursor getAllItems(Category category, int limit, @Nullable String[] mimeTypes,
-            @Nullable UserId userId) throws IllegalArgumentException {
-        if (DEBUG) {
-            Log.d(TAG, "getAllItems() userId=" + userId + " cat=" + category
-                    + " mimeTypes=" + Arrays.toString(mimeTypes) + " limit=" + limit);
-            Log.v(TAG, "Thread=" + Thread.currentThread() + "; Stacktrace:", new Throwable());
-        }
-
+    public Cursor getAllItems(Category category, PaginationParameters pagingParameters,
+            @Nullable String[] mimeTypes,
+            @Nullable UserId userId,
+            @Nullable CancellationSignal cancellationSignal) throws IllegalArgumentException {
         Trace.beginSection("ItemsProvider.getAllItems");
         try {
-            sNotificationHandler.onLoadingStarted();
-
-            return queryMedia(URI_MEDIA_ALL, limit, mimeTypes, category, userId);
+            return queryMedia(URI_MEDIA_ALL, pagingParameters, mimeTypes, category, userId,
+                    cancellationSignal);
         } finally {
-            sNotificationHandler.onLoadingFinished();
             Trace.endSection();
         }
     }
@@ -132,9 +137,10 @@
      * <p>
      * By default, the returned {@link Cursor} sorts by latest date taken.
      *
-     * @param category the category of items to return. May be local or merged albums like
-     * favorites or videos.
-     * @param limit the limit of number of items to return.
+     * @param category  the category of items to return. May be local or merged albums like
+     *                  favorites or videos.
+     * @param pagingParameters parameters to represent the page for which the items need to be
+     *                            returned.
      * @param mimeTypes the mime type of item. {@code null} returns all images/videos that are
      *                 scanned by {@link MediaStore}.
      * @param userId the {@link UserId} of the user to get items as.
@@ -149,23 +155,40 @@
      * this method is called with a non-local album.
      */
     @Nullable
-    public Cursor getLocalItems(Category category, int limit, @Nullable String[] mimeTypes,
-            @Nullable UserId userId) throws IllegalArgumentException {
-        if (DEBUG) {
-            Log.d(TAG, "getLocalItems() userId=" + userId + " cat=" + category
-                    + " mimeTypes=" + Arrays.toString(mimeTypes) + " limit=" + limit);
-            Log.v(TAG, "Thread=" + Thread.currentThread() + "; Stacktrace:", new Throwable());
-        }
-
+    public Cursor getLocalItems(Category category, PaginationParameters pagingParameters,
+            @Nullable String[] mimeTypes,
+            @Nullable UserId userId,
+            @Nullable CancellationSignal cancellationSignal) throws IllegalArgumentException {
         Trace.beginSection("ItemsProvider.getLocalItems");
         try {
-            return queryMedia(URI_MEDIA_LOCAL, limit, mimeTypes, category, userId);
+            return queryMedia(URI_MEDIA_LOCAL, pagingParameters, mimeTypes, category, userId,
+                    cancellationSignal);
         } finally {
             Trace.endSection();
         }
     }
 
     /**
+     * Gets cursor for items corresponding to the ids passed as an argument.
+     *
+     * @param category  the category of items to return.
+     * @param mimeTypes the mime type of item. {@code null} returns all images/videos that are
+     *                 scanned by {@link MediaStore}.
+     * @param userId the {@link UserId} of the user to get items as.
+     *               {@code null} defaults to {@link UserId#CURRENT_USER}
+     * @param localIdSelection list of ids for which the item objects are required
+     */
+    public Cursor getLocalItemsForSelection(Category category,
+            @NonNull List<Integer> localIdSelection,
+            @Nullable String[] mimeTypes,
+            @Nullable UserId userId,
+            @Nullable CancellationSignal cancellationSignal) throws IllegalArgumentException {
+        Objects.requireNonNull(localIdSelection);
+        return queryMedia(URI_MEDIA_LOCAL, new PaginationParameters(), mimeTypes, category, userId,
+                localIdSelection, cancellationSignal);
+    }
+
+    /**
      * Returns a {@link Cursor} to all non-empty categories in which images/videos are categorised.
      * This includes:
      * * A constant list of local categories for on-device images/videos: {@link Category}
@@ -180,20 +203,12 @@
       * in the relative order.
      */
     @Nullable
-    public Cursor getAllCategories(@Nullable String[] mimeTypes, @Nullable UserId userId) {
-        if (DEBUG) {
-            Log.d(TAG, "getAllCategories() userId=" + userId
-                    + " mimeTypes=" + Arrays.toString(mimeTypes));
-            Log.v(TAG, "Thread=" + Thread.currentThread() + "; Stacktrace:", new Throwable());
-        }
-
+    public Cursor getAllCategories(@Nullable String[] mimeTypes, @Nullable UserId userId,
+            @Nullable CancellationSignal cancellationSignal) {
         Trace.beginSection("ItemsProvider.getAllCategories");
         try {
-            sNotificationHandler.onLoadingStarted();
-
-            return queryAlbums(URI_ALBUMS_ALL, mimeTypes, userId);
+            return queryAlbums(URI_ALBUMS_ALL, mimeTypes, userId, cancellationSignal);
         } finally {
-            sNotificationHandler.onLoadingFinished();
             Trace.endSection();
         }
     }
@@ -211,32 +226,41 @@
      * in the relative order.
      */
     @Nullable
-    public Cursor getLocalCategories(@Nullable String[] mimeTypes, @Nullable UserId userId) {
-        if (DEBUG) {
-            Log.d(TAG, "getLocalCategories() userId=" + userId
-                    + " mimeTypes=" + Arrays.toString(mimeTypes));
-            Log.v(TAG, "Thread=" + Thread.currentThread() + "; Stacktrace:", new Throwable());
-        }
-
+    public Cursor getLocalCategories(@Nullable String[] mimeTypes, @Nullable UserId userId,
+            @Nullable CancellationSignal cancellationSignal) {
         Trace.beginSection("ItemsProvider.getLocalCategories");
         try {
-            return queryAlbums(URI_ALBUMS_LOCAL, mimeTypes, userId);
+            return queryAlbums(URI_ALBUMS_LOCAL, mimeTypes, userId, cancellationSignal);
         } finally {
             Trace.endSection();
         }
     }
 
     @Nullable
-    private Cursor queryMedia(@NonNull Uri uri, int limit, String[] mimeTypes,
-            @NonNull Category category, @Nullable UserId userId) throws IllegalStateException {
+    private Cursor queryMedia(@NonNull Uri uri, PaginationParameters paginationParameters,
+            String[] mimeTypes, @NonNull Category category, @Nullable UserId userId,
+            @Nullable CancellationSignal cancellationSignal) {
+        return queryMedia(uri, paginationParameters, mimeTypes, category, userId, null,
+                cancellationSignal);
+    }
+
+    @Nullable
+    private Cursor queryMedia(@NonNull Uri uri, PaginationParameters paginationParameters,
+            String[] mimeTypes, @NonNull Category category, @Nullable UserId userId,
+            List<Integer> localIdSelection,
+            @Nullable CancellationSignal cancellationSignal)
+            throws IllegalStateException {
         if (userId == null) {
             userId = UserId.CURRENT_USER;
         }
 
         if (DEBUG) {
-            Log.d(TAG, "queryMedia() userId=" + userId + " uri=" + uri + " cat=" + category
-                    + " mimeTypes=" + Arrays.toString(mimeTypes) + " limit=" + limit);
-            Log.v(TAG, "Thread=" + Thread.currentThread() + "; Stacktrace:", new Throwable());
+            Log.d(TAG, "queryMedia() uri=" + uri
+                    + " cat=" + category
+                    + " mimeTypes=" + Arrays.toString(mimeTypes)
+                    + " limit=" + paginationParameters.getPageSize()
+                    + " date_taken_before_ms = " + paginationParameters.getDateBeforeMs()
+                    + " row_id = " + paginationParameters.getRowId());
         }
         Trace.beginSection("ItemsProvider.queryMedia");
 
@@ -249,15 +273,25 @@
                         + MediaStore.AUTHORITY);
                 return null;
             }
-            extras.putInt(QUERY_ARG_LIMIT, limit);
+            extras.putInt(QUERY_ARG_LIMIT, paginationParameters.getPageSize());
             if (mimeTypes != null) {
                 extras.putStringArray(MediaStore.QUERY_ARG_MIME_TYPE, mimeTypes);
             }
             extras.putString(MediaStore.QUERY_ARG_ALBUM_ID, category.getId());
             extras.putString(MediaStore.QUERY_ARG_ALBUM_AUTHORITY, category.getAuthority());
 
+            if (paginationParameters.getRowId() >= 0
+                    && paginationParameters.getDateBeforeMs() > Long.MIN_VALUE) {
+                extras.putInt(QUERY_ROW_ID, paginationParameters.getRowId());
+                extras.putLong(QUERY_DATE_TAKEN_BEFORE_MS, paginationParameters.getDateBeforeMs());
+            }
+            if (localIdSelection != null) {
+                extras.putIntegerArrayList(QUERY_LOCAL_ID_SELECTION,
+                        (ArrayList<Integer>) localIdSelection);
+            }
+
             result = client.query(uri, /* projection */ null, extras,
-                    /* cancellationSignal */ null);
+                    /* cancellationSignal */ cancellationSignal);
             return result;
         } catch (RemoteException | NameNotFoundException ignored) {
             // Do nothing, return null.
@@ -281,15 +315,14 @@
 
     @Nullable
     private Cursor queryAlbums(@NonNull Uri uri, @Nullable String[] mimeTypes,
-                @Nullable UserId userId) {
+                @Nullable UserId userId, @Nullable CancellationSignal cancellationSignal) {
         if (userId == null) {
             userId = UserId.CURRENT_USER;
         }
 
         if (DEBUG) {
-            Log.d(TAG, "queryAlbums() userId=" + userId + " uri=" + uri
+            Log.d(TAG, "queryAlbums() uri=" + uri
                     + " mimeTypes=" + Arrays.toString(mimeTypes));
-            Log.v(TAG, "Thread=" + Thread.currentThread() + "; Stacktrace:", new Throwable());
         }
         Trace.beginSection("ItemsProvider.queryAlbums");
 
@@ -307,7 +340,7 @@
             }
 
             result = client.query(uri, /* projection */ null, extras,
-                    /* cancellationSignal */ null);
+                    /* cancellationSignal */ cancellationSignal);
             return result;
         } catch (RemoteException | NameNotFoundException ignored) {
             // Do nothing, return null.
@@ -380,93 +413,64 @@
         return !TextUtils.isEmpty(uri.getUserInfo());
     }
 
-    // TODO(b/257887919): Build proper UI and remove all this monstrosity below!
-    private static volatile @Nullable NotificationHandler sNotificationHandler;
-
-    private static void ensureNotificationHandler(@NonNull Context context) {
-        if (sNotificationHandler == null) {
-            synchronized (PickerSyncController.class) {
-                if (sNotificationHandler == null) {
-                    sNotificationHandler = new NotificationHandler(context);
+    /**
+     * Fetches file Uris for items having {@link com.android.providers.media.MediaGrants} for the
+     * given package. Returns an empty list if no grants are present.
+     */
+    @NonNull
+    public List<Uri> fetchReadGrantedItemsUrisForPackage(int packageUid, String[] mimeTypes) {
+        final ContentResolver resolver = mContext.getContentResolver();
+        try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) {
+            assert client != null;
+            final Bundle extras = new Bundle();
+            extras.putInt(Intent.EXTRA_UID, packageUid);
+            extras.putStringArray(EXTRA_MIME_TYPE_SELECTION, mimeTypes);
+            List<Uri> filesUriList = new ArrayList<>();
+            try (Cursor c = client.query(Uri.parse(MEDIA_GRANTS_URI_PATH),
+                    /* projection= */ null,
+                    /* queryArgs= */ extras,
+                    null)) {
+                while (c.moveToNext()) {
+                    final String file_path = c.getString(c.getColumnIndexOrThrow(DATA));
+                    final Integer file_id = c.getInt(c.getColumnIndexOrThrow(FILE_ID_COLUMN));
+                    filesUriList.add(getContentUriForPath(
+                            file_path).buildUpon().appendPath(String.valueOf(file_id)).build());
                 }
             }
+            return filesUriList;
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
         }
     }
 
-    private static class NotificationHandler extends Handler {
-        static final int MESSAGE_CODE_STARTED_LOADING = 1;
-        static final int MESSAGE_CODE_TICK = 2;
-        static final int MESSAGE_CODE_FINISHED_LOADING = 3;
-
-        static final int FIRST_TICK_DELAY = 1_000; // 1 second
-        static final int TICK_DELAY = 30_000; // 30 seconds
-
-        final Context mContext;
-
-        NotificationHandler(@NonNull Context context) {
-            // It will be running on the UI thread.
-            super(Looper.getMainLooper());
-            mContext = context.getApplicationContext();
+    /**
+     * Sends a data init notification to the MP process.
+     */
+    public void initPhotoPickerData(@Nullable String albumId,
+            @Nullable String albumAuthority,
+            boolean initLocalOnlyData,
+            @Nullable UserId userId) {
+        if (userId == null) {
+            Log.e(TAG, "Could not determine the current active user id in Picker. "
+                    + "Init media call cannot go through.");
+            return;
         }
 
-        @Override
-        public void handleMessage(@NonNull Message msg) {
-            switch (msg.what) {
-                case MESSAGE_CODE_STARTED_LOADING:
-                    if (hasMessages(MESSAGE_CODE_TICK)) {
-                        // Already have scheduled ticks - do nothing.
-                        return;
-                    }
-                    // Wait 1 sec before actually showing the first notification (so that we don't
-                    // annoy users with our Toasts if the loading actually takes less than 1 sec).
-                    sendTickMessageDelayed(/* seqNum */ 1, FIRST_TICK_DELAY);
-                    break;
-
-                case MESSAGE_CODE_TICK:
-                    final int seqNum = msg.arg1;
-
-                    // These Strings are intentionally hardcoded here instead of being added to
-                    // the res/values/strings.xml.
-                    // They are to be used in droidfood only, not to be translated, and must be
-                    // removed very soon!
-                    final String text;
-                    if (seqNum == 1) {
-                        text = "Syncing your cloud media library...";
-                    } else {
-                        text = "Still syncing your cloud media library...";
-                    }
-                    Toast.makeText(mContext, "[Dogfood: known issue] " + text, LENGTH_LONG).show();
-
-                    // Do not show more than 10 of these.
-                    if (seqNum < 10) {
-                        // Show next tick in 30 seconds.
-                        sendTickMessageDelayed(/* seqNum */ seqNum + 1, TICK_DELAY);
-                    }
-                    break;
-
-                case MESSAGE_CODE_FINISHED_LOADING:
-                    removeMessages(MESSAGE_CODE_STARTED_LOADING);
-                    removeMessages(MESSAGE_CODE_TICK);
-                    break;
-
-                default:
-                    super.handleMessage(msg);
+        try (ContentProviderClient client = getContentProviderClient(userId)) {
+            if (client == null) {
+                throw new IllegalStateException("ContentProviderClient is null.");
             }
+            sendInitPhotoPickerDataNotification(client, albumId, albumAuthority, initLocalOnlyData);
+        } catch (RuntimeException | NameNotFoundException | RemoteException e) {
+            Log.e(TAG, "Could not send init media call to Media Provider", e);
         }
+    }
 
-        void onLoadingStarted() {
-            sendEmptyMessage(MESSAGE_CODE_STARTED_LOADING);
-        }
-
-        void onLoadingFinished() {
-            sendEmptyMessage(MESSAGE_CODE_FINISHED_LOADING);
-        }
-
-        private void sendTickMessageDelayed(int seqNum, int delay) {
-            final Message message = obtainMessage(MESSAGE_CODE_TICK);
-            message.arg1 = seqNum;
-
-            sendMessageDelayed(message, delay);
-        }
+    @Nullable
+    private ContentProviderClient getContentProviderClient(@NonNull UserId userId)
+            throws NameNotFoundException {
+        return userId
+                .getContentResolver(mContext)
+                .acquireContentProviderClient(AUTHORITY);
     }
 }
diff --git a/src/com/android/providers/media/photopicker/data/PaginationParameters.java b/src/com/android/providers/media/photopicker/data/PaginationParameters.java
new file mode 100644
index 0000000..2d05eb4
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/data/PaginationParameters.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2023 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.data;
+
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.INT_DEFAULT;
+
+/**
+ * Holder for parameters required for pagination of photos and category items grid recyclerView in
+ * photoPicker.
+ */
+public class PaginationParameters {
+    private int mPageSize = INT_DEFAULT;
+    private long mDateBeforeMs = Long.MIN_VALUE;
+    private int mRowId = INT_DEFAULT;
+    public static final int PAGINATION_PAGE_SIZE_ITEMS = 600;
+
+    public static final int PAGINATION_PAGE_SIZE_ALBUM_ITEMS = 600;
+
+    /**
+     * Instantiates UI pagination parameters for photoPicker. Use this when all the fields needs to
+     * be set to default, i.e. to return complete list of items.
+     */
+    public PaginationParameters() {
+    }
+
+    /**
+     * Instantiates UI pagination parameters for photoPicker.
+     *
+     * <p>The parameters will be used similar to this sample query :
+     * {@code SELECT * FROM TABLE_NAME WHERE (column_date_before_ms < dateBeforeMs
+     * OR ( column_date_before_ms = dateBeforeMs AND column_row_id < rowID)) LIMIT pageSize;}
+     *
+     * @param pageSize     used to represent the upper limit of the number of rows that should be
+     *                     returned by the query. Set as -1 to ignore this parameter in the query.
+     * @param dateBeforeMs when set items with date less that this will be returned. Set as -1 to
+     *                     ignore this parameter in the query.
+     * @param rowId        when set items with id less than this will be returned. Set as -1 to
+     *                     ignore this parameter in the query.
+     */
+    public PaginationParameters(int pageSize, long dateBeforeMs, int rowId) {
+        mPageSize = pageSize;
+        mDateBeforeMs = dateBeforeMs;
+        mRowId = rowId;
+    }
+
+    /**
+     * Instantiates UI pagination parameters for photoPicker.
+     *
+     * <p>When using this constructor the value for pageSize will be the default value i.e. -1.</p>
+     *
+     * @param dateBeforeMs when set items with date less that this will be returned. Set as -1 to
+     *                     ignore this parameter in the query.
+     * @param rowId        when set items with id less than this will be returned. Set as -1 to
+     *                     ignore this parameter in the query.
+     */
+    public PaginationParameters(long dateBeforeMs, int rowId) {
+        this(PAGINATION_PAGE_SIZE_ITEMS, dateBeforeMs, rowId);
+    }
+
+    /**
+     * @return page size for pagination. It is used as the LIMIT clause in the query to database.
+     */
+    public int getPageSize() {
+        return mPageSize;
+    }
+
+    /**
+     * @return date in ms which can be used as the parameter in the query to load items.
+     *
+     * <p>This is combination with row id is used to find the next page of data.</p>
+     *
+     * <b>Note: This parameter is only used in the query if the row id is set. Else it is
+     * ignored.</b>
+     */
+    public Long getDateBeforeMs() {
+        return mDateBeforeMs;
+    }
+
+    /**
+     * @return row id which can be used as the parameter in the query to load items.
+     *
+     * <p>This is combination with date_taken_before_ms is used to find the next page of data.</p>
+     *
+     * <p>When the {@link PaginationParameters#mDateBeforeMs} for two rows is same, this
+     * parameter is used to figure out which row to return.</p>
+     */
+    public int getRowId() {
+        return mRowId;
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/data/PickerDatabaseHelper.java b/src/com/android/providers/media/photopicker/data/PickerDatabaseHelper.java
index 6e8df9b..141807c 100644
--- a/src/com/android/providers/media/photopicker/data/PickerDatabaseHelper.java
+++ b/src/com/android/providers/media/photopicker/data/PickerDatabaseHelper.java
@@ -40,9 +40,8 @@
     private static final String TAG = "PickerDatabaseHelper";
 
     public static final String PICKER_DATABASE_NAME = "picker.db";
-
-    private static final int VERSION_T = 9;
-    public static final int VERSION_LATEST = VERSION_T;
+    private static final int VERSION_U = 11;
+    public static final int VERSION_LATEST = VERSION_U;
 
     final Context mContext;
     final String mName;
@@ -98,12 +97,15 @@
 
     private void resetData(SQLiteDatabase db) {
         clearPickerPrefs(mContext);
+
+        dropAllTables(db);
+
         createLatestSchema(db);
         createLatestIndexes(db);
     }
 
     @VisibleForTesting
-    static void makePristineSchema(SQLiteDatabase db) {
+    static void dropAllTables(SQLiteDatabase db) {
         // drop all tables
         Cursor c = db.query("sqlite_master", new String[] {"name"}, "type is 'table'", null, null,
                 null, null);
@@ -114,26 +116,13 @@
         c.close();
     }
 
-    @VisibleForTesting
-    static void makePristineIndexes(SQLiteDatabase db) {
-        // drop all indexes
-        Cursor c = db.query("sqlite_master", new String[] {"name"}, "type is 'index'",
-                null, null, null, null);
-        while (c.moveToNext()) {
-            if (c.getString(0).startsWith("sqlite_")) continue;
-            db.execSQL("DROP INDEX IF EXISTS " + c.getString(0));
-        }
-        c.close();
-    }
-
     private static void createLatestSchema(SQLiteDatabase db) {
-        makePristineSchema(db);
 
         db.execSQL("CREATE TABLE media (_id INTEGER PRIMARY KEY AUTOINCREMENT,"
                 + "local_id TEXT,"
                 + "cloud_id TEXT UNIQUE,"
                 + "is_visible INTEGER CHECK(is_visible == 1),"
-                + "date_taken_ms INTEGER NOT NULL CHECK(date_taken_ms >= 0),"
+                + "date_taken_ms INTEGER NOT NULL,"
                 + "sync_generation INTEGER NOT NULL CHECK(sync_generation >= 0),"
                 + "width INTEGER,"
                 + "height INTEGER,"
@@ -150,7 +139,7 @@
                 + "local_id TEXT,"
                 + "cloud_id TEXT,"
                 + "album_id TEXT,"
-                + "date_taken_ms INTEGER NOT NULL CHECK(date_taken_ms >= 0),"
+                + "date_taken_ms INTEGER NOT NULL,"
                 + "sync_generation INTEGER NOT NULL CHECK(sync_generation >= 0),"
                 + "size_bytes INTEGER NOT NULL CHECK(size_bytes > 0),"
                 + "duration_ms INTEGER CHECK(duration_ms >= 0),"
@@ -163,21 +152,20 @@
     }
 
     private static void createLatestIndexes(SQLiteDatabase db) {
-        makePristineIndexes(db);
 
         db.execSQL("CREATE INDEX local_id_index on media(local_id)");
         db.execSQL("CREATE INDEX cloud_id_index on media(cloud_id)");
         db.execSQL("CREATE INDEX is_visible_index on media(is_visible)");
-        db.execSQL("CREATE INDEX date_taken_index on media(date_taken_ms)");
         db.execSQL("CREATE INDEX size_index on media(size_bytes)");
         db.execSQL("CREATE INDEX mime_type_index on media(mime_type)");
         db.execSQL("CREATE INDEX is_favorite_index on media(is_favorite)");
+        db.execSQL("CREATE INDEX date_taken_row_id_index on media(date_taken_ms, _id)");
 
         db.execSQL("CREATE INDEX local_id_album_index on album_media(local_id)");
         db.execSQL("CREATE INDEX cloud_id_album_index on album_media(cloud_id)");
-        db.execSQL("CREATE INDEX date_taken_album_index on album_media(date_taken_ms)");
         db.execSQL("CREATE INDEX size_album_index on album_media(size_bytes)");
         db.execSQL("CREATE INDEX mime_type_album_index on album_media(mime_type)");
+        db.execSQL("CREATE INDEX date_taken_album_row_id_index on album_media(date_taken_ms,_id)");
     }
 
     private static void clearPickerPrefs(Context context) {
diff --git a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
index 15cf150..3fcdad9 100644
--- a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
+++ b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
@@ -22,6 +22,7 @@
 import static android.provider.CloudMediaProviderContract.MediaColumns;
 import static android.provider.MediaStore.PickerMediaColumns;
 
+import static com.android.providers.media.photopicker.PickerSyncController.PAGE_SIZE;
 import static com.android.providers.media.photopicker.util.CursorUtils.getCursorLong;
 import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString;
 import static com.android.providers.media.util.DatabaseUtils.replaceMatchAnyChar;
@@ -32,6 +33,7 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.database.MatrixCursor;
+import android.database.MergeCursor;
 import android.database.sqlite.SQLiteConstraintException;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteQueryBuilder;
@@ -47,10 +49,18 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.data.model.Item;
+import com.android.providers.media.photopicker.sync.CloseableReentrantLock;
+import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
+import com.android.providers.media.photopicker.sync.SyncTrackerRegistry;
+import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
+import com.android.providers.media.util.MimeUtils;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.stream.Collectors;
 
 /**
  * This is a facade that hides the complexities of executing some SQL statements on the picker db.
@@ -59,34 +69,32 @@
  */
 public class PickerDbFacade {
     private static final String VIDEO_MIME_TYPES = "video/%";
-
-    // TODO(b/278562157): If there is a dependency on
-    //  {@link PickerSyncController#mCloudProviderLock}, always acquire
-    //  {@link PickerSyncController#mCloudProviderLock} before {@link mLock} to avoid deadlock.
-    @NonNull
-    private final Object mLock = new Object();
     private final Context mContext;
     private final SQLiteDatabase mDatabase;
+    private final PickerSyncLockManager mPickerSyncLockManager;
     private final String mLocalProvider;
     // This is the cloud provider the database is synced with. It can be set as null to disable
     // cloud queries when database is not in sync with the current cloud provider.
     @Nullable
     private String mCloudProvider;
 
-    public PickerDbFacade(Context context) {
-        this(context, PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY);
+    public PickerDbFacade(Context context, PickerSyncLockManager pickerSyncLockManager) {
+        this(context, pickerSyncLockManager, PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY);
     }
 
     @VisibleForTesting
-    public PickerDbFacade(Context context, String localProvider) {
-        this(context, localProvider, new PickerDatabaseHelper(context));
+    public PickerDbFacade(Context context, PickerSyncLockManager pickerSyncLockManager,
+            String localProvider) {
+        this(context, pickerSyncLockManager, localProvider, new PickerDatabaseHelper(context));
     }
 
     @VisibleForTesting
-    public PickerDbFacade(Context context, String localProvider, PickerDatabaseHelper dbHelper) {
+    public PickerDbFacade(Context context, PickerSyncLockManager pickerSyncLockManager,
+            String localProvider, PickerDatabaseHelper dbHelper) {
         mContext = context;
         mLocalProvider = localProvider;
         mDatabase = dbHelper.getWritableDatabase();
+        mPickerSyncLockManager = pickerSyncLockManager;
     }
 
     private static final String TAG = "PickerDbFacade";
@@ -148,6 +156,7 @@
             String.format("%s < ? OR (%s = ? AND %s < ?)",
                     KEY_DATE_TAKEN_MS, KEY_DATE_TAKEN_MS, KEY_ID);
     private static final String WHERE_ALBUM_ID = KEY_ALBUM_ID  + " = ?";
+    private static final String WHERE_LOCAL_ID_IN = KEY_LOCAL_ID  + " IN ";
 
     // This where clause returns all rows for media items that are local-only and are marked as
     // favorite.
@@ -220,19 +229,39 @@
     /**
      * Sets the cloud provider to be returned after querying the picker db
      * If null, cloud media will be excluded from all queries.
+     * This should not be used in picker sync paths because we should not wait on a lock
+     * indefinitely during the picker sync process.
+     * Use {@link this#setCloudProviderWithTimeout} instead.
      */
     public void setCloudProvider(String authority) {
-        synchronized (mLock) {
+        try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                .lock(PickerSyncLockManager.DB_CLOUD_LOCK)) {
             mCloudProvider = authority;
         }
     }
 
     /**
-     * Returns the cloud provider that will be returned after querying the picker db
+     * Sets the cloud provider to be returned after querying the picker db
+     * If null, cloud media will be excluded from all queries.
+     * This should be used in picker sync paths because we should not wait on a lock
+     * indefinitely during the picker sync process
+     */
+    public void setCloudProviderWithTimeout(String authority) throws UnableToAcquireLockException {
+        try (CloseableReentrantLock ignored =
+                     mPickerSyncLockManager.tryLock(PickerSyncLockManager.DB_CLOUD_LOCK)) {
+            mCloudProvider = authority;
+        }
+    }
+
+    /**
+     * Returns the cloud provider that will be returned after querying the picker db.
+     * This should not be used in picker sync paths because we should not wait on a lock
+     * indefinitely during the picker sync process.
      */
     @VisibleForTesting
     public String getCloudProvider() {
-        synchronized (mLock) {
+        try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                .lock(PickerSyncLockManager.DB_CLOUD_LOCK)) {
             return mCloudProvider;
         }
     }
@@ -393,6 +422,16 @@
 
             return null;
         }
+
+        /**
+         * Returns the first date taken present in the columns affected by the DB write operation
+         * when this method is overridden. Otherwise, it returns Long.MIN_VALUE.
+         */
+        public long getFirstDateTakenMillis() {
+            Log.e(TAG, "Method getFirstDateTakenMillis() is not overridden. "
+                    + "It will always return Long.MIN_VALUE");
+            return Long.MIN_VALUE;
+        }
     }
 
     /**
@@ -442,32 +481,41 @@
             final SQLiteQueryBuilder qb = isLocal ? QB_MATCH_LOCAL_ONLY : QB_MATCH_CLOUD;
             int counter = 0;
 
-            while (cursor.moveToNext()) {
-                ContentValues values = cursorToContentValue(cursor, isLocal);
-
-                String[] upsertArgs = {values.getAsString(isLocal ?
-                        KEY_LOCAL_ID : KEY_CLOUD_ID)};
-                if (upsertMedia(qb, values, upsertArgs) == SUCCESS) {
-                    counter++;
-                    continue;
-                }
-
-                // Because we want to prioritize visible local media over visible cloud media,
-                // we do the following if the upsert above failed
-                if (isLocal) {
-                    // For local syncs, we attempt hiding the visible cloud media
-                    String cloudId = getVisibleCloudIdFromDb(values.getAsString(KEY_LOCAL_ID));
-                    demoteCloudMediaToHidden(cloudId);
-                } else {
-                    // For cloud syncs, we prepare an upsert as hidden cloud media
-                    values.putNull(KEY_IS_VISIBLE);
-                }
-
-                // Now attempt upsert again, this should succeed
-                if (upsertMedia(qb, values, upsertArgs) == SUCCESS) {
-                    counter++;
-                }
+            if (cursor.getCount() > PAGE_SIZE) {
+                Log.w(TAG,
+                        String.format("Expected a cursor page size of %d, but received a cursor "
+                            + "with %d rows instead.", PAGE_SIZE, cursor.getCount()));
             }
+
+            if (cursor.moveToFirst()) {
+                do {
+                    ContentValues values = cursorToContentValue(cursor, isLocal);
+
+                    String[] upsertArgs = {values.getAsString(isLocal ? KEY_LOCAL_ID
+                            : KEY_CLOUD_ID)};
+                    if (upsertMedia(qb, values, upsertArgs) == SUCCESS) {
+                        counter++;
+                        continue;
+                    }
+
+                    // Because we want to prioritize visible local media over visible cloud media,
+                    // we do the following if the upsert above failed
+                    if (isLocal) {
+                        // For local syncs, we attempt hiding the visible cloud media
+                        String cloudId = getVisibleCloudIdFromDb(values.getAsString(KEY_LOCAL_ID));
+                        demoteCloudMediaToHidden(cloudId);
+                    } else {
+                        // For cloud syncs, we prepare an upsert as hidden cloud media
+                        values.putNull(KEY_IS_VISIBLE);
+                    }
+
+                    // Now attempt upsert again, this should succeed
+                    if (upsertMedia(qb, values, upsertArgs) == SUCCESS) {
+                        counter++;
+                    }
+                } while (cursor.moveToNext());
+            }
+
             return counter;
         }
 
@@ -517,6 +565,8 @@
     }
 
     private static final class RemoveMediaOperation extends DbWriteOperation {
+        private static final String[] sDateTakenProjection = new String[] {KEY_DATE_TAKEN_MS};
+        private long mFirstDateTakenMillis = Long.MIN_VALUE;
 
         private RemoveMediaOperation(SQLiteDatabase database, boolean isLocal) {
             super(database, isLocal);
@@ -530,6 +580,10 @@
             int counter = 0;
 
             while (cursor.moveToNext()) {
+                if (cursor.isFirst()) {
+                    updateFirstDateTakenMillis(cursor, isLocal);
+                }
+
                 // Need to fetch the local_id before delete because for cloud items
                 // we need a db query to fetch the local_id matching the id received from
                 // cursor (cloud_id).
@@ -549,6 +603,11 @@
             return counter;
         }
 
+        @Override
+        public long getFirstDateTakenMillis() {
+            return mFirstDateTakenMillis;
+        }
+
         private void promoteCloudMediaToVisible(@Nullable String localId) {
             if (localId == null) {
                 return;
@@ -585,6 +644,34 @@
                         /* columnIndex */ 0);
             }
         }
+
+        private void updateFirstDateTakenMillis(Cursor inputCursor, boolean isLocal) {
+            final int idIndex = inputCursor
+                    .getColumnIndex(CloudMediaProviderContract.MediaColumns.ID);
+            if (idIndex < 0) {
+                Log.e(TAG, "Id is not present in the cursor");
+                return;
+            }
+
+            final String id = inputCursor.getString(idIndex);
+            if (TextUtils.isEmpty((id))) {
+                Log.e(TAG, "Input id is empty");
+                return;
+            }
+
+            final SQLiteQueryBuilder qb = isLocal ? QB_MATCH_LOCAL_ONLY : QB_MATCH_CLOUD;
+            final String[] queryArgs = new String[]{id};
+
+            try (Cursor outputCursor = qb.query(getDatabase(), sDateTakenProjection,
+                    /* selection */ null, queryArgs, /* groupBy */ null, /* having */ null,
+                    /* orderBy */ null)) {
+                if (outputCursor.moveToFirst()) {
+                    mFirstDateTakenMillis = outputCursor.getLong(/* columnIndex */ 0);
+                } else {
+                    Log.e(TAG, "Could not get first date taken millis for media id: " + id);
+                }
+            }
+        }
     }
 
     private static final class ResetMediaOperation extends DbWriteOperation {
@@ -631,10 +718,15 @@
         private final boolean mIsFavorite;
         private final boolean mIsVideo;
         public boolean mIsLocalOnly;
+        private int mPageSize;
+        private String mPageToken;
+
+        private List<Integer> mLocalIdSelection;
 
         private QueryFilter(int limit, long dateTakenBeforeMs, long dateTakenAfterMs, long id,
                 String albumId, long sizeBytes, String[] mimeTypes, boolean isFavorite,
-                boolean isVideo, boolean isLocalOnly) {
+                boolean isVideo, boolean isLocalOnly, List<Integer> localIdSelection, int pageSize,
+                String pageToken) {
             this.mLimit = limit;
             this.mDateTakenBeforeMs = dateTakenBeforeMs;
             this.mDateTakenAfterMs = dateTakenAfterMs;
@@ -645,21 +737,26 @@
             this.mIsFavorite = isFavorite;
             this.mIsVideo = isVideo;
             this.mIsLocalOnly = isLocalOnly;
+            this.mLocalIdSelection = localIdSelection;
+            this.mPageSize = pageSize;
+            this.mPageToken = pageToken;
         }
     }
 
     /** Builder for {@link Query} filter. */
     public static class QueryFilterBuilder {
+        public static final int INT_DEFAULT = -1;
         public static final long LONG_DEFAULT = -1;
         public static final String STRING_DEFAULT = null;
         public static final String[] STRING_ARRAY_DEFAULT = null;
         public static final boolean BOOLEAN_DEFAULT = false;
 
+        public static final List LIST_DEFAULT = null;
         public static final int LIMIT_DEFAULT = 1000;
 
         private final int limit;
-        private long dateTakenBeforeMs = LONG_DEFAULT;
-        private long dateTakenAfterMs = LONG_DEFAULT;
+        private long mDateTakenBeforeMs = Long.MIN_VALUE;
+        private long mDateTakenAfterMs = Long.MIN_VALUE;
         private long id = LONG_DEFAULT;
         private String albumId = STRING_DEFAULT;
         private long sizeBytes = LONG_DEFAULT;
@@ -667,18 +764,22 @@
         private boolean isFavorite = BOOLEAN_DEFAULT;
         private boolean mIsVideo = BOOLEAN_DEFAULT;
         private boolean mIsLocalOnly = BOOLEAN_DEFAULT;
+        private int mPageSize = INT_DEFAULT;
+        private String mPageToken = STRING_DEFAULT;
+
+        private List<Integer> mLocalIdSelection = LIST_DEFAULT;
 
         public QueryFilterBuilder(int limit) {
             this.limit = limit;
         }
 
         public QueryFilterBuilder setDateTakenBeforeMs(long dateTakenBeforeMs) {
-            this.dateTakenBeforeMs = dateTakenBeforeMs;
+            this.mDateTakenBeforeMs = dateTakenBeforeMs;
             return this;
         }
 
         public QueryFilterBuilder setDateTakenAfterMs(long dateTakenAfterMs) {
-            this.dateTakenAfterMs = dateTakenAfterMs;
+            this.mDateTakenAfterMs = dateTakenAfterMs;
             return this;
         }
 
@@ -698,6 +799,7 @@
             this.id = id;
             return this;
         }
+
         public QueryFilterBuilder setAlbumId(String albumId) {
             this.albumId = albumId;
             return this;
@@ -714,6 +816,14 @@
         }
 
         /**
+         * Sets the local id selection filter.
+         */
+        public QueryFilterBuilder setLocalIdSelection(List<Integer> localIdSelection) {
+            this.mLocalIdSelection = localIdSelection;
+            return this;
+        }
+
+        /**
          * If {@code isFavorite} is {@code true}, the {@link QueryFilter} returns only
          * favorited items, however, if it is {@code false}, it returns all items including
          * favorited and non-favorited items.
@@ -742,9 +852,26 @@
             return this;
         }
 
+        /**
+         * Sets the page size.
+         */
+        public QueryFilterBuilder setPageSize(int pageSize) {
+            mPageSize = pageSize;
+            return this;
+        }
+
+        /**
+         * Sets the page token.
+         */
+        public QueryFilterBuilder setPageToken(String pageToken) {
+            mPageToken = pageToken;
+            return this;
+        }
+
         public QueryFilter build() {
-            return new QueryFilter(limit, dateTakenBeforeMs, dateTakenAfterMs, id, albumId,
-                    sizeBytes, mimeTypes, isFavorite, mIsVideo, mIsLocalOnly);
+            return new QueryFilter(limit, mDateTakenBeforeMs, mDateTakenAfterMs, id, albumId,
+                    sizeBytes, mimeTypes, isFavorite, mIsVideo, mIsLocalOnly, mLocalIdSelection,
+                    mPageSize, mPageToken);
         }
     }
 
@@ -758,18 +885,55 @@
      * {@code limit}. They can also be filtered with {@code query}.
      */
     public Cursor queryMediaForUi(QueryFilter query) {
-        final SQLiteQueryBuilder qb = createVisibleMediaQueryBuilder();
-        final String[] selectionArgs = buildSelectionArgs(qb, query);
-
-        final String cloudProvider;
-        synchronized (mLock) {
-            // If the cloud sync is in progress or the cloud provider has changed but a sync has not
-            // been completed and committed, {@link PickerDBFacade.mCloudProvider} will be
-            // {@code null}.
-            cloudProvider = mCloudProvider;
+        if (query.mIsLocalOnly && query.mLocalIdSelection != null
+                && !query.mLocalIdSelection.isEmpty()) {
+            return queryMediaForUiWithLocalIdSelection(query);
         }
 
-        return queryMediaForUi(qb, selectionArgs, query.mLimit, TABLE_MEDIA, cloudProvider);
+        final SQLiteQueryBuilder qb = createVisibleMediaQueryBuilder();
+        final String[] selectionArgs = buildSelectionArgs(qb, query);
+        if (query.mIsLocalOnly) {
+            return queryMediaForUi(qb, selectionArgs, query.mLimit,  /* isLocalOnly*/true,
+                    TABLE_MEDIA, /* cloudProvider*/ null);
+        }
+
+        // If the cloud sync is in progress or the cloud provider has changed but a sync has not
+        // been completed and committed, {@link PickerDBFacade.mCloudProvider} will be
+        // {@code null}.
+        final String cloudProvider = getCloudProvider();
+
+        return queryMediaForUi(qb, selectionArgs, query.mLimit, query.mIsLocalOnly,
+                TABLE_MEDIA, cloudProvider);
+    }
+
+
+    private Cursor queryMediaForUiWithLocalIdSelection(QueryFilter query) {
+        // Since 'WHERE IN' clause has an upper limit of items that can be included in the sql
+        // statement and also there is an upper limit to the size of the sql statement.
+        // Splitting the query into multiple smaller ones.
+        // This query will now process 150 items in a batch.
+        List<List<Integer>> listOfSelectionArgsForLocalId = splitArrayList(
+                query.mLocalIdSelection,
+                /* number of ids per query */ 150);
+        List<Cursor> resultCursor = new ArrayList<>();
+
+        for (List<Integer> selectionArgForLocalIdSelection : listOfSelectionArgsForLocalId) {
+            final SQLiteQueryBuilder qb = createVisibleMediaQueryBuilder();
+            query.mLocalIdSelection = selectionArgForLocalIdSelection;
+            final String[] selectionArgs = buildSelectionArgs(qb, query);
+            resultCursor.add(queryMediaForUi(qb, selectionArgs, query.mLimit, true,
+                    TABLE_MEDIA, /* cloud provider */null));
+        }
+
+        return new MergeCursor(resultCursor.toArray(new Cursor[resultCursor.size()]));
+    }
+
+    private static <T> List<List<T>> splitArrayList(List<T> list, int chunkSize) {
+        List<List<T>> subLists = new ArrayList<>();
+        for (int i = 0; i < list.size(); i += chunkSize) {
+            subLists.add(list.subList(i, Math.min(i + chunkSize, list.size())));
+        }
+        return subLists;
     }
 
     /**
@@ -783,11 +947,12 @@
      * The result is sorted in reverse chronological order, i.e. newest first, up to a maximum of
      * {@code limit}. They can also be filtered with {@code query}.
      */
-    public Cursor queryAlbumMediaForUi(QueryFilter query, String authority) {
+    public Cursor queryAlbumMediaForUi(@NonNull QueryFilter query, @NonNull String authority) {
         final SQLiteQueryBuilder qb = createAlbumMediaQueryBuilder(isLocal(authority));
         final String[] selectionArgs = buildSelectionArgs(qb, query);
 
-        return queryMediaForUi(qb, selectionArgs, query.mLimit, TABLE_ALBUM_MEDIA, authority);
+        return queryMediaForUi(qb, selectionArgs, query.mLimit, query.mIsLocalOnly,
+                TABLE_ALBUM_MEDIA, authority);
     }
 
     /**
@@ -808,19 +973,20 @@
         }
 
         if (authority.equals(mLocalProvider)) {
-            return queryMediaIdForAppsInternal(qb, projection, selectionArgs);
+            return queryMediaIdForAppsLocked(qb, projection, selectionArgs);
         }
 
-        synchronized (mLock) {
+        try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                .lock(PickerSyncLockManager.DB_CLOUD_LOCK)) {
             if (authority.equals(mCloudProvider)) {
-                return queryMediaIdForAppsInternal(qb, projection, selectionArgs);
+                return queryMediaIdForAppsLocked(qb, projection, selectionArgs);
             }
         }
 
         return null;
     }
 
-    private Cursor queryMediaIdForAppsInternal(@NonNull SQLiteQueryBuilder qb,
+    private Cursor queryMediaIdForAppsLocked(@NonNull SQLiteQueryBuilder qb,
             @NonNull String[] projection, @NonNull String[] selectionArgs) {
         return qb.query(mDatabase, getMediaStoreProjectionLocked(projection),
                 /* selection */ null, selectionArgs, /* groupBy */ null, /* having */ null,
@@ -831,7 +997,7 @@
      * Returns empty {@link Cursor} if there are no items matching merged album constraints {@code
      * query}
      */
-    public Cursor getMergedAlbums(QueryFilter query) {
+    public Cursor getMergedAlbums(QueryFilter query, String cloudProvider) {
         final MatrixCursor c = new MatrixCursor(AlbumColumns.ALL_PROJECTION);
         List<String> mergedAlbums = List.of(ALBUM_ID_FAVORITES, ALBUM_ID_VIDEOS);
         for (String albumId : mergedAlbums) {
@@ -859,7 +1025,9 @@
             }
 
             long count = getCursorLong(cursor, CloudMediaProviderContract.AlbumColumns.MEDIA_COUNT);
-            if (count == 0) {
+
+            // We want to display empty merged folder in case of cloud picker.
+            if (shouldHideMergedAlbum(query, albumId, cloudProvider, count)) {
                 continue;
             }
 
@@ -876,6 +1044,27 @@
         return c;
     }
 
+    private static boolean shouldHideMergedAlbum(QueryFilter query, String albumId,
+            String cloudProvider, long count) {
+        final boolean isAlbumEmpty = (count == 0);
+        final boolean shouldNotShowCloudItems = (query.mIsLocalOnly || cloudProvider == null);
+
+        return (isAlbumEmpty && (shouldNotShowCloudItems || hideVideosAlbum(query, albumId)));
+    }
+
+    private static boolean hideVideosAlbum(QueryFilter query, String albumId) {
+        String[] mimeTypes = query.mMimeTypes;
+        if (!albumId.equals(ALBUM_ID_VIDEOS) || mimeTypes == null) {
+            return false;
+        }
+        for (String mimeType : mimeTypes) {
+            if (MimeUtils.isVideoMimeType(mimeType)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     private String[] getMergedAlbumProjection() {
         return new String[] {
                 "COUNT(" + KEY_ID + ") AS " + CloudMediaProviderContract.AlbumColumns.MEDIA_COUNT,
@@ -897,27 +1086,40 @@
         return mLocalProvider.equals(authority);
     }
 
+    /**
+     * Returns sorted and deduped cloud and local media or album content items from the picker db.
+     */
     private Cursor queryMediaForUi(SQLiteQueryBuilder qb, String[] selectionArgs,
-            int limit, String tableName, String authority) {
+            int limit, boolean isLocalOnly, String tableName, String authority) {
         // Use the <table>.<column> form to order _id to avoid ordering against the projection '_id'
         final String orderBy = getOrderClause(tableName);
         final String limitStr = String.valueOf(limit);
 
+        if (isLocalOnly) {
+            qb.appendWhereStandalone(WHERE_NULL_CLOUD_ID);
+            return queryMediaForUiLocked(qb, selectionArgs, orderBy, limitStr);
+        }
+
         // Hold lock while checking the cloud provider and querying so that cursor extras containing
         // the cloud provider is consistent with the cursor results and doesn't race with
         // #setCloudProvider
-        synchronized (mLock) {
+        try (CloseableReentrantLock ignored = mPickerSyncLockManager
+                .lock(PickerSyncLockManager.DB_CLOUD_LOCK)) {
             if (mCloudProvider == null || !Objects.equals(mCloudProvider, authority)) {
                 // TODO(b/278086344): If cloud provider is null or has changed from what we received
                 //  from the UI, skip all cloud items in the picker db.
                 qb.appendWhereStandalone(WHERE_NULL_CLOUD_ID);
             }
-
-            return qb.query(mDatabase, getCloudMediaProjectionLocked(), /* selection */ null,
-                    selectionArgs, /* groupBy */ null, /* having */ null, orderBy, limitStr);
+            return queryMediaForUiLocked(qb, selectionArgs, orderBy, limitStr);
         }
     }
 
+    private Cursor queryMediaForUiLocked(SQLiteQueryBuilder qb, String[] selectionArgs,
+            String orderBy, String limitStr) {
+        return qb.query(mDatabase, getCloudMediaProjectionLocked(), /* selection */ null,
+                selectionArgs, /* groupBy */ null, /* having */ null, orderBy, limitStr);
+    }
+
     private static String getOrderClause(String tableName) {
         return "date_taken_ms DESC," + tableName + "._id DESC";
     }
@@ -927,6 +1129,8 @@
             getProjectionAuthorityLocked(),
             getProjectionDataLocked(MediaColumns.DATA),
             getProjectionId(MediaColumns.ID),
+            // The id in the picker.db table represents the row id. This is used in UI pagination.
+            getProjectionSimple(KEY_ID, Item.ROW_ID),
             getProjectionSimple(KEY_DATE_TAKEN_MS, MediaColumns.DATE_TAKEN_MILLIS),
             getProjectionSimple(KEY_SYNC_GENERATION, MediaColumns.SYNC_GENERATION),
             getProjectionSimple(KEY_SIZE_BYTES, MediaColumns.SIZE_BYTES),
@@ -1176,6 +1380,22 @@
             selectArgs.add(query.mAlbumId);
         }
 
+        if (query.mLocalIdSelection != null && !query.mLocalIdSelection.isEmpty()) {
+            StringBuilder localIdSelectionPlaceholder = new StringBuilder("(");
+            for (int itr = 0; itr < query.mLocalIdSelection.size(); itr++) {
+                localIdSelectionPlaceholder.append("?,");
+            }
+            localIdSelectionPlaceholder.deleteCharAt(localIdSelectionPlaceholder.length() - 1);
+            localIdSelectionPlaceholder.append(")");
+
+            // Append the where clause for local id selection to the query builder.
+            qb.appendWhereStandalone(WHERE_LOCAL_ID_IN + localIdSelectionPlaceholder);
+
+            // Add local ids to the selection args.
+            selectArgs.addAll(query.mLocalIdSelection.stream().map(
+                    String::valueOf).collect(Collectors.toList()));
+        }
+
         if (selectArgs.isEmpty()) {
             return null;
         }
@@ -1349,50 +1569,94 @@
             final boolean isLocal = isLocal();
             final String albumId = getAlbumId();
             final SQLiteQueryBuilder qb = createAlbumMediaQueryBuilder(isLocal);
+            final SQLiteQueryBuilder qbMedia = createMediaQueryBuilder();
             int counter = 0;
 
-            while (cursor.moveToNext()) {
-                ContentValues values = cursorToContentValue(cursor, isLocal, albumId);
+            if (cursor.getCount() > PAGE_SIZE) {
+                Log.w(TAG,
+                        String.format("Expected a cursor page size of %d, but received a cursor "
+                            + "with %d rows instead.", PAGE_SIZE, cursor.getCount()));
+            }
 
-                // In case of cloud albums, cloud provider returns both local and cloud ids.
-                // We give preference to inserting media data for the local copy of an item instead
-                // of the cloud copy. Hence, if local copy is available, fetch metadata from media
-                // table and update the album_media row accordingly.
-                if (!isLocal) {
-                    final String localId = values.getAsString(KEY_LOCAL_ID);
-                    final String cloudId = values.getAsString(KEY_CLOUD_ID);
-                    if (!TextUtils.isEmpty(localId) && !TextUtils.isEmpty(cloudId)) {
-                        // Fetch local media item details from media table.
-                        try (Cursor cursorLocalMedia = getLocalMediaMetadata(localId)) {
-                            if (cursorLocalMedia != null && cursorLocalMedia.getCount() == 1) {
-                                // If local media item details are present in the media table,
-                                // update content values and remove cloud id.
-                                values.putNull(KEY_CLOUD_ID);
-                                updateContentValues(values, cursorLocalMedia);
-                            } else {
-                                // If local media item details are NOT present in the media table,
-                                // insert cloud row after removing local_id. This will only happen
-                                // when local id points to a deleted item.
-                                values.putNull(KEY_LOCAL_ID);
+            if (cursor.moveToFirst()) {
+                do {
+                    ContentValues values = cursorToContentValue(cursor, isLocal, albumId);
+
+                    // In case of cloud albums, cloud provider returns both local and cloud ids.
+                    // We give preference to inserting media data for the local copy of an item
+                    // instea of the cloud copy. Hence, if local copy is available, fetch metadata
+                    // from media table and update the album_media row accordingly.
+                    if (!isLocal) {
+                        final String localId = values.getAsString(KEY_LOCAL_ID);
+                        final String cloudId = values.getAsString(KEY_CLOUD_ID);
+                        if (!TextUtils.isEmpty(localId) && !TextUtils.isEmpty(cloudId)) {
+                            // Fetch local media item details from media table.
+                            try (Cursor cursorLocalMedia = getLocalMediaMetadata(localId)) {
+                                if (cursorLocalMedia != null && cursorLocalMedia.getCount() == 1) {
+                                    // If local media item details are present in the media table,
+                                    // update content values and remove cloud id.
+                                    values.putNull(KEY_CLOUD_ID);
+                                    updateContentValues(values, cursorLocalMedia);
+                                } else {
+                                    // If local media item details are NOT present in the media
+                                    // table, insert cloud row after removing local_id. This will
+                                    // only happen when local id points to a deleted item.
+                                    values.putNull(KEY_LOCAL_ID);
+                                }
                             }
                         }
                     }
-                }
 
-                try {
-                    if (qb.insert(getDatabase(), values) > 0) {
-                        counter++;
-                    } else {
-                        Log.v(TAG, "Failed to insert album_media. ContentValues: " + values);
+                    try {
+                        if (qb.insert(getDatabase(), values) > 0) {
+                            counter++;
+                        } else {
+                            Log.v(TAG, "Failed to insert album_media. ContentValues: " + values);
+                        }
+                    } catch (SQLiteConstraintException e) {
+                        Log.v(TAG, "Failed to insert album_media. ContentValues: " + values, e);
                     }
-                } catch (SQLiteConstraintException e) {
-                    Log.v(TAG, "Failed to insert album_media. ContentValues: " + values, e);
-                }
+
+                    // Check if a Cloud sync is running, and additionally insert this row to media
+                    // table if true.
+                    maybeInsertFileToMedia(qbMedia, cursor, isLocal);
+                } while (cursor.moveToNext());
             }
 
             return counter;
         }
 
+        /**
+         * Will (possibly) insert this file to the Picker database's media table if there's an
+         * existing Cloud Sync running.
+         *
+         * <p>This is necessary to guarantee it exists in case it is selected by the user. (So that
+         * the pre-loader can load it to the device before the session is closed.)
+         *
+         * @param queryBuilder The media table query builder to use for the insert
+         * @param cursor The current cursor being processed (this method does not advance the
+         *     cursor).
+         * @param isLocal Whether this is the local provider sync or not.
+         */
+        private void maybeInsertFileToMedia(
+                SQLiteQueryBuilder queryBuilder, Cursor cursor, boolean isLocal) {
+            if (SyncTrackerRegistry.getCloudSyncTracker().pendingSyncFutures().size() > 0) {
+                ContentValues values = cursorToContentValue(cursor, isLocal);
+                Log.d(
+                        TAG,
+                        String.format(
+                                "Encountered running Cloud sync during AddAlbumMediaOperation while"
+                                    + " processing row. Will additional insert to media table:  %s",
+                                values));
+                try {
+                    queryBuilder.insert(getDatabase(), values);
+                } catch (SQLiteConstraintException ignored) {
+                    // If we hit a constraint exception it means this row is already in media,
+                    // so nothing to do here.
+                }
+            }
+        }
+
         private void updateContentValues(ContentValues values, Cursor cursor) {
             if (cursor.moveToFirst()) {
                 for (int columnIndex = 0; columnIndex < cursor.getColumnCount(); columnIndex++) {
@@ -1426,4 +1690,13 @@
                     /* orderBy */ null);
         }
     }
+
+    /**
+     * Print the {@link PickerDbFacade} state into the given stream.
+     */
+    public void dump(PrintWriter writer) {
+        writer.println("Picker db facade state:");
+        writer.println("  mLocalProvider=" + getLocalProvider());
+        writer.println("  mCloudProvider=" + getCloudProvider());
+    }
 }
diff --git a/src/com/android/providers/media/photopicker/data/PickerSyncRequestExtras.java b/src/com/android/providers/media/photopicker/data/PickerSyncRequestExtras.java
new file mode 100644
index 0000000..f479d51
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/data/PickerSyncRequestExtras.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 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.data;
+
+import static com.android.providers.media.photopicker.data.CloudProviderQueryExtras.isMergedAlbum;
+
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * Encapsulate all picker sync request arguments related logic.
+ */
+public class PickerSyncRequestExtras {
+    @Nullable
+    private final String mAlbumId;
+    @Nullable
+    private final String mAlbumAuthority;
+    private final boolean mInitLocalOnlyData;
+    public PickerSyncRequestExtras(@Nullable String albumId,
+            @Nullable String albumAuthority,
+            boolean initLocalOnlyData) {
+        mAlbumId = albumId;
+        mAlbumAuthority = albumAuthority;
+        mInitLocalOnlyData = initLocalOnlyData;
+    }
+
+    /**
+     * Create a {@link PickerSyncRequestExtras} object from an input bundle.
+     */
+    public static PickerSyncRequestExtras fromBundle(@NonNull Bundle extras) {
+        Objects.requireNonNull(extras);
+
+        final String albumId = extras.getString(MediaStore.EXTRA_ALBUM_ID);
+        final String albumAuthority = extras.getString(MediaStore.EXTRA_ALBUM_AUTHORITY);
+        final boolean initLocalOnlyData =
+                extras.getBoolean(MediaStore.EXTRA_LOCAL_ONLY);
+        return new PickerSyncRequestExtras(albumId, albumAuthority, initLocalOnlyData);
+    }
+
+    /**
+     * Returns true when media data should be synced.
+     */
+    public boolean shouldSyncMediaData() {
+        return TextUtils.isEmpty(mAlbumId);
+    }
+
+    /**
+     * Returns true when only local data needs to be synced.
+     */
+    public boolean shouldSyncLocalOnlyData() {
+        return mInitLocalOnlyData;
+    }
+
+    /**
+     * Returns true when the sync request is for a merged album.
+     */
+    public boolean shouldSyncMergedAlbum() {
+        return isMergedAlbum(mAlbumId);
+    }
+
+    /**
+     * Return album id for the sync request.
+     */
+    @Nullable
+    public String getAlbumId() {
+        return mAlbumId;
+    }
+
+    /**
+     * Return album authority for the sync request.
+     */
+    @Nullable
+    public String getAlbumAuthority() {
+        return mAlbumAuthority;
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/data/Selection.java b/src/com/android/providers/media/photopicker/data/Selection.java
index 4894977..d7667d3 100644
--- a/src/com/android/providers/media/photopicker/data/Selection.java
+++ b/src/com/android/providers/media/photopicker/data/Selection.java
@@ -16,10 +16,12 @@
 
 package com.android.providers.media.photopicker.data;
 
+import android.annotation.Nullable;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.MediaStore;
+import android.util.Log;
 
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
@@ -27,31 +29,126 @@
 import com.android.providers.media.photopicker.data.model.Item;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * A class that tracks Selection
  */
 public class Selection {
+    /**
+     * Contains positions of checked Item at UI. {@link #mCheckedItemIndexes} may have more number
+     * of indexes , from the number of items present in {@link #mSelectedItems}. The index in
+     * {@link #mCheckedItemIndexes} is a potential index that needs to be rechecked in
+     * notifyItemChanged() at the time of deselecting the unavailable item at UI when user is
+     * offline and tries adding unavailable non cached items. the item corresponding to the index in
+     * {@link #mCheckedItemIndexes} may no longer be selected.
+     */
+    private final Map<Item, Integer> mCheckedItemIndexes = new HashMap<>();
+
     // The list of selected items.
-    private Map<Uri, Item> mSelectedItems = new HashMap<>();
+    private Map<Uri, Item> mSelectedItems = new LinkedHashMap<>();
+    private Map<Uri, MutableLiveData<Integer>> mSelectedItemsOrder = new HashMap<>();
+    private Map<String, Item> mItemGrantRevocationMap = new HashMap<>();
+
     private MutableLiveData<Integer> mSelectedItemSize = new MutableLiveData<>();
     // The list of selected items for preview. This needs to be saved separately so that if activity
     // gets killed, we will still have deselected items for preview.
     private List<Item> mSelectedItemsForPreview = new ArrayList<>();
+    private boolean mIsSelectionOrdered = false;
     private boolean mSelectMultiple = false;
     private int mMaxSelectionLimit = 1;
     // This is set to false when max selection limit is reached.
     private boolean mIsSelectionAllowed = true;
 
+    private int mTotalNumberOfPreGrantedItems = 0;
+
+    private Set<String> mPreGrantedItemsSet;
+
+    private static final String TAG = "PhotoPickerSelection";
+
+    /**
+     * Updates the list of pre granted items and the count of selected items.
+     */
+    public void setPreGrantedItemSet(@Nullable Set<String> preGrantedItemSet) {
+        if (preGrantedItemSet != null) {
+            mPreGrantedItemsSet = preGrantedItemSet;
+            setTotalNumberOfPreGrantedItems(preGrantedItemSet.size());
+            Log.d(TAG, "Pre-Granted items have been loaded. Number of items:"
+                    + preGrantedItemSet.size());
+        } else {
+            mPreGrantedItemsSet = new HashSet<>(0);
+            Log.d(TAG, "No Pre-Granted items present");
+        }
+    }
+
+    /**
+     * @return a set of item ids that are pre granted for the current package and user.
+     */
+    @Nullable
+    public Set<String> getPreGrantedItems() {
+        return mPreGrantedItemsSet;
+    }
+
     /**
      * @return {@link #mSelectedItems} - A {@link List} of selected {@link Item}
      */
     public List<Item> getSelectedItems() {
-        return Collections.unmodifiableList(new ArrayList<>(mSelectedItems.values()));
+        ArrayList<Item> result = new ArrayList<>(mSelectedItems.values());
+        return Collections.unmodifiableList(result);
+    }
+
+    /**
+     * @return A {@link Set} of selected {@link Item} ids.
+     */
+    public Set<String> getSelectedItemsIds() {
+        return mSelectedItems.values().stream().map(Item::getId).collect(
+                Collectors.toSet());
+    }
+
+    /**
+     * @return A {@link List} of selected {@link Item} that do not hold a READ_GRANT.
+     */
+    public List<Item> getSelectedItemsWithoutGrants() {
+        return mSelectedItems.values().stream().filter((Item item) -> !item.isPreGranted())
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * @return Indexes - A {@link List} of checked {@link Item} positions.
+     */
+    public Collection<Integer> getCheckedItemsIndexes() {
+        return mCheckedItemIndexes.values();
+    }
+
+    /**
+     * @return A {@link List} of items for which the grants need to be revoked.
+     */
+    public List<Item> getPreGrantedItemsToBeRevoked() {
+        return mItemGrantRevocationMap.values().stream().collect(Collectors.toList());
+    }
+
+    /**
+     * @return A {@link List} of ids for which the grants need to be revoked.
+     */
+    public List<String> getPreGrantedItemIdsToBeRevoked() {
+        return mItemGrantRevocationMap.keySet().stream().collect(Collectors.toList());
+    }
+
+    /**
+     * Sets the count of pre granted items to ensure that the correct number is displayed in
+     * preview and on the add button.
+     */
+    public void setTotalNumberOfPreGrantedItems(int totalNumberOfPreGrantedItems) {
+        mTotalNumberOfPreGrantedItems = totalNumberOfPreGrantedItems;
+        mSelectedItemSize.postValue(getTotalItemsCount());
     }
 
     /**
@@ -59,51 +156,116 @@
      */
     public LiveData<Integer> getSelectedItemCount() {
         if (mSelectedItemSize.getValue() == null) {
-            mSelectedItemSize.setValue(mSelectedItems.size());
+            mSelectedItemSize.setValue(getTotalItemsCount());
         }
         return mSelectedItemSize;
     }
 
     /**
+     * @return {@link LiveData} of the item selection order.
+     */
+    public LiveData<Integer> getSelectedItemOrder(Item item) {
+        return mSelectedItemsOrder.get(item.getContentUri());
+    }
+
+    private int getTotalItemsCount() {
+        return mSelectedItems.size() - countOfPreGrantedItems() + mTotalNumberOfPreGrantedItems
+                - mItemGrantRevocationMap.size();
+    }
+
+    /**
      * Add the selected {@code item} into {@link #mSelectedItems}.
      */
     public void addSelectedItem(Item item) {
+        if (item.isPreGranted() && mItemGrantRevocationMap.containsKey(item.getId())) {
+            mItemGrantRevocationMap.remove(item.getId());
+        }
+        if (mIsSelectionOrdered) {
+            mSelectedItemsOrder.put(
+                    item.getContentUri(), new MutableLiveData(getTotalItemsCount() + 1));
+        }
         mSelectedItems.put(item.getContentUri(), item);
-        mSelectedItemSize.postValue(mSelectedItems.size());
+        mSelectedItemSize.postValue(getTotalItemsCount());
         updateSelectionAllowed();
     }
 
     /**
+     * Add the checked {@code item} index into {@link #mCheckedItemIndexes}.
+     */
+    public void addCheckedItemIndex(Item item, Integer index) {
+        mCheckedItemIndexes.put(item, index);
+    }
+
+    /**
      * Clears {@link #mSelectedItems} and sets the selected item as given {@code item}
      */
     public void setSelectedItem(Item item) {
+        mSelectedItemsOrder.clear();
         mSelectedItems.clear();
         mSelectedItems.put(item.getContentUri(), item);
-        mSelectedItemSize.postValue(mSelectedItems.size());
+        if (mIsSelectionOrdered) {
+            mSelectedItemsOrder.put(
+                    item.getContentUri(), new MutableLiveData(getTotalItemsCount()));
+        }
+        mSelectedItemSize.postValue(getTotalItemsCount());
         updateSelectionAllowed();
     }
 
     /**
-     * Remove the {@code item} from the selected item list {@link #mSelectedItems}.
+     * Remove the {@code item} from the selected item list {@link #mSelectedItems}
      *
      * @param item the item to be removed from the selected item list
      */
     public void removeSelectedItem(Item item) {
+        if (item.isPreGranted()) {
+            // Maintain a list of items that were pre-granted but the user has deselected them in
+            // the current session. This list will be used to revoke existing grants for these
+            // items.
+            mItemGrantRevocationMap.put(item.getId(), item);
+        }
+        if (mIsSelectionOrdered) {
+            MutableLiveData<Integer> removedItem = mSelectedItemsOrder.remove(item.getContentUri());
+            int removedItemOrder = removedItem.getValue().intValue();
+            mSelectedItemsOrder.values().stream()
+                    .filter(order -> order.getValue().intValue() > removedItemOrder)
+                    .forEach(
+                            order -> {
+                                order.setValue(order.getValue().intValue() - 1);
+                            });
+        }
         mSelectedItems.remove(item.getContentUri());
-        mSelectedItemSize.postValue(mSelectedItems.size());
+        mSelectedItemSize.postValue(getTotalItemsCount());
         updateSelectionAllowed();
     }
 
     /**
-     * Clear all selected items
+     * Remove the {@code item} index from the checked item  index list {@link #mCheckedItemIndexes}.
+     *
+     * @param item the item to be removed from the selected item list
+     */
+    public void removeCheckedItemIndex(Item item) {
+        mCheckedItemIndexes.remove(item);
+    }
+
+    /**
+     * Clear all selected items and checked positions
      */
     public void clearSelectedItems() {
+        mSelectedItemsOrder.clear();
         mSelectedItems.clear();
-        mSelectedItemSize.postValue(mSelectedItems.size());
+        mCheckedItemIndexes.clear();
+        mSelectedItemSize.postValue(getTotalItemsCount());
         updateSelectionAllowed();
     }
 
     /**
+     * Clear all checked items
+     */
+    public void clearCheckedItemList() {
+        mCheckedItemIndexes.clear();
+    }
+
+    /**
      * @return {@code true} if give {@code item} is present in selected items
      *         {@link #mSelectedItems}, {@code false} otherwise
      */
@@ -113,7 +275,7 @@
 
     private void updateSelectionAllowed() {
         final int size = mSelectedItems.size();
-        if (size >= mMaxSelectionLimit) {
+        if (size  - countOfPreGrantedItems() >= mMaxSelectionLimit) {
             if (mIsSelectionAllowed) {
                 mIsSelectionAllowed = false;
             }
@@ -125,6 +287,14 @@
         }
     }
 
+    private int countOfPreGrantedItems() {
+        if (mSelectedItems.values() != null) {
+            return (int) mSelectedItems.values().stream().filter(Item::isPreGranted).count();
+        } else {
+            return 0;
+        }
+    }
+
     /**
      * @return returns whether more items can be selected or not. {@code true} if the number of
      *         selected items is lower than or equal to {@code mMaxLimit}, {@code false} otherwise.
@@ -163,11 +333,15 @@
         final Bundle extras = intent.getExtras();
         final boolean isExtraPickImagesMaxSet =
                 extras != null && extras.containsKey(MediaStore.EXTRA_PICK_IMAGES_MAX);
+        final boolean isExtraOrderedSelectionSet =
+                extras != null && extras.containsKey(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER);
 
         if (intent.getAction() != null
                 && intent.getAction().equals(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)) {
             // If this is picking media for an app, enable multiselect.
             mSelectMultiple = true;
+            // disable ordered selection.
+            mIsSelectionOrdered = false;
             // Allow selections up to the limit.
             // TODO(b/255301849): Update max limit after discussing with product team.
             mMaxSelectionLimit = MediaStore.getPickImagesMaxLimit();
@@ -181,6 +355,11 @@
                         "EXTRA_PICK_IMAGES_MAX is not supported for " + "ACTION_GET_CONTENT");
             }
 
+            if (isExtraOrderedSelectionSet) {
+                throw new IllegalArgumentException(
+                        "EXTRA_PICK_IMAGES_IN_ORDER is not supported for ACTION_GET_CONTENT");
+            }
+
             mSelectMultiple = intent.getBooleanExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
             if (mSelectMultiple) {
                 mMaxSelectionLimit = MediaStore.getPickImagesMaxLimit();
@@ -189,6 +368,10 @@
             return;
         }
 
+        if (isExtraOrderedSelectionSet) {
+            mIsSelectionOrdered = extras.getBoolean(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER);
+        }
+
         // Check EXTRA_PICK_IMAGES_MAX value only if the flag is set.
         if (isExtraPickImagesMaxSet) {
             final int extraMax =
@@ -202,6 +385,7 @@
             mSelectMultiple = true;
             mMaxSelectionLimit = extraMax;
         }
+
     }
 
     /**
@@ -211,6 +395,11 @@
         return mSelectMultiple;
     }
 
+    /** Return whether ordered selection is enabled or not. */
+    public boolean isSelectionOrdered() {
+        return mIsSelectionOrdered;
+    }
+
     /**
      * Return maximum limit of items that can be selected
      */
diff --git a/src/com/android/providers/media/photopicker/data/glide/GlideLoadable.java b/src/com/android/providers/media/photopicker/data/glide/GlideLoadable.java
new file mode 100644
index 0000000..7b536a4
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/data/glide/GlideLoadable.java
@@ -0,0 +1,70 @@
+/*
+ * 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.photopicker.data.glide;
+
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.providers.media.photopicker.data.model.Category;
+import com.android.providers.media.photopicker.data.model.Item;
+
+import com.bumptech.glide.signature.ObjectKey;
+
+import java.util.Optional;
+
+/**
+ * A data class to coalesce {@link Item} and {@link Category} into a common loadable glide object,
+ * with the relevant data required for Glide loading.
+ */
+public class GlideLoadable {
+
+    private final Optional<String> mCacheKey;
+
+    @NonNull private final Uri mUri;
+
+    public GlideLoadable(@NonNull Uri uri) {
+        this(uri, /* cacheKey= */ null);
+    }
+
+    public GlideLoadable(@NonNull Uri uri, @Nullable String cacheKey) {
+        this.mUri = uri;
+        this.mCacheKey = Optional.ofNullable(cacheKey);
+    }
+
+    /**
+     * Get a signature string to represent this item in the Glide cache.
+     *
+     * @param prefix Optional prefix to prepend to this item's signature.
+     * @return A glide cache signature string.
+     */
+    @Nullable
+    public ObjectKey getLoadableSignature(@Nullable String prefix) {
+        return new ObjectKey(
+                Optional.ofNullable(prefix).orElse("") + mUri.toString() + mCacheKey.orElse(""));
+    }
+    ;
+
+    /**
+     * @return A {@link Uri} object to locate the media for this loadable.
+     */
+    public Uri getLoadableUri() {
+        return mUri;
+    }
+    ;
+}
diff --git a/src/com/android/providers/media/photopicker/data/glide/PickerGlideModule.java b/src/com/android/providers/media/photopicker/data/glide/PickerGlideModule.java
index cc48670..8ef68d9 100644
--- a/src/com/android/providers/media/photopicker/data/glide/PickerGlideModule.java
+++ b/src/com/android/providers/media/photopicker/data/glide/PickerGlideModule.java
@@ -17,7 +17,6 @@
 package com.android.providers.media.photopicker.data.glide;
 
 import android.content.Context;
-import android.net.Uri;
 
 import com.bumptech.glide.Glide;
 import com.bumptech.glide.Registry;
@@ -34,6 +33,7 @@
 
     @Override
     public void registerComponents(Context context, Glide glide, Registry registry) {
-        registry.prepend(Uri.class, InputStream.class, new PickerModelLoaderFactory(context));
+        registry.append(
+                GlideLoadable.class, InputStream.class, new PickerModelLoaderFactory(context));
     }
 }
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 1f3bb4c..f381630 100644
--- a/src/com/android/providers/media/photopicker/data/glide/PickerModelLoader.java
+++ b/src/com/android/providers/media/photopicker/data/glide/PickerModelLoader.java
@@ -20,7 +20,6 @@
 
 import android.content.Context;
 import android.content.UriMatcher;
-import android.net.Uri;
 import android.provider.CloudMediaProviderContract;
 
 import com.bumptech.glide.load.Options;
@@ -29,10 +28,8 @@
 
 import java.io.InputStream;
 
-/**
- * Custom {@link ModelLoader} to load thumbnails from cloud media provider.
- */
-public final class PickerModelLoader implements ModelLoader<Uri, InputStream> {
+/** Custom {@link ModelLoader} to load thumbnails from cloud media provider. */
+public final class PickerModelLoader implements ModelLoader<GlideLoadable, InputStream> {
     private final Context mContext;
 
     PickerModelLoader(Context context) {
@@ -40,21 +37,24 @@
     }
 
     @Override
-    public LoadData<InputStream> buildLoadData(Uri model, int width, int height,
-            Options options) {
+    public LoadData<InputStream> buildLoadData(
+            GlideLoadable model, int width, int height, Options options) {
         final boolean isThumbRequest = Boolean.TRUE.equals(options.get(THUMBNAIL_REQUEST));
-        return new LoadData<>(new ObjectKey(model),
+        return new LoadData<>(
+                new ObjectKey(model.getLoadableSignature(/* prefix= */ null)),
                 new PickerThumbnailFetcher(mContext, model, width, height, isThumbRequest));
     }
 
     @Override
-    public boolean handles(Uri model) {
+    public boolean handles(GlideLoadable model) {
         final int pickerId = 1;
         final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
-        matcher.addURI(model.getAuthority(),
-                CloudMediaProviderContract.URI_PATH_MEDIA + "/*", pickerId);
+        matcher.addURI(
+                model.getLoadableUri().getAuthority(),
+                CloudMediaProviderContract.URI_PATH_MEDIA + "/*",
+                pickerId);
 
         // Matches picker URIs of the form content://<authority>/media
-        return matcher.match(model) == pickerId;
+        return matcher.match(model.getLoadableUri()) == pickerId;
     }
 }
diff --git a/src/com/android/providers/media/photopicker/data/glide/PickerModelLoaderFactory.java b/src/com/android/providers/media/photopicker/data/glide/PickerModelLoaderFactory.java
index 938ef88..d7086d4 100644
--- a/src/com/android/providers/media/photopicker/data/glide/PickerModelLoaderFactory.java
+++ b/src/com/android/providers/media/photopicker/data/glide/PickerModelLoaderFactory.java
@@ -17,7 +17,6 @@
 package com.android.providers.media.photopicker.data.glide;
 
 import android.content.Context;
-import android.net.Uri;
 
 import com.bumptech.glide.load.model.ModelLoader;
 import com.bumptech.glide.load.model.ModelLoaderFactory;
@@ -29,7 +28,7 @@
  * Custom {@link ModelLoaderFactory} which provides a {@link ModelLoader} for loading thumbnails
  * from cloud media provider.
  */
-public class PickerModelLoaderFactory implements ModelLoaderFactory<Uri, InputStream> {
+public class PickerModelLoaderFactory implements ModelLoaderFactory<GlideLoadable, InputStream> {
 
     private final Context mContext;
 
@@ -38,7 +37,7 @@
     }
 
     @Override
-    public ModelLoader<Uri, InputStream> build(MultiModelLoaderFactory unused) {
+    public ModelLoader<GlideLoadable, InputStream> build(MultiModelLoaderFactory unused) {
         return new PickerModelLoader(mContext);
     }
 
diff --git a/src/com/android/providers/media/photopicker/data/glide/PickerPreloadModelProvider.java b/src/com/android/providers/media/photopicker/data/glide/PickerPreloadModelProvider.java
new file mode 100644
index 0000000..9e5b4dd
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/data/glide/PickerPreloadModelProvider.java
@@ -0,0 +1,101 @@
+/*
+ * 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.photopicker.data.glide;
+
+import static com.android.providers.media.photopicker.ui.ImageLoader.THUMBNAIL_REQUEST;
+
+import static com.bumptech.glide.load.resource.bitmap.Downsampler.PREFERRED_COLOR_SPACE;
+
+import android.content.Context;
+import android.provider.MediaStore;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.providers.media.photopicker.data.model.Item;
+import com.android.providers.media.photopicker.ui.PhotosTabAdapter;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.ListPreloader.PreloadModelProvider;
+import com.bumptech.glide.RequestBuilder;
+import com.bumptech.glide.load.PreferredColorSpace;
+import com.bumptech.glide.request.RequestOptions;
+import com.bumptech.glide.signature.ObjectKey;
+
+import java.util.Collections;
+import java.util.List;
+
+/** Custom glide module to enable the loading of thumbnails from CloudMediaProvider. */
+public class PickerPreloadModelProvider implements PreloadModelProvider<GlideLoadable> {
+
+    private final Context mContext;
+    private final PreferredColorSpace mPreferredColorSpace;
+    private final PhotosTabAdapter mAdapter;
+
+    public PickerPreloadModelProvider(Context context, PhotosTabAdapter adapter) {
+        mContext = context;
+        mAdapter = adapter;
+
+        final boolean isScreenWideColorGamut =
+                mContext.getResources().getConfiguration().isScreenWideColorGamut();
+        mPreferredColorSpace =
+                isScreenWideColorGamut ? PreferredColorSpace.DISPLAY_P3 : PreferredColorSpace.SRGB;
+    }
+
+    /**
+     * Return a list of items that should be preloaded for the given RecyclerView adapter position.
+     *
+     * @param position the current position of the RecyclerView's adapter.
+     * @return A list of items to begin preloading.
+     */
+    @Override
+    @NonNull
+    public List<GlideLoadable> getPreloadItems(int position) {
+        if (mAdapter.isItemTypeMediaItem(position)) {
+            Object adapterItem = mAdapter.getAdapterItem(position);
+            if (adapterItem instanceof Item) {
+                Item item = (Item) adapterItem;
+                return Collections.singletonList(item.toGlideLoadable());
+            }
+        }
+        return Collections.emptyList();
+    }
+
+    /**
+     * This should generate a load request identical to the load request generated by the
+     * RecyclerView itself. This ensures that there are not inadvertent cache misses because the
+     * preload succeeded, but the actual RecyclerView request didn't match what was in the cache
+     * already.
+     *
+     * @param loadable The {@link GlideLoadable} model for the thumbnail.
+     * @return An identical glide RequestBuilder to what the RecyclerView will generate when it
+     *     attempts to load this item.
+     */
+    @Override
+    @Nullable
+    public RequestBuilder getPreloadRequestBuilder(GlideLoadable loadable) {
+        RequestOptions options =
+                RequestOptions.option(THUMBNAIL_REQUEST, true)
+                        .set(PREFERRED_COLOR_SPACE, mPreferredColorSpace);
+        // TODO(b/224725723): Remove media store version from key once MP ids are
+        // stable.
+        ObjectKey signature =
+                loadable.getLoadableSignature(/* prefix= */ MediaStore.getVersion(mContext));
+
+        return Glide.with(mContext).asBitmap().apply(options).signature(signature).load(loadable);
+    }
+}
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 0d85196..b4ed01a 100644
--- a/src/com/android/providers/media/photopicker/data/glide/PickerThumbnailFetcher.java
+++ b/src/com/android/providers/media/photopicker/data/glide/PickerThumbnailFetcher.java
@@ -20,43 +20,46 @@
 import android.content.Context;
 import android.content.res.AssetFileDescriptor;
 import android.graphics.Point;
-import android.net.Uri;
 import android.os.Bundle;
+import android.os.CancellationSignal;
 import android.provider.CloudMediaProviderContract;
+import android.provider.MediaStore;
 import android.util.Log;
 
-import com.bumptech.glide.Glide;
+import androidx.annotation.Nullable;
+
 import com.bumptech.glide.Priority;
 import com.bumptech.glide.load.DataSource;
-import com.bumptech.glide.load.ImageHeaderParserUtils;
 import com.bumptech.glide.load.data.DataFetcher;
-import com.bumptech.glide.load.data.ExifOrientationStream;
-
 
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 
 /**
- * Custom {@link DataFetcher} to fetch a {@link InputStream} for a thumbnail from a cloud
- * media provider.
+ * Custom {@link DataFetcher} to fetch a {@link InputStream} for a thumbnail from a cloud media
+ * provider.
  */
 public class PickerThumbnailFetcher implements DataFetcher<InputStream> {
 
     private static final String TAG = "PickerThumbnailFetcher";
     private final Context mContext;
-    private final Uri mModel;
+    private final GlideLoadable mModel;
     private final int mWidth;
     private final int mHeight;
     private final boolean mIsThumbRequest;
+    private final CancellationSignal mCancellationSignal;
+    @Nullable private AssetFileDescriptor mAssetFileDescriptor = null;
+    @Nullable private InputStream mInputStream = null;
 
-    PickerThumbnailFetcher(Context context, Uri model, int width, int height,
-            boolean isThumbRequest) {
+    PickerThumbnailFetcher(
+            Context context, GlideLoadable model, int width, int height, boolean isThumbRequest) {
         mContext = context;
         mModel = model;
         mWidth = width;
         mHeight = height;
         mIsThumbRequest = isThumbRequest;
+        mCancellationSignal = new CancellationSignal();
     }
 
     @Override
@@ -70,57 +73,50 @@
             opts.putBoolean(CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB, true);
         }
 
-        try (AssetFileDescriptor afd = contentResolver.openTypedAssetFileDescriptor(mModel,
-                /* mimeType */ "image/*", opts, /* cancellationSignal */ null)) {
-            if (afd == null) {
+        try {
+            // Do not close the afd or InputStream as it will close the input stream. The
+            // afd needs to be closed when cleanup is called, so save a reference so it can
+            // be closed when Glide is done with it.
+            mAssetFileDescriptor =
+                    contentResolver.openTypedAssetFileDescriptor(
+                            mModel.getLoadableUri(),
+                            /* mimeType= */ "image/*",
+                            opts,
+                            /* cancellationSignal= */ mCancellationSignal);
+            if (mAssetFileDescriptor == null) {
                 final String err = "Failed to load data for " + mModel;
                 callback.onLoadFailed(new FileNotFoundException(err));
                 return;
             }
-
-            final InputStream inputStream;
-            if (mIsThumbRequest) {
-                inputStream = getOrientationInputStream(afd);
-            } else {
-                // We don't need to handle orientation for preview requests. Glide load takes care
-                // of loading the image in the right orientation.
-                inputStream = afd.createInputStream();
-            }
-            callback.onDataReady(inputStream);
+            mInputStream = mAssetFileDescriptor.createInputStream();
+            callback.onDataReady(mInputStream);
         } catch (IOException e) {
             callback.onLoadFailed(e);
         }
     }
 
-    private InputStream getOrientationInputStream(AssetFileDescriptor afd) throws IOException {
-        InputStream inputStream = afd.createInputStream();
-
-        int orientation = -1;
-        if (inputStream != null) {
-            try {
-                orientation = ImageHeaderParserUtils.getOrientation(
-                        Glide.get(mContext).getRegistry().getImageHeaderParsers(), inputStream,
-                        Glide.get(mContext).getArrayPool());
-            } catch (IOException | NullPointerException ignored) {
-                Log.d(TAG, "Unable to fetch orientation for " + mModel, ignored);
-            }
-        }
-
-        if (orientation != -1) {
-            inputStream = new ExifOrientationStream(inputStream, orientation);
-        }
-        return inputStream;
-    }
-
+    /**
+     * Cleanup is called after Glide is done with this Fetcher instance, and it is now safe to close
+     * the remembered AssetFileDescriptor.
+     */
     @Override
     public void cleanup() {
-        // Intentionally empty only because we're not opening an InputStream or another I/O
-        // resource.
+        try {
+            if (mInputStream != null) {
+                mInputStream.close();
+            }
+
+            if (mAssetFileDescriptor != null) {
+                mAssetFileDescriptor.close();
+            }
+        } catch (IOException e) {
+            Log.d(TAG, "Unexpected error during thumbnail request cleanup.", e);
+        }
     }
 
     @Override
     public void cancel() {
-        // Intentionally empty.
+        mCancellationSignal.cancel();
     }
 
     @Override
@@ -130,6 +126,13 @@
 
     @Override
     public DataSource getDataSource() {
-        return DataSource.LOCAL;
+        // If the authority belongs to MediaProvider, we can consider this a local load.
+        if (mModel.getLoadableUri().getAuthority().equals(MediaStore.AUTHORITY)) {
+            return DataSource.LOCAL;
+        } else {
+            // Otherwise, let's assume it's a Remote data source so that Glide will cache
+            // the raw return value rather than manipulated bytes.
+            return DataSource.REMOTE;
+        }
     }
 }
diff --git a/src/com/android/providers/media/photopicker/data/model/Category.java b/src/com/android/providers/media/photopicker/data/model/Category.java
index 90a2143..927a031 100644
--- a/src/com/android/providers/media/photopicker/data/model/Category.java
+++ b/src/com/android/providers/media/photopicker/data/model/Category.java
@@ -39,7 +39,9 @@
 
 import com.android.providers.media.R;
 import com.android.providers.media.photopicker.data.ItemsProvider;
+import com.android.providers.media.photopicker.data.glide.GlideLoadable;
 
+import java.util.List;
 import java.util.Locale;
 
 /**
@@ -48,6 +50,9 @@
 public class Category {
     public static final String TAG = "PhotoPicker";
     public static final Category DEFAULT = new Category();
+    public static final Category EMPTY_VIEW = new Category("EMPTY_VIEW");
+    private static final List<String> TRANSLATABLE_CATEGORIES = List.of(ALBUM_ID_VIDEOS,
+            ALBUM_ID_CAMERA, ALBUM_ID_SCREENSHOTS, ALBUM_ID_DOWNLOADS, ALBUM_ID_FAVORITES);
 
     private final String mId;
     private final String mAuthority;
@@ -60,6 +65,9 @@
         this(null, null, null, null, 0, false);
     }
 
+    private Category(String id) {
+        this(id, null, null, null, 0, false);
+    }
     @VisibleForTesting
     public Category(String id, String authority, String displayName, Uri coverUri, int itemCount,
             boolean isLocal) {
@@ -74,7 +82,7 @@
     @Override
     public String toString() {
         return String.format(Locale.ROOT, "Category: {mId: %s, mAuthority: %s, mDisplayName: %s, " +
-                "mCoverUri: %s, mItemCount: %d, mIsLocal: %b",
+                        "mCoverUri: %s, mItemCount: %d, mIsLocal: %b",
                 mId, mAuthority, mDisplayName, mCoverUri, mItemCount, mIsLocal);
     }
 
@@ -87,7 +95,7 @@
     }
 
     public String getDisplayName(Context context) {
-        if (mIsLocal) {
+        if (TRANSLATABLE_CATEGORIES.contains(mId)) {
             return getLocalizedDisplayName(context, mId);
         }
         return mDisplayName;
@@ -159,7 +167,7 @@
         return new Category(getCursorString(cursor, AlbumColumns.ID),
                 authority,
                 getCursorString(cursor, AlbumColumns.DISPLAY_NAME),
-                coverUri,
+                getCursorString(cursor, AlbumColumns.MEDIA_COVER_ID) != null ? coverUri : null,
                 getCursorInt(cursor, AlbumColumns.MEDIA_COUNT),
                 isLocal);
     }
@@ -180,4 +188,13 @@
                 return albumId;
         }
     }
+
+    /**
+     * Convert this category into a loadable object for Glide.
+     *
+     * @return {@link GlideLoadable} that represents the relevant loadable data for this item.
+     */
+    public GlideLoadable toGlideLoadable() {
+        return new GlideLoadable(getCoverUri());
+    }
 }
diff --git a/src/com/android/providers/media/photopicker/data/model/Item.java b/src/com/android/providers/media/photopicker/data/model/Item.java
index eee2e83..83b0bdb 100644
--- a/src/com/android/providers/media/photopicker/data/model/Item.java
+++ b/src/com/android/providers/media/photopicker/data/model/Item.java
@@ -21,6 +21,7 @@
 import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_GIF;
 import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_MOTION_PHOTO;
 
+import static com.android.providers.media.photopicker.PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
 import static com.android.providers.media.photopicker.util.CursorUtils.getCursorInt;
 import static com.android.providers.media.photopicker.util.CursorUtils.getCursorLong;
 import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString;
@@ -37,13 +38,23 @@
 
 import com.android.providers.media.R;
 import com.android.providers.media.photopicker.data.ItemsProvider;
+import com.android.providers.media.photopicker.data.glide.GlideLoadable;
 import com.android.providers.media.photopicker.util.DateTimeUtils;
 import com.android.providers.media.util.MimeUtils;
 
+import java.util.Objects;
+
 /**
  * Base class for representing a single media item (a picture, a video, etc.) in the PhotoPicker.
  */
 public class Item {
+    public static final Item EMPTY_VIEW = new Item("EMPTY_VIEW");
+    public static final String ROW_ID = "row_id";
+
+    /**
+     * This id represents the cloud id or the local id of the media, with priority given to cloud id
+     * if present.
+     */
     private String mId;
     private long mDateTaken;
     private long mGenerationModified;
@@ -54,6 +65,13 @@
     private boolean mIsVideo;
     private int mSpecialFormat;
 
+    private boolean mIsPreGranted;
+
+    /**
+     * This is the row id for the item in the db.
+     */
+    private int mRowId;
+
     public Item(@NonNull Cursor cursor, @NonNull UserId userId) {
         updateFromCursor(cursor, userId);
     }
@@ -71,6 +89,10 @@
         parseMimeType();
     }
 
+    private Item(String id) {
+        this(id, null, 0, 0, 0, null, 0);
+    }
+
     public String getId() {
         return mId;
     }
@@ -124,6 +146,20 @@
         return mSpecialFormat;
     }
 
+    public int getRowId() {
+        return mRowId;
+    }
+
+    /**
+     * Setting this represents that the item has READ_GRANT for the current package.
+     */
+    public void setPreGranted() {
+        mIsPreGranted = true;
+    }
+    public boolean isPreGranted() {
+        return mIsPreGranted;
+    }
+
     public static Item fromCursor(@NonNull Cursor cursor, UserId userId) {
         return new Item(requireNonNull(cursor), userId);
     }
@@ -143,6 +179,7 @@
         mDuration = getCursorLong(cursor, MediaColumns.DURATION_MILLIS);
         mSpecialFormat = getCursorInt(cursor, MediaColumns.STANDARD_MIME_TYPE_EXTENSION);
         mUri = ItemsProvider.getItemsUri(mId, authority, userId);
+        mRowId = getCursorInt(cursor, ROW_ID);
 
         parseMimeType();
     }
@@ -196,4 +233,34 @@
             return mId.compareTo(anotherItem.getId());
         }
     }
+
+    /**
+     * @return {@code true} iff this item is local (available on device), {@code false} otherwise.
+     */
+    public boolean isLocal() {
+        return LOCAL_PICKER_PROVIDER_AUTHORITY.equals(mUri.getAuthority());
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (obj == null || !(obj instanceof Item)) return false;
+
+        Item other = (Item) obj;
+        return mUri.equals(other.mUri);
+    }
+
+    @Override public int hashCode() {
+        return Objects.hash(mUri);
+    }
+
+    /**
+     * Convert this item into a loadable object for Glide.
+     *
+     * @return {@link GlideLoadable} that represents the relevant loadable data for this item.
+     */
+    public GlideLoadable toGlideLoadable() {
+        return new GlideLoadable(mUri, String.valueOf(getGenerationModified()));
+    }
+
 }
diff --git a/src/com/android/providers/media/photopicker/metrics/NonUiEventLogger.java b/src/com/android/providers/media/photopicker/metrics/NonUiEventLogger.java
new file mode 100644
index 0000000..b15d2ed
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/metrics/NonUiEventLogger.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2023 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.metrics;
+
+import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.InstanceIdSequence;
+import com.android.internal.logging.UiEvent;
+import com.android.internal.logging.UiEventLogger;
+import com.android.providers.media.metrics.MPUiEventLoggerImpl;
+
+/**
+ * Logger for the Non UI Events triggered indirectly by some UI event(s).
+ */
+public class NonUiEventLogger {
+    enum NonUiEvent implements UiEventLogger.UiEventEnum {
+        @UiEvent(doc = "User changed the active Photo picker cloud provider")
+        PHOTO_PICKER_CLOUD_PROVIDER_CHANGED(1135),
+        @UiEvent(doc = "Photo Picker uri is queried with an unknown column")
+        PHOTO_PICKER_QUERY_UNKNOWN_COLUMN(1227),
+        @UiEvent(doc = "Triggered a full sync in photo picker")
+        PHOTO_PICKER_FULL_SYNC_START(1442),
+        @UiEvent(doc = "Triggered an incremental sync in photo picker")
+        PHOTO_PICKER_INCREMENTAL_SYNC_START(1443),
+        @UiEvent(doc = "Triggered an album media sync in photo picker")
+        PHOTO_PICKER_ALBUM_MEDIA_SYNC_START(1444),
+        @UiEvent(doc = "Triggered get media collection info in photo picker")
+        PHOTO_PICKER_GET_MEDIA_COLLECTION_INFO_START(1448),
+        @UiEvent(doc = "Triggered get albums in photo picker")
+        PHOTO_PICKER_GET_ALBUMS_START(1449),
+        @UiEvent(doc = "Ended an add media sync in photo picker")
+        PHOTO_PICKER_ADD_MEDIA_SYNC_END(1445),
+        @UiEvent(doc = "Ended a remove media sync in photo picker")
+        PHOTO_PICKER_REMOVE_MEDIA_SYNC_END(1446),
+        @UiEvent(doc = "Ended an add album media sync in photo picker")
+        PHOTO_PICKER_ADD_ALBUM_MEDIA_SYNC_END(1447),
+        @UiEvent(doc = "Ended get media collection info in photo picker")
+        PHOTO_PICKER_GET_MEDIA_COLLECTION_INFO_END(1450),
+        @UiEvent(doc = "Ended get albums in photo picker")
+        PHOTO_PICKER_GET_ALBUMS_END(1451),
+        @UiEvent(doc = "Read grants added count.")
+        PHOTO_PICKER_GRANTS_ADDED_COUNT(1528),
+        @UiEvent(doc = "Read grants revoked count.")
+        PHOTO_PICKER_GRANTS_REVOKED_COUNT(1529),
+        @UiEvent(doc = "Total initial grants count.")
+        PHOTO_PICKER_INIT_GRANTS_COUNT(1530);
+
+        private final int mId;
+
+        NonUiEvent(int id) {
+            mId = id;
+        }
+
+        @Override
+        public int getId() {
+            return mId;
+        }
+    }
+
+    private static final int INSTANCE_ID_MAX = 1 << 15;
+    private static final InstanceIdSequence INSTANCE_ID_SEQUENCE =
+            new InstanceIdSequence(INSTANCE_ID_MAX);
+    private static final UiEventLogger LOGGER = new MPUiEventLoggerImpl();
+
+    /**
+     * Generate and {@return} a new unique instance id to group some events for aggregated metrics
+     */
+    public static InstanceId generateInstanceId() {
+        return INSTANCE_ID_SEQUENCE.newInstanceId();
+    }
+
+    /**
+     * Log metrics to notify that the user has changed the active cloud provider
+     * @param cloudProviderUid     new active cloud provider uid
+     * @param cloudProviderPackage new active cloud provider package name
+     */
+    public static void logPickerCloudProviderChanged(int cloudProviderUid,
+            String cloudProviderPackage) {
+        LOGGER.log(NonUiEvent.PHOTO_PICKER_CLOUD_PROVIDER_CHANGED, cloudProviderUid,
+                cloudProviderPackage);
+    }
+
+    /**
+     * Log metrics to notify that a picker uri was queried for an unknown column (that is not
+     * supported yet)
+     * @param callingUid              the uid of the app initiating the picker query
+     * @param callingPackageAndColumn the package name of the app initiating the picker query,
+     *                                followed by the unknown column name, separated by a ':'
+     */
+    public static void logPickerQueriedWithUnknownColumn(int callingUid,
+            String callingPackageAndColumn) {
+        LOGGER.log(NonUiEvent.PHOTO_PICKER_QUERY_UNKNOWN_COLUMN, callingUid,
+                callingPackageAndColumn);
+    }
+
+    /**
+     * Log metrics to notify that a full sync started
+     * @param instanceId an identifier for the current sync
+     * @param uid        the uid of the MediaProvider logging this metric
+     * @param authority  the authority of the provider syncing with
+     */
+    public static void logPickerFullSyncStart(InstanceId instanceId, int uid, String authority) {
+        LOGGER.logWithInstanceId(NonUiEvent.PHOTO_PICKER_FULL_SYNC_START, uid, authority,
+                instanceId);
+    }
+
+    /**
+     * Log metrics to notify that an incremental sync started
+     * @param instanceId an identifier for the current sync
+     * @param uid        the uid of the MediaProvider logging this metric
+     * @param authority  the authority of the provider syncing with
+     */
+    public static void logPickerIncrementalSyncStart(InstanceId instanceId, int uid,
+            String authority) {
+        LOGGER.logWithInstanceId(NonUiEvent.PHOTO_PICKER_INCREMENTAL_SYNC_START, uid, authority,
+                instanceId);
+    }
+
+    /**
+     * Log metrics to notify that an album media sync started
+     * @param instanceId an identifier for the current sync
+     * @param uid        the uid of the MediaProvider logging this metric
+     * @param authority  the authority of the provider syncing with
+     */
+    public static void logPickerAlbumMediaSyncStart(InstanceId instanceId, int uid,
+            String authority) {
+        LOGGER.logWithInstanceId(NonUiEvent.PHOTO_PICKER_ALBUM_MEDIA_SYNC_START, uid, authority,
+                instanceId);
+    }
+
+    /**
+     * Log metrics to notify get media collection info triggered
+     * @param instanceId an identifier for the current query session
+     * @param uid        the uid of the MediaProvider logging this metric
+     * @param authority  the authority of the provider
+     */
+    public static void logPickerGetMediaCollectionInfoStart(InstanceId instanceId, int uid,
+            String authority) {
+        LOGGER.logWithInstanceId(NonUiEvent.PHOTO_PICKER_GET_MEDIA_COLLECTION_INFO_START, uid,
+                authority, instanceId);
+    }
+
+    /**
+     * Log metrics to notify get albums triggered
+     * @param instanceId an identifier for the current query session
+     * @param uid        the uid of the MediaProvider logging this metric
+     * @param authority  the authority of the provider
+     */
+    public static void logPickerGetAlbumsStart(InstanceId instanceId, int uid, String authority) {
+        LOGGER.logWithInstanceId(NonUiEvent.PHOTO_PICKER_GET_ALBUMS_START, uid, authority,
+                instanceId);
+    }
+
+    /**
+     * Log metrics to notify that an add media sync ended
+     * @param instanceId an identifier for the current sync
+     * @param uid        the uid of the MediaProvider logging this metric
+     * @param authority  the authority of the provider syncing with
+     * @param count      the number of items synced
+     */
+    public static void logPickerAddMediaSyncCompletion(InstanceId instanceId, int uid,
+            String authority, int count) {
+        LOGGER.logWithInstanceIdAndPosition(NonUiEvent.PHOTO_PICKER_ADD_MEDIA_SYNC_END, uid,
+                authority, instanceId, count);
+    }
+
+    /**
+     * Log metrics to notify that a remove media sync ended
+     * @param instanceId an identifier for the current sync
+     * @param uid        the uid of the MediaProvider logging this metric
+     * @param authority  the authority of the provider syncing with
+     * @param count      the number of items synced
+     */
+    public static void logPickerRemoveMediaSyncCompletion(InstanceId instanceId, int uid,
+            String authority, int count) {
+        LOGGER.logWithInstanceIdAndPosition(NonUiEvent.PHOTO_PICKER_REMOVE_MEDIA_SYNC_END, uid,
+                authority, instanceId, count);
+    }
+
+    /**
+     * Log metrics to notify that an add album media sync ended
+     * @param instanceId an identifier for the current sync
+     * @param uid        the uid of the MediaProvider logging this metric
+     * @param authority  the authority of the provider syncing with
+     * @param count      the number of items synced
+     */
+    public static void logPickerAddAlbumMediaSyncCompletion(InstanceId instanceId, int uid,
+            String authority, int count) {
+        LOGGER.logWithInstanceIdAndPosition(NonUiEvent.PHOTO_PICKER_ADD_ALBUM_MEDIA_SYNC_END, uid,
+                authority, instanceId, count);
+    }
+
+    /**
+     * Log metrics to notify get media collection info ended
+     * @param instanceId an identifier for the current query session
+     * @param uid        the uid of the MediaProvider logging this metric
+     * @param authority  the authority of the provider
+     */
+    public static void logPickerGetMediaCollectionInfoEnd(InstanceId instanceId, int uid,
+            String authority) {
+        LOGGER.logWithInstanceId(NonUiEvent.PHOTO_PICKER_GET_MEDIA_COLLECTION_INFO_END, uid,
+                authority, instanceId);
+    }
+
+    /**
+     * Log metrics to notify get albums ended
+     * @param instanceId an identifier for the current query session
+     * @param uid        the uid of the MediaProvider logging this metric
+     * @param authority  the authority of the provider
+     * @param count      the number of albums fetched
+     */
+    public static void logPickerGetAlbumsEnd(InstanceId instanceId, int uid, String authority,
+            int count) {
+        LOGGER.logWithInstanceIdAndPosition(NonUiEvent.PHOTO_PICKER_GET_ALBUMS_END, uid, authority,
+                instanceId, count);
+    }
+
+    /**
+     * Log metrics for count of grants added for a package.
+     * @param instanceId   an identifier for the current session
+     * @param uid          the uid of the MediaProvider logging this metric
+     * @param packageName  the package name receiving the grant.
+     * @param count        the number of items for which the grants have been added.
+     */
+    public static void logPickerChoiceGrantsAdditionCount(InstanceId instanceId, int uid,
+            String packageName, int count) {
+        LOGGER.logWithInstanceIdAndPosition(NonUiEvent.PHOTO_PICKER_GRANTS_ADDED_COUNT, uid,
+                packageName, instanceId, count);
+    }
+
+    /**
+     * Log metrics for count of grants revoked for a package.
+     * @param instanceId   an identifier for the current session
+     * @param uid          the uid of the MediaProvider logging this metric
+     * @param packageName  the package name for which the grants are being revoked.
+     * @param count        the number of items for which the grants have been revoked.
+     */
+    public static void logPickerChoiceGrantsRemovedCount(InstanceId instanceId, int uid,
+            String packageName, int count) {
+        LOGGER.logWithInstanceIdAndPosition(NonUiEvent.PHOTO_PICKER_GRANTS_REVOKED_COUNT, uid,
+                packageName, instanceId, count);
+    }
+
+    /**
+     * Log metrics for total count of grants previously added for the package.
+     * @param instanceId   an identifier for the current session
+     * @param uid          the uid of the MediaProvider logging this metric
+     * @param packageName  the package name for which the grants are being initialized.
+     * @param count        the number of items for which the grants have been initialized.
+     */
+    public static void logPickerChoiceInitGrantsCount(InstanceId instanceId, int uid,
+            String packageName, int count) {
+        LOGGER.logWithInstanceIdAndPosition(NonUiEvent.PHOTO_PICKER_INIT_GRANTS_COUNT, uid,
+                packageName, instanceId, count);
+    }
+
+}
diff --git a/src/com/android/providers/media/photopicker/metrics/PhotoPickerUiEventLogger.java b/src/com/android/providers/media/photopicker/metrics/PhotoPickerUiEventLogger.java
index 6368b74..e127e05 100644
--- a/src/com/android/providers/media/photopicker/metrics/PhotoPickerUiEventLogger.java
+++ b/src/com/android/providers/media/photopicker/metrics/PhotoPickerUiEventLogger.java
@@ -16,6 +16,9 @@
 
 package com.android.providers.media.photopicker.metrics;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
 import com.android.internal.logging.InstanceId;
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
@@ -23,7 +26,8 @@
 
 public class PhotoPickerUiEventLogger {
 
-    enum PhotoPickerEvent implements UiEventLogger.UiEventEnum {
+    @VisibleForTesting
+    public enum PhotoPickerEvent implements UiEventLogger.UiEventEnum {
         @UiEvent(doc = "Photo picker opened in personal profile")
         PHOTO_PICKER_OPEN_PERSONAL_PROFILE(942),
         @UiEvent(doc = "Photo picker opened in work profile")
@@ -56,10 +60,76 @@
         PHOTO_PICKER_CONFIRM_PERSONAL_PROFILE(1128),
         @UiEvent(doc = "Photo picker opened with an active cloud provider")
         PHOTO_PICKER_CLOUD_PROVIDER_ACTIVE(1198),
-        @UiEvent(doc = "User changed the active Photo picker cloud provider")
-        PHOTO_PICKER_CLOUD_PROVIDER_CHANGED(1135),
-        @UiEvent(doc = "Photo Picker uri is queried with an unknown column")
-        PHOTO_PICKER_QUERY_UNKNOWN_COLUMN(1227);
+        @UiEvent(doc = "Clicked the mute / unmute button in a photo picker video preview")
+        PHOTO_PICKER_VIDEO_PREVIEW_AUDIO_BUTTON_CLICK(1413),
+        @UiEvent(doc = "Clicked the 'view selected' button in photo picker")
+        PHOTO_PICKER_PREVIEW_ALL_SELECTED(1414),
+        @UiEvent(doc = "Photo picker opened with the 'switch profile' button visible and enabled")
+        PHOTO_PICKER_PROFILE_SWITCH_BUTTON_ENABLED(1415),
+        @UiEvent(doc = "Photo picker opened with the 'switch profile' button visible but disabled")
+        PHOTO_PICKER_PROFILE_SWITCH_BUTTON_DISABLED(1416),
+        @UiEvent(doc = "Clicked the 'switch profile' button in photo picker")
+        PHOTO_PICKER_PROFILE_SWITCH_BUTTON_CLICK(1417),
+        @UiEvent(doc = "Exited photo picker by swiping down")
+        PHOTO_PICKER_EXIT_SWIPE_DOWN(1420),
+        @UiEvent(doc = "Back pressed in photo picker")
+        PHOTO_PICKER_BACK_GESTURE(1421),
+        @UiEvent(doc = "Action bar home button clicked in photo picker")
+        PHOTO_PICKER_ACTION_BAR_HOME_BUTTON_CLICK(1422),
+        @UiEvent(doc = "Expanded from half screen to full in photo picker")
+        PHOTO_PICKER_FROM_HALF_TO_FULL_SCREEN(1423),
+        @UiEvent(doc = "Photo picker menu opened")
+        PHOTO_PICKER_MENU(1424),
+        @UiEvent(doc = "User switched to the photos tab in photo picker")
+        PHOTO_PICKER_TAB_PHOTOS_OPEN(1425),
+        @UiEvent(doc = "User switched to the albums tab in photo picker")
+        PHOTO_PICKER_TAB_ALBUMS_OPEN(1426),
+        @UiEvent(doc = "Opened the device favorites album in photo picker")
+        PHOTO_PICKER_ALBUM_FAVORITES_OPEN(1427),
+        @UiEvent(doc = "Opened the device camera album in photo picker")
+        PHOTO_PICKER_ALBUM_CAMERA_OPEN(1428),
+        @UiEvent(doc = "Opened the device downloads album in photo picker")
+        PHOTO_PICKER_ALBUM_DOWNLOADS_OPEN(1429),
+        @UiEvent(doc = "Opened the device screenshots album in photo picker")
+        PHOTO_PICKER_ALBUM_SCREENSHOTS_OPEN(1430),
+        @UiEvent(doc = "Opened the device videos album in photo picker")
+        PHOTO_PICKER_ALBUM_VIDEOS_OPEN(1431),
+        @UiEvent(doc = "Opened a cloud album in photo picker")
+        PHOTO_PICKER_ALBUM_FROM_CLOUD_OPEN(1432),
+        @UiEvent(doc = "Selected a media item in the main grid")
+        PHOTO_PICKER_SELECTED_ITEM_MAIN_GRID(1433),
+        @UiEvent(doc = "Selected a media item in an album")
+        PHOTO_PICKER_SELECTED_ITEM_ALBUM(1434),
+        @UiEvent(doc = "Selected a cloud only media item")
+        PHOTO_PICKER_SELECTED_ITEM_CLOUD_ONLY(1435),
+        @UiEvent(doc = "Previewed a media item in the main grid")
+        PHOTO_PICKER_PREVIEW_ITEM_MAIN_GRID(1436),
+        @UiEvent(doc = "Loaded media items in the main grid in photo picker")
+        PHOTO_PICKER_UI_LOADED_PHOTOS(1437),
+        @UiEvent(doc = "Loaded albums in photo picker")
+        PHOTO_PICKER_UI_LOADED_ALBUMS(1438),
+        @UiEvent(doc = "Loaded media items in an album grid in photo picker")
+        PHOTO_PICKER_UI_LOADED_ALBUM_CONTENTS(1439),
+        @UiEvent(doc = "Triggered create surface controller in photo picker")
+        PHOTO_PICKER_CREATE_SURFACE_CONTROLLER_START(1452),
+        @UiEvent(doc = "Ended create surface controller in photo picker")
+        PHOTO_PICKER_CREATE_SURFACE_CONTROLLER_END(1453),
+        @UiEvent(doc = "Started the selected media preloading in photo picker")
+        PHOTO_PICKER_PRELOADING_STARTED(1524),
+        @UiEvent(doc = "Finished the selected media preloading in photo picker")
+        PHOTO_PICKER_PRELOADING_FINISHED(1525),
+        @UiEvent(doc = "User cancelled the selected media preloading in photo picker")
+        PHOTO_PICKER_PRELOADING_CANCELLED(1526),
+        @UiEvent(doc = "Failed to preload some selected media items in photo picker")
+        PHOTO_PICKER_PRELOADING_FAILED(1527),
+        @UiEvent(doc = "The banner is added to display in the recycler view grids in photo picker")
+        PHOTO_PICKER_BANNER_ADDED(1539),
+        @UiEvent(doc = "The user clicks the dismiss button of the banner in photo picker")
+        PHOTO_PICKER_BANNER_DISMISSED(1540),
+        @UiEvent(doc = "The user clicks the action button of the banner in photo picker")
+        PHOTO_PICKER_BANNER_ACTION_BUTTON_CLICKED(1541),
+        @UiEvent(doc = "The user clicks on the remaining part of the banner in photo picker")
+        PHOTO_PICKER_BANNER_CLICKED(1542);
 
         private final int mId;
 
@@ -79,6 +149,11 @@
         logger = new MPUiEventLoggerImpl();
     }
 
+    @VisibleForTesting
+    public PhotoPickerUiEventLogger(@NonNull UiEventLogger logger) {
+        this.logger = logger;
+    }
+
     public void logPickerOpenPersonal(InstanceId instanceId, int callingUid,
             String callingPackage) {
         logger.logWithInstanceId(
@@ -293,26 +368,334 @@
     }
 
     /**
-     * Log metrics to notify that the user has changed the active cloud provider
-     * @param cloudProviderUid     new active cloud provider uid
-     * @param cloudProviderPackage new active cloud provider package name
+     * Log metrics to notify that the user has clicked the mute / unmute button in a video preview
+     * @param instanceId an identifier for the current picker session
      */
-    public void logPickerCloudProviderChanged(int cloudProviderUid, String cloudProviderPackage) {
-        logger.log(PhotoPickerEvent.PHOTO_PICKER_CLOUD_PROVIDER_CHANGED, cloudProviderUid,
-                cloudProviderPackage);
+    public void logVideoPreviewMuteButtonClick(InstanceId instanceId) {
+        logWithInstance(PhotoPickerEvent.PHOTO_PICKER_VIDEO_PREVIEW_AUDIO_BUTTON_CLICK, instanceId);
     }
 
     /**
-     * Log metrics to notify that a picker uri was queried for an unknown column (that is not
-     * supported yet)
-     * @param callingUid     the uid of the app initiating the picker query
-     * @param callingPackage the package name of the app initiating the picker query
-     *
-     * TODO(b/251425380): Move non-UI events out of PhotoPickerUiEventLogger
+     * Log metrics to notify that the user has clicked the 'view selected' button
+     * @param instanceId        an identifier for the current picker session
+     * @param selectedItemCount the number of items selected for preview all
      */
-    public void logPickerQueriedWithUnknownColumn(int callingUid, String callingPackage) {
-        logger.log(PhotoPickerEvent.PHOTO_PICKER_QUERY_UNKNOWN_COLUMN,
-                callingUid,
-                callingPackage);
+    public void logPreviewAllSelected(InstanceId instanceId, int selectedItemCount) {
+        logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_PREVIEW_ALL_SELECTED, instanceId,
+                selectedItemCount);
+    }
+
+    /**
+     * Log metrics to notify that the 'switch profile' button is visible & enabled
+     * @param instanceId an identifier for the current picker session
+     */
+    public void logProfileSwitchButtonEnabled(InstanceId instanceId) {
+        logWithInstance(PhotoPickerEvent.PHOTO_PICKER_PROFILE_SWITCH_BUTTON_ENABLED, instanceId);
+    }
+
+    /**
+     * Log metrics to notify that the 'switch profile' button is visible but disabled
+     * @param instanceId an identifier for the current picker session
+     */
+    public void logProfileSwitchButtonDisabled(InstanceId instanceId) {
+        logWithInstance(PhotoPickerEvent.PHOTO_PICKER_PROFILE_SWITCH_BUTTON_DISABLED, instanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has clicked the 'switch profile' button
+     * @param instanceId an identifier for the current picker session
+     */
+    public void logProfileSwitchButtonClick(InstanceId instanceId) {
+        logWithInstance(PhotoPickerEvent.PHOTO_PICKER_PROFILE_SWITCH_BUTTON_CLICK, instanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has cancelled the current session by swiping down
+     * @param instanceId an identifier for the current picker session
+     */
+    public void logSwipeDownExit(InstanceId instanceId) {
+        logWithInstance(PhotoPickerEvent.PHOTO_PICKER_EXIT_SWIPE_DOWN, instanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has made a back gesture
+     * @param instanceId          an identifier for the current picker session
+     * @param backStackEntryCount the number of fragment entries currently in the back stack
+     */
+    public void logBackGestureWithStackCount(InstanceId instanceId, int backStackEntryCount) {
+        logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_BACK_GESTURE, instanceId,
+                backStackEntryCount);
+    }
+
+    /**
+     * Log metrics to notify that the user has clicked the action bar home button
+     * @param instanceId          an identifier for the current picker session
+     * @param backStackEntryCount the number of fragment entries currently in the back stack
+     */
+    public void logActionBarHomeButtonClick(InstanceId instanceId, int backStackEntryCount) {
+        logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_ACTION_BAR_HOME_BUTTON_CLICK,
+                instanceId, backStackEntryCount);
+    }
+
+    /**
+     * Log metrics to notify that the user has expanded from half screen to full
+     * @param instanceId an identifier for the current picker session
+     */
+    public void logExpandToFullScreen(InstanceId instanceId) {
+        logWithInstance(PhotoPickerEvent.PHOTO_PICKER_FROM_HALF_TO_FULL_SCREEN, instanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has opened the photo picker menu
+     * @param instanceId an identifier for the current picker session
+     */
+    public void logMenuOpened(InstanceId instanceId) {
+        logWithInstance(PhotoPickerEvent.PHOTO_PICKER_MENU, instanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has switched to the photos tab
+     * @param instanceId an identifier for the current picker session
+     */
+    public void logSwitchToPhotosTab(InstanceId instanceId) {
+        logWithInstance(PhotoPickerEvent.PHOTO_PICKER_TAB_PHOTOS_OPEN, instanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has switched to the albums tab
+     * @param instanceId an identifier for the current picker session
+     */
+    public void logSwitchToAlbumsTab(InstanceId instanceId) {
+        logWithInstance(PhotoPickerEvent.PHOTO_PICKER_TAB_ALBUMS_OPEN, instanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has opened the device favorites album
+     * @param instanceId an identifier for the current picker session
+     */
+    public void logFavoritesAlbumOpened(InstanceId instanceId) {
+        logWithInstance(PhotoPickerEvent.PHOTO_PICKER_ALBUM_FAVORITES_OPEN, instanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has opened the device camera album
+     * @param instanceId an identifier for the current picker session
+     */
+    public void logCameraAlbumOpened(InstanceId instanceId) {
+        logWithInstance(PhotoPickerEvent.PHOTO_PICKER_ALBUM_CAMERA_OPEN, instanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has opened the device downloads album
+     * @param instanceId an identifier for the current picker session
+     */
+    public void logDownloadsAlbumOpened(InstanceId instanceId) {
+        logWithInstance(PhotoPickerEvent.PHOTO_PICKER_ALBUM_DOWNLOADS_OPEN, instanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has opened the device screenshots album
+     * @param instanceId an identifier for the current picker session
+     */
+    public void logScreenshotsAlbumOpened(InstanceId instanceId) {
+        logWithInstance(PhotoPickerEvent.PHOTO_PICKER_ALBUM_SCREENSHOTS_OPEN, instanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has opened the device videos album
+     * @param instanceId an identifier for the current picker session
+     */
+    public void logVideosAlbumOpened(InstanceId instanceId) {
+        logWithInstance(PhotoPickerEvent.PHOTO_PICKER_ALBUM_VIDEOS_OPEN, instanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has opened a cloud album
+     * @param instanceId an identifier for the current picker session
+     * @param position   the position of the album in the recycler view
+     */
+    public void logCloudAlbumOpened(InstanceId instanceId, int position) {
+        logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_ALBUM_FROM_CLOUD_OPEN, instanceId,
+                position);
+    }
+
+    /**
+     * Log metrics to notify that the user selected a media item in the main grid
+     * @param instanceId an identifier for the current picker session
+     * @param position   the position of the album in the recycler view
+     */
+    public void logSelectedMainGridItem(InstanceId instanceId, int position) {
+        logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_SELECTED_ITEM_MAIN_GRID,
+                instanceId, position);
+    }
+
+    /**
+     * Log metrics to notify that the user selected a media item in an album
+     * @param instanceId an identifier for the current picker session
+     * @param position   the position of the album in the recycler view
+     */
+    public void logSelectedAlbumItem(InstanceId instanceId, int position) {
+        logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_SELECTED_ITEM_ALBUM, instanceId,
+                position);
+    }
+
+    /**
+     * Log metrics to notify that the user has selected a cloud only media item
+     * @param instanceId an identifier for the current picker session
+     * @param position   the position of the album in the recycler view
+     */
+    public void logSelectedCloudOnlyItem(InstanceId instanceId, int position) {
+        logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_SELECTED_ITEM_CLOUD_ONLY,
+                instanceId, position);
+    }
+
+    /**
+     * Log metrics to notify that the user has previewed an item in the main grid
+     * @param specialFormat the special format of the previewed item (used to identify special
+     *                      categories like motion photos)
+     * @param mimeType      the mime type of the previewed item
+     * @param instanceId    an identifier for the current picker session
+     * @param position      the position of the album in the recycler view
+     */
+    public void logPreviewedMainGridItem(
+            int specialFormat, String mimeType, InstanceId instanceId, int position) {
+        logger.logWithInstanceIdAndPosition(PhotoPickerEvent.PHOTO_PICKER_PREVIEW_ITEM_MAIN_GRID,
+                specialFormat, mimeType, instanceId, position);
+    }
+
+    /**
+     * Log metrics to notify that the picker has loaded some media items in the main grid
+     * @param authority  the authority of the selected cloud provider, null if no non-local items
+     * @param instanceId an identifier for the current picker session
+     * @param count      the number of media items loaded
+     */
+    public void logLoadedMainGridMediaItems(String authority, InstanceId instanceId, int count) {
+        logger.logWithInstanceIdAndPosition(PhotoPickerEvent.PHOTO_PICKER_UI_LOADED_PHOTOS,
+                /* uid */ 0, authority, instanceId, count);
+    }
+
+    /**
+     * Log metrics to notify that the picker has loaded some albums
+     * @param authority  the authority of the selected cloud provider, null if no non-local albums
+     * @param instanceId an identifier for the current picker session
+     * @param count      the number of albums loaded
+     */
+    public void logLoadedAlbums(String authority, InstanceId instanceId, int count) {
+        logger.logWithInstanceIdAndPosition(PhotoPickerEvent.PHOTO_PICKER_UI_LOADED_ALBUMS,
+                /* uid */ 0, authority, instanceId, count);
+    }
+
+    /**
+     * Log metrics to notify that the picker has loaded some media items in an album grid
+     * @param authority  the authority of the selected cloud provider, null if no non-local items
+     * @param instanceId an identifier for the current picker session
+     * @param count      the number of media items loaded
+     */
+    public void logLoadedAlbumGridMediaItems(String authority, InstanceId instanceId, int count) {
+        logger.logWithInstanceIdAndPosition(PhotoPickerEvent.PHOTO_PICKER_UI_LOADED_ALBUM_CONTENTS,
+                /* uid */ 0, authority, instanceId, count);
+    }
+
+    /**
+     * Log metrics to notify create surface controller triggered
+     * @param instanceId an identifier for the current picker session
+     * @param authority  the authority of the provider
+     */
+    public void logPickerCreateSurfaceControllerStart(InstanceId instanceId, String authority) {
+        logger.logWithInstanceId(PhotoPickerEvent.PHOTO_PICKER_CREATE_SURFACE_CONTROLLER_START,
+                /* uid */ 0, authority, instanceId);
+    }
+
+    /**
+     * Log metrics to notify create surface controller ended
+     * @param instanceId an identifier for the current picker session
+     * @param authority  the authority of the provider
+     */
+    public void logPickerCreateSurfaceControllerEnd(InstanceId instanceId, String authority) {
+        logger.logWithInstanceId(PhotoPickerEvent.PHOTO_PICKER_CREATE_SURFACE_CONTROLLER_END,
+                /* uid */ 0, authority, instanceId);
+    }
+
+    /**
+     * Log metrics to notify that the picker has started preloading the selected media items
+     * @param instanceId an identifier for the current picker session
+     * @param count      the number of items to be preloaded
+     */
+    public void logPreloadingStarted(@NonNull InstanceId instanceId, int count) {
+        logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_PRELOADING_STARTED, instanceId,
+                count);
+    }
+
+    /**
+     * Log metrics to notify that the picker has finished preloading the selected media items
+     * @param instanceId an identifier for the current picker session
+     */
+    public void logPreloadingFinished(@NonNull InstanceId instanceId) {
+        logWithInstance(PhotoPickerEvent.PHOTO_PICKER_PRELOADING_FINISHED, instanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user cancelled the selected media preloading
+     * @param instanceId an identifier for the current picker session
+     * @param count      the number of items pending to preload
+     */
+    public void logPreloadingCancelled(@NonNull InstanceId instanceId, int count) {
+        logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_PRELOADING_CANCELLED, instanceId,
+                count);
+    }
+
+    /**
+     * Log metrics to notify that the selected media preloading failed for some items
+     * @param instanceId an identifier for the current picker session
+     * @param count      the number of items pending / failed to preload
+     */
+    public void logPreloadingFailed(@NonNull InstanceId instanceId, int count) {
+        logWithInstanceAndPosition(PhotoPickerEvent.PHOTO_PICKER_PRELOADING_FAILED, instanceId,
+                count);
+    }
+
+    /**
+     * Log metrics to notify that the banner is added to display in the recycler view grids
+     * @param instanceId an identifier for the current picker session
+     * @param bannerName the name of the banner added,
+     *                   refer {@link com.android.providers.media.photopicker.ui.TabAdapter.Banner}
+     */
+    public void logBannerAdded(@NonNull InstanceId instanceId, @NonNull String bannerName) {
+        logger.logWithInstanceId(PhotoPickerEvent.PHOTO_PICKER_BANNER_ADDED, /* uid= */ 0,
+                bannerName, instanceId);
+    }
+
+    /**
+     * Log metrics to notify that the banner is dismissed by the user
+     * @param instanceId an identifier for the current picker session
+     */
+    public void logBannerDismissed(@NonNull InstanceId instanceId) {
+        logWithInstance(PhotoPickerEvent.PHOTO_PICKER_BANNER_DISMISSED, instanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user clicked the banner action button
+     * @param instanceId an identifier for the current picker session
+     */
+    public void logBannerActionButtonClicked(@NonNull InstanceId instanceId) {
+        logWithInstance(PhotoPickerEvent.PHOTO_PICKER_BANNER_ACTION_BUTTON_CLICKED, instanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user clicked on the remaining part of the banner
+     * @param instanceId an identifier for the current picker session
+     */
+    public void logBannerClicked(@NonNull InstanceId instanceId) {
+        logWithInstance(PhotoPickerEvent.PHOTO_PICKER_BANNER_CLICKED, instanceId);
+    }
+
+    private void logWithInstance(@NonNull UiEventLogger.UiEventEnum event, InstanceId instance) {
+        logger.logWithInstanceId(event, /* uid */ 0, /* packageName */ null, instance);
+    }
+
+    private void logWithInstanceAndPosition(@NonNull UiEventLogger.UiEventEnum event,
+            @NonNull InstanceId instance, int position) {
+        logger.logWithInstanceIdAndPosition(event, /* uid= */ 0, /* packageName= */ null, instance,
+                position);
     }
 }
diff --git a/src/com/android/providers/media/photopicker/sync/CloseableReentrantLock.java b/src/com/android/providers/media/photopicker/sync/CloseableReentrantLock.java
new file mode 100644
index 0000000..bcd54e4
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/sync/CloseableReentrantLock.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * A Reentrant lock that implements AutoCloseable interface.
+ */
+public class CloseableReentrantLock extends ReentrantLock implements AutoCloseable {
+    private static final String TAG = CloseableReentrantLock.class.getSimpleName();
+    private final String mLockName;
+
+    public CloseableReentrantLock(@NonNull String lockName) {
+        super();
+        mLockName = lockName;
+    }
+
+    /**
+     * Try to acquire lock with a timeout after running some validations.
+     */
+    public CloseableReentrantLock lockWithTimeout(long timeout, TimeUnit unit)
+            throws UnableToAcquireLockException {
+        try {
+            final boolean success =
+                    this.tryLock(timeout, unit);
+            if (!success) {
+                throw new UnableToAcquireLockException(
+                        "Could not acquire the lock within timeout " + this);
+            }
+            Log.d(TAG, "Successfully acquired lock " + this);
+            return this;
+        } catch (InterruptedException e) {
+            throw new UnableToAcquireLockException(
+                    "Interrupted while waiting for lock " + this, e);
+        }
+    }
+
+    @Override
+    public void close() {
+        unlock();
+    }
+
+    /**
+     * Attempt to release the lock and swallow IllegalMonitorStateException, if thrown.
+     */
+    @Override
+    public void lock() {
+        super.lock();
+        Log.d(TAG, "Successfully acquired lock " + this);
+    }
+
+    /**
+     * Attempt to release the lock and swallow IllegalMonitorStateException, if thrown.
+     */
+    @Override
+    public void unlock() {
+        try {
+            super.unlock();
+            Log.d(TAG, "Successfully released lock " + this);
+        } catch (IllegalMonitorStateException e) {
+            Log.e(TAG, "Tried to release a lock that is not held by this thread - " + this);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return super.toString() + ". Lock Name = " + mLockName
+                + ". Threads that may be waiting to acquire this lock = " + getQueuedThreads();
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/sync/ImmediateAlbumSyncWorker.java b/src/com/android/providers/media/photopicker/sync/ImmediateAlbumSyncWorker.java
new file mode 100644
index 0000000..71cd5b3
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/sync/ImmediateAlbumSyncWorker.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_ALBUM_ID;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SYNC_SOURCE;
+import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.markAlbumMediaSyncAsComplete;
+
+import android.content.Context;
+import android.os.CancellationSignal;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.work.ForegroundInfo;
+import androidx.work.ListenableWorker;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+
+import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException;
+
+/**
+ * This is a {@link Worker} class responsible for syncing album media with the correct sync source.
+ */
+public class ImmediateAlbumSyncWorker extends Worker {
+    private static final String TAG = "IASyncWorker";
+    private static final int INVALID_SYNC_SOURCE = -1;
+    private final Context mContext;
+    private final CancellationSignal mCancellationSignal = new CancellationSignal();
+
+    /**
+     * Creates an instance of the {@link Worker}.
+     *
+     * @param context the application {@link Context}
+     * @param workerParams the set of {@link WorkerParameters}
+     */
+    public ImmediateAlbumSyncWorker(
+            @NonNull Context context,
+            @NonNull WorkerParameters workerParams) {
+        super(context, workerParams);
+        mContext = context;
+    }
+
+    @NonNull
+    @Override
+    public ListenableWorker.Result doWork() {
+        // Do not allow endless re-runs of this worker, if this isn't the original run,
+        // just succeed and wait until the next scheduled run.
+        if (getRunAttemptCount() > 0) {
+            Log.w(TAG, "Worker retry was detected, ending this run in failure.");
+            return ListenableWorker.Result.failure();
+        }
+        final int syncSource = getInputData()
+                .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, /* defaultValue */ INVALID_SYNC_SOURCE);
+        final String albumId = getInputData().getString(SYNC_WORKER_INPUT_ALBUM_ID);
+
+        Log.i(TAG, String.format(
+                "Starting picker immediate album sync from sync source: %s album id: %s",
+                syncSource, albumId));
+
+        try {
+            validateWorkInput(syncSource, albumId);
+
+            // No need to instantiate a work request tracker for immediate syncs in the worker.
+            // For immediate syncs, the work request tracker is initiated before enqueueing the
+            // request in WorkManager.
+            checkIsWorkerStopped();
+            if (syncSource == SYNC_LOCAL_ONLY) {
+                PickerSyncController.getInstanceOrThrow()
+                        .syncAlbumMediaFromLocalProvider(albumId, mCancellationSignal);
+            } else {
+                PickerSyncController.getInstanceOrThrow()
+                        .syncAlbumMediaFromCloudProvider(albumId, mCancellationSignal);
+            }
+
+            Log.i(TAG, String.format(
+                    "Completed picker immediate album sync from sync source: %s album id: %s",
+                    syncSource, albumId));
+            return ListenableWorker.Result.success();
+        } catch (IllegalArgumentException | IllegalStateException | RequestObsoleteException e) {
+            Log.e(TAG, String.format("Could not complete picker immediate album sync from "
+                            + "sync source: %s album id: %s",
+                    syncSource, albumId), e);
+            return ListenableWorker.Result.failure();
+        } finally {
+            markAlbumMediaSyncAsComplete(syncSource, getId());
+        }
+    }
+
+    /**
+     * Validates input data received by the Worker for an immediate album sync.
+     */
+    private void validateWorkInput(int syncSource, @Nullable String albumId)
+            throws IllegalArgumentException {
+        // Album syncs can only happen with either local provider or cloud provider. This
+        // information needs to be provided in the {@code inputData}.
+        if (syncSource != SYNC_LOCAL_ONLY && syncSource != SYNC_CLOUD_ONLY) {
+            throw new IllegalArgumentException("Invalid album sync source " + syncSource);
+        }
+        if (albumId == null || TextUtils.isEmpty(albumId)) {
+            throw new IllegalArgumentException("Invalid album id " + albumId);
+        }
+    }
+
+    private void checkIsWorkerStopped() throws RequestObsoleteException {
+        if (isStopped()) {
+            throw new RequestObsoleteException("Work is stopped " + getId());
+        }
+    }
+
+    @Override
+    @NonNull
+    public ForegroundInfo getForegroundInfo() {
+        return PickerSyncNotificationHelper.getForegroundInfo(mContext);
+    }
+
+    @Override
+    public void onStopped() {
+        Log.w(TAG, "Worker is stopped. Clearing all pending futures. It's possible that the sync "
+                + "will continue to run if it has started already.");
+        // Send CancellationSignal to any running tasks.
+        mCancellationSignal.cancel();
+        final int syncSource = getInputData()
+                .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, /* defaultValue */ SYNC_LOCAL_ONLY);
+        markAlbumMediaSyncAsComplete(syncSource, getId());
+    }
+
+    @VisibleForTesting
+    CancellationSignal getCancellationSignal() {
+        return mCancellationSignal;
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/sync/ImmediateSyncWorker.java b/src/com/android/providers/media/photopicker/sync/ImmediateSyncWorker.java
new file mode 100644
index 0000000..a284d31
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/sync/ImmediateSyncWorker.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_AND_CLOUD;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SYNC_SOURCE;
+import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.getCloudSyncTracker;
+import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.getLocalSyncTracker;
+import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.markSyncAsComplete;
+
+import android.content.Context;
+import android.os.CancellationSignal;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.work.ForegroundInfo;
+import androidx.work.ListenableWorker;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+
+import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException;
+
+/**
+ * This is a {@link Worker} class responsible for syncing with the correct sync source.
+ */
+public class ImmediateSyncWorker extends Worker {
+    private static final String TAG = "ISyncWorker";
+    private final Context mContext;
+    private final CancellationSignal mCancellationSignal = new CancellationSignal();
+
+    /**
+     * Creates an instance of the {@link Worker}.
+     *
+     * @param context the application {@link Context}
+     * @param workerParams the set of {@link WorkerParameters}
+     */
+    public ImmediateSyncWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
+        super(context, workerParams);
+        mContext = context;
+    }
+
+    @NonNull
+    @Override
+    public ListenableWorker.Result doWork() {
+        // Do not allow endless re-runs of this worker, if this isn't the original run,
+        // just succeed and wait until the next scheduled run.
+        if (getRunAttemptCount() > 0) {
+            Log.w(TAG, "Worker retry was detected, ending this run in failure.");
+            return ListenableWorker.Result.failure();
+        }
+        final int syncSource = getInputData()
+                .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, /* defaultValue */ SYNC_LOCAL_ONLY);
+
+        Log.i(TAG, String.format(
+                "Starting immediate picker sync from sync source: %s", syncSource));
+
+        try {
+            // No need to instantiate a work request tracker for immediate syncs in the worker.
+            // For immediate syncs, the work request tracker is initiated before enqueueing the
+            // request in WorkManager.
+            if (syncSource == SYNC_LOCAL_AND_CLOUD || syncSource == SYNC_LOCAL_ONLY) {
+                checkIsWorkerStopped();
+                PickerSyncController.getInstanceOrThrow()
+                        .syncAllMediaFromLocalProvider(mCancellationSignal);
+                getLocalSyncTracker().markSyncCompleted(getId());
+                Log.i(TAG, "Completed immediate picker sync from local provider.");
+            }
+            if (syncSource == SYNC_LOCAL_AND_CLOUD || syncSource == SYNC_CLOUD_ONLY) {
+                checkIsWorkerStopped();
+                PickerSyncController.getInstanceOrThrow()
+                        .syncAllMediaFromCloudProvider(mCancellationSignal);
+                getCloudSyncTracker().markSyncCompleted(getId());
+                Log.i(TAG, "Completed immediate picker sync from cloud provider.");
+            }
+            return ListenableWorker.Result.success();
+        } catch (IllegalStateException | RequestObsoleteException e) {
+            Log.i(TAG, String.format(
+                    "Could not complete immediate sync from sync source: %s", syncSource), e);
+
+            // Mark all pending syncs as finished and set failure result.
+            markSyncAsComplete(syncSource, getId());
+            return ListenableWorker.Result.failure();
+        }
+    }
+
+    private void checkIsWorkerStopped() throws RequestObsoleteException {
+        if (isStopped()) {
+            throw new RequestObsoleteException("Work is stopped " + getId());
+        }
+    }
+
+    @Override
+    @NonNull
+    public ForegroundInfo getForegroundInfo() {
+        return PickerSyncNotificationHelper.getForegroundInfo(mContext);
+    }
+
+    @Override
+    public void onStopped() {
+        Log.w(TAG, "Worker is stopped. Clearing all pending futures. It's possible that the sync "
+                + "still finishes running if it has started already.");
+        // Send CancellationSignal to any running tasks.
+        mCancellationSignal.cancel();
+        final int syncSource = getInputData()
+                .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, /* defaultValue */ SYNC_LOCAL_AND_CLOUD);
+        markSyncAsComplete(syncSource, getId());
+    }
+
+    @VisibleForTesting
+    CancellationSignal getCancellationSignal() {
+        return mCancellationSignal;
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/sync/MediaResetWorker.java b/src/com/android/providers/media/photopicker/sync/MediaResetWorker.java
new file mode 100644
index 0000000..1558b9f
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/sync/MediaResetWorker.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_RESET_ALBUM;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_RESET_MEDIA;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_ALBUM_ID;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_AUTHORITY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_RESET_TYPE;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SYNC_SOURCE;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_TAG_IS_PERIODIC;
+import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.markAlbumMediaSyncAsComplete;
+import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.trackNewAlbumMediaSyncRequests;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteException;
+import android.os.Trace;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.work.ForegroundInfo;
+import androidx.work.ListenableWorker;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+
+import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.sync.PickerSyncManager.SyncResetType;
+import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
+
+/**
+ * This is a {@link Worker} class responsible for handling table reset operations in the picker
+ * database.
+ */
+public class MediaResetWorker extends Worker {
+
+    private static final String TAG = "MediaResetWorker";
+    private static final int UNDEFINED_RESET_TYPE = -1;
+
+    @Nullable private final String mAlbumId;
+    @NonNull private final Context mContext;
+    @NonNull private final int mResetType;
+    @NonNull private final int mSyncSource;
+
+    @Nullable private String mAuthority;
+
+    public MediaResetWorker(@NonNull Context context, @NonNull WorkerParameters workerParameters) {
+        super(context, workerParameters);
+        mContext = context;
+
+        mAuthority = getInputData().getString(SYNC_WORKER_INPUT_AUTHORITY);
+        mAlbumId = getInputData().getString(SYNC_WORKER_INPUT_ALBUM_ID);
+        mResetType = getInputData().getInt(SYNC_WORKER_INPUT_RESET_TYPE, UNDEFINED_RESET_TYPE);
+        mSyncSource = getInputData().getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1);
+    }
+
+    @Override
+    public ListenableWorker.Result doWork() {
+        Log.i(
+                TAG,
+                String.format(
+                        "MediaReset has been requested. Authority: %s AlbumId: %s",
+                        mAuthority, mAlbumId));
+
+        PickerSyncController controller;
+        PickerDbFacade dBFacade;
+        PickerSyncLockManager pickerSyncLockManager;
+        try {
+            controller = PickerSyncController.getInstanceOrThrow();
+            pickerSyncLockManager = controller.getPickerSyncLockManager();
+            dBFacade = new PickerDbFacade(mContext, pickerSyncLockManager);
+        } catch (IllegalStateException ex) {
+            Log.e(TAG, "Unable to obtain PickerSyncController", ex);
+            return ListenableWorker.Result.failure();
+        } catch (SQLiteException ex) {
+            Log.e(TAG, "Unable to get writeable database", ex);
+            return ListenableWorker.Result.failure();
+        }
+
+
+        try {
+            if (getTags().contains(SYNC_WORKER_TAG_IS_PERIODIC)) {
+                // If this worker is being run as part of periodic work, it needs to register
+                // its own sync with the sync tracker.
+                trackNewAlbumMediaSyncRequests(mSyncSource, getId());
+
+                // Since this is a periodic worker, we'll use the cloud authority, if it exists.
+                // Using the cloud authority will reset files for all providers. If the local
+                // authority is used, it will limit the query to only files with a local_id, but
+                // the cloud authority does not have such a limitation.
+                // (This is not intuitive, it's just how it works.)
+                mAuthority = controller.getCloudProviderWithTimeout();
+                if (mAuthority == null) {
+                    mAuthority = controller.getLocalProvider();
+                }
+                // If the authority is still null, end the operation.
+                if (mAuthority == null) {
+                    Log.e(TAG, "Unable to set authority for periodic worker");
+                    return ListenableWorker.Result.failure();
+                }
+            }
+
+            if (mSyncSource == SYNC_LOCAL_ONLY) {
+                return start(dBFacade);
+            } else {
+                // SyncSource is either CLOUD_ONLY or LOCAL_AND_CLOUD, either way we need the
+                // cloud lock.
+                try (CloseableReentrantLock ignored = pickerSyncLockManager
+                        .tryLock(PickerSyncLockManager.CLOUD_ALBUM_SYNC_LOCK)) {
+                    return start(dBFacade);
+                }
+            }
+        } catch (UnableToAcquireLockException e) {
+            Log.e(TAG, "Could not acquire lock", e);
+            return ListenableWorker.Result.failure();
+        } finally {
+            markAlbumMediaSyncAsComplete(mSyncSource, getId());
+        }
+    }
+
+    private ListenableWorker.Result start(@NonNull PickerDbFacade dbFacade) {
+
+        Trace.beginSection("MediaResetWorker:BeginOperation");
+
+        int deleteCount = 0;
+        try (PickerDbFacade.DbWriteOperation operation =
+                beginResetOperation(dbFacade, mResetType)) {
+
+            deleteCount = operation.execute(/* cursor= */ null);
+
+            // Just ensure the worker hasn't been stopped before allowing the commit.
+            if (isStopped()) {
+                Log.i(TAG, "Worker was stopped before operation was completed");
+                return ListenableWorker.Result.failure();
+            }
+            operation.setSuccess();
+
+        } catch (UnsupportedOperationException | IllegalStateException ex) {
+            Log.e(TAG, "Operation failed.", ex);
+            return ListenableWorker.Result.failure();
+        } finally {
+            Trace.endSection();
+        }
+
+        Log.i(TAG, String.format("Reset operation complete. Deleted rows: %d", deleteCount));
+        return ListenableWorker.Result.success();
+    }
+
+    private PickerDbFacade.DbWriteOperation beginResetOperation(
+            @NonNull PickerDbFacade dbFacade, @NonNull @SyncResetType int resetType) {
+
+        switch (resetType) {
+            case SYNC_RESET_ALBUM:
+                if (mAuthority == null) {
+                    throw new IllegalStateException(
+                            String.format(
+                                    "Failed to begin SYNC_RESET_ALBUM. Unknown provider authority:"
+                                            + " %s",
+                                    mAuthority));
+                }
+
+                if (mSyncSource == SYNC_CLOUD_ONLY && mAlbumId == null) {
+                    Log.w(
+                            TAG,
+                            "Sync Source is set to SYNC_CLOUD_ONLY with no albumId, but the reset"
+                                    + " operation will still remove cloud+local files.");
+                }
+                return dbFacade.beginResetAlbumMediaOperation(mAuthority, mAlbumId);
+            case SYNC_RESET_MEDIA:
+            default:
+                throw new UnsupportedOperationException(
+                        String.format(
+                                "Requested Reset operation not (yet) supported. ResetType: %d",
+                                resetType));
+        }
+    }
+
+    @Override
+    @NonNull
+    public ForegroundInfo getForegroundInfo() {
+        return PickerSyncNotificationHelper.getForegroundInfo(mContext);
+    }
+
+    @Override
+    public void onStopped() {
+        Log.w(
+                TAG,
+                "Worker is stopped. Clearing all pending futures. It's possible that the worker "
+                        + "still finishes running if it has started already.");
+        markAlbumMediaSyncAsComplete(mSyncSource, getId());
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/sync/PickerSyncLockManager.java b/src/com/android/providers/media/photopicker/sync/PickerSyncLockManager.java
new file mode 100644
index 0000000..f01026f
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/sync/PickerSyncLockManager.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+import android.annotation.IntDef;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Manages Java locks acquired during the sync process to ensure that the cloud sync is thread safe.
+ */
+public class PickerSyncLockManager {
+    private static final String TAG = PickerSyncLockManager.class.getSimpleName();
+    private static final Integer LOCK_ACQUIRE_TIMEOUT_MINS = 4;
+    private static final TimeUnit LOCK_ACQUIRE_TIMEOUT_UNIT = TimeUnit.MINUTES;
+
+    @IntDef(value = {CLOUD_SYNC_LOCK, CLOUD_ALBUM_SYNC_LOCK, CLOUD_PROVIDER_LOCK, DB_CLOUD_LOCK})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface LockType {}
+    public static final int CLOUD_SYNC_LOCK = 0;
+    public static final int CLOUD_ALBUM_SYNC_LOCK = 1;
+    public static final int CLOUD_PROVIDER_LOCK = 2;
+    public static final int DB_CLOUD_LOCK = 3;
+
+    private final CloseableReentrantLock mCloudSyncLock =
+            new CloseableReentrantLock("CLOUD_SYNC_LOCK");
+    private final CloseableReentrantLock mCloudAlbumSyncLock =
+            new CloseableReentrantLock("CLOUD_ALBUM_SYNC_LOCK");
+    private final CloseableReentrantLock mCloudProviderLock =
+            new CloseableReentrantLock("CLOUD_PROVIDER_LOCK");
+    private final CloseableReentrantLock mDbCloudLock =
+            new CloseableReentrantLock("DB_CLOUD_LOCK");
+
+    /**
+     * Try to acquire lock with a default timeout after running some validations.
+     */
+    public CloseableReentrantLock tryLock(@LockType int lockType)
+            throws UnableToAcquireLockException {
+        return tryLock(lockType, LOCK_ACQUIRE_TIMEOUT_MINS, LOCK_ACQUIRE_TIMEOUT_UNIT);
+    }
+
+    /**
+     * Try to acquire lock with the provided timeout after running some validations.
+     */
+    public CloseableReentrantLock tryLock(@LockType int lockType, long timeout, TimeUnit unit)
+            throws UnableToAcquireLockException {
+        return tryLock(getLock(lockType), timeout, unit);
+    }
+
+    /**
+     * Try to acquire the given lock with the provided timeout after running some validations.
+     */
+    @VisibleForTesting
+    public CloseableReentrantLock tryLock(@NonNull CloseableReentrantLock lock,
+            long timeout, TimeUnit unit) throws UnableToAcquireLockException {
+        Log.d(TAG, "Trying to acquire lock " + lock + " with timeout.");
+        validateLockOrder(lock);
+        return lock.lockWithTimeout(timeout, unit);
+    }
+
+    /**
+     * Try to acquire the lock after running some validations.
+     */
+    public CloseableReentrantLock lock(@LockType int lockType) {
+        final CloseableReentrantLock reentrantLock = getLock(lockType);
+        Log.d(TAG, "Trying to acquire lock " + reentrantLock);
+        validateLockOrder(reentrantLock);
+        reentrantLock.lock();
+        return reentrantLock;
+    }
+
+    /**
+     * Return the {@link CloseableReentrantLock} corresponding to the given {@link LockType}.
+     * Throws a {@link RuntimeException} if the lock is not recognized.
+     */
+    @VisibleForTesting
+    public CloseableReentrantLock getLock(@LockType int lockType) {
+        switch (lockType) {
+            case CLOUD_SYNC_LOCK:
+                return mCloudSyncLock;
+            case CLOUD_ALBUM_SYNC_LOCK:
+                return mCloudAlbumSyncLock;
+            case CLOUD_PROVIDER_LOCK:
+                return mCloudProviderLock;
+            case DB_CLOUD_LOCK:
+                return mDbCloudLock;
+            default:
+                throw new RuntimeException("Unrecognizable lock type " + lockType);
+        }
+    }
+
+    private void validateLockOrder(@NonNull ReentrantLock lockToBeAcquired) {
+        if (lockToBeAcquired.equals(mCloudSyncLock)) {
+            validateLockOrder(lockToBeAcquired, mCloudAlbumSyncLock);
+            validateLockOrder(lockToBeAcquired, mCloudProviderLock);
+            validateLockOrder(lockToBeAcquired, mDbCloudLock);
+        } else if (lockToBeAcquired.equals(mCloudAlbumSyncLock)) {
+            validateLockOrder(lockToBeAcquired, mCloudSyncLock);
+            validateLockOrder(lockToBeAcquired, mCloudProviderLock);
+            validateLockOrder(lockToBeAcquired, mDbCloudLock);
+        } else if (lockToBeAcquired.equals(mCloudProviderLock)) {
+            validateLockOrder(lockToBeAcquired, mDbCloudLock);
+        }
+    }
+
+    private void validateLockOrder(@NonNull ReentrantLock lockToBeAcquired,
+            @NonNull ReentrantLock lockThatShouldNotBeHeld) {
+        if (lockThatShouldNotBeHeld.isHeldByCurrentThread()) {
+            Log.e(TAG, String.format("Lock {%s} should not be held before acquiring lock {%s}"
+                            + " This could lead to a deadlock.",
+                    lockThatShouldNotBeHeld, lockToBeAcquired));
+        }
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java b/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java
new file mode 100644
index 0000000..b086967
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.markAlbumMediaSyncAsComplete;
+import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.markSyncAsComplete;
+import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.trackNewAlbumMediaSyncRequests;
+import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.trackNewSyncRequests;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.work.Constraints;
+import androidx.work.Data;
+import androidx.work.ExistingPeriodicWorkPolicy;
+import androidx.work.ExistingWorkPolicy;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.Operation;
+import androidx.work.OutOfQuotaPolicy;
+import androidx.work.PeriodicWorkRequest;
+import androidx.work.WorkInfo;
+import androidx.work.WorkManager;
+import androidx.work.Worker;
+
+import com.android.modules.utils.BackgroundThread;
+import com.android.providers.media.ConfigStore;
+import com.android.providers.media.photopicker.PickerSyncController;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class manages all the triggers for Picker syncs.
+ * <p></p>
+ * There are different use cases for triggering a sync:
+ * <p>
+ * 1. Proactive sync - these syncs are proactively performed to minimize the changes that need to be
+ *      synced when the user opens the Photo Picker. The sync should only be performed if the device
+ *      state allows it.
+ * <p>
+ * 2. Reactive sync - these syncs are triggered by the user opening the Photo Picker. These should
+ *      be run immediately since the user is likely to be waiting for the sync response on the UI.
+ */
+public class PickerSyncManager {
+    private static final String TAG = "SyncWorkManager";
+    public static final int SYNC_LOCAL_ONLY = 1;
+    public static final int SYNC_CLOUD_ONLY = 2;
+    public static final int SYNC_LOCAL_AND_CLOUD = 3;
+
+    @IntDef(value = { SYNC_LOCAL_ONLY, SYNC_CLOUD_ONLY, SYNC_LOCAL_AND_CLOUD })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SyncSource {}
+
+    public static final int SYNC_RESET_MEDIA = 1;
+    public static final int SYNC_RESET_ALBUM = 2;
+
+    @IntDef(value = {SYNC_RESET_MEDIA, SYNC_RESET_ALBUM})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SyncResetType {}
+
+    static final String SYNC_WORKER_INPUT_AUTHORITY = "INPUT_AUTHORITY";
+    static final String SYNC_WORKER_INPUT_SYNC_SOURCE = "INPUT_SYNC_TYPE";
+    static final String SYNC_WORKER_INPUT_RESET_TYPE = "INPUT_RESET_TYPE";
+    static final String SYNC_WORKER_INPUT_ALBUM_ID = "INPUT_ALBUM_ID";
+    static final String SYNC_WORKER_TAG_IS_PERIODIC = "PERIODIC";
+    private static final int SYNC_MEDIA_PERIODIC_WORK_INTERVAL = 4; // Time unit is hours.
+    private static final int RESET_ALBUM_MEDIA_PERIODIC_WORK_INTERVAL = 12; // Time unit is hours.
+
+    private static final String PERIODIC_SYNC_WORK_NAME;
+    private static final String PROACTIVE_LOCAL_SYNC_WORK_NAME;
+    private static final String PROACTIVE_SYNC_WORK_NAME;
+    public static final String IMMEDIATE_LOCAL_SYNC_WORK_NAME;
+    private static final String IMMEDIATE_CLOUD_SYNC_WORK_NAME;
+    public static final String IMMEDIATE_ALBUM_SYNC_WORK_NAME;
+    private static final String PERIODIC_ALBUM_RESET_WORK_NAME;
+
+    static {
+        final String syncPeriodicPrefix = "SYNC_MEDIA_PERIODIC_";
+        final String syncProactivePrefix = "SYNC_MEDIA_PROACTIVE_";
+        final String syncImmediatePrefix = "SYNC_MEDIA_IMMEDIATE_";
+        final String syncAllSuffix = "ALL";
+        final String syncLocalSuffix = "LOCAL";
+        final String syncCloudSuffix = "CLOUD";
+
+        PERIODIC_ALBUM_RESET_WORK_NAME = "RESET_ALBUM_MEDIA_PERIODIC";
+        PERIODIC_SYNC_WORK_NAME = syncPeriodicPrefix + syncAllSuffix;
+        PROACTIVE_LOCAL_SYNC_WORK_NAME = syncProactivePrefix + syncLocalSuffix;
+        PROACTIVE_SYNC_WORK_NAME = syncProactivePrefix + syncAllSuffix;
+        IMMEDIATE_LOCAL_SYNC_WORK_NAME = syncImmediatePrefix + syncLocalSuffix;
+        IMMEDIATE_CLOUD_SYNC_WORK_NAME = syncImmediatePrefix + syncCloudSuffix;
+        IMMEDIATE_ALBUM_SYNC_WORK_NAME = "SYNC_ALBUM_MEDIA_IMMEDIATE";
+    }
+
+    private final WorkManager mWorkManager;
+    private final ConfigStore mConfigStore;
+    private final Context mContext;
+
+    public PickerSyncManager(@NonNull WorkManager workManager,
+            @NonNull Context context,
+            @NonNull ConfigStore configStore,
+            boolean shouldSchedulePeriodicSyncs) {
+        mWorkManager = requireNonNull(workManager);
+        mConfigStore = requireNonNull(configStore);
+        mContext = requireNonNull(context);
+
+        if (shouldSchedulePeriodicSyncs) {
+            setUpPeriodicWork();
+        }
+
+        // Subscribe to device config changes so we can enable periodic workers if Cloud
+        // Photopicker is enabled.
+        mConfigStore.addOnChangeListener(BackgroundThread.getExecutor(), this::setUpPeriodicWork);
+    }
+
+    /**
+     * Will register new unique {@link Worker} for periodic sync and picker database maintenance if
+     * the cloud photopicker experiment is currently enabled.
+     */
+    private void setUpPeriodicWork() {
+
+        if (mConfigStore.isCloudMediaInPhotoPickerEnabled()) {
+            PickerSyncNotificationHelper.createNotificationChannel(mContext);
+
+            schedulePeriodicSyncs();
+            schedulePeriodicAlbumReset();
+        } else {
+            // Disable any scheduled ongoing work if the feature is disabled.
+            mWorkManager.cancelUniqueWork(PERIODIC_SYNC_WORK_NAME);
+            mWorkManager.cancelUniqueWork(PERIODIC_ALBUM_RESET_WORK_NAME);
+        }
+    }
+
+    /**
+     * Returns true if the given unique work is pending. In case the unique work is complete or
+     * there was an error in getting the work state, it returns false.
+     */
+    public boolean isUniqueWorkPending(String uniqueWorkName) {
+        ListenableFuture<List<WorkInfo>> future =
+                mWorkManager.getWorkInfosForUniqueWork(uniqueWorkName);
+        try {
+            List<WorkInfo> workInfos = future.get();
+            for (WorkInfo workInfo : workInfos) {
+                if (!workInfo.getState().isFinished()) {
+                    return true;
+                }
+            }
+            return false;
+        } catch (InterruptedException | ExecutionException e) {
+            Log.e(TAG, "Error occurred in fetching work info - ignore pending work");
+            return false;
+        }
+    }
+
+    private void schedulePeriodicSyncs() {
+        Log.i(TAG, "Scheduling periodic proactive syncs");
+
+        final Data inputData =
+                new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_AND_CLOUD));
+        final PeriodicWorkRequest periodicSyncRequest = getPeriodicProactiveSyncRequest(inputData);
+
+        try {
+            // Note that the first execution of periodic work happens immediately or as soon as the
+            // given Constraints are met.
+            final Operation enqueueOperation = mWorkManager
+                    .enqueueUniquePeriodicWork(
+                            PERIODIC_SYNC_WORK_NAME,
+                            ExistingPeriodicWorkPolicy.KEEP,
+                            periodicSyncRequest
+                    );
+
+            // Check that the request has been successfully enqueued.
+            enqueueOperation.getResult().get();
+        } catch (InterruptedException | ExecutionException e) {
+            Log.e(TAG, "Could not enqueue periodic proactive picker sync request", e);
+        }
+    }
+
+    private void schedulePeriodicAlbumReset() {
+        Log.i(TAG, "Scheduling periodic picker album data resets");
+
+        final Data inputData =
+                new Data(
+                        Map.of(
+                                SYNC_WORKER_INPUT_SYNC_SOURCE,
+                                SYNC_LOCAL_AND_CLOUD,
+                                SYNC_WORKER_INPUT_RESET_TYPE,
+                                SYNC_RESET_ALBUM));
+        final PeriodicWorkRequest periodicAlbumResetRequest =
+                getPeriodicAlbumResetRequest(inputData);
+
+        try {
+            // Note that the first execution of periodic work happens immediately or as soon
+            // as the given Constraints are met.
+            Operation enqueueOperation =
+                    mWorkManager.enqueueUniquePeriodicWork(
+                            PERIODIC_ALBUM_RESET_WORK_NAME,
+                            ExistingPeriodicWorkPolicy.KEEP,
+                            periodicAlbumResetRequest);
+
+            // Check that the request has been successfully enqueued.
+            enqueueOperation.getResult().get();
+        } catch (InterruptedException | ExecutionException e) {
+            Log.e(TAG, "Could not enqueue periodic picker album resets request", e);
+        }
+    }
+
+    /**
+     * Use this method for proactive syncs. The sync might take a while to start. Some device state
+     * conditions may apply before the sync can start like battery level etc.
+     *
+     * @param localOnly - whether the proactive sync should only sync with the local provider.
+     */
+    public void syncMediaProactively(Boolean localOnly) {
+
+        final int syncSource = localOnly ? SYNC_LOCAL_ONLY : SYNC_LOCAL_AND_CLOUD;
+        final String workName =
+                localOnly ? PROACTIVE_LOCAL_SYNC_WORK_NAME : PROACTIVE_SYNC_WORK_NAME;
+
+        final Data inputData = new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, syncSource));
+        final OneTimeWorkRequest syncRequest = getOneTimeProactiveSyncRequest(inputData);
+
+        // Don't wait for the sync operation to enqueue so that Picker sync enqueue
+        // requests in
+        // order to avoid adding latency to critical MP code paths.
+
+        mWorkManager.enqueueUniqueWork(workName, ExistingWorkPolicy.KEEP, syncRequest);
+    }
+
+    /**
+     * Use this method for reactive syncs which are user triggered.
+     *
+     * @param shouldSyncLocalOnlyData if true indicates that the sync should only be triggered with
+     *                                the local provider. Otherwise, sync will be triggered for both
+     *                                local and cloud provider.
+     */
+    public void syncMediaImmediately(boolean shouldSyncLocalOnlyData) {
+        syncMediaImmediately(PickerSyncManager.SYNC_LOCAL_ONLY, IMMEDIATE_LOCAL_SYNC_WORK_NAME);
+        if (!shouldSyncLocalOnlyData) {
+            syncMediaImmediately(PickerSyncManager.SYNC_CLOUD_ONLY, IMMEDIATE_CLOUD_SYNC_WORK_NAME);
+        }
+    }
+
+    /**
+     * Use this method for reactive syncs with either, local and cloud providers, or both.
+     */
+    private void syncMediaImmediately(@SyncSource int syncSource, @NonNull String workName) {
+        final Data inputData = new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, syncSource));
+        final OneTimeWorkRequest syncRequest =
+                buildOneTimeWorkerRequest(ImmediateSyncWorker.class, inputData);
+
+        // Track the new sync request(s)
+        trackNewSyncRequests(syncSource, syncRequest.getId());
+
+        // Enqueue local or cloud sync request
+        try {
+            final Operation enqueueOperation = mWorkManager
+                    .enqueueUniqueWork(workName, ExistingWorkPolicy.APPEND_OR_REPLACE, syncRequest);
+
+            // Check that the request has been successfully enqueued.
+            enqueueOperation.getResult().get();
+        } catch (Exception e) {
+            Log.e(TAG, "Could not enqueue expedited picker sync request", e);
+            markSyncAsComplete(syncSource, syncRequest.getId());
+        }
+    }
+
+    /**
+     * Use this method for reactive syncs which are user action triggered.
+     *
+     * @param albumId is the id of the album that needs to be synced.
+     * @param authority The authority of the album media.
+     */
+    public void syncAlbumMediaForProviderImmediately(
+            @NonNull String albumId, @NonNull String authority) {
+        boolean isLocal = PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY.equals(authority);
+        syncAlbumMediaForProviderImmediately(albumId, getSyncSource(isLocal), authority);
+    }
+
+    /**
+     * Use this method for reactive syncs which are user action triggered.
+     *
+     * @param albumId is the id of the album that needs to be synced.
+     * @param syncSource indicates if the sync is required with local provider or cloud provider or
+     *     both.
+     */
+    private void syncAlbumMediaForProviderImmediately(
+            @NonNull String albumId, @SyncSource int syncSource, String authority) {
+        final Data inputData =
+                new Data(
+                        Map.of(
+                                SYNC_WORKER_INPUT_AUTHORITY, authority,
+                                SYNC_WORKER_INPUT_SYNC_SOURCE, syncSource,
+                                SYNC_WORKER_INPUT_RESET_TYPE, SYNC_RESET_ALBUM,
+                                SYNC_WORKER_INPUT_ALBUM_ID, albumId));
+        final OneTimeWorkRequest resetRequest =
+                buildOneTimeWorkerRequest(MediaResetWorker.class, inputData);
+        final OneTimeWorkRequest syncRequest =
+                buildOneTimeWorkerRequest(ImmediateAlbumSyncWorker.class, inputData);
+
+        // Track the new sync request(s)
+        trackNewAlbumMediaSyncRequests(syncSource, resetRequest.getId());
+        trackNewAlbumMediaSyncRequests(syncSource, syncRequest.getId());
+
+        // Enqueue local or cloud sync requests
+        try {
+            final Operation enqueueOperation =
+                    mWorkManager
+                            .beginUniqueWork(
+                                    IMMEDIATE_ALBUM_SYNC_WORK_NAME,
+                                    ExistingWorkPolicy.APPEND_OR_REPLACE,
+                                    resetRequest)
+                            .then(syncRequest).enqueue();
+
+            // Check that the request has been successfully enqueued.
+            enqueueOperation.getResult().get();
+        } catch (Exception e) {
+            Log.e(TAG, "Could not enqueue expedited picker sync request", e);
+            markAlbumMediaSyncAsComplete(syncSource, resetRequest.getId());
+            markAlbumMediaSyncAsComplete(syncSource, syncRequest.getId());
+        }
+    }
+
+    @NotNull
+    private OneTimeWorkRequest buildOneTimeWorkerRequest(
+            @NotNull Class<? extends Worker> workerClass, @NonNull Data inputData) {
+        return new OneTimeWorkRequest.Builder(workerClass)
+                .setInputData(inputData)
+                .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+                .build();
+    }
+
+    @NotNull
+    private PeriodicWorkRequest getPeriodicProactiveSyncRequest(@NotNull Data inputData) {
+        return new PeriodicWorkRequest.Builder(
+                ProactiveSyncWorker.class, SYNC_MEDIA_PERIODIC_WORK_INTERVAL, TimeUnit.HOURS)
+                .setInputData(inputData)
+                .setConstraints(getRequiresChargingAndIdleConstraints())
+                .build();
+    }
+
+    @NotNull
+    private PeriodicWorkRequest getPeriodicAlbumResetRequest(@NotNull Data inputData) {
+
+        return new PeriodicWorkRequest.Builder(
+                        MediaResetWorker.class,
+                        RESET_ALBUM_MEDIA_PERIODIC_WORK_INTERVAL,
+                        TimeUnit.HOURS)
+                .setInputData(inputData)
+                .setConstraints(getRequiresChargingAndIdleConstraints())
+                .addTag(SYNC_WORKER_TAG_IS_PERIODIC)
+                .build();
+    }
+
+    @NotNull
+    private OneTimeWorkRequest getOneTimeProactiveSyncRequest(@NotNull Data inputData) {
+        Constraints constraints =  new Constraints.Builder()
+                .setRequiresBatteryNotLow(true)
+                .build();
+
+        return new OneTimeWorkRequest.Builder(ProactiveSyncWorker.class)
+                .setInputData(inputData)
+                .setConstraints(constraints)
+                .build();
+    }
+
+    @NotNull
+    private static Constraints getRequiresChargingAndIdleConstraints() {
+        return new Constraints.Builder()
+                .setRequiresCharging(true)
+                .setRequiresDeviceIdle(true)
+                .build();
+    }
+
+    @SyncSource
+    private static int getSyncSource(boolean isLocal) {
+        return isLocal
+                ? SYNC_LOCAL_ONLY
+                : SYNC_CLOUD_ONLY;
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/sync/PickerSyncNotificationHelper.java b/src/com/android/providers/media/photopicker/sync/PickerSyncNotificationHelper.java
new file mode 100644
index 0000000..9579ccf
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/sync/PickerSyncNotificationHelper.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.app.NotificationCompat;
+import androidx.work.ForegroundInfo;
+
+import com.android.modules.utils.build.SdkLevel;
+import com.android.providers.media.R;
+
+/**
+ * Helper functions for Picker sync notifications.
+ */
+public class PickerSyncNotificationHelper {
+    private static final String TAG = "SyncNotifHelper";
+    @VisibleForTesting
+    static final String NOTIFICATION_CHANNEL_ID = "PhotoPickerSyncChannel";
+    @VisibleForTesting
+    static final int NOTIFICATION_ID = 0;
+    private static final int NOTIFICATION_TIMEOUT_MILLIS = 1000;
+
+
+    /**
+     * Created notification channel for Picker Sync notifications.
+     * Recreating an existing notification channel with its original values performs no operation,
+     * so it's safe to call this code when starting an app.
+     */
+    public static void createNotificationChannel(@NonNull Context context) {
+        final String contentTitle = context.getResources()
+                .getString(R.string.picker_sync_notification_channel);
+
+        final NotificationChannel channel = new NotificationChannel(
+                NOTIFICATION_CHANNEL_ID, contentTitle, NotificationManager.IMPORTANCE_LOW);
+        channel.enableLights(false);
+        channel.enableVibration(false);
+
+        final NotificationManager notificationManager =
+                context.getSystemService(NotificationManager.class);
+        if (notificationManager != null) {
+            notificationManager.createNotificationChannel(channel);
+        }
+    }
+
+    /**
+     * Return Foreground info. This object contains a Notification and notification id that should
+     * be displayed in the context of a foreground service.
+     * This method should not be invoked by WorkManager in Android S+ devices.
+     */
+    @NonNull
+    public static ForegroundInfo getForegroundInfo(@NonNull Context context) {
+        if (SdkLevel.isAtLeastS()) {
+            Log.w(TAG, "Picker Sync notifications should not be displayed in S+ devices.");
+        }
+        return new ForegroundInfo(NOTIFICATION_ID, getNotification(context));
+    }
+
+    /**
+     * Create a notification to display when Picker sync is happening.
+     */
+    private static Notification getNotification(@NonNull Context context) {
+        final Resources resources = context.getResources();
+        final String contentTitle = resources.getString(R.string.picker_sync_notification_title);
+        final String contentText = resources.getString(R.string.picker_sync_notification_text);
+
+        return new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
+                .setSmallIcon(R.drawable.picker_app_icon)
+                .setContentTitle(contentTitle)
+                .setContentText(contentText)
+                .setPriority(NotificationCompat.PRIORITY_MIN)
+                .setVisibility(NotificationCompat.VISIBILITY_SECRET)
+                .setSilent(true)
+                .setTimeoutAfter(NOTIFICATION_TIMEOUT_MILLIS)
+                .build();
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/sync/ProactiveSyncWorker.java b/src/com/android/providers/media/photopicker/sync/ProactiveSyncWorker.java
new file mode 100644
index 0000000..c93d74f
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/sync/ProactiveSyncWorker.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_AND_CLOUD;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SYNC_SOURCE;
+import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.getCloudSyncTracker;
+import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.getLocalSyncTracker;
+import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.markSyncAsComplete;
+
+import android.content.Context;
+import android.os.CancellationSignal;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.work.ForegroundInfo;
+import androidx.work.ListenableWorker;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+
+import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException;
+
+/**
+ * This is a {@link Worker} class responsible for proactively syncing media with the correct sync
+ * source.
+ */
+public class ProactiveSyncWorker extends Worker {
+    private static final String TAG = "PSyncWorker";
+    private final Context mContext;
+    private final CancellationSignal mCancellationSignal = new CancellationSignal();
+
+    /**
+     * Creates an instance of the {@link Worker}.
+     *
+     * @param context the application {@link Context}
+     * @param workerParams the set of {@link WorkerParameters}
+     */
+    public ProactiveSyncWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
+        super(context, workerParams);
+        mContext = context;
+    }
+
+    @NonNull
+    @Override
+    public ListenableWorker.Result doWork() {
+        // Do not allow endless re-runs of this worker, if this isn't the original run,
+        // just succeed and wait until the next scheduled run.
+        if (getRunAttemptCount() > 0) {
+            Log.w(TAG, "Worker retry was detected, ending this run in failure.");
+            return ListenableWorker.Result.failure();
+        }
+        final int syncSource =
+                getInputData()
+                        .getInt(
+                                SYNC_WORKER_INPUT_SYNC_SOURCE, /* defaultValue */
+                                SYNC_LOCAL_AND_CLOUD);
+
+        Log.i(
+                TAG,
+                String.format("Starting proactive picker sync from sync source: %s", syncSource));
+
+        try {
+            if (syncSource == SYNC_LOCAL_AND_CLOUD || syncSource == SYNC_LOCAL_ONLY) {
+                // Instantiate sync state tracker.
+                final SyncTracker localSyncTracker = getLocalSyncTracker();
+                localSyncTracker.createSyncFuture(getId());
+
+                // Complete sync and mark work tracker as finished.
+                checkIsWorkerStopped();
+                PickerSyncController.getInstanceOrThrow()
+                        .syncAllMediaFromLocalProvider(mCancellationSignal);
+                localSyncTracker.markSyncCompleted(getId());
+                Log.i(TAG, "Completed picker proactive sync complete from local provider.");
+            }
+            if (syncSource == SYNC_LOCAL_AND_CLOUD || syncSource == SYNC_CLOUD_ONLY) {
+                // Instantiate sync state tracker.
+                final SyncTracker cloudSyncTracker = getCloudSyncTracker();
+                cloudSyncTracker.createSyncFuture(getId());
+
+                // Complete sync and mark work tracker as finished.
+                checkIsWorkerStopped();
+                PickerSyncController.getInstanceOrThrow()
+                        .syncAllMediaFromCloudProvider(mCancellationSignal);
+                cloudSyncTracker.markSyncCompleted(getId());
+                Log.i(TAG, "Completed picker proactive sync complete from cloud provider.");
+            }
+            return ListenableWorker.Result.success();
+        } catch (IllegalStateException | RequestObsoleteException e) {
+            Log.e(TAG, "Could not complete proactive sync for sync source: " + syncSource, e);
+
+            // Mark all pending syncs as finished and set failure result.
+            markSyncAsComplete(syncSource, getId());
+            return ListenableWorker.Result.failure();
+        }
+    }
+
+    private void checkIsWorkerStopped() throws RequestObsoleteException {
+        if (isStopped()) {
+            throw new RequestObsoleteException("Work is stopped " + getId());
+        }
+    }
+
+    @Override
+    @NonNull
+    public ForegroundInfo getForegroundInfo() {
+        Log.e(TAG, "Proactive Sync Worker should not run as an expedited task");
+        return PickerSyncNotificationHelper.getForegroundInfo(mContext);
+    }
+
+    @Override
+    public void onStopped() {
+        Log.w(TAG, "Worker is stopped. Clearing all pending futures. It's possible that the sync "
+                + "still finishes running if it has started already.");
+        // Send CancellationSignal to any running tasks.
+        mCancellationSignal.cancel();
+        final int syncSource = getInputData()
+                .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, /* defaultValue */ SYNC_LOCAL_AND_CLOUD);
+        markSyncAsComplete(syncSource, getId());
+    }
+
+    @VisibleForTesting
+    CancellationSignal getCancellationSignal() {
+        return mCancellationSignal;
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/sync/SyncTracker.java b/src/com/android/providers/media/photopicker/sync/SyncTracker.java
new file mode 100644
index 0000000..33c4770
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/sync/SyncTracker.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class tracks all pending syncs in a synchronized map.
+ */
+public class SyncTracker {
+    private static final String TAG = "PickerSyncTracker";
+    private static final long SYNC_FUTURE_TIMEOUT = 20; // Minutes
+    private static final Object FUTURE_RESULT = new Object(); // Placeholder result object
+    private final Map<UUID, CompletableFuture<Object>> mFutureMap =
+            Collections.synchronizedMap(new HashMap<>());
+
+    /**
+     * Use this method to create a picker sync future and track its progress. This should be
+     * called either when a new sync request is enqueued, or when a new sync request starts
+     * processing.
+     * @param workRequestID the work request id of a picker sync.
+     */
+    public void createSyncFuture(UUID workRequestID) {
+        createSyncFuture(workRequestID, SYNC_FUTURE_TIMEOUT, TimeUnit.MINUTES);
+    }
+
+    /**
+     * Use this method to create a picker sync future with a custom timeout. This method is
+     * intended to be used from tests.
+     */
+    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    public void createSyncFuture(UUID workRequestID, long syncFutureTimeout, TimeUnit timeUnit) {
+        // Create a CompletableFuture that tracks a sync operation. The future will
+        // automatically be marked as finished after a given timeout. This is important because
+        // we're not able to track all WorkManager failures. In case of a failure to run the
+        // sync, we'll need to ensure that the future expires automatically after a given
+        // timeout.
+        final CompletableFuture<Object> syncFuture = new CompletableFuture<>();
+        syncFuture.completeOnTimeout(FUTURE_RESULT, syncFutureTimeout, timeUnit);
+        mFutureMap.put(workRequestID, syncFuture);
+        Log.i(TAG, String.format("Created new sync future %s. Future map: %s",
+                syncFuture, mFutureMap));
+    }
+
+    /**
+     * Use this method to mark a picker sync future as complete. If this is not invoked within a
+     * configured time limit, the future will automatically be set as done.
+     * @param workRequestID the work request id of a picker sync.
+     */
+    public void markSyncCompleted(UUID workRequestID) {
+        if (mFutureMap.containsKey(workRequestID)) {
+            mFutureMap.get(workRequestID).complete(FUTURE_RESULT);
+            mFutureMap.remove(workRequestID);
+            Log.i(TAG, String.format(
+                    "Marked sync future complete for work id: %s. Future map: %s",
+                    workRequestID, mFutureMap));
+        } else {
+            Log.w(TAG, String.format("Attempted to complete sync future that is not currently "
+                            + "tracked for work id: %s. Future map: %s",
+                    workRequestID, mFutureMap));
+        }
+    }
+
+    /**
+     * Use this method to check if any sync request is still pending.
+     * @return a {@link Collection} of {@link CompletableFuture} of pending syncs. This can be
+     * used to track when all pending are complete.
+     */
+    public Collection<CompletableFuture<Object>> pendingSyncFutures() {
+        flushAllCompleteFutures();
+        Log.i(TAG, String.format("Returning pending sync future map: %s", mFutureMap));
+        return mFutureMap.values();
+    }
+
+    private void flushAllCompleteFutures() {
+        // The synchronized map only guarantees serial access if all access to the backing map
+        // is accomplished through the returned map. Since the removeIf() method uses iterators to
+        // access the underlying map, it should be in a synchronized block.
+        Log.d(TAG, String.format("Flushing all complete futures: %s", mFutureMap));
+        synchronized (mFutureMap) {
+            mFutureMap.values().removeIf(CompletableFuture::isDone);
+        }
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/sync/SyncTrackerRegistry.java b/src/com/android/providers/media/photopicker/sync/SyncTrackerRegistry.java
new file mode 100644
index 0000000..5a5f6c9
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/sync/SyncTrackerRegistry.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_AND_CLOUD;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.UUID;
+
+/**
+ * This class stores all sync trackers.
+ */
+public class SyncTrackerRegistry {
+    private static SyncTracker sLocalSyncTracker = new SyncTracker();
+    private static SyncTracker sLocalAlbumSyncTracker = new SyncTracker();
+    private static SyncTracker sCloudSyncTracker = new SyncTracker();
+    private static SyncTracker sCloudAlbumSyncTracker = new SyncTracker();
+
+    public static SyncTracker getLocalSyncTracker() {
+        return sLocalSyncTracker;
+    }
+
+    /**
+     * This setter is required to inject mock data for tests. Do not use this anywhere else.
+     */
+    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    public static void setLocalSyncTracker(SyncTracker syncTracker) {
+        sLocalSyncTracker = syncTracker;
+    }
+
+    public static SyncTracker getLocalAlbumSyncTracker() {
+        return sLocalAlbumSyncTracker;
+    }
+
+    /**
+     * This setter is required to inject mock data for tests. Do not use this anywhere else.
+     */
+    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    public static void setLocalAlbumSyncTracker(
+            SyncTracker localAlbumSyncTracker) {
+        sLocalAlbumSyncTracker = localAlbumSyncTracker;
+    }
+
+    public static SyncTracker getCloudSyncTracker() {
+        return sCloudSyncTracker;
+    }
+
+    /**
+     * This setter is required to inject mock data for tests. Do not use this anywhere else.
+     */
+    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    public static void setCloudSyncTracker(
+            SyncTracker cloudSyncTracker) {
+        sCloudSyncTracker = cloudSyncTracker;
+    }
+
+    public static SyncTracker getCloudAlbumSyncTracker() {
+        return sCloudAlbumSyncTracker;
+    }
+
+    /**
+     * This setter is required to inject mock data for tests. Do not use this anywhere else.
+     */
+    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    public static void setCloudAlbumSyncTracker(
+            SyncTracker cloudAlbumSyncTracker) {
+        sCloudAlbumSyncTracker = cloudAlbumSyncTracker;
+    }
+
+    /**
+     * Return the appropriate sync tracker.
+     * @param isLocal is true when sync with local provider needs to be tracked. It is false for
+     *                sync with cloud provider.
+     * @return the appropriate {@link SyncTracker} object.
+     */
+    public static SyncTracker getSyncTracker(boolean isLocal) {
+        if (isLocal) {
+            return sLocalSyncTracker;
+        } else {
+            return sCloudSyncTracker;
+        }
+    }
+
+    /**
+     * Return the appropriate album sync tracker.
+     * @param isLocal is true when sync with local provider needs to be tracked. It is false for
+     *                sync with cloud provider.
+     * @return the appropriate {@link SyncTracker} object.
+     */
+    public static SyncTracker getAlbumSyncTracker(boolean isLocal) {
+        if (isLocal) {
+            return sLocalAlbumSyncTracker;
+        } else {
+            return sCloudAlbumSyncTracker;
+        }
+    }
+
+    /**
+     * Create the required completable futures for new media sync requests that need to be tracked.
+     */
+    public static void trackNewSyncRequests(
+            @PickerSyncManager.SyncSource int syncSource,
+            @NonNull UUID syncRequestId) {
+        if (syncSource == SYNC_LOCAL_ONLY || syncSource == SYNC_LOCAL_AND_CLOUD) {
+            getLocalSyncTracker().createSyncFuture(syncRequestId);
+        }
+        if (syncSource == SYNC_CLOUD_ONLY || syncSource == SYNC_LOCAL_AND_CLOUD) {
+            getCloudSyncTracker().createSyncFuture(syncRequestId);
+        }
+    }
+
+    /**
+     * Create the required completable futures for new album media sync requests that need to be
+     * tracked.
+     */
+    public static void trackNewAlbumMediaSyncRequests(
+            @PickerSyncManager.SyncSource int syncSource,
+            @NonNull UUID syncRequestId) {
+        if (syncSource == SYNC_LOCAL_ONLY || syncSource == SYNC_LOCAL_AND_CLOUD) {
+            getLocalAlbumSyncTracker().createSyncFuture(syncRequestId);
+        }
+        if (syncSource == SYNC_CLOUD_ONLY || syncSource == SYNC_LOCAL_AND_CLOUD) {
+            getCloudAlbumSyncTracker().createSyncFuture(syncRequestId);
+        }
+    }
+
+    /**
+     * Mark the required futures as complete for existing media sync requests.
+     */
+    public static void markSyncAsComplete(
+            @PickerSyncManager.SyncSource int syncSource,
+            @NonNull UUID syncRequestId) {
+        if (syncSource == SYNC_LOCAL_ONLY || syncSource == SYNC_LOCAL_AND_CLOUD) {
+            getLocalSyncTracker().markSyncCompleted(syncRequestId);
+        }
+        if (syncSource == SYNC_CLOUD_ONLY || syncSource == SYNC_LOCAL_AND_CLOUD) {
+            getCloudSyncTracker().markSyncCompleted(syncRequestId);
+        }
+    }
+
+    /**
+     * Mark the required futures as complete for existing album media sync requests.
+     */
+    public static void markAlbumMediaSyncAsComplete(
+            @PickerSyncManager.SyncSource int syncSource,
+            @NonNull UUID syncRequestId) {
+        if (syncSource == SYNC_LOCAL_ONLY || syncSource == SYNC_LOCAL_AND_CLOUD) {
+            getLocalAlbumSyncTracker().markSyncCompleted(syncRequestId);
+        }
+        if (syncSource == SYNC_CLOUD_ONLY || syncSource == SYNC_LOCAL_AND_CLOUD) {
+            getCloudAlbumSyncTracker().markSyncCompleted(syncRequestId);
+        }
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/ui/AlbumGridHolder.java b/src/com/android/providers/media/photopicker/ui/AlbumGridHolder.java
index 657ecc8..a90f638 100644
--- a/src/com/android/providers/media/photopicker/ui/AlbumGridHolder.java
+++ b/src/com/android/providers/media/photopicker/ui/AlbumGridHolder.java
@@ -16,6 +16,7 @@
 
 package com.android.providers.media.photopicker.ui;
 
+import android.provider.CloudMediaProviderContract;
 import android.text.TextUtils;
 import android.view.View;
 import android.widget.ImageView;
@@ -38,6 +39,7 @@
 
     private final ImageLoader mImageLoader;
     private final ImageView mIconThumb;
+    private final ImageView mIconDefaultThumb;
     private final TextView mAlbumName;
     private final TextView mItemCount;
     private final boolean mHasMimeTypeFilter;
@@ -50,6 +52,7 @@
         super(itemView);
 
         mIconThumb = itemView.findViewById(R.id.icon_thumbnail);
+        mIconDefaultThumb = itemView.findViewById(R.id.icon_default_thumbnail);
         mAlbumName = itemView.findViewById(R.id.album_name);
         mItemCount = itemView.findViewById(R.id.item_count);
         mImageLoader = imageLoader;
@@ -58,23 +61,38 @@
     }
 
     void bind(@NonNull Category category) {
-        itemView.setOnClickListener(v -> mOnAlbumClickListener.onAlbumClick(category));
-        mImageLoader.loadAlbumThumbnail(category, mIconThumb);
-        mAlbumName.setText(category.getDisplayName(itemView.getContext()));
+        int position = getAbsoluteAdapterPosition();
+        itemView.setOnClickListener(v -> mOnAlbumClickListener.onAlbumClick(category, position));
 
+        // Show default thumbnail icons if merged album is empty.
+        int defaultResId = -1;
+        if (CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES.equals(category.getId())) {
+            defaultResId = R.drawable.thumbnail_favorites;
+        } else if (CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS
+                .equals(category.getId())) {
+            defaultResId = R.drawable.thumbnail_videos;
+        }
+        mImageLoader.loadAlbumThumbnail(category, mIconThumb, defaultResId, mIconDefaultThumb);
+        mAlbumName.setText(category.getDisplayName(itemView.getContext()));
         // Check whether there is a mime type filter or not. If yes, hide the item count. Otherwise,
         // show the item count and update the count.
-        if (mHasMimeTypeFilter) {
-            mItemCount.setVisibility(View.GONE);
-        } else {
-            mItemCount.setVisibility(View.VISIBLE);
-            final int itemCount = category.getItemCount();
-            final String quantityText =
-                    StringUtils.getICUFormatString(
-                        itemView.getResources(), itemCount, R.string.picker_album_item_count);
-            final String itemCountString = NumberFormat.getInstance(Locale.getDefault()).format(
-                    itemCount);
-            mItemCount.setText(TextUtils.expandTemplate(quantityText, itemCountString));
+        if (mItemCount.getVisibility() == View.VISIBLE) {
+            // As per the current requirements, we are hiding album's item count and this piece of
+            // code will never execute. for now keeping it here as it is, in case if in future we
+            // need to show album's item count.
+            if (mHasMimeTypeFilter) {
+                mItemCount.setVisibility(View.GONE);
+            } else {
+                mItemCount.setVisibility(View.VISIBLE);
+                final int itemCount = category.getItemCount();
+                final String quantityText =
+                        StringUtils.getICUFormatString(
+                                itemView.getResources(), itemCount,
+                                R.string.picker_album_item_count);
+                final String itemCountString = NumberFormat.getInstance(Locale.getDefault()).format(
+                        itemCount);
+                mItemCount.setText(TextUtils.expandTemplate(quantityText, itemCountString));
+            }
         }
     }
 }
diff --git a/src/com/android/providers/media/photopicker/ui/AlbumsTabAdapter.java b/src/com/android/providers/media/photopicker/ui/AlbumsTabAdapter.java
index 8cae259..7b2ed05 100644
--- a/src/com/android/providers/media/photopicker/ui/AlbumsTabAdapter.java
+++ b/src/com/android/providers/media/photopicker/ui/AlbumsTabAdapter.java
@@ -16,6 +16,8 @@
 
 package com.android.providers.media.photopicker.ui;
 
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_VIEW_CREATED;
+
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -80,10 +82,10 @@
     }
 
     void updateCategoryList(@NonNull List<Category> categoryList) {
-        setAllItems(categoryList);
+        setAllItems(categoryList, /* reset */ ACTION_VIEW_CREATED);
     }
 
     interface OnAlbumClickListener {
-        void onAlbumClick(@NonNull Category category);
+        void onAlbumClick(@NonNull Category category, int position);
     }
 }
diff --git a/src/com/android/providers/media/photopicker/ui/AlbumsTabFragment.java b/src/com/android/providers/media/photopicker/ui/AlbumsTabFragment.java
index de8bcb8..3781e26 100644
--- a/src/com/android/providers/media/photopicker/ui/AlbumsTabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/AlbumsTabFragment.java
@@ -17,6 +17,7 @@
 
 import android.content.Context;
 import android.os.Bundle;
+import android.util.Log;
 import android.view.View;
 
 import androidx.annotation.NonNull;
@@ -27,18 +28,20 @@
 import com.android.providers.media.R;
 import com.android.providers.media.photopicker.util.LayoutModeUtils;
 
+import java.util.ArrayList;
+
 /**
  * Albums tab fragment for showing the albums
  */
 public class AlbumsTabFragment extends TabFragment {
-
+    private static final String TAG = PhotosTabFragment.class.getSimpleName();
     private static final int MINIMUM_SPAN_COUNT = 2;
     private static final int GRID_COLUMN_COUNT = 2;
 
     @Override
     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
-        final Context context = getContext();
+        final Context context = requireContext();
 
         // Set the pane title for A11y.
         view.setAccessibilityPaneTitle(getString(R.string.picker_albums));
@@ -56,9 +59,14 @@
                 mOnChooseAppBannerEventListener, mOnCloudMediaAvailableBannerEventListener,
                 mOnAccountUpdatedBannerEventListener, mOnChooseAccountBannerEventListener);
         mPickerViewModel.getCategories().observe(this, categoryList -> {
-            adapter.updateCategoryList(categoryList);
-            // Handle emptyView's visibility
-            updateVisibilityForEmptyView(/* shouldShowEmptyView */ categoryList.size() == 0);
+            if (categoryList.size() == 1 && categoryList.get(0).getId().equals("EMPTY_VIEW")) {
+                adapter.updateCategoryList(new ArrayList<>());
+                updateVisibilityForEmptyView(false);
+            } else {
+                adapter.updateCategoryList(categoryList);
+                // Handle emptyView's visibility
+                updateVisibilityForEmptyView(/* shouldShowEmptyView */ categoryList.size() == 0);
+            }
         });
 
         final AlbumsTabItemDecoration itemDecoration = new AlbumsTabItemDecoration(context);
@@ -68,7 +76,7 @@
         mRecyclerView.setColumnWidth(albumSize + spacing);
         mRecyclerView.setMinimumSpanCount(MINIMUM_SPAN_COUNT);
 
-        setLayoutManager(adapter, GRID_COLUMN_COUNT);
+        setLayoutManager(context, adapter, GRID_COLUMN_COUNT);
         mRecyclerView.setAdapter(adapter);
         mRecyclerView.addItemDecoration(itemDecoration);
     }
@@ -76,11 +84,20 @@
     @Override
     public void onResume() {
         super.onResume();
-        getPickerActivity().updateCommonLayouts(LayoutModeUtils.MODE_ALBUMS_TAB, /* title */ "");
+
+        requirePickerActivity()
+                .updateCommonLayouts(LayoutModeUtils.MODE_ALBUMS_TAB, /* title */ "");
     }
 
-    private final AlbumsTabAdapter.OnAlbumClickListener mOnAlbumClickListener = category ->
-        PhotosTabFragment.show(getActivity().getSupportFragmentManager(), category);
+    private final AlbumsTabAdapter.OnAlbumClickListener mOnAlbumClickListener =
+            (category, position) -> {
+                mPickerViewModel.logAlbumOpened(category, position);
+                try {
+                    PhotosTabFragment.show(requireActivity().getSupportFragmentManager(), category);
+                } catch (RuntimeException e) {
+                    Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
+                }
+            };
 
     /**
      * Create the albums tab fragment and add it into the FragmentManager
diff --git a/src/com/android/providers/media/photopicker/ui/ImageLoader.java b/src/com/android/providers/media/photopicker/ui/ImageLoader.java
index 0d77b82..12433fc 100644
--- a/src/com/android/providers/media/photopicker/ui/ImageLoader.java
+++ b/src/com/android/providers/media/photopicker/ui/ImageLoader.java
@@ -20,17 +20,16 @@
 
 import android.content.Context;
 import android.graphics.Bitmap;
-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.view.View;
 import android.widget.ImageView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.providers.media.photopicker.data.glide.GlideLoadable;
 import com.android.providers.media.photopicker.data.model.Category;
 import com.android.providers.media.photopicker.data.model.Item;
 
@@ -42,6 +41,8 @@
 import com.bumptech.glide.request.RequestOptions;
 import com.bumptech.glide.signature.ObjectKey;
 
+import java.util.Optional;
+
 /**
  * A class to assist with loading and managing the Images (i.e. thumbnails and preview) associated
  * with item.
@@ -55,6 +56,7 @@
             RequestOptions.option(THUMBNAIL_REQUEST, /* enableThumbnail */ true);
     private final Context mContext;
     private final PreferredColorSpace mPreferredColorSpace;
+    private static final String PREVIEW_PREFIX = "preview_";
 
     public ImageLoader(Context context) {
         mContext = context;
@@ -71,12 +73,24 @@
      * @param category  the album
      * @param imageView the imageView shows the thumbnail
      */
-    public void loadAlbumThumbnail(@NonNull Category category, @NonNull ImageView imageView) {
+    public void loadAlbumThumbnail(@NonNull Category category, @NonNull ImageView imageView,
+            int defaultThumbnailRes, @NonNull ImageView defaultIcon) {
         // Always show all thumbnails as bitmap images instead of drawables
         // This is to ensure that we do not animate any thumbnail (for eg GIF)
         // TODO(b/194285082): Use drawable instead of bitmap, as it saves memory.
-        loadWithGlide(getBitmapRequestBuilder(category.getCoverUri()), THUMBNAIL_OPTION,
-                /* signature */ null, imageView);
+        if (category.getCoverUri() != null || defaultThumbnailRes == -1) {
+            defaultIcon.setVisibility(View.GONE);
+            imageView.setVisibility(View.VISIBLE);
+
+            loadWithGlide(getBitmapRequestBuilder(category.toGlideLoadable()), THUMBNAIL_OPTION,
+                    /* signature */ null, imageView);
+        } else {
+            imageView.setVisibility(View.INVISIBLE);
+            defaultIcon.setVisibility(View.VISIBLE);
+
+            loadWithGlide(getDrawableRequestBuilder(mContext.getDrawable(defaultThumbnailRes)),
+                    THUMBNAIL_OPTION,  /* signature */ null, defaultIcon);
+        }
     }
 
     /**
@@ -86,11 +100,12 @@
      * @param imageView the imageView shows the thumbnail
      */
     public void loadPhotoThumbnail(@NonNull Item item, @NonNull ImageView imageView) {
+        final GlideLoadable loadable = item.toGlideLoadable();
         // Always show all thumbnails as bitmap images instead of drawables
         // This is to ensure that we do not animate any thumbnail (for eg GIF)
         // TODO(b/194285082): Use drawable instead of bitmap, as it saves memory.
-        loadWithGlide(getBitmapRequestBuilder(item.getContentUri()), THUMBNAIL_OPTION,
-                getGlideSignature(item, /* prefix */ ""), imageView);
+        loadWithGlide(getBitmapRequestBuilder(loadable), THUMBNAIL_OPTION,
+                getGlideSignature(loadable, /* prefix */ null), imageView);
     }
 
     /**
@@ -100,65 +115,63 @@
      * @param imageView the imageView shows the image
      */
     public void loadImagePreview(@NonNull Item item, @NonNull ImageView imageView)  {
+        final GlideLoadable loadable = item.toGlideLoadable();
         if (item.isGif()) {
-            loadWithGlide(getGifRequestBuilder(item.getContentUri()), /* requestOptions */ null,
-                    getGlideSignature(item, /* prefix */ ""), imageView);
+            loadWithGlide(
+                    getGifRequestBuilder(loadable),
+                    /* requestOptions */ null,
+                    getGlideSignature(loadable, /* prefix= */ PREVIEW_PREFIX),
+                    imageView);
             return;
         }
 
         if (item.isAnimatedWebp()) {
-            loadAnimatedWebpPreview(item, imageView);
+            loadWithGlide(
+                    getDrawableRequestBuilder(loadable),
+                    /* requestOptions */ null,
+                    getGlideSignature(loadable, PREVIEW_PREFIX),
+                    imageView);
             return;
         }
 
         // Preview as bitmap image for all other image types
-        loadWithGlide(getBitmapRequestBuilder(item.getContentUri()), /* requestOptions */ null,
-                getGlideSignature(item, /* prefix */ ""), imageView);
-    }
-
-    private void loadAnimatedWebpPreview(@NonNull Item item, @NonNull ImageView imageView) {
-        final Uri uri = item.getContentUri();
-        final ImageDecoder.Source source = ImageDecoder.createSource(mContext.getContentResolver(),
-                uri);
-        Drawable drawable = null;
-        try {
-            drawable = ImageDecoder.decodeDrawable(source);
-        } catch (Exception e) {
-            Log.d(TAG, "Failed to decode drawable for uri: " + uri, e);
-        }
-
-        // If we failed to decode drawable for a source using ImageDecoder, then try using uri
-        // directly. Glide will show static image for an animated webp. That is okay as we tried our
-        // best to load animated webp but couldn't, and we anyway show the GIF badge in preview.
-        loadWithGlide(getDrawableRequestBuilder(drawable == null ? uri : drawable),
-                /* requestOptions */ null, getGlideSignature(item, /* prefix */ ""), imageView);
+        loadWithGlide(
+                getBitmapRequestBuilder(loadable),
+                /* requestOptions */ null,
+                getGlideSignature(loadable, /* prefix= */ PREVIEW_PREFIX),
+                imageView);
     }
 
     /**
      * Loads the image from first frame of the given video item
      */
     public void loadImageFromVideoForPreview(@NonNull Item item, @NonNull ImageView imageView) {
-        loadWithGlide(getBitmapRequestBuilder(item.getContentUri()),
-                new RequestOptions().frame(1000), getGlideSignature(item, "Preview"), imageView);
+        final GlideLoadable loadable = item.toGlideLoadable();
+        loadWithGlide(
+                getBitmapRequestBuilder(loadable),
+                new RequestOptions().frame(1000),
+                getGlideSignature(loadable, /* prefix= */ PREVIEW_PREFIX),
+                imageView);
     }
 
-    private ObjectKey getGlideSignature(Item item, String prefix) {
-        // TODO(b/224725723): Remove media store version from key once MP ids are stable.
-        return new ObjectKey(
-                MediaStore.getVersion(mContext) + prefix + item.getContentUri().toString() +
-                        item.getGenerationModified());
+    private ObjectKey getGlideSignature(GlideLoadable loadable, @Nullable String prefix) {
+        // TODO(b/224725723): Remove media store version from key once MP ids are
+        // stable.
+        return loadable.getLoadableSignature(
+                /* prefix= */ MediaStore.getVersion(mContext)
+                        + Optional.ofNullable(prefix).orElse(""));
     }
 
-    private RequestBuilder<Bitmap> getBitmapRequestBuilder(Uri uri) {
+    private RequestBuilder<Bitmap> getBitmapRequestBuilder(GlideLoadable loadable) {
         return Glide.with(mContext)
                 .asBitmap()
-                .load(uri);
+                .load(loadable);
     }
 
-    private RequestBuilder<GifDrawable> getGifRequestBuilder(Uri uri) {
+    private RequestBuilder<GifDrawable> getGifRequestBuilder(GlideLoadable loadable) {
         return Glide.with(mContext)
                 .asGif()
-                .load(uri);
+                .load(loadable);
     }
 
     private RequestBuilder<Drawable> getDrawableRequestBuilder(Object model) {
diff --git a/src/com/android/providers/media/photopicker/ui/ItemsAction.java b/src/com/android/providers/media/photopicker/ui/ItemsAction.java
new file mode 100644
index 0000000..04c6f04
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/ui/ItemsAction.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 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.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Represents the actions that can be performed on lis of items / category items, based on different
+ * scenarios like next page load, refreshing the list, updating the list on profile switch etc.
+ */
+public class ItemsAction {
+
+    // This is basically a no-op action which will meet no conditions in the code.
+    public static final int ACTION_DEFAULT = 0;
+    public static final int ACTION_VIEW_CREATED = 1;
+    public static final int ACTION_LOAD_NEXT_PAGE = 2;
+    public static final int ACTION_CLEAR_AND_UPDATE_LIST = 3;
+    public static final int ACTION_CLEAR_GRID = 4;
+    public static final int ACTION_REFRESH_ITEMS = 5;
+
+
+    private ItemsAction() {
+    }
+
+    /** @hide */
+    @IntDef({ACTION_DEFAULT,
+            ACTION_VIEW_CREATED,
+            ACTION_LOAD_NEXT_PAGE,
+            ACTION_CLEAR_AND_UPDATE_LIST,
+            ACTION_CLEAR_GRID,
+            ACTION_REFRESH_ITEMS})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Type {
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/ui/MediaItemGridViewHolder.java b/src/com/android/providers/media/photopicker/ui/MediaItemGridViewHolder.java
index f149955..fef4ad3 100644
--- a/src/com/android/providers/media/photopicker/ui/MediaItemGridViewHolder.java
+++ b/src/com/android/providers/media/photopicker/ui/MediaItemGridViewHolder.java
@@ -25,6 +25,8 @@
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.providers.media.R;
@@ -35,6 +37,7 @@
  * a video).
  */
 class MediaItemGridViewHolder extends RecyclerView.ViewHolder {
+    private final LifecycleOwner mLifecycleOwner;
     private final ImageLoader mImageLoader;
     private final ImageView mIconThumb;
     private final ImageView mIconGif;
@@ -43,10 +46,24 @@
     private final TextView mVideoDuration;
     private final View mOverlayGradient;
     private final boolean mCanSelectMultiple;
+    private final boolean mShowOrderedSelectionLabel;
+    private final TextView mSelectedOrderText;
+    private LiveData<Integer> mSelectionOrder;
+    private final ImageView mCheckIcon;
 
-    MediaItemGridViewHolder(@NonNull View itemView, @NonNull ImageLoader imageLoader,
-            boolean canSelectMultiple) {
+    private final View.OnHoverListener mOnMediaItemHoverListener;
+    private final PhotosTabAdapter.OnMediaItemClickListener mOnMediaItemClickListener;
+
+    MediaItemGridViewHolder(
+            @NonNull LifecycleOwner lifecycleOwner,
+            @NonNull View itemView,
+            @NonNull ImageLoader imageLoader,
+            @NonNull PhotosTabAdapter.OnMediaItemClickListener onMediaItemClickListener,
+            View.OnHoverListener onMediaItemHoverListener,
+            boolean canSelectMultiple,
+            boolean isOrderedSelection) {
         super(itemView);
+        mLifecycleOwner = lifecycleOwner;
         mIconThumb = itemView.findViewById(R.id.icon_thumbnail);
         mIconGif = itemView.findViewById(R.id.icon_gif);
         mIconMotionPhoto = itemView.findViewById(R.id.icon_motion_photo);
@@ -54,12 +71,25 @@
         mVideoDuration = mVideoBadgeContainer.findViewById(R.id.video_duration);
         mOverlayGradient = itemView.findViewById(R.id.overlay_gradient);
         mImageLoader = imageLoader;
+        mOnMediaItemClickListener = onMediaItemClickListener;
         mCanSelectMultiple = canSelectMultiple;
-
-        itemView.findViewById(R.id.icon_check).setVisibility(mCanSelectMultiple ? VISIBLE : GONE);
+        mShowOrderedSelectionLabel = isOrderedSelection;
+        mOnMediaItemHoverListener = onMediaItemHoverListener;
+        mSelectedOrderText = itemView.findViewById(R.id.selected_order);
+        mCheckIcon = itemView.findViewById(R.id.icon_check);
+        mCheckIcon.setVisibility(
+                (mCanSelectMultiple && !mShowOrderedSelectionLabel) ? VISIBLE : GONE);
+        mSelectedOrderText.setVisibility(
+                (mCanSelectMultiple && mShowOrderedSelectionLabel) ? VISIBLE : GONE);
     }
 
     public void bind(@NonNull Item item, boolean isSelected) {
+        int position = getAbsoluteAdapterPosition();
+        itemView.setOnClickListener(v -> mOnMediaItemClickListener.onItemClick(v, position, this));
+        itemView.setOnLongClickListener(v ->
+                mOnMediaItemClickListener.onItemLongClick(v, position));
+        itemView.setOnHoverListener(mOnMediaItemHoverListener);
+
         mImageLoader.loadPhotoThumbnail(item, mIconThumb);
 
         mIconGif.setVisibility(item.isGifOrAnimatedWebp() ? VISIBLE : GONE);
@@ -83,6 +113,7 @@
 
         if (mCanSelectMultiple) {
             itemView.setSelected(isSelected);
+            mSelectedOrderText.setText("");
             // There is an issue b/223695510 about not selected in Accessibility mode. It only
             // says selected state, but it doesn't say not selected state. Add the not selected
             // only to avoid that it says selected twice.
@@ -91,15 +122,49 @@
         }
     }
 
+    /** Sets the LiveData selection order for the current grid item view. */
+    public void setSelectionOrder(LiveData<Integer> selectionOrder) {
+        if (selectionOrder == null) {
+            mSelectedOrderText.setText("");
+            if (mSelectionOrder != null) {
+                mSelectionOrder.removeObservers(mLifecycleOwner);
+            }
+        } else {
+            mSelectedOrderText.setText(selectionOrder.getValue().toString());
+            selectionOrder.observe(
+                    mLifecycleOwner,
+                    val -> {
+                        mSelectedOrderText.setText(val.toString());
+                    });
+        }
+        mSelectionOrder = selectionOrder;
+    }
+
     @NonNull
     private Context getContext() {
         return itemView.getContext();
     }
 
+    /**
+     * Get the {@link ImageView} for the thumbnail image representing this MediaItem.
+     * @return the image view for the thumbnail.
+     */
+    public ImageView getThumbnailImageView() {
+        return mIconThumb;
+    }
+
     private boolean showShowOverlayGradient(@NonNull Item item) {
         return mCanSelectMultiple
                 || item.isGifOrAnimatedWebp()
                 || item.isVideo()
                 || item.isMotionPhoto();
     }
+
+    /** Release any non-reusable resources, as the view is being recycled. */
+    public void release() {
+        if (mSelectionOrder != null) {
+            mSelectionOrder.removeObservers(mLifecycleOwner);
+            mSelectionOrder = null;
+        }
+    }
 }
diff --git a/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java b/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java
index d7568f3..1a4f4e7 100644
--- a/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java
+++ b/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java
@@ -16,6 +16,8 @@
 
 package com.android.providers.media.photopicker.ui;
 
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_CLEAR_AND_UPDATE_LIST;
+
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.TextView;
@@ -31,6 +33,8 @@
 import com.android.providers.media.photopicker.data.model.Item;
 import com.android.providers.media.photopicker.util.DateTimeUtils;
 
+import com.bumptech.glide.util.ViewPreloadSizeProvider;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -38,20 +42,21 @@
 /**
  * Adapts from model to something RecyclerView understands.
  */
-class PhotosTabAdapter extends TabAdapter {
+public class PhotosTabAdapter extends TabAdapter {
 
     private static final int RECENT_MINIMUM_COUNT = 12;
-
+    private final LifecycleOwner mLifecycleOwner;
     private final boolean mShowRecentSection;
-    private final View.OnClickListener mOnMediaItemClickListener;
-    private final View.OnLongClickListener mOnMediaItemLongClickListener;
+    private final OnMediaItemClickListener mOnMediaItemClickListener;
     private final Selection mSelection;
+    private final ViewPreloadSizeProvider mPreloadSizeProvider;
+
+    private final View.OnHoverListener mOnMediaItemHoverListener;
 
     PhotosTabAdapter(boolean showRecentSection,
             @NonNull Selection selection,
             @NonNull ImageLoader imageLoader,
-            @NonNull View.OnClickListener onMediaItemClickListener,
-            @NonNull View.OnLongClickListener onMediaItemLongClickListener,
+            @NonNull OnMediaItemClickListener onMediaItemClickListener,
             @NonNull LifecycleOwner lifecycleOwner,
             @NonNull LiveData<String> cloudMediaProviderAppTitle,
             @NonNull LiveData<String> cloudMediaAccountName,
@@ -62,16 +67,20 @@
             @NonNull OnBannerEventListener onChooseAppBannerEventListener,
             @NonNull OnBannerEventListener onCloudMediaAvailableBannerEventListener,
             @NonNull OnBannerEventListener onAccountUpdatedBannerEventListener,
-            @NonNull OnBannerEventListener onChooseAccountBannerEventListener) {
+            @NonNull OnBannerEventListener onChooseAccountBannerEventListener,
+            @NonNull View.OnHoverListener onMediaItemHoverListener,
+            @NonNull ViewPreloadSizeProvider preloadSizeProvider) {
         super(imageLoader, lifecycleOwner, cloudMediaProviderAppTitle, cloudMediaAccountName,
                 shouldShowChooseAppBanner, shouldShowCloudMediaAvailableBanner,
                 shouldShowAccountUpdatedBanner, shouldShowChooseAccountBanner,
                 onChooseAppBannerEventListener, onCloudMediaAvailableBannerEventListener,
                 onAccountUpdatedBannerEventListener, onChooseAccountBannerEventListener);
+        mLifecycleOwner = lifecycleOwner;
         mShowRecentSection = showRecentSection;
         mSelection = selection;
         mOnMediaItemClickListener = onMediaItemClickListener;
-        mOnMediaItemLongClickListener = onMediaItemLongClickListener;
+        mOnMediaItemHoverListener = onMediaItemHoverListener;
+        mPreloadSizeProvider = preloadSizeProvider;
     }
 
     @NonNull
@@ -85,10 +94,17 @@
     @Override
     RecyclerView.ViewHolder createMediaItemViewHolder(@NonNull ViewGroup viewGroup) {
         final View view = getView(viewGroup, R.layout.item_photo_grid);
-        view.setOnClickListener(mOnMediaItemClickListener);
-        view.setOnLongClickListener(mOnMediaItemLongClickListener);
-
-        return new MediaItemGridViewHolder(view, mImageLoader, mSelection.canSelectMultiple());
+        final MediaItemGridViewHolder viewHolder =
+                new MediaItemGridViewHolder(
+                        mLifecycleOwner,
+                        view,
+                        mImageLoader,
+                        mOnMediaItemClickListener,
+                        mOnMediaItemHoverListener,
+                        mSelection.canSelectMultiple(),
+                        mSelection.isSelectionOrdered());
+        mPreloadSizeProvider.setView(viewHolder.getThumbnailImageView());
+        return viewHolder;
     }
 
     @Override
@@ -102,12 +118,19 @@
     @Override
     void onBindMediaItemViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
         final Item item = (Item) getAdapterItem(position);
-        final MediaItemGridViewHolder mediaItemVH  = (MediaItemGridViewHolder) viewHolder;
+        final MediaItemGridViewHolder mediaItemVH = (MediaItemGridViewHolder) viewHolder;
 
         final boolean isSelected = mSelection.canSelectMultiple()
                 && mSelection.isItemSelected(item);
-        mediaItemVH.bind(item, isSelected);
 
+        if (isSelected) {
+            mSelection.addCheckedItemIndex(item, position);
+        }
+
+        mediaItemVH.bind(item, isSelected);
+        if (isSelected && mSelection.isSelectionOrdered()) {
+            mediaItemVH.setSelectionOrder(mSelection.getSelectedItemOrder(item));
+        }
         // We also need to set Item as a tag so that OnClick/OnLongClickListeners can then
         // retrieve it.
         mediaItemVH.itemView.setTag(item);
@@ -119,11 +142,15 @@
     }
 
     @Override
-    boolean isItemTypeMediaItem(int position) {
+    public boolean isItemTypeMediaItem(int position) {
         return getAdapterItem(position) instanceof Item;
     }
 
     void setMediaItems(@NonNull List<Item> mediaItems) {
+        setMediaItems(mediaItems, ACTION_CLEAR_AND_UPDATE_LIST);
+    }
+
+    void setMediaItems(@NonNull List<Item> mediaItems, @ItemsAction.Type int action) {
         final List<Object> mediaItemsWithDateHeaders;
         if (!mediaItems.isEmpty()) {
             // We'll have at least one section
@@ -155,9 +182,7 @@
         } else {
             mediaItemsWithDateHeaders = Collections.emptyList();
         }
-        setAllItems(mediaItemsWithDateHeaders);
-
-        notifyDataSetChanged();
+        setAllItems(mediaItemsWithDateHeaders, action);
     }
 
     @VisibleForTesting
@@ -186,4 +211,10 @@
             }
         }
     }
+
+    interface OnMediaItemClickListener {
+        void onItemClick(@NonNull View view, int position, MediaItemGridViewHolder viewHolder);
+
+        boolean onItemLongClick(@NonNull View view, int position);
+    }
 }
diff --git a/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java b/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
index 8607734..0917b55 100644
--- a/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
@@ -15,13 +15,25 @@
  */
 package com.android.providers.media.photopicker.ui;
 
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_LOAD_NEXT_PAGE;
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_REFRESH_ITEMS;
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_VIEW_CREATED;
+import static com.android.providers.media.photopicker.ui.TabAdapter.ITEM_TYPE_MEDIA_ITEM;
 import static com.android.providers.media.photopicker.util.LayoutModeUtils.MODE_ALBUM_PHOTOS_TAB;
 import static com.android.providers.media.photopicker.util.LayoutModeUtils.MODE_PHOTOS_TAB;
 
+import android.animation.ObjectAnimator;
 import android.content.Context;
+import android.content.Intent;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
 import android.text.TextUtils;
+import android.util.Log;
+import android.view.MotionEvent;
 import android.view.View;
+import android.widget.ProgressBar;
+import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -30,29 +42,63 @@
 import androidx.fragment.app.FragmentTransaction;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.providers.media.R;
+import com.android.providers.media.photopicker.data.PaginationParameters;
+import com.android.providers.media.photopicker.data.glide.PickerPreloadModelProvider;
 import com.android.providers.media.photopicker.data.model.Category;
 import com.android.providers.media.photopicker.data.model.Item;
 import com.android.providers.media.photopicker.util.LayoutModeUtils;
+import com.android.providers.media.photopicker.util.MimeFilterUtils;
+import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
 import com.android.providers.media.util.StringUtils;
 
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.RequestManager;
+import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
+import com.bumptech.glide.util.ViewPreloadSizeProvider;
 import com.google.android.material.snackbar.Snackbar;
 
+import org.jetbrains.annotations.NotNull;
+
 import java.text.NumberFormat;
-import java.util.List;
+import java.util.ArrayList;
 import java.util.Locale;
+import java.util.Objects;
 
 /**
  * Photos tab fragment for showing the photos
  */
 public class PhotosTabFragment extends TabFragment {
+    private static final String TAG = PhotosTabFragment.class.getSimpleName();
     private static final int MINIMUM_SPAN_COUNT = 3;
     private static final int GRID_COLUMN_COUNT = 3;
     private static final String FRAGMENT_TAG = "PhotosTabFragment";
 
     private Category mCategory = Category.DEFAULT;
 
+    private boolean mIsCurrentPageLoading = false;
+
+    private boolean mAtLeastOnePageLoaded = false;
+
+    private boolean mIsCloudMediaInPhotoPickerEnabled;
+
+    private int mPageSize;
+    private PickerPreloadModelProvider mPreloadModelProvider;
+
+    @Nullable
+    private RequestManager mGlideRequestManager = null;
+
+    private ProgressBar mProgressBar;
+    private TextView mLoadingTextView;
+    private ObjectAnimator mObjectAnimator = new ObjectAnimator();
+    private int mRecyclerViewTopPadding;
+    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+
+    private final Object mHideProgressBarToken = new Object();
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -67,7 +113,13 @@
     @Override
     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
-        final Context context = getContext();
+        final Context context = requireContext();
+
+        // Init is only required for album content tab fragments when the fragment is not being
+        // recreated from a previous state.
+        if (savedInstanceState == null && !mCategory.isDefault()) {
+            mPickerViewModel.initPhotoPickerData(mCategory);
+        }
 
         // We only add the RECENT header on the PhotosTabFragment with CATEGORY_DEFAULT. In this
         // case, we call this method {loadItems} with null category. When the category is not
@@ -86,26 +138,86 @@
         final LiveData<Boolean> showChooseAccountBanner = shouldShowBanners
                 ? mPickerViewModel.shouldShowChooseAccountBannerLiveData() : doNotShowBanner;
 
-        final PhotosTabAdapter adapter = new PhotosTabAdapter(showRecentSection, mSelection,
-                mImageLoader, this::onItemClick, this::onItemLongClick, /* lifecycleOwner */ this,
-                mPickerViewModel.getCloudMediaProviderAppTitleLiveData(),
-                mPickerViewModel.getCloudMediaAccountNameLiveData(), showChooseAppBanner,
-                showCloudMediaAvailableBanner, showAccountUpdatedBanner, showChooseAccountBanner,
-                mOnChooseAppBannerEventListener, mOnCloudMediaAvailableBannerEventListener,
-                mOnAccountUpdatedBannerEventListener, mOnChooseAccountBannerEventListener);
+        mIsCloudMediaInPhotoPickerEnabled =
+                mPickerViewModel.getConfigStore().isCloudMediaInPhotoPickerEnabled();
+
+        if (savedInstanceState == null) {
+            initProgressBar(view);
+        }
+        mSelection.clearCheckedItemList();
+
+        ViewPreloadSizeProvider viewSizeProvider = new ViewPreloadSizeProvider();
+
+        final PhotosTabAdapter adapter =
+                new PhotosTabAdapter(
+                        showRecentSection,
+                        mSelection,
+                        mImageLoader,
+                        mOnMediaItemClickListener,
+                        this, /* lifecycleOwner */
+                        mPickerViewModel.getCloudMediaProviderAppTitleLiveData(),
+                        mPickerViewModel.getCloudMediaAccountNameLiveData(),
+                        showChooseAppBanner,
+                        showCloudMediaAvailableBanner,
+                        showAccountUpdatedBanner,
+                        showChooseAccountBanner,
+                        mOnChooseAppBannerEventListener,
+                        mOnCloudMediaAvailableBannerEventListener,
+                        mOnAccountUpdatedBannerEventListener,
+                        mOnChooseAccountBannerEventListener,
+                        mOnMediaItemHoverListener,
+                        viewSizeProvider);
+
+        mPreloadModelProvider = new PickerPreloadModelProvider(getContext(), adapter);
+        mGlideRequestManager = Glide.with(this);
+
+        RecyclerViewPreloader<Item> preloader =
+                new RecyclerViewPreloader<>(
+                        Glide.with(getContext()),
+                        mPreloadModelProvider,
+                        viewSizeProvider,
+                        /* maxPreload= */ 8);
+        mRecyclerView.addOnScrollListener(preloader);
+
+
+        // initialise pre-granted items is necessary.
+        Intent activityIntent = requireActivity().getIntent();
+        mPickerViewModel.initialisePreGrantsIfNecessary(mPickerViewModel.getSelection(),
+                activityIntent.getExtras(), MimeFilterUtils.getMimeTypeFilters(activityIntent));
 
         if (mCategory.isDefault()) {
+            mPageSize = mIsCloudMediaInPhotoPickerEnabled
+                    ? PaginationParameters.PAGINATION_PAGE_SIZE_ITEMS : -1;
             setEmptyMessage(R.string.picker_photos_empty_message);
             // Set the pane title for A11y
             view.setAccessibilityPaneTitle(getString(R.string.picker_photos));
-            mPickerViewModel.getItems()
-                    .observe(this, itemList -> onChangeMediaItems(itemList, adapter));
+            // Get items with pagination parameters representing the first page.
+            mPickerViewModel.getPaginatedItemsForAction(
+                            ACTION_VIEW_CREATED,
+                            new PaginationParameters(
+                                    mPageSize,
+                                    /* dateBeforeMs */ Long.MIN_VALUE,
+                                    /* rowId */ -1))
+                    .observe(this, itemListResult -> {
+                        onChangeMediaItems(itemListResult, adapter);
+                    });
         } else {
+            mPageSize = mIsCloudMediaInPhotoPickerEnabled
+                    ? PaginationParameters.PAGINATION_PAGE_SIZE_ALBUM_ITEMS : -1;
             setEmptyMessage(R.string.picker_album_media_empty_message);
             // Set the pane title for A11y
             view.setAccessibilityPaneTitle(mCategory.getDisplayName(context));
-            mPickerViewModel.getCategoryItems(mCategory)
-                    .observe(this, itemList -> onChangeMediaItems(itemList, adapter));
+            // Get items with pagination parameters representing the first page.
+            mPickerViewModel.getPaginatedCategoryItemsForAction(
+                    mCategory,
+                            ACTION_VIEW_CREATED,
+                            new PaginationParameters(
+                                     mPageSize,
+                                    /* dateBeforeMs */ Long.MIN_VALUE,
+                                    /* rowId */ -1))
+                    .observe(this, itemListResult -> {
+                        onChangeMediaItems(itemListResult, adapter);
+                    });
         }
 
         final PhotosTabItemDecoration itemDecoration = new PhotosTabItemDecoration(context);
@@ -115,9 +227,118 @@
         mRecyclerView.setColumnWidth(photoSize + spacing);
         mRecyclerView.setMinimumSpanCount(MINIMUM_SPAN_COUNT);
 
-        setLayoutManager(adapter, GRID_COLUMN_COUNT);
+        setLayoutManager(context, adapter, GRID_COLUMN_COUNT);
         mRecyclerView.setAdapter(adapter);
         mRecyclerView.addItemDecoration(itemDecoration);
+
+        mRecyclerView.addRecyclerListener(
+                new RecyclerView.RecyclerListener() {
+                    @Override
+                    public void onViewRecycled(RecyclerView.ViewHolder holder) {
+                        if (mGlideRequestManager != null
+                                && holder.getItemViewType() == ITEM_TYPE_MEDIA_ITEM) {
+                            // This cast is safe as we've already checked the view type is
+                            MediaItemGridViewHolder vh = (MediaItemGridViewHolder) holder;
+                            // Cancel pending glide load requests on recycling, to prevent a large
+                            // backlog of requests building up in the event of large scrolls.
+                            cancelGlideLoadForViewHolder(vh);
+                            vh.release();
+                        }
+                    }
+                });
+        mRecyclerView.setItemViewCacheSize(10);
+
+        if (mIsCloudMediaInPhotoPickerEnabled) {
+            setOnScrollListenerForRecyclerView();
+        }
+
+        // uncheck the unavailable items at UI those are no longer available in the selection list
+        requirePickerActivity().isItemPhotoGridViewChanged()
+                .observe(this, isItemViewChanged -> {
+                    if (isItemViewChanged) {
+                        // To re-bind the view just to uncheck the unavailable media items at UI
+                        // Size of mCheckItems is going to be constant ( Iterating over mCheckItems
+                        // is not a heavy operation)
+                        for (Integer index : mSelection.getCheckedItemsIndexes()) {
+                            adapter.notifyItemChanged(index);
+                        }
+                    }
+                });
+    }
+
+    private void initProgressBar(@NonNull View view) {
+        // Check feature flag for cloud media and if it is not true then hide progress bar and
+        // loading text.
+        if (mIsCloudMediaInPhotoPickerEnabled) {
+            mLoadingTextView = view.findViewById(R.id.loading_text_view);
+            mProgressBar = view.findViewById(R.id.progress_bar);
+            mRecyclerViewTopPadding = getResources().getDimensionPixelSize(
+                    R.dimen.picker_recycler_view_top_padding);
+            if (mCategory == Category.DEFAULT) {
+                mPickerViewModel.isSyncInProgress().observe(this, inProgress -> {
+                    if (inProgress) {
+                        bringProgressBarAndLoadingTextInView();
+                    }
+                });
+            } else {
+                bringProgressBarAndLoadingTextInView();
+            }
+        }
+    }
+    private void setOnScrollListenerForRecyclerView() {
+        mRecyclerView.addOnScrollListener(
+                new RecyclerView.OnScrollListener() {
+                    @Override
+                    public void onScrolled(@NonNull @NotNull RecyclerView recyclerView, int dx,
+                            int dy) {
+                        super.onScrolled(recyclerView, dx, dy);
+
+                        // check to ensure that the current page is not still loading and the last
+                        // page has not been loaded.
+                        if (!mIsCurrentPageLoading) {
+                            LinearLayoutManager layoutManager =
+                                    (LinearLayoutManager) mRecyclerView.getLayoutManager();
+
+                            assert layoutManager != null;
+                            // Total items visible at the screen at any current time.
+                            int visibleItemCount = layoutManager.getChildCount();
+                            // Total items in the layout.
+                            int totalItemCount = layoutManager.getItemCount();
+                            // The position of the first visible view
+                            int firstVisibleItemPosition =
+                                    layoutManager.findFirstVisibleItemPosition();
+
+                            // If the number of items have exceeded the threshold, a call will be
+                            // triggered to load the next page.
+                            int thresholdNumberOfItems = totalItemCount - mPageSize;
+                            if (visibleItemCount + firstVisibleItemPosition
+                                    >= thresholdNumberOfItems
+                                    && firstVisibleItemPosition >= 0
+                            ) {
+
+                                Log.d(FRAGMENT_TAG, "Scrolled beyond page threshold, sending a"
+                                        + " call to load the next page.");
+
+                                // setting this to true ensures that only one call is sent on
+                                // crossing the threshold and only required number of pages are
+                                // loaded.
+                                mIsCurrentPageLoading = true;
+                                if (mCategory.isDefault()) {
+                                    mPickerViewModel.getPaginatedItemsForAction(
+                                            ACTION_LOAD_NEXT_PAGE,
+                                            null);
+                                } else {
+                                    mPickerViewModel.getPaginatedCategoryItemsForAction(
+                                            mCategory,
+                                            ACTION_LOAD_NEXT_PAGE,
+                                            null);
+                                }
+                            }
+                        }
+
+                    }
+                });
+
     }
 
     /**
@@ -133,83 +354,169 @@
     @Override
     public void onResume() {
         super.onResume();
-
         final String title;
         final LayoutModeUtils.Mode layoutMode;
         final boolean shouldHideProfileButton;
+
         if (mCategory.isDefault()) {
             title = "";
             layoutMode = MODE_PHOTOS_TAB;
             shouldHideProfileButton = false;
         } else {
-            title = mCategory.getDisplayName(getContext());
+            title = mCategory.getDisplayName(requireContext());
             layoutMode = MODE_ALBUM_PHOTOS_TAB;
             shouldHideProfileButton = true;
         }
+        requirePickerActivity().updateCommonLayouts(layoutMode, title);
 
-        getPickerActivity().updateCommonLayouts(layoutMode, title);
         hideProfileButton(shouldHideProfileButton);
-    }
 
-    private void onChangeMediaItems(@NonNull List<Item> itemList,
-            @NonNull PhotosTabAdapter adapter) {
-        adapter.setMediaItems(itemList);
-        // Handle emptyView's visibility
-        updateVisibilityForEmptyView(/* shouldShowEmptyView */ itemList.size() == 0);
-    }
+        if (mIsCloudMediaInPhotoPickerEnabled
+                && mCategory == Category.DEFAULT
+                && mAtLeastOnePageLoaded) {
+            // mAtLeastOnePageLoaded is checked to avoid calling this method while the view is
+            // being created
+            LinearLayoutManager layoutManager =
+                    (LinearLayoutManager) mRecyclerView.getLayoutManager();
 
-    private void onItemClick(@NonNull View view) {
-        if (mSelection.canSelectMultiple()) {
-            final boolean isSelectedBefore = view.isSelected();
-
-            if (isSelectedBefore) {
-                mSelection.removeSelectedItem((Item) view.getTag());
-            } else {
-                if (!mSelection.isSelectionAllowed()) {
-                    final int maxCount = mSelection.getMaxSelectionLimit();
-                    final CharSequence quantityText =
-                        StringUtils.getICUFormatString(
-                            getResources(), maxCount, R.string.select_up_to);
-                    final String itemCountString = NumberFormat.getInstance(Locale.getDefault())
-                        .format(maxCount);
-                    final CharSequence message = TextUtils.expandTemplate(quantityText,
-                        itemCountString);
-                    Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show();
-                    return;
-                } else {
-                    final Item item = (Item) view.getTag();
-                    mSelection.addSelectedItem(item);
-                }
+            if (layoutManager != null) {
+                int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition();
+                mPickerViewModel.getPaginatedItemsForAction(
+                        ACTION_REFRESH_ITEMS,
+                        new PaginationParameters(firstVisibleItemPosition
+                                + PaginationParameters.PAGINATION_PAGE_SIZE_ITEMS,
+                                /*dateBeforeMs*/ Long.MIN_VALUE, -1));
             }
-            view.setSelected(!isSelectedBefore);
-            // There is an issue b/223695510 about not selected in Accessibility mode. It only says
-            // selected state, but it doesn't say not selected state. Add the not selected only to
-            // avoid that it says selected twice.
-            view.setStateDescription(isSelectedBefore ? getString(R.string.not_selected) : null);
-        } else {
-            final Item item = (Item) view.getTag();
-            mSelection.setSelectedItem(item);
-            getPickerActivity().setResultAndFinishSelf();
         }
     }
 
-    private boolean onItemLongClick(@NonNull View view) {
-        final Item item = (Item) view.getTag();
-        if (!mSelection.canSelectMultiple()) {
-            // In single select mode, if the item is previewed, we set it as selected item. This is
-            // will assist in "Add" button click to return all selected items.
-            // For multi select, long click only previews the item, and until user selects the item,
-            // it doesn't get added to selected items. Also, there is no "Add" button in the preview
-            // layout that can return selected items.
-            mSelection.setSelectedItem(item);
+    private void onChangeMediaItems(@NonNull PickerViewModel.PaginatedItemsResult itemList,
+            @NonNull PhotosTabAdapter adapter) {
+        Objects.requireNonNull(itemList);
+        if (isClearGridAction(itemList)) {
+            adapter.setMediaItems(new ArrayList<>(), itemList.getAction());
+            updateVisibilityForEmptyView(false);
+        } else {
+            adapter.setMediaItems(itemList.getItems(), itemList.getAction());
+            // Handle emptyView's visibility
+            boolean shouldShowEmptyView = (itemList.getItems().size() == 0);
+            updateVisibilityForEmptyView(shouldShowEmptyView);
+            if (shouldShowEmptyView) {
+                mPickerViewModel.setEmptyPageDisplayed(true);
+            }
         }
-        mSelection.prepareItemForPreviewOnLongPress(item);
-        // Transition to PreviewFragment.
-        PreviewFragment.show(getActivity().getSupportFragmentManager(),
-                PreviewFragment.getArgsForPreviewOnLongPress());
-        return true;
+        mIsCurrentPageLoading = false;
+        mAtLeastOnePageLoaded = true;
+        hideProgressBarAndLoadingText();
     }
 
+    private boolean isClearGridAction(@NonNull PickerViewModel.PaginatedItemsResult itemList) {
+        return itemList.getItems() != null
+                && itemList.getItems().size() == 1
+                && itemList.getItems().get(0).getId().equals("EMPTY_VIEW");
+    }
+
+    private final PhotosTabAdapter.OnMediaItemClickListener mOnMediaItemClickListener =
+            new PhotosTabAdapter.OnMediaItemClickListener() {
+                @Override
+                public void onItemClick(
+                        @NonNull View view, int position, MediaItemGridViewHolder viewHolder) {
+
+                    if (mSelection.canSelectMultiple()) {
+                        final boolean isSelectedBefore =
+                                mSelection.isItemSelected((Item) view.getTag())
+                                        && view.isSelected();
+
+                        Item item = (Item) view.getTag();
+                        if (isSelectedBefore) {
+                            if (mSelection.isSelectionOrdered()) {
+                                viewHolder.setSelectionOrder(null);
+                            }
+                            mSelection.removeSelectedItem((Item) view.getTag());
+                            mSelection.removeCheckedItemIndex((Item) view.getTag());
+                        } else {
+                            mSelection.addCheckedItemIndex((Item) view.getTag(), position);
+                            if (!mSelection.isSelectionAllowed()) {
+                                final int maxCount = mSelection.getMaxSelectionLimit();
+                                final CharSequence quantityText =
+                                        StringUtils.getICUFormatString(
+                                                getResources(), maxCount, R.string.select_up_to);
+                                final String itemCountString =
+                                        NumberFormat.getInstance(Locale.getDefault())
+                                                .format(maxCount);
+                                final CharSequence message =
+                                        TextUtils.expandTemplate(quantityText, itemCountString);
+                                Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show();
+                                return;
+                            } else {
+                                mSelection.addSelectedItem(item);
+                                if (mSelection.isSelectionOrdered()) {
+                                    viewHolder.setSelectionOrder(
+                                            mSelection.getSelectedItemOrder(item));
+                                }
+                                mPickerViewModel.logMediaItemSelected(item, mCategory, position);
+                            }
+                        }
+                        view.setSelected(!isSelectedBefore);
+
+                        // There is an issue b/223695510 about not selected in Accessibility mode.
+                        // It only says selected state, but it doesn't say not selected state.
+                        // Add the not selected only to avoid that it says selected twice.
+                        view.setStateDescription(
+                                isSelectedBefore ? getString(R.string.not_selected) : null);
+                    } else {
+                        final Item item = (Item) view.getTag();
+                        mSelection.setSelectedItem(item);
+                        mPickerViewModel.logMediaItemSelected(item, mCategory, position);
+                        try {
+                            requirePickerActivity().setResultAndFinishSelf();
+                        } catch (RuntimeException e) {
+                            Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
+                        }
+                    }
+                }
+
+                @Override
+                public boolean onItemLongClick(@NonNull View view, int position) {
+                    final Item item = (Item) view.getTag();
+                    if (!mSelection.canSelectMultiple()) {
+                        // In single select mode, if the item is previewed, we set it as selected
+                        // item. This assists in "Add" button click to return all selected items.
+                        // For multi select, long click only previews the item, and until user
+                        // selects the item, it doesn't get added to selected items. Also, there is
+                        // no "Add" button in the preview layout that can return selected items.
+                        mSelection.setSelectedItem(item);
+                    }
+                    mSelection.prepareItemForPreviewOnLongPress(item);
+                    mPickerViewModel.logMediaItemPreviewed(item, mCategory, position);
+
+                    try {
+                        // Transition to PreviewFragment.
+                        PreviewFragment.show(
+                                requireActivity().getSupportFragmentManager(),
+                                PreviewFragment.getArgsForPreviewOnLongPress());
+                    } catch (RuntimeException e) {
+                        Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
+                    }
+
+                    // Consume the long click so that it doesn't propagate in the View hierarchy.
+                    return true;
+                }
+            };
+
+    public View.OnHoverListener mOnMediaItemHoverListener = (v, event) -> {
+        // When a cursor is hovered over an item the item should appear selected and when the
+        // cursor moves out of the bounds of the view, it should go back to being unselected.
+        if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER) {
+            v.setSelected(true);
+        } else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
+            if (!mSelection.isItemSelected((Item) v.getTag())) {
+                v.setSelected(false);
+            }
+        }
+        return true;
+    };
+
     /**
      * Create the fragment with the category and add it into the FragmentManager
      *
@@ -235,4 +542,85 @@
     public static Fragment get(FragmentManager fm) {
         return fm.findFragmentByTag(FRAGMENT_TAG);
     }
+
+    /**
+     * Hides progress bar and the loading photos message.
+     * <p>This is executed with a delay of 0.6ms.
+     * This is done so that for the cases where the loading happens very quickly the user will not
+     * see the progressBar flicker.</p>
+     *
+     * <p>This results in progressBar and loadingText to remain in view for loadingTime + 0.6ms.</p>
+     */
+    private synchronized void hideProgressBarAndLoadingText() {
+        if (mProgressBar != null
+                && mLoadingTextView != null
+                && mProgressBar.getVisibility() == View.VISIBLE
+                && mLoadingTextView.getVisibility() == View.VISIBLE) {
+            // clear previous calls, extra caution.
+            mMainThreadHandler.removeCallbacksAndMessages(mHideProgressBarToken);
+            Runnable runnable = new Runnable() {
+                @Override
+                public void run() {
+                    if (mProgressBar != null
+                            && mLoadingTextView != null
+                            && mProgressBar.getVisibility() == View.VISIBLE
+                            && mLoadingTextView.getVisibility() == View.VISIBLE) {
+                        mProgressBar.setVisibility(View.GONE);
+                        mLoadingTextView.setVisibility(View.GONE);
+                        // Move recyclerView up to cover up the space taken up by progressBar and
+                        // loadingTest.
+                        if (mRecyclerView != null
+                                && mRecyclerView.getVisibility() == View.VISIBLE) {
+                            mObjectAnimator.ofFloat(
+                                            mRecyclerView,
+                                            /* property name */ "y",
+                                            /* final position */0f)
+                                    .setDuration(300).start();
+                        }
+                    }
+                }
+            };
+            // With this runnable the hiding of progress bar is delayed by 600ms.
+            mMainThreadHandler.postDelayed(runnable, mHideProgressBarToken, /* delay duration */
+                    600);
+        }
+    }
+
+    private void bringProgressBarAndLoadingTextInView() {
+        if (mIsCloudMediaInPhotoPickerEnabled) {
+            if (mObjectAnimator != null) {
+                // stop any pending/ongoing animations.
+                mObjectAnimator.cancel();
+            }
+            if (mRecyclerView.getVisibility() == View.VISIBLE) {
+                // move recycler view down to make space for progress bar and loading text.
+                mObjectAnimator.ofFloat(
+                                mRecyclerView,
+                                /* property name */ "y",
+                                /* final position */mRecyclerViewTopPadding)
+                        .setDuration(1).start();
+            }
+            // bring progressBar and Loading text in view.
+            mLoadingTextView.setVisibility(View.VISIBLE);
+            mProgressBar.setVisibility(View.VISIBLE);
+        }
+    }
+
+    /**
+     * Attempts to cancel any outstanding Glide requests for the given ViewHolder.
+     *
+     * @param holder The View holder in the RecyclerView to cancel requests for.
+     */
+    private void cancelGlideLoadForViewHolder(MediaItemGridViewHolder vh) {
+        // Attempt to clear the potential pending load out of glide's request
+        // manager.
+        mGlideRequestManager.clear(vh.getThumbnailImageView());
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        mMainThreadHandler.removeCallbacksAndMessages(mHideProgressBarToken);
+        mGlideRequestManager = null;
+    }
 }
diff --git a/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java b/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
index 1df4877..d00aad9 100644
--- a/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
+++ b/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
@@ -33,7 +33,7 @@
 /**
  * Adapter for Preview RecyclerView to preview all images and videos.
  */
-class PreviewAdapter extends RecyclerView.Adapter<BaseViewHolder> {
+public class PreviewAdapter extends RecyclerView.Adapter<BaseViewHolder> {
 
     private static final int ITEM_TYPE_IMAGE = 1;
     private static final int ITEM_TYPE_VIDEO = 2;
@@ -41,10 +41,15 @@
     private List<Item> mItemList = new ArrayList<>();
     private final ImageLoader mImageLoader;
     private final RemotePreviewHandler mRemotePreviewHandler;
+    private final OnVideoPreviewClickListener mOnVideoPreviewClickListener;
 
-    PreviewAdapter(Context context, MuteStatus muteStatus) {
+    PreviewAdapter(Context context, MuteStatus muteStatus,
+            @NonNull OnCreateSurfaceController onCreateSurfaceController,
+            @NonNull OnVideoPreviewClickListener onVideoPreviewClickListener) {
         mImageLoader = new ImageLoader(context);
-        mRemotePreviewHandler = new RemotePreviewHandler(context, muteStatus);
+        mRemotePreviewHandler = new RemotePreviewHandler(context, muteStatus,
+                onCreateSurfaceController);
+        mOnVideoPreviewClickListener = onVideoPreviewClickListener;
     }
 
     @NonNull
@@ -53,7 +58,8 @@
         if (viewType == ITEM_TYPE_IMAGE) {
             return new PreviewImageHolder(viewGroup.getContext(), viewGroup, mImageLoader);
         }
-        return new PreviewVideoHolder(viewGroup.getContext(), viewGroup, mImageLoader);
+        return new PreviewVideoHolder(viewGroup.getContext(), viewGroup, mImageLoader,
+                mOnVideoPreviewClickListener);
     }
 
     @Override
@@ -112,4 +118,25 @@
         mItemList = itemList;
         notifyDataSetChanged();
     }
+
+    interface OnVideoPreviewClickListener {
+        void logMuteButtonClick();
+    }
+
+    /**
+     * Log metrics related to the surface controller creation
+     */
+    public interface OnCreateSurfaceController {
+        /**
+         * Log metrics to notify create surface controller triggered
+         * @param authority the authority of the provider
+         */
+        void logStart(String authority);
+
+        /**
+         * Log metrics to notify create surface controller ended
+         * @param authority the authority of the provider
+         */
+        void logEnd(String authority);
+    }
 }
diff --git a/src/com/android/providers/media/photopicker/ui/PreviewFragment.java b/src/com/android/providers/media/photopicker/ui/PreviewFragment.java
index 57c7e53..a70e8fb 100644
--- a/src/com/android/providers/media/photopicker/ui/PreviewFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/PreviewFragment.java
@@ -48,6 +48,7 @@
 import java.text.NumberFormat;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 
 /**
  * Displays a selected items in one up view. Supports deselecting items.
@@ -114,11 +115,7 @@
         final List<Item> selectedItemsList = mSelection.getSelectedItemsForPreview();
         final int selectedItemsListSize = selectedItemsList.size();
 
-        if (selectedItemsListSize <= 0) {
-            // This can happen if we lost PickerViewModel to optimize memory.
-            Log.e(TAG, "No items to preview. Returning back to photo grid");
-            requireActivity().getSupportFragmentManager().popBackStack();
-        } else if (selectedItemsListSize > 1 && !mSelection.canSelectMultiple()) {
+        if (selectedItemsListSize > 1 && !mSelection.canSelectMultiple()) {
             // This should never happen
             throw new IllegalStateException("Found more than one preview items in single select"
                     + " mode. Selected items count: " + selectedItemsListSize);
@@ -130,10 +127,40 @@
             throw new IllegalStateException("Expected to find ViewPager2 in " + view
                     + ", but found null");
         }
-        mViewPager2Wrapper = new ViewPager2Wrapper(viewPager, selectedItemsList, mMuteStatus);
+        mViewPager2Wrapper = new ViewPager2Wrapper(viewPager, selectedItemsList, mMuteStatus,
+                mOnCreateSurfaceController, mPickerViewModel::logVideoPreviewMuteButtonClick);
 
         setUpPreviewLayout(view, getArguments());
         setupScrimLayerAndBottomBar(view);
+        // Don't add any code post this line. The lazy loading setup should be the last thing we do
+        // to avoid the UI getting overwritten.
+        setUpUIForLazyLoading(view, selectedItemsListSize);
+    }
+
+    private void setUpUIForLazyLoading(View view, int selectedItemsListSize) {
+        final Button selectedCheckButton = view.findViewById(R.id.preview_selected_check_button);
+        Objects.requireNonNull(selectedCheckButton);
+        if (selectedItemsListSize == 0) {
+            // This can happen in two cases -
+            // 1. ACTION_USER_SELECT_IMAGES_FOR_APP launched the Photo Picker UI, and we are waiting
+            //    for items that's not preloaded due to pagination
+            // 2. PreviewFragment was launched from SavedPreference state but PickerViewModel was
+            //    killed and hence there is no selected items.
+            // In both these cases, user will see a blank UI with only Add/Allow button
+            selectedCheckButton.setVisibility(View.GONE);
+            Log.i(TAG, "No items to preview yet" + selectedCheckButton.getVisibility());
+        }
+
+        if (mPickerViewModel.isManagedSelectionEnabled()) {
+            mPickerViewModel.getIsAllPreGrantedMediaLoaded().observe(this, (isLoadComplete) -> {
+                if (!isLoadComplete) return;
+
+                selectedCheckButton.setVisibility(View.VISIBLE);
+
+                mSelection.prepareSelectedItemsForPreviewAll();
+                mViewPager2Wrapper.updateList(mSelection.getSelectedItemsForPreview());
+            });
+        }
     }
 
     private void setupScrimLayerAndBottomBar(View fragmentView) {
@@ -200,7 +227,7 @@
             // For preview on long press, we always preview only one item.
             // Selection#getSelectedItemsForPreview is guaranteed to return only one item. Hence,
             // we can always use position=0 as current position.
-            updateSelectButtonText(addOrSelectButton,
+            updateSelectButtonTextAndVisibility(addOrSelectButton,
                     mSelection.isItemSelected(mViewPager2Wrapper.getItemAt(/* position */ 0)));
             addOrSelectButton.setOnClickListener(v -> onClickSelectButton(addOrSelectButton));
         }
@@ -242,7 +269,9 @@
                                             /* context= */ getContext(),
                                             /* size= */ selectedItemCount,
                                             /* isUserSelectForApp= */ mPickerViewModel
-                                                    .isUserSelectForApp()));
+                                                    .isUserSelectForApp(),
+                                            /* isManagedSelectionEnabled */
+                                            mPickerViewModel.isManagedSelectionEnabled()));
                         });
 
         selectedCheckButton.setOnClickListener(
@@ -285,7 +314,7 @@
 
     private void onClickSelectButton(@NonNull Button selectButton) {
         final boolean isSelectedNow = updateSelectionAndGetState();
-        updateSelectButtonText(selectButton, isSelectedNow);
+        updateSelectButtonTextAndVisibility(selectButton, isSelectedNow);
     }
 
     private void onClickSelectedCheckButton(@NonNull Button selectedCheckButton) {
@@ -337,9 +366,11 @@
         }
     }
 
-    private static void updateSelectButtonText(@NonNull Button selectButton,
+    private void updateSelectButtonTextAndVisibility(@NonNull Button selectButton,
             boolean isSelected) {
         selectButton.setText(isSelected ? R.string.deselect : R.string.select);
+        selectButton.setVisibility(
+                (isSelected || mSelection.isSelectionAllowed()) ? View.VISIBLE : View.GONE);
     }
 
     private static void updateSelectedCheckButtonStateAndText(@NonNull Button selectedCheckButton,
@@ -388,7 +419,11 @@
 
     // TODO: There is a same method in TabFragment. To find a way to reuse it.
     private static String generateAddButtonString(
-            @NonNull Context context, int size, boolean isUserSelectForApp) {
+            @NonNull Context context, int size, boolean isUserSelectForApp,
+            boolean isManagedSelection) {
+        if (isManagedSelection && size == 0) {
+            return context.getString(R.string.picker_add_button_allow_none_option);
+        }
         final String sizeString = NumberFormat.getInstance(Locale.getDefault()).format(size);
         final String template =
                 isUserSelectForApp
@@ -396,4 +431,17 @@
                         : context.getString(R.string.picker_add_button_multi_select);
         return TextUtils.expandTemplate(template, sizeString).toString();
     }
+
+    private final PreviewAdapter.OnCreateSurfaceController mOnCreateSurfaceController =
+            new PreviewAdapter.OnCreateSurfaceController() {
+                @Override
+                public void logStart(String authority) {
+                    mPickerViewModel.logCreateSurfaceControllerStart(authority);
+                }
+
+                @Override
+                public void logEnd(String authority) {
+                    mPickerViewModel.logCreateSurfaceControllerEnd(authority);
+                }
+            };
 }
diff --git a/src/com/android/providers/media/photopicker/ui/PreviewVideoHolder.java b/src/com/android/providers/media/photopicker/ui/PreviewVideoHolder.java
index 162f6d1..4da12b9 100644
--- a/src/com/android/providers/media/photopicker/ui/PreviewVideoHolder.java
+++ b/src/com/android/providers/media/photopicker/ui/PreviewVideoHolder.java
@@ -24,6 +24,7 @@
 import android.widget.ImageButton;
 import android.widget.ImageView;
 
+import androidx.annotation.NonNull;
 import androidx.viewpager2.widget.ViewPager2;
 
 import com.android.providers.media.R;
@@ -38,6 +39,7 @@
 public class PreviewVideoHolder extends BaseViewHolder {
 
     private final ImageLoader mImageLoader;
+    private final PreviewAdapter.OnVideoPreviewClickListener mOnVideoPreviewClickListener;
     private final ImageView mImageView;
     private final SurfaceView mSurfaceView;
     private final AspectRatioFrameLayout mPlayerFrame;
@@ -47,10 +49,12 @@
     private final ImageButton mMuteButton;
     private final CircularProgressIndicator mCircularProgressIndicator;
 
-    PreviewVideoHolder(Context context, ViewGroup parent, ImageLoader imageLoader) {
+    PreviewVideoHolder(Context context, ViewGroup parent, ImageLoader imageLoader,
+            @NonNull PreviewAdapter.OnVideoPreviewClickListener onVideoPreviewClickListener) {
         super(context, parent, R.layout.item_video_preview);
 
         mImageLoader = imageLoader;
+        mOnVideoPreviewClickListener = onVideoPreviewClickListener;
         mImageView = itemView.findViewById(R.id.preview_video_image);
         mSurfaceView = itemView.findViewById(R.id.preview_player_view);
         mPlayerFrame = itemView.findViewById(R.id.preview_player_frame);
@@ -103,4 +107,11 @@
     public CircularProgressIndicator getCircularProgressIndicator() {
         return mCircularProgressIndicator;
     }
+
+    /**
+     * Log metrics to notify that the user has clicked the mute / unmute button in a video preview
+     */
+    public void logMuteButtonClick() {
+        mOnVideoPreviewClickListener.logMuteButtonClick();
+    }
 }
diff --git a/src/com/android/providers/media/photopicker/ui/TEST_MAPPING b/src/com/android/providers/media/photopicker/ui/TEST_MAPPING
new file mode 100644
index 0000000..842d035
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/ui/TEST_MAPPING
@@ -0,0 +1,26 @@
+{
+  "mainline-presubmit": [
+    {
+      "name": "MediaProviderTests[com.google.android.mediaprovider.apex]",
+      "options": [
+        {
+          // For changes in Photopicker UI we want to run all the photopicker
+          // tests in the given package regardless of @RunOnlyOnPostsubmit annotation
+          "include-filter": "com.android.providers.media.photopicker"
+        }
+      ]
+    }
+  ],
+  "presubmit": [
+    {
+      "name": "MediaProviderTests",
+      "options": [
+        {
+          // For changes in Photopicker UI we want to run all the photopicker
+          // tests in the given package regardless of @RunOnlyOnPostsubmit annotation
+          "include-filter": "com.android.providers.media.photopicker"
+        }
+      ]
+    }
+  ]
+}
diff --git a/src/com/android/providers/media/photopicker/ui/TabAdapter.java b/src/com/android/providers/media/photopicker/ui/TabAdapter.java
index 643e280..86f2de7 100644
--- a/src/com/android/providers/media/photopicker/ui/TabAdapter.java
+++ b/src/com/android/providers/media/photopicker/ui/TabAdapter.java
@@ -16,7 +16,15 @@
 
 package com.android.providers.media.photopicker.ui;
 
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_CLEAR_AND_UPDATE_LIST;
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_CLEAR_GRID;
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_LOAD_NEXT_PAGE;
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_REFRESH_ITEMS;
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_VIEW_CREATED;
+import static com.android.providers.media.photopicker.viewmodel.PickerViewModel.TAG;
+
 import android.content.Context;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -39,15 +47,14 @@
 /**
  * Adapts from model to something RecyclerView understands.
  */
-@VisibleForTesting
 public abstract class TabAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
 
     @VisibleForTesting
     public static final int ITEM_TYPE_BANNER = 0;
     // Date header sections for "Photos" tab
-    static final int ITEM_TYPE_SECTION = 1;
+    public static final int ITEM_TYPE_SECTION = 1;
     // Media items (a.k.a. Items) for "Photos" tab, Albums (a.k.a. Categories) for "Albums" tab
-    private static final int ITEM_TYPE_MEDIA_ITEM = 2;
+    public static final int ITEM_TYPE_MEDIA_ITEM = 2;
 
     @NonNull final ImageLoader mImageLoader;
     @NonNull private final LiveData<String> mCloudMediaProviderAppTitle;
@@ -209,7 +216,7 @@
                 mBanner = banner;
                 mOnBannerEventListener = onBannerEventListener;
                 notifyItemInserted(/* position */ 0);
-                mOnBannerEventListener.onBannerAdded();
+                mOnBannerEventListener.onBannerAdded(banner.name());
             } else {
                 mBanner = banner;
                 mOnBannerEventListener = onBannerEventListener;
@@ -225,14 +232,47 @@
     /**
      * Update the List of all items (excluding the banner) in tab adapter {@link #mAllItems}
      */
-    protected final void setAllItems(@NonNull List<?> items) {
+    protected final void setAllItems(@NonNull List<?> items,
+            @ItemsAction.Type int action) {
+        int previousItemCount = getItemCount();
         mAllItems.clear();
         mAllItems.addAll(items);
-        notifyDataSetChanged();
+        notifyOnListChanged(previousItemCount, items.size(), action);
+    }
+
+    private void notifyOnListChanged(int previousItemCount, int sizeOfUpdatedList,
+            @ItemsAction.Type int action) {
+        Log.d(TAG, "Updating adapter for action: " + action);
+        switch (action) {
+            case ACTION_VIEW_CREATED:
+            case ACTION_CLEAR_AND_UPDATE_LIST: {
+                notifyDataSetChanged();
+                break;
+            }
+            case ACTION_CLEAR_GRID: {
+                notifyItemRangeRemoved(0, previousItemCount);
+                break;
+            }
+            case ACTION_LOAD_NEXT_PAGE: {
+                notifyItemRangeInserted(previousItemCount,
+                        sizeOfUpdatedList - previousItemCount);
+                break;
+            }
+            case ACTION_REFRESH_ITEMS: {
+                notifyItemRangeChanged(0, sizeOfUpdatedList);
+                if (sizeOfUpdatedList < previousItemCount) {
+                    notifyItemRangeRemoved(sizeOfUpdatedList,
+                            previousItemCount - sizeOfUpdatedList);
+                }
+                break;
+            }
+            default:
+                Log.w(TAG, "Invalid action passed. No update to adapter");
+        }
     }
 
     @NonNull
-    final Object getAdapterItem(int position) {
+    public final Object getAdapterItem(int position) {
         if (position < 0) {
             throw new IllegalStateException("Get adapter item for negative position " + position);
         }
@@ -276,7 +316,7 @@
 
             mDismissButton.setOnClickListener(v -> onBannerEventListener.onDismissButtonClick());
 
-            if (banner.mActionButtonText != -1) {
+            if (banner.mActionButtonText != -1 && onBannerEventListener.shouldShowActionButton()) {
                 mActionButton.setText(banner.mActionButtonText);
                 mActionButton.setVisibility(View.VISIBLE);
                 mActionButton.setOnClickListener(v -> onBannerEventListener.onActionButtonClick());
@@ -287,18 +327,14 @@
     }
 
     private enum Banner {
-        // TODO(b/274426228): Replace `CLOUD_MEDIA_AVAILABLE` `mActionButtonText` from `-1` to
-        //  `R.string.picker_banner_cloud_change_account_button`, post change cloud account
-        //  functionality implementation from the Picker settings (b/261999521).
         CLOUD_MEDIA_AVAILABLE(R.string.picker_banner_cloud_first_time_available_title,
-                R.string.picker_banner_cloud_first_time_available_desc, /* no action button */ -1),
+                R.string.picker_banner_cloud_first_time_available_desc,
+                R.string.picker_banner_cloud_change_account_button),
         ACCOUNT_UPDATED(R.string.picker_banner_cloud_account_changed_title,
                 R.string.picker_banner_cloud_account_changed_desc, /* no action button */ -1),
-        // TODO(b/274426228): Replace `CHOOSE_ACCOUNT` `mActionButtonText` from `-1` to
-        //  `R.string.picker_banner_cloud_choose_account_button`, post change cloud account
-        //  functionality implementation from the Picker settings (b/261999521).
         CHOOSE_ACCOUNT(R.string.picker_banner_cloud_choose_account_title,
-                R.string.picker_banner_cloud_choose_account_desc, /* no action button */ -1),
+                R.string.picker_banner_cloud_choose_account_desc,
+                R.string.picker_banner_cloud_choose_account_button),
         CHOOSE_APP(R.string.picker_banner_cloud_choose_app_title,
                 R.string.picker_banner_cloud_choose_app_desc,
                 R.string.picker_banner_cloud_choose_app_button);
@@ -349,10 +385,12 @@
 
         void onDismissButtonClick();
 
-        default void onBannerClick() {
-            onActionButtonClick();
-        }
+        void onBannerClick();
 
-        void onBannerAdded();
+        void onBannerAdded(@NonNull String name);
+
+        default boolean shouldShowActionButton() {
+            return true;
+        }
     }
 }
diff --git a/src/com/android/providers/media/photopicker/ui/TabContainerFragment.java b/src/com/android/providers/media/photopicker/ui/TabContainerFragment.java
index 5ec4d65..8e070b5 100644
--- a/src/com/android/providers/media/photopicker/ui/TabContainerFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/TabContainerFragment.java
@@ -15,6 +15,8 @@
  */
 package com.android.providers.media.photopicker.ui;
 
+import static com.android.providers.media.util.MimeUtils.isVideoMimeType;
+
 import android.os.Bundle;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -26,11 +28,14 @@
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentTransaction;
+import androidx.lifecycle.ViewModelProvider;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.viewpager2.widget.CompositePageTransformer;
 import androidx.viewpager2.widget.ViewPager2;
 
 import com.android.providers.media.R;
+import com.android.providers.media.photopicker.util.MimeFilterUtils;
+import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
 
 import com.google.android.material.bottomsheet.BottomSheetBehavior;
 import com.google.android.material.tabs.TabLayout;
@@ -50,6 +55,7 @@
     private TabContainerAdapter mTabContainerAdapter;
     private TabLayoutMediator mTabLayoutMediator;
     private ViewPager2 mViewPager;
+    private PickerViewModel mPickerViewModel;
 
     @Override
     @NonNull
@@ -65,6 +71,8 @@
         mTabContainerAdapter = new TabContainerAdapter(/* fragment */ this);
         mViewPager = view.findViewById(R.id.picker_tab_viewpager);
         mViewPager.setAdapter(mTabContainerAdapter);
+        final ViewModelProvider viewModelProvider = new ViewModelProvider(requireActivity());
+        mPickerViewModel = viewModelProvider.get(PickerViewModel.class);
 
         // If the ViewPager2 has more than one page with BottomSheetBehavior, the scrolled view
         // (e.g. RecyclerView) on the second page can't be scrolled. The workaround is to update
@@ -96,18 +104,41 @@
         }
 
         final TabLayout tabLayout = getActivity().findViewById(R.id.tab_layout);
+
         mTabLayoutMediator = new TabLayoutMediator(tabLayout, mViewPager, (tab, pos) -> {
             if (pos == PHOTOS_TAB_POSITION) {
-                tab.setText(R.string.picker_photos);
+                if (isOnlyVideoMimeTypeFilterAvailable()) {
+                    tab.setText(R.string.picker_videos);
+                } else {
+                    tab.setText(R.string.picker_photos);
+                }
             } else if (pos == ALBUMS_TAB_POSITION) {
                 tab.setText(R.string.picker_albums);
             }
         });
+
         mTabLayoutMediator.attach();
         // TabLayout only supports colorDrawable in xml. And if we set the color in the drawable by
         // setSelectedTabIndicator method, it doesn't apply the color. So, we set color in xml and
         // set the drawable for the shape here.
         tabLayout.setSelectedTabIndicator(R.drawable.picker_tab_indicator);
+        tabLayout.addOnTabSelectedListener(mOnTabSelectedListener);
+    }
+
+    private boolean isOnlyVideoMimeTypeFilterAvailable() {
+        String [] mimeTypeFilters = MimeFilterUtils.getMimeTypeFilters(getActivity().getIntent());
+        boolean hasVideoMimeTypeFilterOnly = false;
+        if (mimeTypeFilters != null && mimeTypeFilters.length > 0) {
+            for (String mimeTypeFilter : mimeTypeFilters) {
+                if (isVideoMimeType(mimeTypeFilter)) {
+                    hasVideoMimeTypeFilterOnly = true;
+                } else {
+                    hasVideoMimeTypeFilterOnly = false;
+                    break;
+                }
+            }
+        }
+        return hasVideoMimeTypeFilterOnly;
     }
 
     @Override
@@ -128,6 +159,29 @@
         ft.commitAllowingStateLoss();
     }
 
+    private final TabLayout.OnTabSelectedListener mOnTabSelectedListener =
+            new TabLayout.OnTabSelectedListener() {
+                @Override
+                public void onTabSelected(TabLayout.Tab tab) {
+                    int position = tab.getPosition();
+                    if (position == PHOTOS_TAB_POSITION) {
+                        mPickerViewModel.logSwitchToPhotosTab();
+                    } else if (position == ALBUMS_TAB_POSITION) {
+                        mPickerViewModel.logSwitchToAlbumsTab();
+                    }
+                }
+
+                @Override
+                public void onTabUnselected(TabLayout.Tab tab) {
+                    // No=op
+                }
+
+                @Override
+                public void onTabReselected(TabLayout.Tab tab) {
+                    // No-op
+                }
+            };
+
     private static class AnimationPageTransformer implements ViewPager2.PageTransformer {
 
         @Override
diff --git a/src/com/android/providers/media/photopicker/ui/TabFragment.java b/src/com/android/providers/media/photopicker/ui/TabFragment.java
index 90bb439..14159c2 100644
--- a/src/com/android/providers/media/photopicker/ui/TabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/TabFragment.java
@@ -24,12 +24,14 @@
 
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
+import android.content.Intent;
 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.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -44,6 +46,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 import androidx.lifecycle.ViewModelProvider;
@@ -66,7 +69,7 @@
  * The base abstract Tab fragment
  */
 public abstract class TabFragment extends Fragment {
-
+    private static final String TAG = TabFragment.class.getSimpleName();
     protected PickerViewModel mPickerViewModel;
     protected Selection mSelection;
     protected ImageLoader mImageLoader;
@@ -80,6 +83,8 @@
     private boolean mIsAccessibilityEnabled;
 
     private Button mAddButton;
+
+    private Button mViewSelectedButton;
     private View mBottomBar;
     private Animation mSlideUpAnimation;
     private Animation mSlideDownAnimation;
@@ -98,6 +103,8 @@
 
     private int mRecyclerViewBottomPadding;
 
+    private RecyclerView.OnScrollListener mOnScrollListenerForMultiProfileButton;
+
     private final MutableLiveData<Boolean> mIsBottomBarVisible = new MutableLiveData<>(false);
     private final MutableLiveData<Boolean> mIsProfileButtonVisible = new MutableLiveData<>(false);
 
@@ -113,11 +120,13 @@
     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
 
-        final Context context = getContext();
+        final Context context = requireContext();
+        final FragmentActivity activity = requireActivity();
+
         mImageLoader = new ImageLoader(context);
         mRecyclerView = view.findViewById(R.id.picker_tab_recyclerview);
         mRecyclerView.setHasFixedSize(true);
-        final ViewModelProvider viewModelProvider = new ViewModelProvider(requireActivity());
+        final ViewModelProvider viewModelProvider = new ViewModelProvider(activity);
         mPickerViewModel = viewModelProvider.get(PickerViewModel.class);
         mSelection = mPickerViewModel.getSelection();
         mRecyclerViewBottomPadding = getResources().getDimensionPixelSize(
@@ -144,58 +153,89 @@
         mButtonIconAndTextColor = ta.getColor(/* index */ 1, /* defValue */ -1);
         ta.recycle();
 
-        mProfileButton = getActivity().findViewById(R.id.profile_button);
+        mProfileButton = activity.findViewById(R.id.profile_button);
         mUserIdManager = mPickerViewModel.getUserIdManager();
 
         final boolean canSelectMultiple = mSelection.canSelectMultiple();
         if (canSelectMultiple) {
-            mAddButton = getActivity().findViewById(R.id.button_add);
+            mAddButton = activity.findViewById(R.id.button_add);
+            mViewSelectedButton = activity.findViewById(R.id.button_view_selected);
             mAddButton.setOnClickListener(v -> {
-                ((PhotoPickerActivity) getActivity()).setResultAndFinishSelf();
+                try {
+                    requirePickerActivity().setResultAndFinishSelf();
+                } catch (RuntimeException e) {
+                    Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
+                }
             });
-
-            final Button viewSelectedButton = getActivity().findViewById(R.id.button_view_selected);
             // Transition to PreviewFragment on clicking "View Selected".
-            viewSelectedButton.setOnClickListener(v -> {
+            mViewSelectedButton.setOnClickListener(v -> {
+                // Load items for preview that are pre granted but not yet loaded for UI. This is an
+                // async call. Until the items are loaded, we can still preview already available
+                // items
+                mPickerViewModel.getRemainingPreGrantedItems();
                 mSelection.prepareSelectedItemsForPreviewAll();
-                PreviewFragment.show(getActivity().getSupportFragmentManager(),
-                        PreviewFragment.getArgsForPreviewOnViewSelected());
+
+                int selectedItemCount = mSelection.getSelectedItemCount().getValue();
+                mPickerViewModel.logPreviewAllSelected(selectedItemCount);
+
+                try {
+                    PreviewFragment.show(requireActivity().getSupportFragmentManager(),
+                            PreviewFragment.getArgsForPreviewOnViewSelected());
+                } catch (RuntimeException e) {
+                    Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
+                }
             });
 
-            mBottomBar = getActivity().findViewById(R.id.picker_bottom_bar);
-            mSlideUpAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.slide_up);
-            mSlideDownAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.slide_down);
+            mBottomBar = activity.findViewById(R.id.picker_bottom_bar);
+            // consume the event so that it doesn't get passed through to the next view b/287661737
+            mBottomBar.setOnClickListener(v -> {});
+            mSlideUpAnimation = AnimationUtils.loadAnimation(context, R.anim.slide_up);
+            mSlideDownAnimation = AnimationUtils.loadAnimation(context, R.anim.slide_down);
 
             mSelection.getSelectedItemCount().observe(this, selectedItemListSize -> {
-                updateProfileButtonVisibility();
-                updateVisibilityAndAnimateBottomBar(selectedItemListSize);
+                // Fetch activity or context again instead of capturing existing variable in lambdas
+                // to avoid memory leaks.
+                try {
+                    updateProfileButtonVisibility();
+                    updateVisibilityAndAnimateBottomBar(requireContext(), selectedItemListSize);
+                } catch (RuntimeException e) {
+                    Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
+                }
             });
         }
 
-        // Initial setup
-        setUpProfileButtonWithListeners(mUserIdManager.isMultiUserProfiles());
-
         // Observe for cross profile access changes.
         final LiveData<Boolean> crossProfileAllowed = mUserIdManager.getCrossProfileAllowed();
         if (crossProfileAllowed != null) {
             crossProfileAllowed.observe(this, isCrossProfileAllowed -> {
                 setUpProfileButton();
+                if (Boolean.TRUE.equals(mIsProfileButtonVisible.getValue())) {
+                    if (isCrossProfileAllowed) {
+                        mPickerViewModel.logProfileSwitchButtonEnabled();
+                    } else {
+                        mPickerViewModel.logProfileSwitchButtonDisabled();
+                    }
+                }
             });
         }
 
+
+        final AccessibilityManager accessibilityManager =
+                context.getSystemService(AccessibilityManager.class);
+        mIsAccessibilityEnabled = accessibilityManager.isEnabled();
+        accessibilityManager.addAccessibilityStateChangeListener(enabled -> {
+            mIsAccessibilityEnabled = enabled;
+            setUpProfileButtonWithListeners(mUserIdManager.isMultiUserProfiles());
+        });
+
         // Observe for multi-user changes.
         final LiveData<Boolean> isMultiUserProfiles = mUserIdManager.getIsMultiUserProfiles();
         if (isMultiUserProfiles != null) {
             isMultiUserProfiles.observe(this, this::setUpProfileButtonWithListeners);
         }
 
-        final AccessibilityManager accessibilityManager =
-                context.getSystemService(AccessibilityManager.class);
-        mIsAccessibilityEnabled = accessibilityManager.isEnabled();
-        accessibilityManager.addAccessibilityStateChangeListener(enabled -> {
-            mIsAccessibilityEnabled = enabled;
-            updateProfileButtonVisibility();
-        });
+        // Initial setup
+        setUpProfileButtonWithListeners(mUserIdManager.isMultiUserProfiles());
     }
 
     private void updateRecyclerViewBottomPadding() {
@@ -209,29 +249,49 @@
         mRecyclerView.setPadding(0, 0, 0, recyclerViewBottomPadding);
     }
 
-    private void updateVisibilityAndAnimateBottomBar(int selectedItemListSize) {
+    private void updateVisibilityAndAnimateBottomBar(@NonNull Context context,
+            int selectedItemListSize) {
         if (!mSelection.canSelectMultiple()) {
             return;
         }
 
-        if (selectedItemListSize == 0) {
-            if (mBottomBar.getVisibility() == View.VISIBLE) {
-                mBottomBar.setVisibility(View.GONE);
-                mBottomBar.startAnimation(mSlideDownAnimation);
+        if (mPickerViewModel.isManagedSelectionEnabled()) {
+            animateAndShowBottomBar(context, selectedItemListSize);
+            if (selectedItemListSize == 0) {
+                mViewSelectedButton.setVisibility(View.GONE);
+                // Update the add button to show "Allow none".
+                mAddButton.setText(R.string.picker_add_button_allow_none_option);
             }
         } else {
-            if (mBottomBar.getVisibility() == View.GONE) {
-                mBottomBar.setVisibility(View.VISIBLE);
-                mBottomBar.startAnimation(mSlideUpAnimation);
+            if (selectedItemListSize == 0) {
+                animateAndHideBottomBar();
+            } else {
+                animateAndShowBottomBar(context, selectedItemListSize);
             }
-            mAddButton.setText(generateAddButtonString(getContext(), selectedItemListSize));
         }
-        mIsBottomBarVisible.setValue(selectedItemListSize > 0);
+        mIsBottomBarVisible.setValue(
+                mPickerViewModel.isManagedSelectionEnabled() || selectedItemListSize > 0);
+    }
+
+    private void animateAndShowBottomBar(Context context, int selectedItemListSize) {
+        if (mBottomBar.getVisibility() == View.GONE) {
+            mBottomBar.setVisibility(View.VISIBLE);
+            mBottomBar.startAnimation(mSlideUpAnimation);
+        }
+        mViewSelectedButton.setVisibility(View.VISIBLE);
+        mAddButton.setText(generateAddButtonString(context, selectedItemListSize));
+    }
+
+    private void animateAndHideBottomBar() {
+        if (mBottomBar.getVisibility() == View.VISIBLE) {
+            mBottomBar.setVisibility(View.GONE);
+            mBottomBar.startAnimation(mSlideDownAnimation);
+        }
     }
 
     private void setUpListenersForProfileButton() {
         mProfileButton.setOnClickListener(v -> onClickProfileButton());
-        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+        mOnScrollListenerForMultiProfileButton = new RecyclerView.OnScrollListener() {
             @Override
             public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                 super.onScrolled(recyclerView, dx, dy);
@@ -248,7 +308,8 @@
                     updateProfileButtonVisibility();
                 }
             }
-        });
+        };
+        mRecyclerView.addOnScrollListener(mOnScrollListenerForMultiProfileButton);
     }
 
     @Override
@@ -260,10 +321,11 @@
     }
 
     private void setUpProfileButtonWithListeners(boolean isMultiUserProfile) {
+        if (mOnScrollListenerForMultiProfileButton != null) {
+            mRecyclerView.removeOnScrollListener(mOnScrollListenerForMultiProfileButton);
+        }
         if (isMultiUserProfile) {
             setUpListenersForProfileButton();
-        } else {
-            mRecyclerView.clearOnScrollListeners();
         }
         setUpProfileButton();
     }
@@ -287,8 +349,14 @@
     }
 
     private void onClickProfileButton() {
+        mPickerViewModel.logProfileSwitchButtonClick();
+
         if (!mUserIdManager.isCrossProfileAllowed()) {
-            ProfileDialogFragment.show(getActivity().getSupportFragmentManager());
+            try {
+                ProfileDialogFragment.show(requireActivity().getSupportFragmentManager());
+            } catch (RuntimeException e) {
+                Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
+            }
         } else {
             changeProfile();
         }
@@ -308,60 +376,79 @@
 
         updateProfileButtonContent(mUserIdManager.isManagedUserSelected());
 
-        mPickerViewModel.onUserSwitchedProfile();
+        mPickerViewModel.onSwitchedProfile();
     }
 
     private void updateProfileButtonContent(boolean isManagedUserSelected) {
         final Drawable icon;
         final String text;
+        final Context context;
+        try {
+            context = requireContext();
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Could not update profile button content because the fragment is not"
+                    + " attached.");
+            return;
+        }
+
         if (isManagedUserSelected) {
-            icon = getContext().getDrawable(R.drawable.ic_personal_mode);
-            text = getSwitchToPersonalMessage();
+            icon = context.getDrawable(R.drawable.ic_personal_mode);
+            text = getSwitchToPersonalMessage(context);
         } else {
-            icon = getWorkProfileIcon();
-            text = getSwitchToWorkMessage();
+            icon = getWorkProfileIcon(context);
+            text = getSwitchToWorkMessage(context);
         }
         mProfileButton.setIcon(icon);
         mProfileButton.setText(text);
     }
 
-    private String getSwitchToPersonalMessage() {
+    private String getSwitchToPersonalMessage(@NonNull Context context) {
         if (SdkLevel.isAtLeastT()) {
             return getUpdatedEnterpriseString(
-                    SWITCH_TO_PERSONAL_MESSAGE, R.string.picker_personal_profile);
+                    context, SWITCH_TO_PERSONAL_MESSAGE, R.string.picker_personal_profile);
         } else {
-            return getContext().getString(R.string.picker_personal_profile);
+            return context.getString(R.string.picker_personal_profile);
         }
     }
 
-    private String getSwitchToWorkMessage() {
+    private String getSwitchToWorkMessage(@NonNull Context context) {
         if (SdkLevel.isAtLeastT()) {
             return getUpdatedEnterpriseString(
-                    SWITCH_TO_WORK_MESSAGE, R.string.picker_work_profile);
+                    context, SWITCH_TO_WORK_MESSAGE, R.string.picker_work_profile);
         } else {
-            return getContext().getString(R.string.picker_work_profile);
+            return context.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);
+    private String getUpdatedEnterpriseString(@NonNull Context context,
+            @NonNull String updatableStringId,
+            int defaultStringId) {
+        final DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
         return dpm.getResources().getString(updatableStringId, () -> getString(defaultStringId));
     }
 
-    private Drawable getWorkProfileIcon() {
+    private Drawable getWorkProfileIcon(@NonNull Context context) {
         if (SdkLevel.isAtLeastT()) {
-            return getUpdatedWorkProfileIcon();
+            return getUpdatedWorkProfileIcon(context);
         } else {
-            return getContext().getDrawable(R.drawable.ic_work_outline);
+            return context.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 Drawable getUpdatedWorkProfileIcon(@NonNull Context context) {
+        DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
+        return dpm.getResources().getDrawable(WORK_PROFILE_ICON, OUTLINE, () -> {
+            // Fetch activity or context again instead of capturing existing variable in
+            // lambdas to avoid memory leaks.
+            try {
+                return requireContext().getDrawable(R.drawable.ic_work_outline);
+            } catch (RuntimeException e) {
+                Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
+                return null;
+            }
+        });
     }
 
     private void updateProfileButtonColor(boolean isDisabled) {
@@ -397,6 +484,8 @@
     /**
      * If we show the {@link #mEmptyView}, hide the {@link #mRecyclerView}. If we don't hide the
      * {@link #mEmptyView}, show the {@link #mRecyclerView}
+     * when user switches the profile ,till the time when updated profile data is loading,
+     * on the UI we hide {@link #mEmptyView} and show Empty {@link #mRecyclerView}
      */
     protected void updateVisibilityForEmptyView(boolean shouldShowEmptyView) {
         mEmptyView.setVisibility(shouldShowEmptyView ? View.VISIBLE : View.GONE);
@@ -420,13 +509,18 @@
         return TextUtils.expandTemplate(template, sizeString).toString();
     }
 
-    protected final PhotoPickerActivity getPickerActivity() {
-        return (PhotoPickerActivity) getActivity();
+    /**
+     * Returns {@link PhotoPickerActivity} if the fragment is attached to one. Otherwise, throws an
+     * {@link IllegalStateException}.
+     */
+    protected final PhotoPickerActivity requirePickerActivity() throws IllegalStateException {
+        return (PhotoPickerActivity) requireActivity();
     }
 
-    protected final void setLayoutManager(@NonNull TabAdapter adapter, int spanCount) {
+    protected final void setLayoutManager(@NonNull Context context,
+            @NonNull TabAdapter adapter, int spanCount) {
         final GridLayoutManager layoutManager =
-                new GridLayoutManager(getContext(), spanCount);
+                new GridLayoutManager(context, spanCount);
         final GridLayoutManager.SpanSizeLookup lookup = new GridLayoutManager.SpanSizeLookup() {
             @Override
             public int getSpanSize(int position) {
@@ -447,17 +541,28 @@
     private abstract class OnBannerEventListener implements TabAdapter.OnBannerEventListener {
         @Override
         public void onActionButtonClick() {
+            mPickerViewModel.logBannerActionButtonClicked();
             dismissBanner();
-            getPickerActivity().startSettingsActivity();
+            launchCloudProviderSettings();
         }
 
         @Override
         public void onDismissButtonClick() {
+            mPickerViewModel.logBannerDismissed();
             dismissBanner();
         }
 
         @Override
-        public void onBannerAdded() {
+        public void onBannerClick() {
+            mPickerViewModel.logBannerClicked();
+            dismissBanner();
+            launchCloudProviderSettings();
+        }
+
+        @Override
+        public void onBannerAdded(@NonNull String name) {
+            mPickerViewModel.logBannerAdded(name);
+
             // Should scroll to the banner only if the first completely visible item is the one
             // just below it. The possible adapter item positions of such an item are 0 and 1.
             // During onViewCreated, before restoring the state, the first visible item position
@@ -477,6 +582,21 @@
         }
 
         abstract void dismissBanner();
+
+        private void launchCloudProviderSettings() {
+            final Intent accountChangeIntent =
+                    mPickerViewModel.getChooseCloudMediaAccountActivityIntent();
+
+            try {
+                if (accountChangeIntent != null) {
+                    requirePickerActivity().startActivity(accountChangeIntent);
+                } else {
+                    requirePickerActivity().startSettingsActivity();
+                }
+            } catch (RuntimeException e) {
+                Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
+            }
+        }
     }
 
     protected final OnBannerEventListener mOnChooseAppBannerEventListener =
@@ -493,6 +613,11 @@
                 void dismissBanner() {
                     mPickerViewModel.onUserDismissedCloudMediaAvailableBanner();
                 }
+
+                @Override
+                public boolean shouldShowActionButton() {
+                    return mPickerViewModel.getChooseCloudMediaAccountActivityIntent() != null;
+                }
             };
 
     protected final OnBannerEventListener mOnAccountUpdatedBannerEventListener =
@@ -509,5 +634,10 @@
                 void dismissBanner() {
                     mPickerViewModel.onUserDismissedChooseAccountBanner();
                 }
+
+                @Override
+                public boolean shouldShowActionButton() {
+                    return mPickerViewModel.getChooseCloudMediaAccountActivityIntent() != null;
+                }
             };
 }
diff --git a/src/com/android/providers/media/photopicker/ui/ViewPager2Wrapper.java b/src/com/android/providers/media/photopicker/ui/ViewPager2Wrapper.java
index 563f777..f55376d 100644
--- a/src/com/android/providers/media/photopicker/ui/ViewPager2Wrapper.java
+++ b/src/com/android/providers/media/photopicker/ui/ViewPager2Wrapper.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.view.View;
 
+import androidx.annotation.NonNull;
 import androidx.viewpager2.widget.CompositePageTransformer;
 import androidx.viewpager2.widget.MarginPageTransformer;
 import androidx.viewpager2.widget.ViewPager2;
@@ -42,12 +43,15 @@
     private final PreviewAdapter mAdapter;
     private final List<ViewPager2.OnPageChangeCallback> mOnPageChangeCallbacks = new ArrayList<>();
 
-    ViewPager2Wrapper(ViewPager2 viewPager, List<Item> selectedItems, MuteStatus muteStatus) {
+    ViewPager2Wrapper(ViewPager2 viewPager, List<Item> selectedItems, MuteStatus muteStatus,
+            @NonNull PreviewAdapter.OnCreateSurfaceController onCreateSurfaceController,
+            @NonNull PreviewAdapter.OnVideoPreviewClickListener onVideoPreviewClickListener) {
         mViewPager = viewPager;
 
         final Context context = mViewPager.getContext();
 
-        mAdapter = new PreviewAdapter(context, muteStatus);
+        mAdapter = new PreviewAdapter(context, muteStatus, onCreateSurfaceController,
+                onVideoPreviewClickListener);
         mAdapter.updateItemList(selectedItems);
         mViewPager.setAdapter(mAdapter);
 
@@ -58,6 +62,10 @@
         mViewPager.setPageTransformer(compositePageTransformer);
     }
 
+    void updateList(List<Item> selectedItems) {
+        mAdapter.updateItemList(selectedItems);
+    }
+
     /**
      * Registers given {@link ViewPager2.OnPageChangeCallback} to the {@link ViewPager2}. This class
      * also takes care of unregistering the callback onDestroy()
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 b07fc9b..dae4e5b 100644
--- a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java
+++ b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java
@@ -41,9 +41,12 @@
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 
+import androidx.annotation.NonNull;
+
 import com.android.providers.media.photopicker.RemoteVideoPreviewProvider;
 import com.android.providers.media.photopicker.data.MuteStatus;
 import com.android.providers.media.photopicker.data.model.Item;
+import com.android.providers.media.photopicker.ui.PreviewAdapter.OnCreateSurfaceController;
 import com.android.providers.media.photopicker.ui.PreviewVideoHolder;
 
 import java.util.Map;
@@ -72,13 +75,16 @@
     private final ItemPreviewState mCurrentPreviewState = new ItemPreviewState();
     private final PlayerControlsVisibilityStatus mPlayerControlsVisibilityStatus =
             new PlayerControlsVisibilityStatus();
+    private final OnCreateSurfaceController mOnCreateSurfaceController;
 
     private boolean mIsInBackground = false;
     private int mSurfaceCounter = 0;
 
-    public RemotePreviewHandler(Context context, MuteStatus muteStatus) {
+    public RemotePreviewHandler(Context context, MuteStatus muteStatus,
+            @NonNull OnCreateSurfaceController onCreateSurfaceController) {
         mContext = context;
         mMuteStatus = muteStatus;
+        mOnCreateSurfaceController = onCreateSurfaceController;
     }
 
     /**
@@ -200,9 +206,11 @@
 
         SurfaceControllerProxy controller = null;
         try {
+            mOnCreateSurfaceController.logStart(authority);
             controller = createController(authority, localControllerFallback);
             if (controller != null) {
                 mControllers.put(authority, controller);
+                mOnCreateSurfaceController.logEnd(authority);
             }
         } catch (RuntimeException e) {
             Log.e(TAG, "Could not create SurfaceController.", e);
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 0fa8068..cac25f5 100644
--- a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java
+++ b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java
@@ -89,6 +89,7 @@
     private final View.OnClickListener mMuteButtonClickListener = new View.OnClickListener() {
         @Override
         public void onClick(View v) {
+            mPreviewVideoHolder.logMuteButtonClick();
             boolean newMutedValue = !mMuteStatus.isVolumeMuted();
             mMuteStatus.setVolumeMuted(newMutedValue);
             handleAudioFocusAndInitVolumeState();
@@ -267,6 +268,12 @@
             case PLAYBACK_STATE_BUFFERING:
                 mPreviewVideoHolder.getCircularProgressIndicator().setVisibility(View.VISIBLE);
                 return;
+            case PLAYBACK_STATE_COMPLETED:
+                // TODO(b/296543163): Investigate CloudMediaProviderContract for future OEM
+                //  implementers. Should the provider be expected to loop the video themselves
+                //  instead of ending the playback state?
+                requestPlayMedia();
+                return;
             default:
         }
     }
@@ -374,7 +381,8 @@
         // media size, then we hide the thumbnail view.
         mPreviewVideoHolder.getPlayerContainer().setVisibility(View.INVISIBLE);
         mPreviewVideoHolder.getThumbnailView().setVisibility(View.VISIBLE);
-        mPreviewVideoHolder.getPlayerControlsRoot().setVisibility(View.GONE);
+        updatePlayerControlsVisibilityState(
+                mPlayerControlsVisibilityStatus.shouldShowPlayerControls());
         mPreviewVideoHolder.getCircularProgressIndicator().setVisibility(View.GONE);
 
         updatePlayPauseButtonState(false /* isPlaying */);
@@ -449,7 +457,11 @@
         mIsAccessibilityEnabled = enabled;
         mPreviewVideoHolder.getPlayerContainer().setOnClickListener(
                 mIsAccessibilityEnabled ? null : mPlayerContainerClickListener);
-        updatePlayerControlsVisibilityState(mIsAccessibilityEnabled);
+        if (mIsAccessibilityEnabled) {
+            updatePlayerControlsVisibilityState(/* visible= */ true);
+        } else {
+            hidePlayerControlsWithDelay();
+        }
     }
 
     private void updatePlayPauseButtonState(boolean isPlaying) {
diff --git a/src/com/android/providers/media/photopicker/ui/settings/CloudMediaProviderAccount.java b/src/com/android/providers/media/photopicker/ui/settings/CloudMediaProviderAccount.java
deleted file mode 100644
index fc2d332..0000000
--- a/src/com/android/providers/media/photopicker/ui/settings/CloudMediaProviderAccount.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2023 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.settings;
-
-import static java.util.Objects.requireNonNull;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-/* POJO for encapsulating cloud provider authority and it's linked account name. */
-class CloudMediaProviderAccount {
-    @NonNull
-    private final String mCloudProviderAuthority;
-    @Nullable
-    private final String mCloudProviderAccountName;
-
-    CloudMediaProviderAccount(
-            @NonNull String cloudProviderAuthority,
-            @Nullable String cloudProviderAccountName) {
-        mCloudProviderAuthority = requireNonNull(cloudProviderAuthority);
-        mCloudProviderAccountName = cloudProviderAccountName;
-    }
-
-    @NonNull
-    String getCloudProviderAuthority() {
-        return mCloudProviderAuthority;
-    }
-
-    @Nullable
-    String getCloudProviderAccountName() {
-        return mCloudProviderAccountName;
-    }
-}
diff --git a/src/com/android/providers/media/photopicker/ui/settings/CloudProviderMediaCollectionInfo.java b/src/com/android/providers/media/photopicker/ui/settings/CloudProviderMediaCollectionInfo.java
new file mode 100644
index 0000000..7c6d546
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/ui/settings/CloudProviderMediaCollectionInfo.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 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.settings;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.Intent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/* POJO for encapsulating the cloud provider authority and it's media collection info. */
+class CloudProviderMediaCollectionInfo {
+    @NonNull
+    private final String mAuthority;
+    @Nullable
+    private final String mAccountName;
+    @Nullable
+    private final Intent mAccountConfigurationIntent;
+
+    CloudProviderMediaCollectionInfo(@NonNull String authority) {
+        mAuthority = requireNonNull(authority);
+        mAccountName = null;
+        mAccountConfigurationIntent = null;
+    }
+
+    CloudProviderMediaCollectionInfo(@NonNull String authority, @Nullable String accountName,
+            @Nullable Intent accountConfigurationIntent) {
+        mAuthority = requireNonNull(authority);
+        mAccountName = accountName;
+        mAccountConfigurationIntent = accountConfigurationIntent;
+    }
+
+    @NonNull
+    String getAuthority() {
+        return mAuthority;
+    }
+
+    @Nullable
+    String getAccountName() {
+        return mAccountName;
+    }
+
+    @Nullable
+    Intent getAccountConfigurationIntent() {
+        return mAccountConfigurationIntent;
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaSelectFragment.java b/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaSelectFragment.java
index c8d1cc7..f08bd75 100644
--- a/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaSelectFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaSelectFragment.java
@@ -21,6 +21,7 @@
 import static java.util.Objects.requireNonNull;
 
 import android.content.Context;
+import android.content.Intent;
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.text.TextUtils;
@@ -67,7 +68,7 @@
     public void onResume() {
         super.onResume();
 
-        mSettingsCloudMediaViewModel.loadAccountNameAsync();
+        mSettingsCloudMediaViewModel.loadMediaCollectionInfoAsync();
     }
 
     @UiThread
@@ -93,7 +94,7 @@
         super.addPreferencesFromResource(R.xml.pref_screen_picker_settings);
 
         mSettingsCloudMediaViewModel.loadData(getConfigStore());
-        observeAccountNameChanges();
+        observeMediaCollectionInfoChanges();
         refreshUI();
     }
 
@@ -111,23 +112,34 @@
         updateSelectedRadioButton();
     }
 
-    private void observeAccountNameChanges() {
-        mSettingsCloudMediaViewModel.getCurrentProviderAccount()
-                .observe(this, accountDetails -> {
-                    // Only update current account name on the UI if cloud provider linked to the
-                    // account name matches the current provider.
-                    if (accountDetails != null
-                            && accountDetails.getCloudProviderAuthority()
-                            .equals(mSettingsCloudMediaViewModel.getSelectedProviderAuthority())) {
-                        final Preference selectedPref = findPreference(
-                                mSettingsCloudMediaViewModel.getSelectedPreferenceKey());
-                        // TODO(b/262002538): {@code selectedPref} could be null if the selected
-                        //  cloud provider is not in the allowed list. This is not something a
-                        //  typical user will encounter.
-                        if (selectedPref != null) {
-                            selectedPref.setSummary(accountDetails.getCloudProviderAccountName());
-                        }
+    private void observeMediaCollectionInfoChanges() {
+        mSettingsCloudMediaViewModel.getCurrentProviderMediaCollectionInfo().observe(this,
+                providerMediaCollectionInfo -> {
+                    // Only update the UI preference if the cloud provider linked to the media
+                    // collection info matches the current provider.
+                    if (providerMediaCollectionInfo == null
+                            || !TextUtils.equals(providerMediaCollectionInfo.getAuthority(),
+                            mSettingsCloudMediaViewModel.getSelectedProviderAuthority())) {
+                        return;
                     }
+
+                    final SelectorWithWidgetPreference selectedPref =
+                            findPreference(mSettingsCloudMediaViewModel.getSelectedPreferenceKey());
+
+                    // TODO(b/262002538): {@code selectedPref} could be null if the selected
+                    //  cloud provider is not in the allowed list. This is not something a
+                    //  typical user will encounter.
+                    if (selectedPref == null) {
+                        return;
+                    }
+
+                    selectedPref.setSummary(providerMediaCollectionInfo.getAccountName());
+
+                    final Intent accountConfigurationIntent =
+                            providerMediaCollectionInfo.getAccountConfigurationIntent();
+                    selectedPref.setExtraWidgetOnClickListener(
+                            accountConfigurationIntent == null ? null : v ->
+                                    requireActivity().startActivity(accountConfigurationIntent));
                 });
     }
 
@@ -137,19 +149,19 @@
                 mSettingsCloudMediaViewModel.getSelectedPreferenceKey();
         for (CloudMediaProviderOption providerOption
                 : mSettingsCloudMediaViewModel.getProviderOptions()) {
-            final Preference pref = findPreference(providerOption.getKey());
-            if (pref instanceof SelectorWithWidgetPreference) {
-                final SelectorWithWidgetPreference providerPref =
-                        (SelectorWithWidgetPreference) pref;
+            final SelectorWithWidgetPreference preference = findPreference(providerOption.getKey());
+            if (preference == null) {
+                continue;
+            }
 
-                final boolean newSelectionState =
-                        TextUtils.equals(providerPref.getKey(), selectedPreferenceKey);
-                providerPref.setChecked(newSelectionState);
+            final boolean isSelected = TextUtils.equals(preference.getKey(), selectedPreferenceKey);
+            preference.setChecked(isSelected);
 
-                providerPref.setSummary(null);
-                if (newSelectionState) {
-                    mSettingsCloudMediaViewModel.loadAccountNameAsync();
-                }
+            preference.setSummary(null);
+            preference.setExtraWidgetOnClickListener(null);
+
+            if (isSelected) {
+                mSettingsCloudMediaViewModel.loadMediaCollectionInfoAsync();
             }
         }
     }
diff --git a/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaViewModel.java b/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaViewModel.java
index 346eed3..e316970 100644
--- a/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaViewModel.java
+++ b/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaViewModel.java
@@ -20,17 +20,21 @@
 
 import static com.android.providers.media.photopicker.util.CloudProviderUtils.fetchProviderAuthority;
 import static com.android.providers.media.photopicker.util.CloudProviderUtils.getAvailableCloudProviders;
-import static com.android.providers.media.photopicker.util.CloudProviderUtils.getCloudMediaAccountName;
+import static com.android.providers.media.photopicker.util.CloudProviderUtils.getCloudMediaCollectionInfo;
 import static com.android.providers.media.photopicker.util.CloudProviderUtils.persistSelectedProvider;
 
 import static java.util.Objects.requireNonNull;
 
 import android.content.ContentProviderClient;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.graphics.drawable.Drawable;
+import android.os.Bundle;
 import android.os.Looper;
 import android.os.UserHandle;
+import android.provider.CloudMediaProviderContract;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -56,11 +60,13 @@
 public class SettingsCloudMediaViewModel extends ViewModel {
     static final String NONE_PREF_KEY = "none";
     private static final String TAG = "SettingsFragVM";
+    private static final long GET_CLOUD_MEDIA_COLLECTION_INFO_TIMEOUT_IN_MILLIS = 10000L;
 
     @NonNull
     private final Context mContext;
     @NonNull
-    private final MutableLiveData<CloudMediaProviderAccount> mCurrentProviderAccount;
+    private final MutableLiveData<CloudProviderMediaCollectionInfo>
+            mCurrentProviderMediaCollectionInfo;
     @NonNull
     private final List<CloudMediaProviderOption> mProviderOptions;
     @NonNull
@@ -77,7 +83,7 @@
         mUserId = requireNonNull(userId);
         mProviderOptions = new ArrayList<>();
         mSelectedProviderAuthority = null;
-        mCurrentProviderAccount = new MutableLiveData<CloudMediaProviderAccount>();
+        mCurrentProviderMediaCollectionInfo = new MutableLiveData<>();
     }
 
     @NonNull
@@ -91,11 +97,11 @@
     }
 
     @NonNull
-    LiveData<CloudMediaProviderAccount> getCurrentProviderAccount() {
-        return mCurrentProviderAccount;
+    LiveData<CloudProviderMediaCollectionInfo> getCurrentProviderMediaCollectionInfo() {
+        return mCurrentProviderMediaCollectionInfo;
     }
 
-    @Nullable
+    @NonNull
     String getSelectedPreferenceKey() {
         return getPreferenceKey(mSelectedProviderAuthority);
     }
@@ -140,7 +146,7 @@
                 ? null : preferenceKey;
     }
 
-    @Nullable
+    @NonNull
     private String getPreferenceKey(@Nullable String providerAuthority) {
         return providerAuthority == null
                 ? SettingsCloudMediaViewModel.NONE_PREF_KEY : providerAuthority;
@@ -171,38 +177,50 @@
     }
 
     @UiThread
-    void loadAccountNameAsync() {
+    void loadMediaCollectionInfoAsync() {
         if (!Looper.getMainLooper().isCurrentThread()) {
-            // This method should only be run from the UI thread so that fetch account name
+            // This method should only be run from the UI thread so that fetch media collection info
             // requests are executed serially.
-            Log.d(TAG, "loadAccountNameAsync method needs to be called from the UI thread");
+            Log.w(TAG, "loadMediaCollectionInfoAsync method needs to be called from the UI thread");
             return;
         }
 
         final String providerAuthority = getSelectedProviderAuthority();
         // Foreground thread internally uses a queue to execute each request in a serialized manner.
         ForegroundThread.getExecutor().execute(() -> {
-            mCurrentProviderAccount.postValue(
-                    fetchAccountFromProvider(providerAuthority));
+            mCurrentProviderMediaCollectionInfo.postValue(
+                    fetchMediaCollectionInfoFromProvider(providerAuthority));
         });
     }
 
     @Nullable
-    private CloudMediaProviderAccount fetchAccountFromProvider(
+    private CloudProviderMediaCollectionInfo fetchMediaCollectionInfoFromProvider(
             @Nullable String currentProviderAuthority) {
+        // If the selected cloud provider preference is "None", the media collection info is not
+        // applicable.
         if (currentProviderAuthority == null) {
-            // If the selected cloud provider preference is "None", account name is not applicable.
             return null;
-        } else {
-            try {
-                final String accountName = getCloudMediaAccountName(
-                        mUserId.getContentResolver(mContext), currentProviderAuthority);
-                return new CloudMediaProviderAccount(currentProviderAuthority, accountName);
-            } catch (Exception e) {
-                Log.w(TAG, "Failed to fetch account name from the cloud media provider.", e);
-                return null;
-            }
         }
+
+        Bundle cloudMediaCollectionInfo = null;
+        try {
+            final ContentResolver currentUserContentResolver = mUserId.getContentResolver(mContext);
+            cloudMediaCollectionInfo = getCloudMediaCollectionInfo(currentUserContentResolver,
+                    currentProviderAuthority, GET_CLOUD_MEDIA_COLLECTION_INFO_TIMEOUT_IN_MILLIS);
+        } catch (Exception e) {
+            Log.w(TAG, "Failed to fetch media collection info from the cloud media provider.", e);
+        }
+
+        if (cloudMediaCollectionInfo == null) {
+            return new CloudProviderMediaCollectionInfo(currentProviderAuthority);
+        }
+
+        final String accountName = cloudMediaCollectionInfo.getString(
+                CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME);
+        final Intent cloudProviderSettingsActivityIntent = cloudMediaCollectionInfo.getParcelable(
+                CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT);
+        return new CloudProviderMediaCollectionInfo(currentProviderAuthority, accountName,
+                cloudProviderSettingsActivityIntent);
     }
 
     @NonNull
diff --git a/src/com/android/providers/media/photopicker/ui/settings/SettingsProfileSelectFragment.java b/src/com/android/providers/media/photopicker/ui/settings/SettingsProfileSelectFragment.java
index d490684..fb31c2d 100644
--- a/src/com/android/providers/media/photopicker/ui/settings/SettingsProfileSelectFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/settings/SettingsProfileSelectFragment.java
@@ -29,7 +29,7 @@
 
 import com.android.providers.media.photopicker.data.UserIdManager;
 import com.android.settingslib.widget.ProfileSelectFragment;
-import com.android.settingslib.widget.R;
+import com.android.settingslib.widget.profileselector.R;
 
 import com.google.android.material.tabs.TabLayout;
 
diff --git a/src/com/android/providers/media/photopicker/util/CategoryOrganiserUtils.java b/src/com/android/providers/media/photopicker/util/CategoryOrganiserUtils.java
new file mode 100644
index 0000000..8857ce6
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/util/CategoryOrganiserUtils.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 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.util;
+
+import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA;
+import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS;
+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 com.android.providers.media.photopicker.data.model.Category;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Reorders categories per requirements.
+ */
+public class CategoryOrganiserUtils {
+    static final int DEFAULT_PRIORITY = 100;
+    static Map<String, Integer> sCategoryPriorityMapping;
+
+    /**
+     * Rearranges categoryList in the required order: Favourites, camera, videos,
+     * screenshots, downloads, ... cloud albums ordered by last modified time stamp.
+     */
+    public static void getReorganisedCategoryList(List<Category> categoryList) {
+        // Items having the same priority will not be modified in order.
+        categoryList.sort(new CategoryComparator());
+    }
+
+    private static void populateCategoryPriorityMapping() {
+
+        // DO NOT ALTER THIS ORDER.
+        // These priorities decide the order in which the categories will be displayed on UI.
+        sCategoryPriorityMapping = new HashMap<String, Integer>() {
+            {
+                put(ALBUM_ID_FAVORITES, 0);
+                put(ALBUM_ID_CAMERA, 1);
+                put(ALBUM_ID_VIDEOS, 2);
+                put(ALBUM_ID_SCREENSHOTS, 3);
+                put(ALBUM_ID_DOWNLOADS, 4);
+            }
+        };
+    }
+
+    private static int getPriority(Category category) {
+        if (sCategoryPriorityMapping == null) {
+            populateCategoryPriorityMapping();
+        }
+        if (sCategoryPriorityMapping.containsKey(category.getId())) {
+            return sCategoryPriorityMapping.get(category.getId());
+        }
+        return DEFAULT_PRIORITY;
+    }
+
+    static class CategoryComparator implements java.util.Comparator<Category> {
+        @Override
+        public int compare(Category category1, Category category2) {
+            return getPriority(category1) - getPriority(category2);
+        }
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/util/CloudProviderUtils.java b/src/com/android/providers/media/photopicker/util/CloudProviderUtils.java
index b2ba057..57e6f75 100644
--- a/src/com/android/providers/media/photopicker/util/CloudProviderUtils.java
+++ b/src/com/android/providers/media/photopicker/util/CloudProviderUtils.java
@@ -18,14 +18,21 @@
 
 import static android.provider.CloudMediaProviderContract.MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION;
 import static android.provider.CloudMediaProviderContract.METHOD_GET_MEDIA_COLLECTION_INFO;
-import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME;
+import static android.provider.MediaStore.EXTRA_ALBUM_AUTHORITY;
+import static android.provider.MediaStore.EXTRA_ALBUM_ID;
 import static android.provider.MediaStore.EXTRA_CLOUD_PROVIDER;
+import static android.provider.MediaStore.EXTRA_LOCAL_ONLY;
 import static android.provider.MediaStore.GET_CLOUD_PROVIDER_CALL;
 import static android.provider.MediaStore.GET_CLOUD_PROVIDER_RESULT;
+import static android.provider.MediaStore.PICKER_MEDIA_INIT_CALL;
 import static android.provider.MediaStore.SET_CLOUD_PROVIDER_CALL;
 
-import static java.util.Collections.emptyList;
+import static com.android.providers.media.PickerUriResolver.getMediaCollectionInfoUri;
 
+import static java.util.Collections.emptyList;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.annotation.DurationMillisLong;
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -51,6 +58,9 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
 
 /**
  * Utility methods for retrieving available and/or allowlisted Cloud Providers.
@@ -191,6 +201,24 @@
     }
 
     /**
+     * Send data init call.
+     */
+    public static boolean sendInitPhotoPickerDataNotification(
+            @NonNull ContentProviderClient client,
+            @Nullable String albumId,
+            @Nullable String albumAuthority,
+            boolean initLocalOnlyData) throws RemoteException {
+        final Bundle input = new Bundle();
+        input.putString(EXTRA_ALBUM_ID, albumId);
+        input.putString(EXTRA_ALBUM_AUTHORITY, albumAuthority);
+        input.putBoolean(EXTRA_LOCAL_ONLY, initLocalOnlyData);
+        Log.i(TAG, "Sending media init query for extras: " + input);
+
+        client.call(PICKER_MEDIA_INIT_CALL, /* arg */ null, /* extras */ input);
+        return true;
+    }
+
+    /**
      * @return the label for the {@link ProviderInfo} with {@code authority} for the given
      *         {@link UserHandle}.
      */
@@ -226,23 +254,24 @@
     }
 
     /**
-     * @return the current cloud media account name for the {@link CloudMediaProvider} with the
+     * @param resolver                    {@link ContentResolver} for the related user
+     * @param cloudMediaProviderAuthority authority {@link String} of the {@link CloudMediaProvider}
+     * @param timeout                     timeout in milliseconds for this query (<= 0 for timeout)
+     * @return the current cloud media collection info for the {@link CloudMediaProvider} with the
      *         given {@code cloudMediaProviderAuthority}.
      */
     @Nullable
-    public static String getCloudMediaAccountName(@NonNull ContentResolver resolver,
-            @Nullable String cloudMediaProviderAuthority) {
+    public static Bundle getCloudMediaCollectionInfo(@NonNull ContentResolver resolver,
+            @Nullable String cloudMediaProviderAuthority, @DurationMillisLong long timeout)
+            throws ExecutionException, InterruptedException, TimeoutException {
         if (cloudMediaProviderAuthority == null) {
             return null;
         }
 
-        try (ContentProviderClient client =
-                     resolver.acquireContentProviderClient(cloudMediaProviderAuthority)) {
-            final Bundle out = client.call(METHOD_GET_MEDIA_COLLECTION_INFO, /* arg */ null,
-                    /* extras */ null);
-            return out.getString(ACCOUNT_NAME);
-        } catch (RemoteException e) {
-            throw e.rethrowAsRuntimeException();
-        }
+        CompletableFuture<Bundle> future = CompletableFuture.supplyAsync(() ->
+                resolver.call(getMediaCollectionInfoUri(cloudMediaProviderAuthority),
+                        METHOD_GET_MEDIA_COLLECTION_INFO, /* arg */ null, /* extras */ null));
+
+        return (timeout > 0) ? future.get(timeout, MILLISECONDS) : future.get();
     }
 }
diff --git a/src/com/android/providers/media/photopicker/util/ThreadUtils.java b/src/com/android/providers/media/photopicker/util/ThreadUtils.java
new file mode 100644
index 0000000..c3555d6
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/util/ThreadUtils.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 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.util;
+
+import android.os.Looper;
+
+/**
+ * Provide the utility methods to handle thread.
+ */
+public class ThreadUtils {
+    /**
+     * Assert if the current {@link Thread} is the {@link androidx.annotation.MainThread}.
+     */
+    public static void assertMainThread() {
+        if (Looper.getMainLooper().isCurrentThread()) {
+            return;
+        }
+        throw new IllegalStateException("Must be called from the Main thread. Current thread: "
+                + Thread.currentThread());
+    }
+
+    /**
+     * Assert if the current {@link Thread} is NOT the {@link androidx.annotation.MainThread}.
+     */
+    public static void assertNonMainThread() {
+        if (Looper.getMainLooper().isCurrentThread()) {
+            throw new IllegalStateException("Must NOT be called from the Main thread."
+                    + " Current thread: " + Thread.currentThread());
+        }
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/util/exceptions/UnableToAcquireLockException.java b/src/com/android/providers/media/photopicker/util/exceptions/UnableToAcquireLockException.java
new file mode 100644
index 0000000..fad0c01
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/util/exceptions/UnableToAcquireLockException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 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.util.exceptions;
+
+/**
+ * Exception thrown when the current thread tries to acquire a lock but fails. The failure could be
+ * because of a timeout or thread interruption.
+ */
+public class UnableToAcquireLockException extends Exception {
+    public UnableToAcquireLockException(String message) {
+        super(message);
+    }
+
+    public UnableToAcquireLockException(String message, Exception e) {
+        super(message, e);
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/viewmodel/BannerController.java b/src/com/android/providers/media/photopicker/viewmodel/BannerController.java
index 746ebd6..0087adc 100644
--- a/src/com/android/providers/media/photopicker/viewmodel/BannerController.java
+++ b/src/com/android/providers/media/photopicker/viewmodel/BannerController.java
@@ -18,16 +18,17 @@
 
 import static android.provider.MediaStore.getCurrentCloudProvider;
 
-import static com.android.providers.media.MediaApplication.getConfigStore;
 import static com.android.providers.media.photopicker.util.CloudProviderUtils.getAvailableCloudProviders;
-import static com.android.providers.media.photopicker.util.CloudProviderUtils.getCloudMediaAccountName;
+import static com.android.providers.media.photopicker.util.CloudProviderUtils.getCloudMediaCollectionInfo;
 import static com.android.providers.media.photopicker.util.CloudProviderUtils.getProviderLabelForUser;
 
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.PackageManager;
-import android.os.Looper;
+import android.os.Bundle;
 import android.os.UserHandle;
+import android.provider.CloudMediaProviderContract.MediaCollectionInfo;
 import android.text.TextUtils;
 import android.util.AtomicFile;
 import android.util.Log;
@@ -36,7 +37,9 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.providers.media.ConfigStore;
 import com.android.providers.media.photopicker.data.model.UserId;
+import com.android.providers.media.photopicker.util.ThreadUtils;
 import com.android.providers.media.util.XmlUtils;
 
 import java.io.File;
@@ -44,6 +47,8 @@
 import java.io.FileOutputStream;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
 
 /**
  * Banner Controller to store and handle the banner data per user for
@@ -64,9 +69,11 @@
      * {@link android.provider.CloudMediaProvider}.
      */
     private static final String ACCOUNT_NAME = "account_name";
+    private static final long GET_CLOUD_MEDIA_COLLECTION_INFO_TIMEOUT_IN_MILLIS = 100L;
 
     private final Context mContext;
     private final UserHandle mUserHandle;
+    private final ConfigStore mConfigStore;
 
     /**
      * {@link File} for persisting the last fetched {@link android.provider.CloudMediaProvider}
@@ -82,6 +89,9 @@
     // Label of the current cloud media provider
     private String mCmpLabel;
 
+    // Account selection activity intent of the current cloud media provider
+    private Intent mChooseCloudMediaAccountActivityIntent;
+
     // Boolean 'Choose App' banner visibility
     private boolean mShowChooseAppBanner;
 
@@ -94,10 +104,12 @@
     // Boolean 'Choose Account' banner visibility
     private boolean mShowChooseAccountBanner;
 
-    BannerController(@NonNull Context context, @NonNull UserHandle userHandle) {
+    BannerController(@NonNull Context context, @NonNull UserHandle userHandle,
+            @NonNull ConfigStore configStore) {
         Log.d(TAG, "Constructing the BannerController for user " + userHandle.getIdentifier());
         mContext = context;
         mUserHandle = userHandle;
+        mConfigStore = configStore;
 
         final String lastCloudProviderDataFilePath = DATA_MEDIA_DIRECTORY_PATH
                 + userHandle.getIdentifier() + LAST_CLOUD_PROVIDER_DATA_FILE_PATH_IN_USER_MEDIA_DIR;
@@ -127,7 +139,9 @@
      * block the UI thread on the heavy Binder calls to fetch the cloud media provider info.
      */
     private void initialise() {
-        final String cmpAuthority, cmpAccountName;
+        String cmpAuthority = null, cmpAccountName = null;
+        mCmpLabel = null;
+        mChooseCloudMediaAccountActivityIntent = null;
         // TODO(b/245746037): Remove try-catch for the RuntimeException.
         //  Under the hood MediaStore.getCurrentCloudProvider() makes an IPC call to the primary
         //  MediaProvider process, where we currently perform a UID check (making sure that
@@ -139,21 +153,30 @@
         //  check for MANAGE_CLOUD_MEDIA_PROVIDER permission.
         try {
             // 0. Assert non-main thread.
-            assertNonMainThread();
+            ThreadUtils.assertNonMainThread();
 
             // 1. Fetch the latest cloud provider info.
             final ContentResolver contentResolver =
                     UserId.of(mUserHandle).getContentResolver(mContext);
             cmpAuthority = getCurrentCloudProvider(contentResolver);
             mCmpLabel = getProviderLabelForUser(mContext, mUserHandle, cmpAuthority);
-            cmpAccountName = getCloudMediaAccountName(contentResolver, cmpAuthority);
+            final Bundle cloudMediaCollectionInfo = getCloudMediaCollectionInfo(contentResolver,
+                    cmpAuthority, GET_CLOUD_MEDIA_COLLECTION_INFO_TIMEOUT_IN_MILLIS);
+            if (cloudMediaCollectionInfo != null) {
+                cmpAccountName = cloudMediaCollectionInfo.getString(
+                        MediaCollectionInfo.ACCOUNT_NAME);
+                mChooseCloudMediaAccountActivityIntent = cloudMediaCollectionInfo.getParcelable(
+                        MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT);
+            }
 
             // Not logging the account name due to privacy concerns
             Log.d(TAG, "Current CloudMediaProvider authority: " + cmpAuthority + ", label: "
                     + mCmpLabel);
-        } catch (PackageManager.NameNotFoundException | RuntimeException e) {
+        } catch (PackageManager.NameNotFoundException | RuntimeException | ExecutionException
+                | InterruptedException | TimeoutException e) {
             Log.w(TAG, "Could not fetch the current CloudMediaProvider", e);
-            resetToDefault();
+            updateCloudProviderDataMap(cmpAuthority, cmpAccountName);
+            clearBanners();
             return;
         }
 
@@ -210,15 +233,6 @@
     }
 
     /**
-     * Reset all the controller data to their default values.
-     */
-    private void resetToDefault() {
-        mCloudProviderDataMap.clear();
-        mCmpLabel = null;
-        clearBanners();
-    }
-
-    /**
      * Clear all banners
      *
      * Reset all should show banner {@code boolean} values to {@code false}.
@@ -232,7 +246,7 @@
 
     @VisibleForTesting
     boolean areCloudProviderOptionsAvailable() {
-        return !getAvailableCloudProviders(mContext, getConfigStore(), mUserHandle).isEmpty();
+        return !getAvailableCloudProviders(mContext, mConfigStore, mUserHandle).isEmpty();
     }
 
     /**
@@ -260,6 +274,21 @@
     }
 
     /**
+     * @return the account selection activity {@link Intent} of the current
+     *         {@link android.provider.CloudMediaProvider}.
+     */
+    @Nullable
+    Intent getChooseCloudMediaAccountActivityIntent() {
+        return mChooseCloudMediaAccountActivityIntent;
+    }
+
+    @VisibleForTesting
+    void setChooseCloudMediaAccountActivityIntent(
+            @Nullable Intent chooseCloudMediaAccountActivityIntent) {
+        mChooseCloudMediaAccountActivityIntent = chooseCloudMediaAccountActivityIntent;
+    }
+
+    /**
      * @return the 'Choose App' banner visibility {@link #mShowChooseAppBanner}.
      */
     boolean shouldShowChooseAppBanner() {
@@ -344,15 +373,6 @@
         }
     }
 
-    private static void assertNonMainThread() {
-        if (!Looper.getMainLooper().isCurrentThread()) {
-            return;
-        }
-
-        throw new IllegalStateException("Expected to NOT be called from the main thread."
-                + " Current thread: " + Thread.currentThread());
-    }
-
     private void loadCloudProviderInfo() {
         FileInputStream fis = null;
         final Map<String, String> lastCloudProviderDataMap = new HashMap<>();
@@ -382,6 +402,12 @@
 
     private void persistCloudProviderInfo(@Nullable String cmpAuthority,
             @Nullable String cmpAccountName) {
+        updateCloudProviderDataMap(cmpAuthority, cmpAccountName);
+        updateCloudProviderDataFile();
+    }
+
+    private void updateCloudProviderDataMap(@Nullable String cmpAuthority,
+            @Nullable String cmpAccountName) {
         mCloudProviderDataMap.clear();
         if (cmpAuthority != null) {
             mCloudProviderDataMap.put(AUTHORITY, cmpAuthority);
@@ -389,8 +415,6 @@
         if (cmpAccountName != null) {
             mCloudProviderDataMap.put(ACCOUNT_NAME, cmpAccountName);
         }
-
-        updateCloudProviderDataFile();
     }
 
     @VisibleForTesting
diff --git a/src/com/android/providers/media/photopicker/viewmodel/BannerManager.java b/src/com/android/providers/media/photopicker/viewmodel/BannerManager.java
index 04928e2..7601ee2 100644
--- a/src/com/android/providers/media/photopicker/viewmodel/BannerManager.java
+++ b/src/com/android/providers/media/photopicker/viewmodel/BannerManager.java
@@ -16,23 +16,30 @@
 
 package com.android.providers.media.photopicker.viewmodel;
 
+import static com.android.providers.media.photopicker.DataLoaderThread.TOKEN;
+
 import android.annotation.UserIdInt;
 import android.content.Context;
+import android.content.Intent;
 import android.os.UserHandle;
 import android.util.Log;
 
+import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
+import androidx.annotation.VisibleForTesting;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 
+import com.android.providers.media.ConfigStore;
+import com.android.providers.media.photopicker.DataLoaderThread;
 import com.android.providers.media.photopicker.data.UserIdManager;
-import com.android.providers.media.util.ForegroundThread;
+import com.android.providers.media.photopicker.util.ThreadUtils;
 import com.android.providers.media.util.PerUser;
 
 class BannerManager {
     private static final String TAG = "BannerManager";
+    private static final int DELAY_MILLIS = 0;
 
     private final UserIdManager mUserIdManager;
 
@@ -42,6 +49,8 @@
     private final MutableLiveData<String> mCloudMediaProviderLabel = new MutableLiveData<>();
     // Account name of the current CloudMediaProvider of the current user
     private final MutableLiveData<String> mCloudMediaAccountName = new MutableLiveData<>();
+    // Account selection activity intent of the current CloudMediaProvider of the current user
+    private Intent mChooseCloudMediaAccountActivityIntent = null;
 
     // Boolean Choose App Banner visibility
     private final MutableLiveData<Boolean> mShowChooseAppBanner = new MutableLiveData<>(false);
@@ -56,18 +65,26 @@
     // The banner controllers per user
     private final PerUser<BannerController> mBannerControllers;
 
-    BannerManager(@NonNull Context context, @NonNull UserIdManager userIdManager) {
+    BannerManager(@NonNull Context context, @NonNull UserIdManager userIdManager,
+            @NonNull ConfigStore configStore) {
         mUserIdManager = userIdManager;
         mBannerControllers = new PerUser<BannerController>() {
             @NonNull
             @Override
             protected BannerController create(@UserIdInt int userId) {
-                return new BannerController(context, UserHandle.of(userId));
+                return createBannerController(context, UserHandle.of(userId), configStore);
             }
         };
         maybeInitialiseAndSetBannersForCurrentUser();
     }
 
+    @VisibleForTesting
+    @NonNull
+    BannerController createBannerController(@NonNull Context context,
+            @NonNull UserHandle userHandle, @NonNull ConfigStore configStore) {
+        return new BannerController(context, userHandle, configStore);
+    }
+
     @UserIdInt int getCurrentUserProfileId() {
         return mUserIdManager.getCurrentUserProfileId().getIdentifier();
     }
@@ -96,6 +113,24 @@
     }
 
     /**
+     * @return the account selection activity {@link Intent} of the current
+     *         {@link android.provider.CloudMediaProvider}.
+     */
+    @Nullable
+    Intent getChooseCloudMediaAccountActivityIntent() {
+        return mChooseCloudMediaAccountActivityIntent;
+    }
+
+
+    /**
+     * Update the account selection activity {@link Intent} of the current
+     * {@link android.provider.CloudMediaProvider}.
+     */
+    void setChooseCloudMediaAccountActivityIntent(Intent intent) {
+        mChooseCloudMediaAccountActivityIntent = intent;
+    }
+
+    /**
      * @return a {@link LiveData} that holds the value (once it's fetched) of the account name
      *         of the current {@link android.provider.CloudMediaProvider}.
      */
@@ -139,8 +174,10 @@
     /**
      * Dismiss (hide) the 'Choose App' banner for the current user.
      */
-    @UiThread
+    @MainThread
     void onUserDismissedChooseAppBanner() {
+        ThreadUtils.assertMainThread();
+
         if (Boolean.FALSE.equals(mShowChooseAppBanner.getValue())) {
             Log.d(TAG, "Choose App banner visibility live data value is false on dismiss");
         } else {
@@ -156,8 +193,10 @@
     /**
      * Dismiss (hide) the 'Cloud Media Available' banner for the current user.
      */
-    @UiThread
+    @MainThread
     void onUserDismissedCloudMediaAvailableBanner() {
+        ThreadUtils.assertMainThread();
+
         if (Boolean.FALSE.equals(mShowCloudMediaAvailableBanner.getValue())) {
             Log.d(TAG, "Cloud Media Available banner visibility live data value is false on "
                     + "dismiss");
@@ -174,8 +213,10 @@
     /**
      * Dismiss (hide) the 'Account Updated' banner for the current user.
      */
-    @UiThread
+    @MainThread
     void onUserDismissedAccountUpdatedBanner() {
+        ThreadUtils.assertMainThread();
+
         if (Boolean.FALSE.equals(mShowAccountUpdatedBanner.getValue())) {
             Log.d(TAG, "Account Updated banner visibility live data value is false on dismiss");
         } else {
@@ -191,8 +232,10 @@
     /**
      * Dismiss (hide) the 'Choose Account' banner for the current user.
      */
-    @UiThread
+    @MainThread
     void onUserDismissedChooseAccountBanner() {
+        ThreadUtils.assertMainThread();
+
         if (Boolean.FALSE.equals(mShowChooseAccountBanner.getValue())) {
             Log.d(TAG, "Choose Account banner visibility live data value is false on dismiss");
         } else {
@@ -212,50 +255,38 @@
     }
 
     /**
-     * Resets the banner controller per user.
+     * Resets the banner controller per user and sets the banner data for the current user.
      *
      * Note - Since {@link BannerController#reset()} cannot be called in the Main thread, using
-     * {@link ForegroundThread} here.
+     * {@link DataLoaderThread} here.
      */
-    void maybeResetAllBannerData() {
+    void reset() {
         for (int arrayIndex = 0, numControllers = mBannerControllers.size();
                 arrayIndex < numControllers; arrayIndex++) {
             final BannerController bannerController = mBannerControllers.valueAt(arrayIndex);
-            ForegroundThread.getExecutor().execute(bannerController::reset);
+            DataLoaderThread.getHandler().postDelayed(bannerController::reset, TOKEN, DELAY_MILLIS);
         }
-    }
 
-    /**
-     * Update the banner {@link LiveData} values.
-     *
-     * 1. {@link #hideAllBanners()} in the Main thread to ensure consistency with the media items
-     * displayed for the period when the items and categories have been updated but the
-     * {@link BannerController} construction or {@link BannerController#reset()} is still in
-     * progress.
-     *
-     * 2. Initialise and set the banner data for the current user
-     * {@link #maybeInitialiseAndSetBannersForCurrentUser()}.
-     */
-    @UiThread
-    void maybeUpdateBannerLiveDatas() {
-        // Hide all banners in the Main thread to ensure consistency with the media items
-        hideAllBanners();
-
-        // Initialise and set the banner data for the current user
+        // Set the banner data for the current user
         maybeInitialiseAndSetBannersForCurrentUser();
     }
 
     /**
-     * Hide all banners in the Main thread.
+     * Hide all the banners in the DataLoader thread.
      *
-     * Set all banner {@link LiveData} values to {@code false}.
+     * Since this is always followed by a reset, they need to be done in the same threads (currently
+     * DataLoaderThread thread). For the case when multiple hideAllBanners & reset are triggered
+     * simultaneously, this ensures that they are called sequentially for each such trigger.
+     *
+     * Post all the banner {@link LiveData} values as {@code false}.
      */
-    @UiThread
-    private void hideAllBanners() {
-        mShowChooseAppBanner.setValue(false);
-        mShowCloudMediaAvailableBanner.setValue(false);
-        mShowAccountUpdatedBanner.setValue(false);
-        mShowChooseAccountBanner.setValue(false);
+    void hideAllBanners() {
+        DataLoaderThread.getHandler().postDelayed(() -> {
+            mShowChooseAppBanner.postValue(false);
+            mShowCloudMediaAvailableBanner.postValue(false);
+            mShowAccountUpdatedBanner.postValue(false);
+            mShowChooseAccountBanner.postValue(false);
+        }, TOKEN, DELAY_MILLIS);
     }
 
 
@@ -269,8 +300,9 @@
     }
 
     static class CloudBannerManager extends BannerManager {
-        CloudBannerManager(@NonNull Context context, @NonNull UserIdManager userIdManager) {
-            super(context, userIdManager);
+        CloudBannerManager(@NonNull Context context, @NonNull UserIdManager userIdManager,
+                @NonNull ConfigStore configStore) {
+            super(context, userIdManager, configStore);
         }
 
         /**
@@ -279,14 +311,14 @@
          * 1. Get or create the {@link BannerController} for
          * {@link UserIdManager#getCurrentUserProfileId()} using {@link PerUser#forUser(int)}.
          * Since, the {@link BannerController} construction cannot be done in the Main thread,
-         * using {@link ForegroundThread} here.
+         * using {@link DataLoaderThread} here.
          *
          * 2. Post the updated {@link BannerController} {@link LiveData} values.
          */
         @Override
         void maybeInitialiseAndSetBannersForCurrentUser() {
             final int currentUserProfileId = getCurrentUserProfileId();
-            ForegroundThread.getExecutor().execute(() -> {
+            DataLoaderThread.getHandler().postDelayed(() -> {
                 // Get (iff exists) or create the banner controller for the current user
                 final BannerController bannerController =
                         getBannerControllersPerUser().forUser(currentUserProfileId);
@@ -297,6 +329,8 @@
                         .postValue(bannerController.getCloudMediaProviderLabel());
                 getCloudMediaAccountNameLiveData()
                         .postValue(bannerController.getCloudMediaProviderAccountName());
+                setChooseCloudMediaAccountActivityIntent(
+                        bannerController.getChooseCloudMediaAccountActivityIntent());
                 shouldShowChooseAppBannerLiveData()
                         .postValue(bannerController.shouldShowChooseAppBanner());
                 shouldShowCloudMediaAvailableBannerLiveData()
@@ -305,7 +339,7 @@
                         .postValue(bannerController.shouldShowAccountUpdatedBanner());
                 shouldShowChooseAccountBannerLiveData()
                         .postValue(bannerController.shouldShowChooseAccountBanner());
-            });
+            }, TOKEN, DELAY_MILLIS);
         }
     }
 }
diff --git a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
index a0b0175..ff5e5c0 100644
--- a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
+++ b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
@@ -18,19 +18,45 @@
 
 import static android.content.Intent.ACTION_GET_CONTENT;
 import static android.content.Intent.EXTRA_LOCAL_ONLY;
+import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA;
+import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS;
+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 com.android.providers.media.PickerUriResolver.REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI;
+import static com.android.providers.media.photopicker.DataLoaderThread.TOKEN;
+import static com.android.providers.media.photopicker.PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_CLEAR_AND_UPDATE_LIST;
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_CLEAR_GRID;
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_DEFAULT;
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_LOAD_NEXT_PAGE;
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_REFRESH_ITEMS;
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_VIEW_CREATED;
 
 import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED;
 import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED;
 
 import android.annotation.SuppressLint;
 import android.app.Application;
+import android.content.ContentResolver;
+import android.content.ContentUris;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.database.ContentObserver;
 import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Handler;
+import android.os.Looper;
 import android.provider.MediaStore;
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
@@ -42,23 +68,35 @@
 
 import com.android.internal.logging.InstanceId;
 import com.android.internal.logging.InstanceIdSequence;
+import com.android.modules.utils.BackgroundThread;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.providers.media.ConfigStore;
 import com.android.providers.media.MediaApplication;
+import com.android.providers.media.photopicker.DataLoaderThread;
+import com.android.providers.media.photopicker.NotificationContentObserver;
 import com.android.providers.media.photopicker.data.ItemsProvider;
 import com.android.providers.media.photopicker.data.MuteStatus;
+import com.android.providers.media.photopicker.data.PaginationParameters;
 import com.android.providers.media.photopicker.data.Selection;
 import com.android.providers.media.photopicker.data.UserIdManager;
 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.photopicker.metrics.NonUiEventLogger;
 import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger;
+import com.android.providers.media.photopicker.ui.ItemsAction;
+import com.android.providers.media.photopicker.util.CategoryOrganiserUtils;
 import com.android.providers.media.photopicker.util.MimeFilterUtils;
-import com.android.providers.media.util.ForegroundThread;
+import com.android.providers.media.photopicker.util.ThreadUtils;
 import com.android.providers.media.util.MimeUtils;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * PickerViewModel to store and handle data for PhotoPickerActivity.
@@ -69,38 +107,71 @@
     private static final int RECENT_MINIMUM_COUNT = 12;
 
     private static final int INSTANCE_ID_MAX = 1 << 15;
+    private static final int DELAY_MILLIS = 0;
+
+    // Token for the tasks to load the category items in the data loader thread's queue
+    private final Object mLoadCategoryItemsThreadToken = new Object();
 
     @NonNull
     @SuppressLint("StaticFieldLeak")
     private final Context mAppContext;
 
     private final Selection mSelection;
+
+    private int mPackageUid = -1;
+
     private final MuteStatus mMuteStatus;
+    public boolean mEmptyPageDisplayed = false;
 
     // TODO(b/193857982): We keep these four data sets now, we may need to find a way to reduce the
     //  data set to reduce memories.
     // The list of Items with all photos and videos
-    private MutableLiveData<List<Item>> mItemList;
+    private MutableLiveData<PaginatedItemsResult> mItemsResult;
+    private int mItemsPageSize = -1;
+
     // The list of Items with all photos and videos in category
-    private MutableLiveData<List<Item>> mCategoryItemList;
+    private MutableLiveData<PaginatedItemsResult> mCategoryItemsResult;
+
+    private int mCategoryItemsPageSize = -1;
+
     // The list of categories.
     private MutableLiveData<List<Category>> mCategoryList;
 
+    private MutableLiveData<Boolean> mIsAllPreGrantedMediaLoaded = new MutableLiveData<>(false);
+    private final MutableLiveData<Boolean> mShouldRefreshUiLiveData = new MutableLiveData<>(false);
+    private final ContentObserver mRefreshUiNotificationObserver = new ContentObserver(null) {
+        @Override
+        public void onChange(boolean selfChange) {
+            mShouldRefreshUiLiveData.postValue(true);
+        }
+    };
+
+    private MutableLiveData<Boolean> mIsSyncInProgress = new MutableLiveData<>(false);
+
     private ItemsProvider mItemsProvider;
     private UserIdManager mUserIdManager;
     private BannerManager mBannerManager;
 
     private InstanceId mInstanceId;
     private PhotoPickerUiEventLogger mLogger;
+    private ConfigStore mConfigStore;
 
     private String[] mMimeTypeFilters = null;
     private int mBottomSheetState;
 
     private Category mCurrentCategory;
 
+    // Content resolver for the currently selected user
+    private ContentResolver mContentResolver;
+
     // Note - Must init banner manager on mIsUserSelectForApp / mIsLocalOnly updates
     private boolean mIsUserSelectForApp;
+
+    private boolean mIsManagedSelectionEnabled;
     private boolean mIsLocalOnly;
+    private boolean mIsAllCategoryItemsLoaded = false;
+    private boolean mIsNotificationForUpdateReceived = false;
+    private CancellationSignal mCancellationSignal = new CancellationSignal();
 
     public PickerViewModel(@NonNull Application application) {
         super(application);
@@ -112,9 +183,53 @@
         mInstanceId = new InstanceIdSequence(INSTANCE_ID_MAX).newInstanceId();
         mLogger = new PhotoPickerUiEventLogger();
         mIsUserSelectForApp = false;
+        mIsManagedSelectionEnabled = false;
         mIsLocalOnly = false;
-        // Must init banner manager on mIsUserSelectForApp / mIsLocalOnly updates
-        initBannerManager();
+
+        initConfigStore();
+
+        // When the user opens the PhotoPickerSettingsActivity and changes the cloud provider, it's
+        // possible that system kills PhotoPickerActivity and PickerViewModel while it's in the
+        // background. In these scenarios, content observer will be unregistered and PickerViewModel
+        // will not be able to receive CMP change notifications.
+        initPhotoPickerData();
+        registerRefreshUiNotificationObserver();
+        // Add notification content observer for any notifications received for changes in media.
+        NotificationContentObserver contentObserver = new NotificationContentObserver(null);
+        contentObserver.registerKeysToObserverCallback(
+                Arrays.asList(NotificationContentObserver.MEDIA),
+                (dateTakenMs, albumId) -> {
+                    onNotificationReceived();
+                });
+        contentObserver.register(mAppContext.getContentResolver());
+    }
+
+    @Override
+    protected void onCleared() {
+        unregisterRefreshUiNotificationObserver();
+
+        // Signal ContentProvider to cancel currently running task.
+        mCancellationSignal.cancel();
+
+        clearQueuedTasksInDataLoaderThread();
+    }
+
+    private void onNotificationReceived() {
+        Log.d(TAG, "Notification for media update has been received");
+        mIsNotificationForUpdateReceived = true;
+        if (mEmptyPageDisplayed && mConfigStore.isCloudMediaInPhotoPickerEnabled()) {
+            (new Handler(Looper.getMainLooper())).post(() -> {
+                Log.d(TAG, "Refreshing UI to display new items.");
+                mEmptyPageDisplayed = false;
+                getPaginatedItemsForAction(ACTION_REFRESH_ITEMS,
+                        new PaginationParameters(mItemsPageSize, -1, -1));
+            });
+        }
+    }
+
+    @VisibleForTesting
+    protected void initConfigStore() {
+        mConfigStore = MediaApplication.getConfigStore();
     }
 
     @VisibleForTesting
@@ -127,6 +242,37 @@
         mUserIdManager = userIdManager;
     }
 
+    @VisibleForTesting
+    public void setBannerManager(@NonNull BannerManager bannerManager) {
+        mBannerManager = bannerManager;
+    }
+
+    @VisibleForTesting
+    public void setNotificationForUpdateReceived(boolean notificationForUpdateReceived) {
+        mIsNotificationForUpdateReceived = notificationForUpdateReceived;
+    }
+
+    @VisibleForTesting
+    public void setLogger(@NonNull PhotoPickerUiEventLogger logger) {
+        mLogger = logger;
+    }
+
+    @VisibleForTesting
+    public void setConfigStore(@NonNull ConfigStore configStore) {
+        mConfigStore = configStore;
+    }
+
+    public void setEmptyPageDisplayed(boolean emptyPageDisplayed) {
+        mEmptyPageDisplayed = emptyPageDisplayed;
+    }
+
+    /**
+     * @return the {@link ConfigStore} for this context.
+     */
+    public ConfigStore getConfigStore() {
+        return mConfigStore;
+    }
+
     /**
      * @return {@link UserIdManager} for this context.
      */
@@ -141,7 +287,6 @@
         return mSelection;
     }
 
-
     /**
      * @return {@code mMuteStatus} that tracks the volume mute status of the video preview
      */
@@ -151,16 +296,25 @@
 
     /**
      * @return {@code mIsUserSelectForApp} if the picker is currently being used
-     *         for the {@link MediaStore#ACTION_USER_SELECT_IMAGES_FOR_APP} action.
+     * for the {@link MediaStore#ACTION_USER_SELECT_IMAGES_FOR_APP} action.
      */
     public boolean isUserSelectForApp() {
         return mIsUserSelectForApp;
     }
 
     /**
+     * @return {@code mIsManagedSelectionEnabled} if the picker is currently being used
+     * for the {@link MediaStore#ACTION_USER_SELECT_IMAGES_FOR_APP} action and flag
+     * pickerChoiceManagedSelection is enabled..
+     */
+    public boolean isManagedSelectionEnabled() {
+        return mIsManagedSelectionEnabled;
+    }
+
+    /**
      * @return a {@link LiveData} that holds the value (once it's fetched) of the
-     *         {@link android.content.ContentProvider#mAuthority authority} of the current
-     *         {@link android.provider.CloudMediaProvider}.
+     * {@link android.content.ContentProvider#mAuthority authority} of the current
+     * {@link android.provider.CloudMediaProvider}.
      */
     @NonNull
     public LiveData<String> getCloudMediaProviderAuthorityLiveData() {
@@ -169,7 +323,7 @@
 
     /**
      * @return a {@link LiveData} that holds the value (once it's fetched) of the label
-     *         of the current {@link android.provider.CloudMediaProvider}.
+     * of the current {@link android.provider.CloudMediaProvider}.
      */
     @NonNull
     public LiveData<String> getCloudMediaProviderAppTitleLiveData() {
@@ -178,7 +332,7 @@
 
     /**
      * @return a {@link LiveData} that holds the value (once it's fetched) of the account name
-     *         of the current {@link android.provider.CloudMediaProvider}.
+     * of the current {@link android.provider.CloudMediaProvider}.
      */
     @NonNull
     public LiveData<String> getCloudMediaAccountNameLiveData() {
@@ -186,137 +340,434 @@
     }
 
     /**
-     * Reset PickerViewModel.
-     * @param switchToPersonalProfile is true then set personal profile as current profile.
+     * @return the account selection activity {@link Intent} of the current
+     *         {@link android.provider.CloudMediaProvider}.
+     */
+    @Nullable
+    public Intent getChooseCloudMediaAccountActivityIntent() {
+        return mBannerManager.getChooseCloudMediaAccountActivityIntent();
+    }
+
+    /**
+     * Reset to personal profile mode.
      */
     @UiThread
-    public void reset(boolean switchToPersonalProfile) {
-        // 1. Clear Selected items
+    public void resetToPersonalProfile() {
+        mUserIdManager.setPersonalAsCurrentUserProfile();
+        onSwitchedProfile();
+    }
+
+    /**
+     * Reset the content observer & all the content on profile switched.
+     */
+    @UiThread
+    public void onSwitchedProfile() {
+        resetRefreshUiNotificationObserver();
+        resetAllContentInCurrentProfile();
+    }
+
+    /**
+     * Reset all the content (items, categories & banners) in the current profile.
+     */
+    @UiThread
+    public void resetAllContentInCurrentProfile() {
+        Log.d(TAG, "Reset all content in current profile");
+
+        // Post 'should refresh UI live data' value as false to avoid unnecessary repetitive resets
+        mShouldRefreshUiLiveData.postValue(false);
+
+        clearQueuedTasksInDataLoaderThread();
+
+        initPhotoPickerData();
+
+        // Clear the existing content - selection, photos grid, albums grid, banners
         mSelection.clearSelectedItems();
-        // 2. Change profile to personal user
-        if (switchToPersonalProfile) {
-            mUserIdManager.setPersonalAsCurrentUserProfile();
+
+        if (mItemsResult != null) {
+            DataLoaderThread.getHandler().postDelayed(() ->
+                    mItemsResult.postValue(new PaginatedItemsResult(List.of(Item.EMPTY_VIEW),
+                            ACTION_CLEAR_GRID)), TOKEN, DELAY_MILLIS);
         }
-        // 3. Update Item and Category lists
-        updateItems();
+
+        if (mCategoryList != null) {
+            DataLoaderThread.getHandler().postDelayed(() ->
+                    mCategoryList.postValue(List.of(Category.EMPTY_VIEW)), TOKEN, DELAY_MILLIS);
+        }
+
+        mBannerManager.hideAllBanners();
+
+        // Update items, categories & banners
+        getPaginatedItemsForAction(ACTION_CLEAR_AND_UPDATE_LIST, null);
         updateCategories();
-        // 4. Update Banners
-        // Note - Banners should always be updated after the items & categories to ensure a
-        // consistent UI.
-        mBannerManager.maybeResetAllBannerData();
-        mBannerManager.maybeUpdateBannerLiveDatas();
+        mBannerManager.reset();
     }
 
     /**
-     * Update items, categories & banners on profile switched by the user.
+     * Loads list of pre granted items for the current package and userID.
      */
-    @UiThread
-    public void onUserSwitchedProfile() {
-        updateItems();
-        updateCategories();
-        // Note - Banners should always be updated after the items & categories to ensure a
-        // consistent UI.
-        mBannerManager.maybeUpdateBannerLiveDatas();
+    public void initialisePreGrantsIfNecessary(Selection selection, Bundle intentExtras,
+            String[] mimeTypeFilters) {
+        if (isManagedSelectionEnabled() && selection.getPreGrantedItems() == null) {
+            DataLoaderThread.getHandler().postDelayed(() -> {
+                Set<String> preGrantedItems = mItemsProvider.fetchReadGrantedItemsUrisForPackage(
+                                intentExtras.getInt(Intent.EXTRA_UID), mimeTypeFilters)
+                        .stream().map((Uri uri) -> String.valueOf(ContentUris.parseId(uri)))
+                        .collect(Collectors.toSet());
+                selection.setPreGrantedItemSet(preGrantedItems);
+                logPickerChoiceInitGrantsCount(preGrantedItems.size(), intentExtras);
+            }, TOKEN, DELAY_MILLIS);
+        }
     }
 
     /**
-     * @return the list of Items with all photos and videos {@link #mItemList} on the device.
+     * Performs required modification to the item list and returns the live data for it.
      */
-    public LiveData<List<Item>> getItems() {
-        if (mItemList == null) {
-            updateItems();
+    public LiveData<PaginatedItemsResult> getPaginatedItemsForAction(
+            @NonNull @ItemsAction.Type int action,
+            @Nullable PaginationParameters paginationParameters) {
+        Objects.requireNonNull(action);
+        switch (action) {
+            case ACTION_VIEW_CREATED: {
+                // Use this when a fresh view is created. If the current list is empty, it will
+                // load the first page and return the list, else it will return previously
+                // existing values.
+                mItemsPageSize = paginationParameters.getPageSize();
+                if (mItemsResult == null) {
+                    updatePaginatedItems(paginationParameters, true, action);
+                }
+                break;
+            }
+            case ACTION_LOAD_NEXT_PAGE: {
+                // Loads next page of the list, using the previously loaded list.
+                // If the current list is empty then it will not perform any actions.
+                if (mItemsResult != null && mItemsResult.getValue() != null) {
+                    List<Item> currentItemList = mItemsResult.getValue().getItems();
+                    // If the list is already empty that would mean that the first page was not
+                    // loaded since there were no items to be loaded.
+                    if (currentItemList != null && !currentItemList.isEmpty()) {
+                        // get the last item of the existing list.
+                        Item item = currentItemList.get(currentItemList.size() - 1);
+                        updatePaginatedItems(
+                                new PaginationParameters(mItemsPageSize, item.getDateTaken(),
+                                        item.getRowId()), false, action);
+                    }
+                }
+                break;
+            }
+            case ACTION_CLEAR_AND_UPDATE_LIST: {
+                // Clears the existing list and loads the list with for mItemsPageSize
+                // number of items. This will be equal to page size for pagination if cloud
+                // picker feature flag is enabled, else it will be -1 implying that the complete
+                // list should be loaded.
+                updatePaginatedItems(new PaginationParameters(mItemsPageSize,
+                        /*dateBeforeMs*/ Long.MIN_VALUE, /*rowId*/ -1), /* isReset */ true, action);
+                break;
+            }
+            case ACTION_REFRESH_ITEMS: {
+                if (mIsNotificationForUpdateReceived
+                        && mItemsResult != null
+                        && mItemsResult.getValue() != null) {
+                    updatePaginatedItems(paginationParameters, true, action);
+                    mIsNotificationForUpdateReceived = false;
+                }
+                break;
+            }
+            default:
+                Log.w(TAG, "Invalid action passed to fetch items");
         }
-        return mItemList;
+        return mItemsResult;
     }
 
-    private List<Item> loadItems(Category category, UserId userId) {
+    /**
+     * Update the item List {@link #mItemsResult}. Loads the page requested represented by the
+     * pagination parameters and replaces/appends it to the existing list of items based on the
+     * reset value.
+     */
+    private void updatePaginatedItems(PaginationParameters pagingParameters, boolean isReset,
+            @ItemsAction.Type int action) {
+        if (mItemsResult == null) {
+            mItemsResult = new MutableLiveData<>();
+        }
+        loadItemsAsync(pagingParameters, /* isReset */ isReset, action);
+    }
+
+    /**
+     * Loads required items and sets it to the {@link PickerViewModel#mItemsResult} while
+     * considering the isReset value.
+     *
+     * @param pagingParameters parameters representing the items that needs to be loaded next.
+     * @param isReset          If this is true, clear the pre-existing list and add the newly loaded
+     *                         items.
+     * @param action           This is used while posting the result of the operation.
+     */
+    private void loadItemsAsync(@NonNull PaginationParameters pagingParameters, boolean isReset,
+            @ItemsAction.Type int action) {
+        final UserId userId = mUserIdManager.getCurrentUserProfileId();
+
+        DataLoaderThread.getHandler().postDelayed(() -> {
+            // Load the items as per the pagination parameters passed as params to this method.
+            List<Item> newPageItemList = loadItems(Category.DEFAULT, userId, pagingParameters);
+
+            // Based on if it is a reset case or not, create an updated list.
+            // If it is a reset case, assign an empty list else use the contents of the pre-existing
+            // list. Then add the newly loaded items.
+            List<Item> updatedList =
+                    mItemsResult.getValue() == null || isReset ? new ArrayList<>()
+                            : mItemsResult.getValue().getItems();
+            updatedList.addAll(newPageItemList);
+            Log.d(TAG, "Next page for photos items have been loaded.");
+            if (newPageItemList.isEmpty()) {
+                Log.d(TAG, "All photos items have been loaded.");
+            }
+
+            // post the result with the action.
+            mItemsResult.postValue(new PaginatedItemsResult(updatedList, action));
+            mIsSyncInProgress.postValue(false);
+        }, TOKEN, DELAY_MILLIS);
+    }
+
+    private List<Item> loadItems(Category category, UserId userId,
+            PaginationParameters pagingParameters) {
         final List<Item> items = new ArrayList<>();
+        String cloudProviderAuthority = null; // NULL if fetched items have NO cloud only media item
 
-        try (Cursor cursor = fetchItems(category, userId)) {
+        try (Cursor cursor = fetchItems(category, userId, pagingParameters)) {
             if (cursor == null || cursor.getCount() == 0) {
                 Log.d(TAG, "Didn't receive any items for " + category
                         + ", either cursor is null or cursor count is zero");
                 return items;
             }
 
+            Set<String> preGrantedItems = new HashSet<>(0);
+            Set<String> deSelectedPreGrantedItems = new HashSet<>(0);
+            if (isManagedSelectionEnabled() && mSelection.getPreGrantedItems() != null) {
+                preGrantedItems = mSelection.getPreGrantedItems();
+                deSelectedPreGrantedItems = new HashSet<>(
+                        mSelection.getPreGrantedItemIdsToBeRevoked());
+            }
             while (cursor.moveToNext()) {
                 // TODO(b/188394433): Return userId in the cursor so that we do not need to pass it
                 //  here again.
-                items.add(Item.fromCursor(cursor, userId));
+                final Item item = Item.fromCursor(cursor, userId);
+                if (preGrantedItems.contains(item.getId())) {
+                    item.setPreGranted();
+                    if (!deSelectedPreGrantedItems.contains(item.getId())) {
+                        mSelection.addSelectedItem(item);
+                    }
+                }
+                String authority = item.getContentUri().getAuthority();
+
+                if (!LOCAL_PICKER_PROVIDER_AUTHORITY.equals(authority)) {
+                    cloudProviderAuthority = authority;
+                }
+                items.add(item);
+            }
+
+            Log.d(TAG, "Loaded " + items.size() + " items in " + category + " for user "
+                    + userId.toString());
+            return items;
+        } finally {
+            int count = items.size();
+            if (category.isDefault()) {
+                mLogger.logLoadedMainGridMediaItems(cloudProviderAuthority, mInstanceId, count);
+            } else {
+                mLogger.logLoadedAlbumGridMediaItems(cloudProviderAuthority, mInstanceId, count);
             }
         }
-
-        Log.d(TAG, "Loaded " + items.size() + " items in " + category + " for user "
-                + userId.toString());
-        return items;
-    }
-
-    private Cursor fetchItems(Category category, UserId userId) {
-        if (shouldShowOnlyLocalFeatures()) {
-            return mItemsProvider.getLocalItems(category, /* limit */ -1, mMimeTypeFilters, userId);
-        } else {
-            return mItemsProvider.getAllItems(category, /* limit */ -1, mMimeTypeFilters, userId);
-        }
-    }
-
-    private void loadItemsAsync() {
-        final UserId userId = mUserIdManager.getCurrentUserProfileId();
-        ForegroundThread.getExecutor().execute(() -> {
-                    mItemList.postValue(loadItems(Category.DEFAULT, userId));
-        });
     }
 
     /**
-     * Update the item List {@link #mItemList}
+     * @return true when all pre-granted items data has been loaded for this session.
      */
-    public void updateItems() {
-        if (mItemList == null) {
-            mItemList = new MutableLiveData<>();
-        }
-        loadItemsAsync();
+    @NonNull
+    public MutableLiveData<Boolean> getIsAllPreGrantedMediaLoaded() {
+        return mIsAllPreGrantedMediaLoaded;
     }
 
     /**
-     * Get the list of all photos and videos with the specific {@code category} on the device.
+     * Gets item data for Uris which have not yet been loaded to the UI. This is important when the
+     * preview fragment is created and hence should be called only before creation.
      *
-     * In our use case, we only keep the list of current category {@link #mCurrentCategory} in
-     * {@link #mCategoryItemList}. If the {@code category} and {@link #mCurrentCategory} are
-     * different, we will create the new LiveData to {@link #mCategoryItemList}.
-     *
-     * @param category the category we want to be queried
-     * @return the list of all photos and videos with the specific {@code category}
-     *         {@link #mCategoryItemList}
+     * <p>This is used during pagination. All the items are not loaded at once and hence the
+     * preGranted item which is on a page that is yet to be loaded will would not be part of the
+     * mSelected list and hence will not show up in the preview fragment. This method fixes this
+     * issue by selectively loading those items and adding them to the selection list.</p>
      */
-    public LiveData<List<Item>> getCategoryItems(@NonNull Category category) {
-        if (mCategoryItemList == null || !TextUtils.equals(mCurrentCategory.getId(),
-                category.getId())) {
-            mCategoryItemList = new MutableLiveData<>();
-            mCurrentCategory = category;
+    public void getRemainingPreGrantedItems() {
+        if (!isManagedSelectionEnabled() || mSelection.getPreGrantedItems() == null) return;
+
+        List<String> idsForItemsToBeFetched =
+                new ArrayList<>(mSelection.getPreGrantedItems());
+        idsForItemsToBeFetched.removeAll(mSelection.getSelectedItemsIds());
+        idsForItemsToBeFetched.removeAll(mSelection.getPreGrantedItemIdsToBeRevoked());
+
+        if (!idsForItemsToBeFetched.isEmpty()) {
+            final UserId userId = mUserIdManager.getCurrentUserProfileId();
+            DataLoaderThread.getHandler().postDelayed(() -> {
+                loadItemsWithLocalIdSelection(Category.DEFAULT, userId,
+                        idsForItemsToBeFetched.stream().map(Integer::valueOf).collect(
+                                Collectors.toList()));
+                // If new data has loaded then post value representing a successful operation.
+                mIsAllPreGrantedMediaLoaded.postValue(true);
+                Log.d(TAG, "Fetched " + idsForItemsToBeFetched.size()
+                        + " items for required preGranted ids");
+            }, TOKEN, 0);
         }
-        updateCategoryItems();
-        return mCategoryItemList;
     }
 
-    private void loadCategoryItemsAsync() {
-        final UserId userId = mUserIdManager.getCurrentUserProfileId();
-        ForegroundThread.getExecutor().execute(() -> {
-            mCategoryItemList.postValue(loadItems(mCurrentCategory, userId));
-        });
+    private void loadItemsWithLocalIdSelection(Category category, UserId userId,
+            List<Integer> selectionArg) {
+        try (Cursor cursor = mItemsProvider.getLocalItemsForSelection(category, selectionArg,
+                mMimeTypeFilters, userId, mCancellationSignal)) {
+            if (cursor == null || cursor.getCount() == 0) {
+                Log.d(TAG, "Didn't receive any items for pre granted URIs" + category
+                        + ", either cursor is null or cursor count is zero");
+                return;
+            }
+
+            Set<String> selectedIdSet = new HashSet<>(mSelection.getSelectedItemsIds());
+            // Add all loaded items to selection after marking them as pre granted.
+            while (cursor.moveToNext()) {
+                final Item item = Item.fromCursor(cursor, userId);
+                item.setPreGranted();
+                if (!selectedIdSet.contains(item.getId())) {
+                    mSelection.addSelectedItem(item);
+                }
+            }
+            Log.d(TAG, "Pre granted items have been loaded.");
+        }
+    }
+
+    private Cursor fetchItems(Category category, UserId userId,
+            PaginationParameters pagingParameters) {
+        try {
+            if (shouldShowOnlyLocalFeatures()) {
+                return mItemsProvider.getLocalItems(category, pagingParameters,
+                        mMimeTypeFilters, userId, mCancellationSignal);
+            } else {
+                return mItemsProvider.getAllItems(category, pagingParameters,
+                        mMimeTypeFilters, userId, mCancellationSignal);
+            }
+        } catch (RuntimeException ignored) {
+            // Catch OperationCanceledException.
+            Log.e(TAG, "Failed to fetch items due to a runtime exception", ignored);
+            return null;
+        }
     }
 
     /**
-     * Update the item List with the {@link #mCurrentCategory} {@link #mCategoryItemList}
+     * Modifies and returns the live data for category items.
+     */
+    public LiveData<PaginatedItemsResult> getPaginatedCategoryItemsForAction(
+            @NonNull Category category,
+            @ItemsAction.Type int action, @Nullable PaginationParameters paginationParameters) {
+        switch (action) {
+            case ACTION_VIEW_CREATED: {
+                // This call is made only for loading the first page of album media,
+                // so the existing data loader thread tasks for updating the category items should
+                // be cleared and the category and category item list should be refreshed each time.
+                DataLoaderThread.getHandler().removeCallbacksAndMessages(
+                        mLoadCategoryItemsThreadToken);
+                mCategoryItemsResult = new MutableLiveData<>();
+                mCurrentCategory = category;
+                assert paginationParameters != null;
+                mCategoryItemsPageSize = paginationParameters.getPageSize();
+                updateCategoryItems(paginationParameters, action);
+                break;
+            }
+            case ACTION_LOAD_NEXT_PAGE: {
+                // Loads next page of the list, using the previously loaded list.
+                // If the current list is empty then it will not perform any actions.
+                if (mCategoryItemsResult == null || mCategoryItemsResult.getValue() == null
+                        || !TextUtils.equals(mCurrentCategory.getId(),
+                        category.getId())) {
+                    break;
+                }
+                List<Item> currentItemList = mCategoryItemsResult.getValue().getItems();
+                // If the categoryItemList does not contain any items, it would mean that the first
+                // page was empty.
+                if (currentItemList != null && !currentItemList.isEmpty()) {
+                    Item item = currentItemList.get(currentItemList.size() - 1);
+                    PaginationParameters pagingParams = new PaginationParameters(
+                            mCategoryItemsPageSize,
+                            item.getDateTaken(),
+                            item.getRowId());
+                    updateCategoryItems(pagingParams, action);
+                }
+                break;
+            }
+            default:
+                Log.w(TAG, "Invalid action passed to fetch category items");
+        }
+        return mCategoryItemsResult;
+    }
+
+    /**
+     * Update the item List with the {@link #mCurrentCategory} {@link #mCategoryItemsResult}
      *
      * @throws IllegalStateException category and category items is not initiated before calling
-     *     this method
+     *                               this method
      */
     @VisibleForTesting
-    public void updateCategoryItems() {
-        if (mCategoryItemList == null || mCurrentCategory == null) {
-            throw new IllegalStateException("mCurrentCategory and mCategoryItemList are not"
+    public void updateCategoryItems(PaginationParameters pagingParameters,
+            @ItemsAction.Type int action) {
+        if (mCategoryItemsResult == null || mCurrentCategory == null) {
+            throw new IllegalStateException("mCurrentCategory and mCategoryItemsResult are not"
                     + " initiated. Please call getCategoryItems before calling this method");
         }
-        loadCategoryItemsAsync();
+        loadCategoryItemsAsync(pagingParameters, action != ACTION_LOAD_NEXT_PAGE, action);
+    }
+
+    /**
+     * Loads required category items and sets it to the {@link PickerViewModel#mCategoryItemsResult}
+     * while considering the isReset value.
+     *
+     * @param pagingParameters parameters representing the items that needs to be loaded next.
+     * @param isReset          If this is true, clear the pre-existing list and add the newly loaded
+     *                         items.
+     * @param action           This is used while posting the result of the operation.
+     */
+    private void loadCategoryItemsAsync(PaginationParameters pagingParameters, boolean isReset,
+            @ItemsAction.Type int action) {
+        final UserId userId = mUserIdManager.getCurrentUserProfileId();
+        final Category category = mCurrentCategory;
+
+        DataLoaderThread.getHandler().postDelayed(() -> {
+            if (action == ACTION_LOAD_NEXT_PAGE && mIsAllCategoryItemsLoaded) {
+                return;
+            }
+            // Load the items as per the pagination parameters passed as params to this method.
+            List<Item> newPageItemList = loadItems(category, userId, pagingParameters);
+
+            // Based on if it is a reset case or not, create an updated list.
+            // If it is a reset case, assign an empty list else use the contents of the pre-existing
+            // list. Then add the newly loaded items.
+            List<Item> updatedList = mCategoryItemsResult.getValue() == null || isReset
+                    ? new ArrayList<>() : mCategoryItemsResult.getValue().getItems();
+            updatedList.addAll(newPageItemList);
+
+            if (isReset) {
+                mIsAllCategoryItemsLoaded = false;
+            }
+            Log.d(TAG, "Next page for category items have been loaded. Category: "
+                    + category + " " + updatedList.size());
+            if (newPageItemList.isEmpty()) {
+                mIsAllCategoryItemsLoaded = true;
+                Log.d(TAG, "All items have been loaded for category: " + mCurrentCategory);
+            }
+            if (Objects.equals(category, mCurrentCategory)) {
+                mCategoryItemsResult.postValue(new PaginatedItemsResult(updatedList, action));
+            }
+        }, mLoadCategoryItemsThreadToken, DELAY_MILLIS);
+    }
+
+    /**
+     * Used only for testing, clears out any data in item list and category item list.
+     */
+    @VisibleForTesting
+    public void clearItemsAndCategoryItemsList() {
+        mItemsResult = null;
+        mCategoryItemsResult = null;
     }
 
     /**
@@ -331,6 +782,7 @@
 
     private List<Category> loadCategories(UserId userId) {
         final List<Category> categoryList = new ArrayList<>();
+        String cloudProviderAuthority = null; // NULL if fetched albums have NO cloud album
         try (Cursor cursor = fetchCategories(userId)) {
             if (cursor == null || cursor.getCount() == 0) {
                 Log.d(TAG, "Didn't receive any categories, either cursor is null or"
@@ -340,28 +792,44 @@
 
             while (cursor.moveToNext()) {
                 final Category category = Category.fromCursor(cursor, userId);
+                String authority = category.getAuthority();
+
+                if (!LOCAL_PICKER_PROVIDER_AUTHORITY.equals(authority)) {
+                    cloudProviderAuthority = authority;
+                }
                 categoryList.add(category);
             }
 
             Log.d(TAG,
                     "Loaded " + categoryList.size() + " categories for user " + userId.toString());
+            CategoryOrganiserUtils.getReorganisedCategoryList(categoryList);
+            return categoryList;
+        } finally {
+            mLogger.logLoadedAlbums(cloudProviderAuthority, mInstanceId, categoryList.size());
         }
-        return categoryList;
     }
 
     private Cursor fetchCategories(UserId userId) {
-        if (shouldShowOnlyLocalFeatures()) {
-            return mItemsProvider.getLocalCategories(mMimeTypeFilters, userId);
-        } else {
-            return mItemsProvider.getAllCategories(mMimeTypeFilters, userId);
+        try {
+            if (shouldShowOnlyLocalFeatures()) {
+                return mItemsProvider
+                        .getLocalCategories(mMimeTypeFilters, userId, mCancellationSignal);
+            } else {
+                return mItemsProvider
+                        .getAllCategories(mMimeTypeFilters, userId, mCancellationSignal);
+            }
+        } catch (RuntimeException ignored) {
+            // Catch OperationCanceledException.
+            Log.e(TAG, "Failed to fetch categories due to a runtime exception", ignored);
+            return null;
         }
     }
 
     private void loadCategoriesAsync() {
         final UserId userId = mUserIdManager.getCurrentUserProfileId();
-        ForegroundThread.getExecutor().execute(() -> {
+        DataLoaderThread.getHandler().postDelayed(() -> {
             mCategoryList.postValue(loadCategories(userId));
-        });
+        }, TOKEN, DELAY_MILLIS);
     }
 
     /**
@@ -405,6 +873,8 @@
 
         mIsUserSelectForApp =
                 MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(intent.getAction());
+        mIsManagedSelectionEnabled = mIsUserSelectForApp
+                && getConfigStore().isPickerChoiceManagedSelectionEnabled();
         if (!SdkLevel.isAtLeastU() && mIsUserSelectForApp) {
             throw new IllegalArgumentException("ACTION_USER_SELECT_IMAGES_FOR_APP is not enabled "
                     + " for this OS version");
@@ -414,20 +884,25 @@
         // in the extras.
         if (mIsUserSelectForApp
                 && (intent.getExtras() == null
-                        || !intent.getExtras()
-                                .containsKey(Intent.EXTRA_UID))) {
+                || !intent.getExtras()
+                .containsKey(Intent.EXTRA_UID))) {
             throw new IllegalArgumentException(
                     "EXTRA_UID is required for" + " ACTION_USER_SELECT_IMAGES_FOR_APP");
         }
 
+        if (mIsUserSelectForApp) {
+            mPackageUid = intent.getExtras().getInt(Intent.EXTRA_UID);
+        }
         // Must init banner manager on mIsUserSelectForApp / mIsLocalOnly updates
-        initBannerManager();
+        if (mBannerManager == null) {
+            initBannerManager();
+        }
     }
 
     private void initBannerManager() {
         mBannerManager = shouldShowOnlyLocalFeatures()
-                ? new BannerManager(mAppContext, mUserIdManager)
-                : new BannerManager.CloudBannerManager(mAppContext, mUserIdManager);
+                ? new BannerManager(mAppContext, mUserIdManager, mConfigStore)
+                : new BannerManager.CloudBannerManager(mAppContext, mUserIdManager, mConfigStore);
     }
 
     /**
@@ -484,8 +959,6 @@
         maybeLogPickerOpenedWithCloudProvider();
     }
 
-    // TODO(b/245745412): Fix log params (uid & package name)
-    // TODO(b/245745424): Solve for active cloud provider without a logged in account
     private void maybeLogPickerOpenedWithCloudProvider() {
         if (shouldShowOnlyLocalFeatures()) {
             return;
@@ -500,8 +973,8 @@
                         + ", log=" + (providerAuthority != null));
 
                 if (providerAuthority != null) {
-                    mLogger.logPickerOpenWithActiveCloudProvider(
-                            mInstanceId, /* cloudProviderUid */ -1, providerAuthority);
+                    BackgroundThread.getExecutor().execute(() ->
+                            logPickerOpenedWithCloudProvider(providerAuthority));
                 }
                 // We only need to get the value once.
                 cloudMediaProviderAuthorityLiveData.removeObserver(this);
@@ -509,6 +982,27 @@
         });
     }
 
+    private void logPickerOpenedWithCloudProvider(@NonNull String providerAuthority) {
+        String cloudProviderPackage = providerAuthority;
+        int cloudProviderUid = -1;
+
+        try {
+            final PackageManager packageManager =
+                    UserId.CURRENT_USER.getPackageManager(mAppContext);
+            final ProviderInfo providerInfo = packageManager.resolveContentProvider(
+                    providerAuthority, /* flags= */ 0);
+
+            cloudProviderPackage = providerInfo.applicationInfo.packageName;
+            cloudProviderUid = providerInfo.applicationInfo.uid;
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.d(TAG, "Logging the ui event 'picker open with an active cloud provider' with its "
+                    + "authority in place of the package name and a default uid.", e);
+        }
+
+        mLogger.logPickerOpenWithActiveCloudProvider(
+                mInstanceId, cloudProviderUid, cloudProviderPackage);
+    }
+
     /**
      * Log metrics to notify that the user has clicked Browse to open DocumentsUi
      */
@@ -540,6 +1034,264 @@
         }
     }
 
+    /**
+     * Log metrics to notify that the user has clicked the mute / unmute button in a video preview
+     */
+    public void logVideoPreviewMuteButtonClick() {
+        mLogger.logVideoPreviewMuteButtonClick(mInstanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has clicked the 'view selected' button
+     *
+     * @param selectedItemCount the number of items selected for preview all
+     */
+    public void logPreviewAllSelected(int selectedItemCount) {
+        mLogger.logPreviewAllSelected(mInstanceId, selectedItemCount);
+    }
+
+    /**
+     * Log metrics to notify that the 'switch profile' button is visible & enabled
+     */
+    public void logProfileSwitchButtonEnabled() {
+        mLogger.logProfileSwitchButtonEnabled(mInstanceId);
+    }
+
+    /**
+     * Log metrics to notify that the 'switch profile' button is visible but disabled
+     */
+    public void logProfileSwitchButtonDisabled() {
+        mLogger.logProfileSwitchButtonDisabled(mInstanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has clicked the 'switch profile' button
+     */
+    public void logProfileSwitchButtonClick() {
+        mLogger.logProfileSwitchButtonClick(mInstanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has cancelled the current session by swiping down
+     */
+    public void logSwipeDownExit() {
+        mLogger.logSwipeDownExit(mInstanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has made a back gesture
+     * @param backStackEntryCount the number of fragment entries currently in the back stack
+     */
+    public void logBackGestureWithStackCount(int backStackEntryCount) {
+        mLogger.logBackGestureWithStackCount(mInstanceId, backStackEntryCount);
+    }
+
+    /**
+     * Log metrics to notify that the user has clicked the action bar home button
+     * @param backStackEntryCount the number of fragment entries currently in the back stack
+     */
+    public void logActionBarHomeButtonClick(int backStackEntryCount) {
+        mLogger.logActionBarHomeButtonClick(mInstanceId, backStackEntryCount);
+    }
+
+    /**
+     * Log metrics to notify that the user has expanded from half screen to full
+     */
+    public void logExpandToFullScreen() {
+        mLogger.logExpandToFullScreen(mInstanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has opened the photo picker menu
+     */
+    public void logMenuOpened() {
+        mLogger.logMenuOpened(mInstanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has switched to the photos tab
+     */
+    public void logSwitchToPhotosTab() {
+        mLogger.logSwitchToPhotosTab(mInstanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has switched to the albums tab
+     */
+    public void logSwitchToAlbumsTab() {
+        mLogger.logSwitchToAlbumsTab(mInstanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user has opened an album
+     *
+     * @param category the opened album metadata
+     * @param position the position of the album in the recycler view
+     */
+    public void logAlbumOpened(@NonNull Category category, int position) {
+        final String albumId = category.getId();
+        if (ALBUM_ID_FAVORITES.equals(albumId)) {
+            mLogger.logFavoritesAlbumOpened(mInstanceId);
+        } else if (ALBUM_ID_CAMERA.equals(albumId)) {
+            mLogger.logCameraAlbumOpened(mInstanceId);
+        } else if (ALBUM_ID_DOWNLOADS.equals(albumId)) {
+            mLogger.logDownloadsAlbumOpened(mInstanceId);
+        } else if (ALBUM_ID_SCREENSHOTS.equals(albumId)) {
+            mLogger.logScreenshotsAlbumOpened(mInstanceId);
+        } else if (ALBUM_ID_VIDEOS.equals(albumId)) {
+            mLogger.logVideosAlbumOpened(mInstanceId);
+        } else if (!category.isLocal()) {
+            mLogger.logCloudAlbumOpened(mInstanceId, position);
+        }
+    }
+
+    /**
+     * Log metrics to notify that the user has selected a media item
+     *
+     * @param item     the selected item metadata
+     * @param category the category of the item selected, {@link Category#DEFAULT} for main grid
+     * @param position the position of the album in the recycler view
+     */
+    public void logMediaItemSelected(@NonNull Item item, @NonNull Category category, int position) {
+        if (category.isDefault()) {
+            mLogger.logSelectedMainGridItem(mInstanceId, position);
+        } else {
+            mLogger.logSelectedAlbumItem(mInstanceId, position);
+        }
+
+        if (!item.isLocal()) {
+            mLogger.logSelectedCloudOnlyItem(mInstanceId, position);
+        }
+    }
+
+    /**
+     * Log metrics to notify that the user has previewed a media item
+     *
+     * @param item     the previewed item metadata
+     * @param category the category of the item previewed, {@link Category#DEFAULT} for main grid
+     * @param position the position of the album in the recycler view
+     */
+    public void logMediaItemPreviewed(
+            @NonNull Item item, @NonNull Category category, int position) {
+        if (category.isDefault()) {
+            mLogger.logPreviewedMainGridItem(
+                    item.getSpecialFormat(), item.getMimeType(), mInstanceId, position);
+        }
+    }
+
+    /**
+     * Log metrics to notify create surface controller triggered
+     * @param authority  the authority of the provider
+     */
+    public void logCreateSurfaceControllerStart(String authority) {
+        mLogger.logPickerCreateSurfaceControllerStart(mInstanceId, authority);
+    }
+
+    /**
+     * Log metrics to notify create surface controller ended
+     * @param authority  the authority of the provider
+     */
+    public void logCreateSurfaceControllerEnd(String authority) {
+        mLogger.logPickerCreateSurfaceControllerEnd(mInstanceId, authority);
+    }
+
+    /**
+     * Log metrics to notify that the selected media preloading started
+     * @param count the number of items to preload
+     */
+    public void logPreloadingStarted(int count) {
+        mLogger.logPreloadingStarted(mInstanceId, count);
+    }
+
+    /**
+     * Log metrics to notify that the selected media preloading finished
+     */
+    public void logPreloadingFinished() {
+        mLogger.logPreloadingFinished(mInstanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user cancelled the selected media preloading
+     * @param count the number of items pending to preload
+     */
+    public void logPreloadingCancelled(int count) {
+        mLogger.logPreloadingCancelled(mInstanceId, count);
+    }
+
+    /**
+     * Log metrics to notify that the selected media preloading failed for some items
+     * @param count the number of items pending / failed to preload
+     */
+    public void logPreloadingFailed(int count) {
+        mLogger.logPreloadingFailed(mInstanceId, count);
+    }
+
+    /**
+     * Logs metrics for count of grants initialised for a package.
+     */
+    public void logPickerChoiceInitGrantsCount(int numberOfGrants, Bundle intentExtras) {
+        NonUiEventLogger.logPickerChoiceInitGrantsCount(mInstanceId, android.os.Process.myUid(),
+                getPackageNameForUid(intentExtras), numberOfGrants);
+
+    }
+
+    /**
+     * Logs metrics for count of grants added for a package.
+     */
+    public void logPickerChoiceAddedGrantsCount(int numberOfGrants, Bundle intentExtras) {
+        NonUiEventLogger.logPickerChoiceGrantsAdditionCount(mInstanceId, android.os.Process.myUid(),
+                getPackageNameForUid(intentExtras), numberOfGrants);
+    }
+
+    /**
+     * Logs metrics for count of grants removed for a package.
+     */
+    public void logPickerChoiceRevokedGrantsCount(int numberOfGrants, Bundle intentExtras) {
+        NonUiEventLogger.logPickerChoiceGrantsRemovedCount(mInstanceId, android.os.Process.myUid(),
+                getPackageNameForUid(intentExtras), numberOfGrants);
+    }
+
+    /**
+     * Log metrics to notify that the banner is added to display in the recycler view grids
+     * @param bannerName the name of the banner added,
+     *                   refer {@link com.android.providers.media.photopicker.ui.TabAdapter.Banner}
+     */
+    public void logBannerAdded(@NonNull String bannerName) {
+        mLogger.logBannerAdded(mInstanceId, bannerName);
+    }
+
+    /**
+     * Log metrics to notify that the banner is dismissed by the user
+     */
+    public void logBannerDismissed() {
+        mLogger.logBannerDismissed(mInstanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user clicked the banner action button
+     */
+    public void logBannerActionButtonClicked() {
+        mLogger.logBannerActionButtonClicked(mInstanceId);
+    }
+
+    /**
+     * Log metrics to notify that the user clicked on the remaining part of the banner
+     */
+    public void logBannerClicked() {
+        mLogger.logBannerClicked(mInstanceId);
+    }
+
+    @NonNull
+    private String getPackageNameForUid(Bundle extras) {
+        final int uid = extras.getInt(Intent.EXTRA_UID);
+        final PackageManager pm = mAppContext.getPackageManager();
+        String[] packageNames = pm.getPackagesForUid(uid);
+        if (packageNames.length != 0) {
+            return packageNames[0];
+        }
+        return new String();
+    }
+
     public InstanceId getInstanceId() {
         return mInstanceId;
     }
@@ -560,23 +1312,18 @@
      *
      * Show only the local features in the following cases -
      * 1. Photo Picker is launched by the {@link MediaStore#ACTION_USER_SELECT_IMAGES_FOR_APP}
-     *    action for the permission flow.
+     * action for the permission flow.
      * 2. Photo Picker is launched with the {@link Intent#EXTRA_LOCAL_ONLY} as {@code true} in the
-     *    {@link Intent#ACTION_GET_CONTENT} or {@link MediaStore#ACTION_PICK_IMAGES} action.
+     * {@link Intent#ACTION_GET_CONTENT} or {@link MediaStore#ACTION_PICK_IMAGES} action.
      * 3. Cloud Media in Photo picker is disabled, i.e.,
-     *    {@link ConfigStore#isCloudMediaInPhotoPickerEnabled()} is {@code false}.
+     * {@link ConfigStore#isCloudMediaInPhotoPickerEnabled()} is {@code false}.
      *
      * @return {@code true} iff either {@link #isUserSelectForApp()} or {@link #isLocalOnly()} is
      * {@code true}, OR if {@link ConfigStore#isCloudMediaInPhotoPickerEnabled()} is {@code false}.
      */
     public boolean shouldShowOnlyLocalFeatures() {
         return isUserSelectForApp() || isLocalOnly()
-                || !getConfigStore().isCloudMediaInPhotoPickerEnabled();
-    }
-
-    @VisibleForTesting
-    protected ConfigStore getConfigStore() {
-        return MediaApplication.getConfigStore();
+                || !mConfigStore.isCloudMediaInPhotoPickerEnabled();
     }
 
     /**
@@ -614,32 +1361,137 @@
     /**
      * Dismiss (hide) the 'Choose App' banner for the current user.
      */
-    @UiThread
+    @MainThread
     public void onUserDismissedChooseAppBanner() {
+        ThreadUtils.assertMainThread();
         mBannerManager.onUserDismissedChooseAppBanner();
     }
 
     /**
      * Dismiss (hide) the 'Cloud Media Available' banner for the current user.
      */
-    @UiThread
+    @MainThread
     public void onUserDismissedCloudMediaAvailableBanner() {
+        ThreadUtils.assertMainThread();
         mBannerManager.onUserDismissedCloudMediaAvailableBanner();
     }
 
     /**
      * Dismiss (hide) the 'Account Updated' banner for the current user.
      */
-    @UiThread
+    @MainThread
     public void onUserDismissedAccountUpdatedBanner() {
+        ThreadUtils.assertMainThread();
         mBannerManager.onUserDismissedAccountUpdatedBanner();
     }
 
     /**
      * Dismiss (hide) the 'Choose Account' banner for the current user.
      */
-    @UiThread
+    @MainThread
     public void onUserDismissedChooseAccountBanner() {
+        ThreadUtils.assertMainThread();
         mBannerManager.onUserDismissedChooseAccountBanner();
     }
+
+    /**
+     * @return a {@link LiveData} that posts Should Refresh Picker UI as {@code true} when notified.
+     */
+    @NonNull
+    public LiveData<Boolean> shouldRefreshUiLiveData() {
+        return mShouldRefreshUiLiveData;
+    }
+
+    private void registerRefreshUiNotificationObserver() {
+        mContentResolver = getContentResolverForSelectedUser();
+        mContentResolver.registerContentObserver(REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI,
+                /* notifyForDescendants */ false, mRefreshUiNotificationObserver);
+    }
+
+    private void unregisterRefreshUiNotificationObserver() {
+        if (mContentResolver != null) {
+            mContentResolver.unregisterContentObserver(mRefreshUiNotificationObserver);
+            mContentResolver = null;
+        }
+    }
+
+    private void resetRefreshUiNotificationObserver() {
+        unregisterRefreshUiNotificationObserver();
+        registerRefreshUiNotificationObserver();
+    }
+
+    private ContentResolver getContentResolverForSelectedUser() {
+        final UserId selectedUserId = mUserIdManager.getCurrentUserProfileId();
+        if (selectedUserId == null) {
+            Log.d(TAG, "Selected user id is NULL; returning the default content resolver.");
+            return mAppContext.getContentResolver();
+        }
+        try {
+            return selectedUserId.getContentResolver(mAppContext);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.d(TAG, "Failed to get the content resolver for the selected user id "
+                    + selectedUserId + "; returning the default content resolver.", e);
+            return mAppContext.getContentResolver();
+        }
+    }
+
+    public LiveData<Boolean> isSyncInProgress() {
+        return mIsSyncInProgress;
+    }
+
+    /**
+     * Class used to store the result of the item modification operations.
+     */
+    public class PaginatedItemsResult {
+        private List<Item> mItems = new ArrayList<>();
+
+        private int mAction = ACTION_DEFAULT;
+
+        public PaginatedItemsResult(@NonNull List<Item> itemList,
+                @ItemsAction.Type int action) {
+            mItems = itemList;
+            mAction = action;
+        }
+
+        public List<Item> getItems() {
+            return mItems;
+        }
+
+        @ItemsAction.Type
+        public int getAction() {
+            return mAction;
+        }
+    }
+
+    /**
+     * This will inform the media Provider process that the UI is preparing to load data for the
+     * main photos grid.
+     */
+    public void initPhotoPickerData() {
+        initPhotoPickerData(Category.DEFAULT);
+    }
+
+    /**
+     * This will inform the media Provider process that the UI is preparing to load data for main
+     * photos grid or album contents grid.
+     */
+    public void initPhotoPickerData(@NonNull Category category) {
+        if (mConfigStore.isCloudMediaInPhotoPickerEnabled()) {
+            UserId userId = mUserIdManager.getCurrentUserProfileId();
+            DataLoaderThread.getHandler().postDelayed(() -> {
+                if (category == Category.DEFAULT) {
+                    mIsSyncInProgress.postValue(true);
+                }
+                mItemsProvider.initPhotoPickerData(category.getId(),
+                        category.getAuthority(),
+                        shouldShowOnlyLocalFeatures(),
+                        userId);
+            }, TOKEN, DELAY_MILLIS);
+        }
+    }
+
+    private void clearQueuedTasksInDataLoaderThread() {
+        DataLoaderThread.getHandler().removeCallbacksAndMessages(TOKEN);
+        DataLoaderThread.getHandler().removeCallbacksAndMessages(mLoadCategoryItemsThreadToken);
+    }
 }
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index 55a4a81..d70d9ef 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -48,6 +48,7 @@
 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 
+import static com.android.providers.media.util.FileUtils.canonicalize;
 import static com.android.providers.media.util.Metrics.translateReason;
 
 import static java.util.Objects.requireNonNull;
@@ -121,6 +122,7 @@
 import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -210,7 +212,7 @@
      * overlap and confuse each other.
      */
     @GuardedBy("mDirectoryLocks")
-    private final Map<Path, DirectoryLock> mDirectoryLocks = new ArrayMap<>();
+    private final Map<String, DirectoryLock> mDirectoryLocks = new ArrayMap<>();
 
     /**
      * Set of MIME types that should be considered to be DRM, meaning we need to
@@ -242,7 +244,7 @@
     public void scanDirectory(@NonNull File file, @ScanReason int reason) {
         requireNonNull(file);
         try {
-            file = file.getCanonicalFile();
+            file = canonicalize(file);
         } catch (IOException e) {
             Log.e(TAG, "Couldn't canonicalize directory to scan" + file, e);
             return;
@@ -262,7 +264,7 @@
     public Uri scanFile(@NonNull File file, @ScanReason int reason) {
         requireNonNull(file);
         try {
-            file = file.getCanonicalFile();
+            file = canonicalize(file);
         } catch (IOException e) {
             Log.e(TAG, "Couldn't canonicalize file to scan" + file, e);
             return null;
@@ -306,14 +308,14 @@
     public void onDirectoryDirty(@NonNull File dir) {
         requireNonNull(dir);
         try {
-            dir = dir.getCanonicalFile();
+            dir = canonicalize(dir);
         } catch (IOException e) {
             Log.e(TAG, "Couldn't canonicalize directory" + dir, e);
             return;
         }
 
         synchronized (mPendingCleanDirectories) {
-            mPendingCleanDirectories.remove(dir.getPath());
+            mPendingCleanDirectories.remove(dir.getPath().toLowerCase(Locale.ROOT));
             FileUtils.setDirectoryDirty(dir, /* isDirty */ true);
         }
     }
@@ -349,7 +351,7 @@
 
         private final long mStartGeneration;
         private final boolean mSingleFile;
-        private final Set<Path> mAcquiredDirectoryLocks = new ArraySet<>();
+        private final Set<String> mAcquiredDirectoryLocks = new ArraySet<>();
         private final ArrayList<ContentProviderOperation> mPending = new ArrayList<>();
         private final LongArray mScannedIds = new LongArray();
         private final LongArray mUnknownIds = new LongArray();
@@ -448,7 +450,7 @@
                     mHiddenDirCount++;
                 }
                 if (mSingleFile) {
-                    acquireDirectoryLock(mRoot.getParentFile().toPath());
+                    acquireDirectoryLock(mRoot.getParentFile().toPath().toString());
                 }
                 try {
                     Files.walkFileTree(mRoot.toPath(), this);
@@ -458,7 +460,7 @@
                     throw new IllegalStateException(e);
                 } finally {
                     if (mSingleFile) {
-                        releaseDirectoryLock(mRoot.getParentFile().toPath());
+                        releaseDirectoryLock(mRoot.getParentFile().toPath().toString());
                     }
                     Trace.endSection();
                 }
@@ -639,19 +641,20 @@
          * thread exclusive access to ensure that parallel scans don't overlap
          * and confuse each other.
          */
-        private void acquireDirectoryLock(@NonNull Path dir) {
+        private void acquireDirectoryLock(@NonNull String dirPath) {
             Trace.beginSection("Scanner.acquireDirectoryLock");
             DirectoryLock lock;
+            final String dirLower = dirPath.toLowerCase(Locale.ROOT);
             synchronized (mDirectoryLocks) {
-                lock = mDirectoryLocks.get(dir);
+                lock = mDirectoryLocks.get(dirLower);
                 if (lock == null) {
                     lock = new DirectoryLock();
-                    mDirectoryLocks.put(dir, lock);
+                    mDirectoryLocks.put(dirLower, lock);
                 }
                 lock.count++;
             }
             lock.lock.lock();
-            mAcquiredDirectoryLocks.add(dir);
+            mAcquiredDirectoryLocks.add(dirLower);
             Trace.endSection();
         }
 
@@ -660,20 +663,21 @@
          * other waiting parallel scans to proceed, and cleaning up data
          * structures if no other threads are waiting.
          */
-        private void releaseDirectoryLock(@NonNull Path dir) {
+        private void releaseDirectoryLock(@NonNull String dirPath) {
             Trace.beginSection("Scanner.releaseDirectoryLock");
             DirectoryLock lock;
+            final String dirLower = dirPath.toLowerCase(Locale.ROOT);
             synchronized (mDirectoryLocks) {
-                lock = mDirectoryLocks.get(dir);
+                lock = mDirectoryLocks.get(dirLower);
                 if (lock == null) {
                     throw new IllegalStateException();
                 }
                 if (--lock.count == 0) {
-                    mDirectoryLocks.remove(dir);
+                    mDirectoryLocks.remove(dirLower);
                 }
             }
             lock.lock.unlock();
-            mAcquiredDirectoryLocks.remove(dir);
+            mAcquiredDirectoryLocks.remove(dirLower);
             Trace.endSection();
         }
 
@@ -682,8 +686,8 @@
             // Release any locks we're still holding, typically when we
             // encountered an exception; we snapshot the original list so we're
             // not confused as it's mutated by release operations
-            for (Path dir : new ArraySet<>(mAcquiredDirectoryLocks)) {
-                releaseDirectoryLock(dir);
+            for (String dirPath : new ArraySet<>(mAcquiredDirectoryLocks)) {
+                releaseDirectoryLock(dirPath);
             }
 
             mClient.close();
@@ -709,11 +713,11 @@
                     // This removes additional dirty state check for subdirectories of nomedia
                     // directory.
                     mIsDirectoryTreeDirty = true;
-                    mPendingCleanDirectories.add(dir.toFile().getPath());
+                    mPendingCleanDirectories.add(dir.toFile().getPath().toLowerCase(Locale.ROOT));
                 } else {
                     Log.d(TAG, "Skipping preVisitDirectory " + dir.toFile());
                     if (mExcludeDirs.size() <= MAX_EXCLUDE_DIRS) {
-                        mExcludeDirs.add(dir.toFile().getPath());
+                        mExcludeDirs.add(dir.toFile().getPath().toLowerCase(Locale.ROOT));
                         return FileVisitResult.SKIP_SUBTREE;
                     } else {
                         Log.w(TAG, "ExcludeDir size exceeded, not skipping preVisitDirectory "
@@ -724,7 +728,7 @@
 
             // Acquire lock on this directory to ensure parallel scans don't
             // overlap and confuse each other
-            acquireDirectoryLock(dir);
+            acquireDirectoryLock(dir.toString());
 
             if (FileUtils.isDirectoryHidden(dir.toFile())) {
                 mHiddenDirCount++;
@@ -920,11 +924,12 @@
 
             // Now that we're finished scanning this directory, release lock to
             // allow other parallel scans to proceed
-            releaseDirectoryLock(dir);
+            releaseDirectoryLock(dir.toString());
 
             if (mIsDirectoryTreeDirty) {
                 synchronized (mPendingCleanDirectories) {
-                    if (mPendingCleanDirectories.remove(dir.toFile().getPath())) {
+                    if (mPendingCleanDirectories.remove(
+                            dir.toFile().getPath().toLowerCase(Locale.ROOT))) {
                         // If |dir| is still clean, then persist
                         FileUtils.setDirectoryDirty(dir.toFile(), false /* isDirty */);
                         mIsDirectoryTreeDirty = false;
@@ -1746,6 +1751,6 @@
     }
 
     static void logTroubleScanning(@NonNull File file, @NonNull Exception e) {
-        if (LOGW) Log.w(TAG, "Trouble scanning " + file + ": " + e);
+        if (LOGW) Log.w(TAG, "Trouble scanning " + file, e);
     }
 }
diff --git a/src/com/android/providers/media/scan/NullMediaScanner.java b/src/com/android/providers/media/scan/NullMediaScanner.java
deleted file mode 100644
index fc475c7..0000000
--- a/src/com/android/providers/media/scan/NullMediaScanner.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.providers.media.scan;
-
-import android.content.Context;
-import android.net.Uri;
-import android.provider.MediaStore;
-import android.util.Log;
-
-import com.android.providers.media.MediaVolume;
-
-import java.io.File;
-
-/**
- * Null scanner that ignores all scanning requests. Can be useful when running
- * as {@link MediaStore#AUTHORITY_LEGACY} or during unit tests.
- */
-public class NullMediaScanner implements MediaScanner {
-    private static final String TAG = "NullMediaScanner";
-
-    private final Context mContext;
-
-    public NullMediaScanner(Context context) {
-        mContext = context;
-    }
-
-    @Override
-    public Context getContext() {
-        return mContext;
-    }
-
-    @Override
-    public void scanDirectory(File file, int reason) {
-        Log.w(TAG, "Ignoring scan request for " + file);
-    }
-
-    @Override
-    public Uri scanFile(File file, int reason) {
-        Log.w(TAG, "Ignoring scan request for " + file);
-        return null;
-    }
-
-    @Override
-    public void onDetachVolume(MediaVolume volume) {
-        // Ignored
-    }
-
-    @Override
-    public void onIdleScanStopped() {
-        // Ignored
-    }
-
-    @Override
-    public void onDirectoryDirty(File file) {
-        // Ignored
-    }
-}
diff --git a/src/com/android/providers/media/stableuris/dao/BackupIdRow.java b/src/com/android/providers/media/stableuris/dao/BackupIdRow.java
index 27060d5..0bd0390 100644
--- a/src/com/android/providers/media/stableuris/dao/BackupIdRow.java
+++ b/src/com/android/providers/media/stableuris/dao/BackupIdRow.java
@@ -20,13 +20,8 @@
 
 import com.android.providers.media.util.StringUtils;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
 import java.io.Serializable;
-import java.util.Base64;
 import java.util.Objects;
 
 /**
@@ -248,24 +243,35 @@
 
     /**
      * Serializes the given {@link BackupIdRow} object to a string
+     * Format is
+     * "is_dirty::_id::is_fav::is_pending::is_trashed::media_type::user_id::owner_id::date_expires"
      */
     public static String serialize(BackupIdRow backupIdRow) throws IOException {
-        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
-        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
-        objectOutputStream.writeObject(backupIdRow);
-        objectOutputStream.close();
-        return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
+        return String.format("%s::%s::%s::%s::%s::%s::%s::%s::%s",
+                backupIdRow.getIsDirty() ? "1" : "0", backupIdRow.getId(),
+                backupIdRow.getIsFavorite(), backupIdRow.getIsPending(), backupIdRow.getIsTrashed(),
+                backupIdRow.getMediaType(), backupIdRow.getUserId(),
+                backupIdRow.getOwnerPackageId(), backupIdRow.getDateExpires());
     }
 
     /**
      * Deserializes the given string to {@link BackupIdRow} object
      */
     public static BackupIdRow deserialize(String s) throws IOException, ClassNotFoundException {
-        byte[] bytes = Base64.getDecoder().decode(s);
-        ObjectInputStream objectInputStream = new ObjectInputStream(
-                new ByteArrayInputStream(bytes));
-        BackupIdRow backupIdRow = (BackupIdRow) objectInputStream.readObject();
-        objectInputStream.close();
-        return backupIdRow;
+        if (s == null || s.isEmpty()) {
+            return null;
+        }
+
+        String[] fields = s.split("::");
+        BackupIdRow.Builder builder = BackupIdRow.newBuilder(Long.parseLong(fields[1]));
+        builder.setIsDirty(Objects.equals(fields[0], "1"));
+        builder.setIsFavorite(Integer.parseInt(fields[2]));
+        builder.setIsPending(Integer.parseInt(fields[3]));
+        builder.setIsTrashed(Integer.parseInt(fields[4]));
+        builder.setMediaType(Integer.parseInt(fields[5]));
+        builder.setUserId(Integer.parseInt(fields[6]));
+        builder.setOwnerPackagedId(Integer.parseInt(fields[7]));
+        builder.setDateExpires(fields[8]);
+        return builder.build();
     }
 }
diff --git a/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceService.java b/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceService.java
index 493333f..d7adaa4 100644
--- a/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceService.java
+++ b/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceService.java
@@ -72,7 +72,7 @@
             final JobInfo job = new JobInfo.Builder(IDLE_JOB_ID,
                     new ComponentName(context,
                             StableUriIdleMaintenanceService.class))
-                    .setPeriodic(TimeUnit.DAYS.toMillis(7))
+                    .setPeriodic(TimeUnit.DAYS.toMillis(3))
                     .setRequiresCharging(true)
                     .setRequiresDeviceIdle(true)
                     .build();
diff --git a/src/com/android/providers/media/util/FileUtils.java b/src/com/android/providers/media/util/FileUtils.java
index 00504f4..1f927f3 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -1740,7 +1740,7 @@
             // Returns true If .nomedia file is empty or content doesn't match |dir|
             // Returns false otherwise
             return !expectedPath.isPresent()
-                    || !expectedPath.get().equals(dir.getPath());
+                    || !expectedPath.get().equalsIgnoreCase(dir.getPath());
         } catch (IOException e) {
             Log.w(TAG, "Failed to read directory dirty" + dir);
             return true;
@@ -1849,4 +1849,16 @@
         Objects.requireNonNull(path);
         return new File(path).getCanonicalPath();
     }
+
+    /**
+     * A wrapper for {@link File#getCanonicalFile()} that catches {@link IOException}-s and
+     * re-throws them as {@link RuntimeException}-s.
+     *
+     * @see File#getCanonicalFile()
+     */
+    @NonNull
+    public static File canonicalize(@NonNull File file) throws IOException {
+        Objects.requireNonNull(file);
+        return file.getCanonicalFile();
+    }
 }
diff --git a/src/com/android/providers/media/util/SpecialFormatDetector.java b/src/com/android/providers/media/util/SpecialFormatDetector.java
index a8569d7..23a5a43 100644
--- a/src/com/android/providers/media/util/SpecialFormatDetector.java
+++ b/src/com/android/providers/media/util/SpecialFormatDetector.java
@@ -110,11 +110,10 @@
         bitmapOptions.inJustDecodeBounds = true;
         BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOptions);
 
-        if (bitmapOptions.outMimeType.equalsIgnoreCase("image/gif")) {
+        if ("image/gif".equalsIgnoreCase(bitmapOptions.outMimeType)) {
             return FileColumns._SPECIAL_FORMAT_GIF;
         }
-        if (bitmapOptions.outMimeType.equalsIgnoreCase("image/webp") &&
-                isAnimatedWebp(file)) {
+        if ("image/webp".equalsIgnoreCase(bitmapOptions.outMimeType) && isAnimatedWebp(file)) {
             return FileColumns._SPECIAL_FORMAT_ANIMATED_WEBP;
         }
         return FileColumns._SPECIAL_FORMAT_NONE;
diff --git a/src/com/android/providers/media/util/UserCache.java b/src/com/android/providers/media/util/UserCache.java
index fa46779..ed3e91f 100644
--- a/src/com/android/providers/media/util/UserCache.java
+++ b/src/com/android/providers/media/util/UserCache.java
@@ -18,6 +18,7 @@
 
 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;
@@ -111,8 +112,7 @@
     private boolean isUnlockedAndMediaSharedWithParent(@NonNull UserHandle profile) {
         Context userContext = getContextForUser(profile);
         UserManager userManager = userContext.getSystemService(UserManager.class);
-        return (SdkLevel.isAtLeastT() ?
-                userManager.isUserUnlocked() : userManager.isUserUnlocked(profile))
+        return userManager.isUserUnlockingOrUnlocked(profile)
                 && userManager.isMediaSharedWithParent();
     }
 
diff --git a/tests/Android.bp b/tests/Android.bp
index 725b722..7fb7a7c 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -191,9 +191,12 @@
         "glide-gifdecoder-prebuilt",
         "glide-disklrucache-prebuilt",
         "glide-annotation-and-compiler-prebuilt",
+        "glide-integration-recyclerview-prebuilt",
         "androidx.fragment_fragment",
         "androidx.vectordrawable_vectordrawable-animated",
         "androidx.exifinterface_exifinterface",
+        "androidx.work_work-runtime",
+        "androidx.work_work-testing",
         "exoplayer-mediaprovider-ui",
         "SettingsLibProfileSelector",
         "SettingsLibSelectorWithWidgetPreference",
@@ -211,12 +214,13 @@
     },
 
     data: [
-        ":MediaProviderTestAppWithStoragePerms",
-        ":MediaProviderTestAppWithMediaPerms",
-        ":MediaProviderTestAppWithoutPerms",
+        ":LegacyMediaProviderTestApp",
         ":MediaProviderTestAppForPermissionActivity",
         ":MediaProviderTestAppForPermissionActivity33",
-        ":LegacyMediaProviderTestApp",
+        ":MediaProviderTestAppWithMediaPerms",
+        ":MediaProviderTestAppWithStoragePerms",
+        ":MediaProviderTestAppWithoutPerms",
+        ":MediaProviderTestAppWithUserSelectedPerms",
     ],
     per_testcase_directory: true,
 
@@ -227,3 +231,10 @@
     name: "mediaprovider-testutils",
     srcs: ["utils/**/*.java"],
 }
+
+filegroup {
+    name: "mediaprovider-library",
+    srcs: [
+        "src/com/android/providers/media/library/RunOnlyOnPostsubmit.java",
+    ],
+}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index b03be2c..7132276 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -55,10 +55,33 @@
             </intent-filter>
         </activity>
 
+        <!-- Intent Action "android.intent.action.MAIN"
+
+             This intent action is used to start the activity as a main entry point, does not expect
+             to receive data.
+
+             {@link androidx.test.core.app.ActivityScenario#launchActivityForResult(Class)} launches
+             the activity with the intent action {@link android.content.Intent#ACTION_MAIN}.
+        -->
+        <activity android:name="com.android.providers.media.photopicker.espresso.PhotoPickerAccessibilityDisabledTestActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+            </intent-filter>
+        </activity>
+
         <provider android:name="com.android.providers.media.photopicker.LocalProvider"
             android:authorities="com.android.providers.media.photopicker.tests.local"
             android:exported="false" />
 
+        <provider android:name="com.android.providers.media.cloudproviders.FlakyCloudProvider"
+                  android:authorities="com.android.providers.media.photopicker.tests.cloud_flaky"
+                  android:permission="com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS"
+                  android:exported="true">
+          <intent-filter>
+            <action android:name="android.content.action.CLOUD_MEDIA_PROVIDER" />
+          </intent-filter>
+        </provider>
+
         <provider android:name="com.android.providers.media.cloudproviders.CloudProviderPrimary"
                   android:authorities="com.android.providers.media.photopicker.tests.cloud_primary"
                   android:permission="com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS"
@@ -90,6 +113,17 @@
                   android:authorities="com.android.providers.media.photopicker.tests.cloud_no_intent_filter"
                   android:exported="true">
         </provider>
+
+        <service
+            android:name=
+                "com.android.providers.media.stableuris.job.StableUriIdleMaintenanceService"
+            android:exported="true"
+            android:permission="android.permission.BIND_JOB_SERVICE" />
+
+        <service
+            android:name="com.android.providers.media.IdleService"
+            android:exported="true"
+            android:permission="android.permission.BIND_JOB_SERVICE" />
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/client/Android.bp b/tests/client/Android.bp
index 1b3959f..771e05e 100644
--- a/tests/client/Android.bp
+++ b/tests/client/Android.bp
@@ -16,6 +16,7 @@
     srcs: [
         "src/**/*.java",
         ":mediaprovider-testutils",
+        ":mediaprovider-library",
     ],
 
     libs: [
diff --git a/tests/client/src/com/android/providers/media/client/ClientPlaylistTest.java b/tests/client/src/com/android/providers/media/client/ClientPlaylistTest.java
index e9b9cbf..dc86aae 100644
--- a/tests/client/src/com/android/providers/media/client/ClientPlaylistTest.java
+++ b/tests/client/src/com/android/providers/media/client/ClientPlaylistTest.java
@@ -41,6 +41,8 @@
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
+
 import org.junit.After;
 import org.junit.Assume;
 import org.junit.Before;
@@ -63,6 +65,7 @@
  * external client app. Exercises all supported playlist formats.
  */
 @RunWith(Parameterized.class)
+@RunOnlyOnPostsubmit
 public class ClientPlaylistTest {
     private static final String TAG = "ClientPlaylistTest";
 
diff --git a/tests/client/src/com/android/providers/media/client/PerformanceTest.java b/tests/client/src/com/android/providers/media/client/PerformanceTest.java
index b792ee2..f7f5c27 100644
--- a/tests/client/src/com/android/providers/media/client/PerformanceTest.java
+++ b/tests/client/src/com/android/providers/media/client/PerformanceTest.java
@@ -36,6 +36,7 @@
 import androidx.test.runner.AndroidJUnit4;
 import androidx.test.uiautomator.UiDevice;
 
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
 import com.android.providers.media.tests.utils.Timer;
 
 import org.junit.Test;
@@ -62,6 +63,7 @@
  */
 @RunWith(AndroidJUnit4.class)
 @LargeTest
+@RunOnlyOnPostsubmit
 public class PerformanceTest {
     private static final String TAG = "PerformanceTest";
 
diff --git a/tests/client/src/com/android/providers/media/client/PlaylistPerformanceTest.java b/tests/client/src/com/android/providers/media/client/PlaylistPerformanceTest.java
index e761814..c773978 100644
--- a/tests/client/src/com/android/providers/media/client/PlaylistPerformanceTest.java
+++ b/tests/client/src/com/android/providers/media/client/PlaylistPerformanceTest.java
@@ -37,6 +37,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
 import com.android.providers.media.tests.utils.Timer;
 
 import org.junit.After;
@@ -49,6 +50,7 @@
 import java.io.OutputStream;
 
 @RunWith(AndroidJUnit4.class)
+@RunOnlyOnPostsubmit
 public class PlaylistPerformanceTest {
     private static final Uri AUDIO_URI = MediaStore.Audio.Media
             .getContentUri(VOLUME_EXTERNAL_PRIMARY);
diff --git a/tests/client/src/com/android/providers/media/client/PublicVolumePlaylistTest.java b/tests/client/src/com/android/providers/media/client/PublicVolumePlaylistTest.java
index af17d50..6f0d33d 100644
--- a/tests/client/src/com/android/providers/media/client/PublicVolumePlaylistTest.java
+++ b/tests/client/src/com/android/providers/media/client/PublicVolumePlaylistTest.java
@@ -40,6 +40,8 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
+
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Ignore;
@@ -49,6 +51,7 @@
 import java.io.OutputStream;
 
 @RunWith(AndroidJUnit4.class)
+@RunOnlyOnPostsubmit
 public class PublicVolumePlaylistTest {
     @BeforeClass
     public static void setUp() throws Exception {
diff --git a/tests/client/src/com/android/providers/media/client/PublicVolumeTest.java b/tests/client/src/com/android/providers/media/client/PublicVolumeTest.java
index e5181d5..da80f41 100644
--- a/tests/client/src/com/android/providers/media/client/PublicVolumeTest.java
+++ b/tests/client/src/com/android/providers/media/client/PublicVolumeTest.java
@@ -36,6 +36,8 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
+
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Ignore;
@@ -45,6 +47,7 @@
 import java.io.OutputStream;
 
 @RunWith(AndroidJUnit4.class)
+@RunOnlyOnPostsubmit
 public class PublicVolumeTest {
     @BeforeClass
     public static void setUp() throws Exception {
diff --git a/tests/src/com/android/providers/media/ConfigStoreTest.java b/tests/src/com/android/providers/media/ConfigStoreTest.java
new file mode 100644
index 0000000..10728b8
--- /dev/null
+++ b/tests/src/com/android/providers/media/ConfigStoreTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Verifies ConfigStore default values.
+ */
+@RunWith(AndroidJUnit4.class)
+public class ConfigStoreTest {
+    ConfigStore mConfigStore = new ConfigStore() {
+        @NonNull
+        @Override
+        public List<String> getTranscodeCompatManifest() {
+            return null;
+        }
+
+        @NonNull
+        @Override
+        public List<String> getTranscodeCompatStale() {
+            return null;
+        }
+
+        @Override
+        public void addOnChangeListener(@NonNull Executor executor,
+                @NonNull Runnable listener) {
+        }
+    };
+
+    @Test
+    public void test_defaultValueConfigStore_allCorrect() {
+        assertTrue(mConfigStore.getAllowedCloudProviderPackages().isEmpty());
+        assertNull(mConfigStore.getDefaultCloudProviderPackage());
+        assertEquals(60000, mConfigStore.getTranscodeMaxDurationMs());
+        assertTrue(mConfigStore.isCloudMediaInPhotoPickerEnabled());
+        assertFalse(mConfigStore.isGetContentTakeOverEnabled());
+        assertTrue(mConfigStore.isPickerChoiceManagedSelectionEnabled());
+        assertFalse(mConfigStore.isStableUrisForExternalVolumeEnabled());
+        assertFalse(mConfigStore.isStableUrisForInternalVolumeEnabled());
+        assertTrue(mConfigStore.isTranscodeEnabled());
+        assertTrue(mConfigStore.isUserSelectForAppEnabled());
+        assertTrue(mConfigStore.shouldEnforceCloudProviderAllowlist());
+        assertTrue(mConfigStore.shouldPickerPreloadForGetContent());
+        assertTrue(mConfigStore.shouldPickerPreloadForPickImages());
+        assertFalse(mConfigStore.shouldPickerRespectPreloadArgumentForPickImages());
+        assertFalse(mConfigStore.shouldTranscodeDefault());
+    }
+}
diff --git a/tests/src/com/android/providers/media/DatabaseBackupAndRecoveryTest.java b/tests/src/com/android/providers/media/DatabaseBackupAndRecoveryTest.java
new file mode 100644
index 0000000..6b45993
--- /dev/null
+++ b/tests/src/com/android/providers/media/DatabaseBackupAndRecoveryTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media;
+
+import static com.android.providers.media.DatabaseHelper.INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY;
+import static com.android.providers.media.DatabaseHelper.INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX;
+import static com.android.providers.media.DatabaseHelper.INTERNAL_DB_SESSION_ID_XATTR_KEY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+@RunWith(AndroidJUnit4.class)
+public class DatabaseBackupAndRecoveryTest {
+
+    @Test
+    public void testXattrOperations() {
+        final Context context = ApplicationProvider.getApplicationContext();
+        final String path = context.getFilesDir().getPath();
+        final Integer value = 1000000;
+        final String sessionId = UUID.randomUUID().toString();
+        DatabaseBackupAndRecovery.setXattr(path, INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY,
+                String.valueOf(value));
+        DatabaseBackupAndRecovery.setXattr(path, INTERNAL_DB_SESSION_ID_XATTR_KEY, sessionId);
+
+        assertTrue(DatabaseBackupAndRecovery.listXattr(path).containsAll(Arrays.asList(
+                INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY, INTERNAL_DB_SESSION_ID_XATTR_KEY)));
+        Optional<Integer> actualIntegerValue = DatabaseBackupAndRecovery.getXattrOfIntegerValue(
+                path,
+                INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY);
+        assertTrue(actualIntegerValue.isPresent());
+        assertThat(actualIntegerValue.get()).isEqualTo(value);
+        Optional<String> actualStringValue = DatabaseBackupAndRecovery.getXattr(path,
+                INTERNAL_DB_SESSION_ID_XATTR_KEY);
+        assertTrue(actualStringValue.isPresent());
+
+        DatabaseBackupAndRecovery.removeXattr(path, INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY);
+        DatabaseBackupAndRecovery.removeXattr(path, INTERNAL_DB_SESSION_ID_XATTR_KEY);
+    }
+
+    @Test
+    public void testGetInvalidUsersList() {
+        List<String> xattrData = Arrays.asList(
+                INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX + "0",
+                INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX + "10",
+                INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX + "11",
+                INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX + "12",
+                INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX + "13");
+
+        assertThat(DatabaseBackupAndRecovery.getInvalidUsersList(xattrData, /* validUserIds */
+                Arrays.asList("0", "13"))).containsExactly("10", "11", "12");
+    }
+}
diff --git a/tests/src/com/android/providers/media/IdleServiceTest.java b/tests/src/com/android/providers/media/IdleServiceTest.java
index 36d8d35..bd0e7bd 100644
--- a/tests/src/com/android/providers/media/IdleServiceTest.java
+++ b/tests/src/com/android/providers/media/IdleServiceTest.java
@@ -29,9 +29,15 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import android.Manifest;
+import android.app.Instrumentation;
+import android.app.job.JobScheduler;
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentUris;
@@ -42,14 +48,21 @@
 import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.os.Environment;
+import android.os.NewUserRequest;
 import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
 import android.provider.MediaStore;
 import android.text.format.DateUtils;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
 import com.android.providers.media.scan.MediaScannerTest;
 import com.android.providers.media.util.FileUtils;
 
@@ -65,11 +78,15 @@
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Locale;
+import java.util.Set;
 
 @RunWith(AndroidJUnit4.class)
 public class IdleServiceTest {
     private static final String TAG = MediaProviderTest.TAG;
+    private static final int IDLE_JOB_ID = -200;
 
     private File mDir;
 
@@ -81,6 +98,8 @@
                         android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
                         android.Manifest.permission.READ_DEVICE_CONFIG,
                         Manifest.permission.INTERACT_ACROSS_USERS,
+                        Manifest.permission.MANAGE_USERS,
+                        Manifest.permission.WRITE_MEDIA_STORAGE,
                         android.Manifest.permission.DUMP);
 
         mDir = new File(context.getExternalMediaDirs()[0], "test_" + System.nanoTime());
@@ -123,28 +142,37 @@
         final File d = touch(buildPath(dir, DIRECTORY_PICTURES, ".thumbnails", "random.bin"));
         final File e = touch(buildPath(dir, DIRECTORY_PICTURES, ".thumbnails", ".nomedia"));
 
-        // Idle maintenance pass should clean up unknown files
-        MediaStore.runIdleMaintenance(resolver);
-        assertFalse(exists(a));
-        assertFalse(exists(b));
-        assertTrue(exists(c));
-        assertFalse(exists(d));
-        assertTrue(exists(e));
+        try {
+            // Idle maintenance pass should clean up unknown files
+            MediaStore.runIdleMaintenance(resolver);
+            assertFalse(exists(a));
+            assertFalse(exists(b));
+            assertTrue(exists(c));
+            assertFalse(exists(d));
+            assertTrue(exists(e));
 
-        // And change the UUID, which emulates ejecting and mounting a different
-        // storage device; all thumbnails should then be invalidated
-        final File uuidFile = buildPath(dir, Environment.DIRECTORY_PICTURES,
-                ".thumbnails", ".database_uuid");
-        delete(uuidFile);
-        touch(uuidFile);
+            // And change the UUID, which emulates ejecting and mounting a different
+            // storage device; all thumbnails should then be invalidated
+            final File uuidFile = buildPath(dir, Environment.DIRECTORY_PICTURES,
+                    ".thumbnails", ".database_uuid");
+            delete(uuidFile);
+            touch(uuidFile);
 
-        // Idle maintenance pass should clean up all files except .nomedia file
-        MediaStore.runIdleMaintenance(resolver);
-        assertFalse(exists(a));
-        assertFalse(exists(b));
-        assertFalse(exists(c));
-        assertFalse(exists(d));
-        assertTrue(exists(e));
+            // Idle maintenance pass should clean up all files except .nomedia file
+            MediaStore.runIdleMaintenance(resolver);
+            assertFalse("File a should have been deleted", exists(a));
+            assertFalse("File b should have been deleted", exists(b));
+            assertFalse("File c should have been deleted", exists(c));
+            assertFalse("File d should have been deleted", exists(d));
+            assertTrue("File e should have existed", exists(e));
+            delete(uuidFile);
+        } finally {
+            a.delete();
+            b.delete();
+            c.delete();
+            d.delete();
+            e.delete();
+        }
     }
 
     /**
@@ -168,9 +196,9 @@
 
         MediaStore.runIdleMaintenance(resolver);
 
-        assertExpiredItemIsExtended(resolver, uri1);
-        assertExpiredItemIsExtended(resolver, uri2);
-        assertExpiredItemIsExtended(resolver, uri3);
+        assertExpiredItemIsExtended(resolver, uri1, dateExpires1);
+        assertExpiredItemIsExtended(resolver, uri2, dateExpires2);
+        assertExpiredItemIsExtended(resolver, uri3, dateExpires3);
     }
 
     @Test
@@ -182,7 +210,7 @@
 
         MediaStore.runIdleMaintenance(resolver);
 
-        assertExpiredItemIsExtended(resolver, uri);
+        assertExpiredItemIsExtended(resolver, uri, dateExpires);
     }
 
     @Test
@@ -257,9 +285,145 @@
         }
     }
 
-    private void assertExpiredItemIsExtended(ContentResolver resolver, Uri uri) throws Exception {
-        final long expectedExtendedTimestamp =
-                (System.currentTimeMillis() + FileUtils.DEFAULT_DURATION_EXTENDED) / 1000 - 1;
+    @Test
+    public void testJobScheduling() {
+        try {
+            final Context context = InstrumentationRegistry.getTargetContext();
+            final JobScheduler scheduler = InstrumentationRegistry.getTargetContext()
+                    .getSystemService(JobScheduler.class);
+            cancelJob();
+            assertNull(scheduler.getPendingJob(IDLE_JOB_ID));
+
+            IdleService.scheduleIdlePass(context);
+            assertNotNull(scheduler.getPendingJob(IDLE_JOB_ID));
+        } finally {
+            cancelJob();
+        }
+    }
+
+    private void cancelJob() {
+        final JobScheduler scheduler = InstrumentationRegistry.getTargetContext()
+                .getSystemService(JobScheduler.class);
+        if (scheduler.getPendingJob(IDLE_JOB_ID) != null) {
+            scheduler.cancel(IDLE_JOB_ID);
+        }
+    }
+
+    /**
+     * Idle maintenance run on non-demo devices should not remove xattr data stored for different
+     * users on /data/media/0. This is not done due to b/305658663.
+     */
+    @Test
+    @RunOnlyOnPostsubmit
+    @LargeTest
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void test_idle_maintenance_nonDemoDevice() throws IOException {
+        assumeTrue(UserManager.supportsMultipleUsers());
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(
+                        Manifest.permission.INTERACT_ACROSS_USERS,
+                        Manifest.permission.CREATE_USERS,
+                        Manifest.permission.MANAGE_USERS,
+                        Manifest.permission.WRITE_MEDIA_STORAGE,
+                        android.Manifest.permission.DUMP);
+        SystemClock.sleep(3000);
+        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        Context context = instrumentation.getContext();
+        UserManager userManager = context.getSystemService(UserManager.class);
+        Integer secondaryUser = -1;
+        boolean secondaryUserPresent = false;
+
+        try {
+            secondaryUser = createNewUser(userManager);
+            secondaryUserPresent = true;
+            startUser(secondaryUser);
+            SystemClock.sleep(3000);
+
+            // Verify presence of recovery data
+            String[] recoveryData = MediaStore.getRecoveryData(context.getContentResolver());
+            assertThat(getUserIdsForUsersFromRecoveryData(recoveryData)).containsAtLeastElementsIn(
+                    Arrays.asList(UserHandle.SYSTEM.getIdentifier(), secondaryUser));
+
+            executeShellCommand(
+                    "content call --uri content://media/external/file --method "
+                            + "run_idle_maintenance --user "
+                            + UserHandle.SYSTEM.getIdentifier());
+            executeShellCommand(
+                    "content call --uri content://media/external/file --method "
+                            + "run_idle_maintenance --user " + secondaryUser);
+
+            // Verify presence of recovery data even after running idle maintenance
+            recoveryData = MediaStore.getRecoveryData(context.getContentResolver());
+            assertThat(getUserIdsForUsersFromRecoveryData(recoveryData)).containsAtLeastElementsIn(
+                    Arrays.asList(UserHandle.SYSTEM.getIdentifier(), secondaryUser));
+
+            // Remove secondary user
+            removeUser(secondaryUser);
+            secondaryUserPresent = false;
+            SystemClock.sleep(3000);
+
+            // Run idle maintenance for user 0
+            executeShellCommand(
+                    "content call --uri content://media/external/file --method "
+                            + "run_idle_maintenance --user "
+                            + UserHandle.SYSTEM.getIdentifier());
+
+            // Verify presence of recovery data
+            recoveryData = MediaStore.getRecoveryData(context.getContentResolver());
+            assertThat(getUserIdsForUsersFromRecoveryData(recoveryData)).containsAtLeastElementsIn(
+                    Arrays.asList(UserHandle.SYSTEM.getIdentifier(), secondaryUser));
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        } finally {
+            if (secondaryUserPresent) {
+                removeUser(secondaryUser);
+            }
+            MediaStore.removeRecoveryData(context.getContentResolver());
+        }
+    }
+
+    private Set<Integer> getUserIdsForUsersFromRecoveryData(String[] recoveryData) {
+        Set<Integer> userIdSet = new HashSet<>();
+        for (String data : recoveryData) {
+            if (data.startsWith("user.extdbnextrowid")) {
+                userIdSet.add(Integer.valueOf(data.substring("user.extdbnextrowid".length())));
+            } else if (data.startsWith("user.extdbsessionid")) {
+                userIdSet.add(Integer.valueOf(data.substring("user.extdbsessionid".length())));
+            }
+        }
+
+        return userIdSet;
+    }
+
+
+    private int createNewUser(UserManager userManager) {
+        final NewUserRequest newUserRequest = new NewUserRequest.Builder().setName(
+                "test_user" + System.currentTimeMillis()).setUserType(
+                UserManager.USER_TYPE_FULL_SECONDARY).build();
+        final UserHandle newUser = userManager.createUser(newUserRequest).getUser();
+        if (newUser == null) {
+            fail("Error while creating a new user");
+        }
+        return newUser.getIdentifier();
+    }
+
+    private void startUser(int userId) throws IOException {
+        Log.i(TAG, "Starting user " + userId);
+        String output = executeShellCommand("am start-user -w " + userId);
+        if (output.startsWith("Error")) {
+            fail(String.format("Failed to start user %d: %s", userId, output));
+        }
+    }
+
+    private void removeUser(int userId) throws IOException {
+        final String output = executeShellCommand("cmd package remove-user " + userId);
+        if (output.startsWith("Error")) {
+            fail("Error removing the user #" + userId + ": " + output);
+        }
+    }
+
+    private void assertExpiredItemIsExtended(ContentResolver resolver, Uri uri,
+            long lastExpiredDate) {
         final String[] projection = new String[]{DATE_EXPIRES};
         final Bundle queryArgs = new Bundle();
         queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE);
@@ -268,10 +432,17 @@
             assertThat(cursor.getCount()).isEqualTo(1);
             cursor.moveToFirst();
             final long dateExpiresAfter = cursor.getLong(0);
-            assertThat(dateExpiresAfter).isGreaterThan(expectedExtendedTimestamp);
+            assertThat(dateExpiresAfter).isGreaterThan(lastExpiredDate);
+            assertTrue(timeDifferenceInSeconds(
+                    (System.currentTimeMillis() + FileUtils.DEFAULT_DURATION_EXTENDED) / 1000,
+                    dateExpiresAfter) <= 10);
         }
     }
 
+    private long timeDifferenceInSeconds(long timeAfter, long timeBefore) {
+        return timeAfter - timeBefore;
+    }
+
     private Uri createExpiredTrashedItem(ContentResolver resolver, long dateExpires)
             throws Exception {
         return createExpiredTrashedItem(resolver, dateExpires,
@@ -319,7 +490,7 @@
     private static String executeShellCommand(String command) throws IOException {
         Log.v(TAG, "$ " + command);
         ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation().getUiAutomation()
-                .executeShellCommand(command.toString());
+                .executeShellCommand(command);
         BufferedReader br = null;
         try (InputStream in = new FileInputStream(pfd.getFileDescriptor());) {
             br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
diff --git a/tests/src/com/android/providers/media/IsolatedContext.java b/tests/src/com/android/providers/media/IsolatedContext.java
index 98b1d2b..ffcae98 100644
--- a/tests/src/com/android/providers/media/IsolatedContext.java
+++ b/tests/src/com/android/providers/media/IsolatedContext.java
@@ -29,7 +29,12 @@
 import android.test.mock.MockContentProvider;
 import android.test.mock.MockContentResolver;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
 import com.android.providers.media.cloudproviders.CloudProviderPrimary;
+import com.android.providers.media.cloudproviders.FlakyCloudProvider;
+import com.android.providers.media.dao.FileRow;
 import com.android.providers.media.photopicker.PhotoPickerProvider;
 import com.android.providers.media.photopicker.PickerSyncController;
 import com.android.providers.media.util.FileUtils;
@@ -45,6 +50,7 @@
     private final MockContentResolver mResolver;
     private final MediaProvider mMediaProvider;
     private final UserHandle mUserHandle;
+    private final FlakyCloudProvider mFlakyCloudProvider;
 
     public IsolatedContext(Context base, String tag, boolean asFuseThread) {
         this(base, tag, asFuseThread, base.getUser());
@@ -85,6 +91,9 @@
         final CloudMediaProvider cmp = new CloudProviderPrimary();
         attachInfoAndAddProvider(base, cmp, CloudProviderPrimary.AUTHORITY);
 
+        mFlakyCloudProvider = new FlakyCloudProvider();
+        attachInfoAndAddProvider(base, mFlakyCloudProvider, FlakyCloudProvider.AUTHORITY);
+
         MediaStore.waitForIdle(mResolver);
     }
 
@@ -110,6 +119,11 @@
             protected void storageNativeBootPropertyChangeListener() {
                 // Ignore this as test app cannot read device config
             }
+
+            @Override
+            protected void updateQuotaTypeForUri(@NonNull FileRow row) {
+                return;
+            }
         };
     }
 
@@ -153,4 +167,13 @@
         }
     }
 
+    @VisibleForTesting
+    public void setFlakyCloudProviderToFlakeInTheNextRequest() {
+        mFlakyCloudProvider.setToFlakeInTheNextRequest();
+    }
+
+    @VisibleForTesting
+    public void resetFlakyCloudProviderToNotFlakeInTheNextRequest() {
+        mFlakyCloudProvider.resetToNotFlakeInTheNextRequest();
+    }
 }
diff --git a/tests/src/com/android/providers/media/MediaGrantsTest.java b/tests/src/com/android/providers/media/MediaGrantsTest.java
index aae4f84..622c790 100644
--- a/tests/src/com/android/providers/media/MediaGrantsTest.java
+++ b/tests/src/com/android/providers/media/MediaGrantsTest.java
@@ -16,8 +16,11 @@
 
 package com.android.providers.media;
 
+import static android.provider.MediaStore.MediaColumns.DATA;
+
 import static com.android.providers.media.util.FileCreationUtils.buildValidPickerUri;
 import static com.android.providers.media.util.FileCreationUtils.insertFileInResolver;
+import static com.android.providers.media.util.FileUtils.getContentUriForPath;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
@@ -25,6 +28,8 @@
 
 import android.Manifest;
 import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
@@ -40,6 +45,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
 import java.util.List;
 
 @RunWith(AndroidJUnit4.class)
@@ -51,8 +57,11 @@
     private MediaGrants mGrants;
 
     private static final String TEST_OWNER_PACKAGE_NAME = "com.android.test.package";
+    private static final String TEST_OWNER_PACKAGE_NAME2 = "com.android.test.package2";
     private static final int TEST_USER_ID = UserHandle.myUserId();
 
+    private static final String PNG_MIME_TYPE = "image/png";
+
     @BeforeClass
     public static void setUpClass() {
         androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
@@ -96,6 +105,225 @@
     }
 
     @Test
+    public void testGetMediaGrantsForPackages() throws Exception {
+        Long fileId1 = insertFileInResolver(mIsolatedResolver, "test_file1");
+        Long fileId2 = insertFileInResolver(mIsolatedResolver, "test_file2");
+        Long fileId3 = insertFileInResolver(mIsolatedResolver, "test_file3");
+        List<Uri> uris1 = List.of(buildValidPickerUri(fileId1), buildValidPickerUri(fileId2));
+        List<Uri> uris2 = List.of(buildValidPickerUri(fileId3));
+
+        mGrants.addMediaGrantsForPackage(TEST_OWNER_PACKAGE_NAME, uris1, TEST_USER_ID);
+        mGrants.addMediaGrantsForPackage(TEST_OWNER_PACKAGE_NAME2, uris2, TEST_USER_ID);
+
+        String[] mimeTypes = {PNG_MIME_TYPE};
+        String[] volumes = {MediaStore.VOLUME_EXTERNAL_PRIMARY};
+
+        List<Uri> fileUris = convertToListOfUri(mGrants.getMediaGrantsForPackages(
+                new String[]{TEST_OWNER_PACKAGE_NAME}, TEST_USER_ID,  mimeTypes, volumes));
+
+        List<Long> expectedFileIdsList = List.of(fileId1, fileId2);
+
+        assertEquals(fileUris.size(), expectedFileIdsList.size());
+        for (Uri uri : fileUris) {
+            assertTrue(expectedFileIdsList.contains(Long.valueOf(ContentUris.parseId(uri))));
+        }
+
+        List<Uri> fileUrisForTestPackage2 = convertToListOfUri(mGrants.getMediaGrantsForPackages(
+                new String[]{TEST_OWNER_PACKAGE_NAME2}, TEST_USER_ID,  mimeTypes, volumes));
+
+        List<Long> expectedFileIdsList2 = List.of(fileId3);
+
+        assertEquals(fileUrisForTestPackage2.size(), expectedFileIdsList2.size());
+        for (Uri uri : fileUrisForTestPackage2) {
+            assertTrue(expectedFileIdsList2.contains(Long.valueOf(ContentUris.parseId(uri))));
+        }
+
+        List<Uri> fileUrisForTestPackage3 = convertToListOfUri(mGrants.getMediaGrantsForPackages(
+                new String[]{"non.existent.package"}, TEST_USER_ID,  mimeTypes, volumes));
+
+        // assert no items are returned for an invalid package.
+        assertEquals(/* expected= */fileUrisForTestPackage3.size(), /* actual= */0);
+    }
+
+    @Test
+    public void test_GetMediaGrantsForPackages_excludesIsTrashed() throws Exception {
+        Long fileId1 = insertFileInResolver(mIsolatedResolver, "test_file1");
+        Long fileId2 = insertFileInResolver(mIsolatedResolver, "test_file2");
+        List<Uri> uris1 = List.of(buildValidPickerUri(fileId1), buildValidPickerUri(fileId2));
+
+        mGrants.addMediaGrantsForPackage(TEST_OWNER_PACKAGE_NAME, uris1, TEST_USER_ID);
+
+        String[] mimeTypes = {PNG_MIME_TYPE};
+        String[] volumes = {MediaStore.VOLUME_EXTERNAL_PRIMARY};
+        // Mark one of the files as trashed.
+        updateFileValues(fileId1, MediaStore.Files.FileColumns.IS_TRASHED, "1");
+
+        List<Uri> fileUris = convertToListOfUri(mGrants.getMediaGrantsForPackages(
+                new String[]{TEST_OWNER_PACKAGE_NAME}, TEST_USER_ID,  mimeTypes, volumes));
+
+        // Now the 1st file with fileId1 should not be part of the returned grants.
+        List<Long> expectedFileIdsList = List.of(fileId2);
+
+        assertEquals(fileUris.size(), expectedFileIdsList.size());
+        for (Uri uri : fileUris) {
+            assertTrue(expectedFileIdsList.contains(Long.valueOf(ContentUris.parseId(uri))));
+        }
+    }
+
+    @Test
+    public void test_GetMediaGrantsForPackages_excludesIsPending() throws Exception {
+        Long fileId1 = insertFileInResolver(mIsolatedResolver, "test_file1");
+        Long fileId2 = insertFileInResolver(mIsolatedResolver, "test_file2");
+        List<Uri> uris1 = List.of(buildValidPickerUri(fileId1), buildValidPickerUri(fileId2));
+
+        mGrants.addMediaGrantsForPackage(TEST_OWNER_PACKAGE_NAME, uris1, TEST_USER_ID);
+
+        String[] mimeTypes = {PNG_MIME_TYPE};
+        String[] volumes = {MediaStore.VOLUME_EXTERNAL_PRIMARY};
+        // Mark one of the files as pending.
+        updateFileValues(fileId1, MediaStore.Files.FileColumns.IS_PENDING, "1");
+
+        List<Uri> fileUris = convertToListOfUri(mGrants.getMediaGrantsForPackages(
+                new String[]{TEST_OWNER_PACKAGE_NAME}, TEST_USER_ID,  mimeTypes, volumes));
+
+        // Now the 1st file with fileId1 should not be part of the returned grants.
+        List<Long> expectedFileIdsList = List.of(fileId2);
+
+        assertEquals(fileUris.size(), expectedFileIdsList.size());
+        for (Uri uri : fileUris) {
+            assertTrue(expectedFileIdsList.contains(Long.valueOf(ContentUris.parseId(uri))));
+        }
+    }
+
+    @Test
+    public void test_GetMediaGrantsForPackages_testMimeTypeFilter() throws Exception {
+        Long fileId1 = insertFileInResolver(mIsolatedResolver, "test_file1");
+        Long fileId2 = insertFileInResolver(mIsolatedResolver, "test_file2");
+        List<Uri> uris1 = List.of(buildValidPickerUri(fileId1), buildValidPickerUri(fileId2));
+
+        Long fileId3 = insertFileInResolver(mIsolatedResolver, "test_file3", "mp4");
+        List<Uri> uris2 = List.of(buildValidPickerUri(fileId3));
+
+        mGrants.addMediaGrantsForPackage(TEST_OWNER_PACKAGE_NAME, uris1, TEST_USER_ID);
+        mGrants.addMediaGrantsForPackage(TEST_OWNER_PACKAGE_NAME, uris2, TEST_USER_ID);
+
+        String[] volumes = {MediaStore.VOLUME_EXTERNAL_PRIMARY};
+
+        // Test image only, should return 2 items.
+        String[] mimeTypes = {PNG_MIME_TYPE};
+
+        List<Uri> fileUris = convertToListOfUri(mGrants.getMediaGrantsForPackages(
+                new String[]{TEST_OWNER_PACKAGE_NAME}, TEST_USER_ID, mimeTypes, volumes));
+
+        List<Long> expectedFileIdsList = List.of(fileId1, fileId2);
+        assertEquals(fileUris.size(), expectedFileIdsList.size());
+        for (Uri uri : fileUris) {
+            assertTrue(expectedFileIdsList.contains(Long.valueOf(ContentUris.parseId(uri))));
+        }
+
+        // Test video only, should return 1 item.
+        String[] mimeTypes2 = {"video/mp4"};
+
+        List<Uri> fileUris2 = convertToListOfUri(mGrants.getMediaGrantsForPackages(
+                new String[]{TEST_OWNER_PACKAGE_NAME}, TEST_USER_ID, mimeTypes2, volumes));
+        List<Long> expectedFileIdsList2 = List.of(fileId3);
+        assertEquals(fileUris2.size(), expectedFileIdsList2.size());
+        for (Uri uri : fileUris2) {
+            assertTrue(expectedFileIdsList2.contains(Long.valueOf(ContentUris.parseId(uri))));
+        }
+
+
+        // Test jpeg mimeType, since no items with this mimeType is granted, empty list should be
+        // returned.
+        String[] mimeTypes3 = {"image/jpeg"};
+        List<Uri> fileUris3 = convertToListOfUri(mGrants.getMediaGrantsForPackages(
+                new String[]{TEST_OWNER_PACKAGE_NAME}, TEST_USER_ID, mimeTypes3, volumes));
+        assertTrue(fileUris3.isEmpty());
+    }
+
+    @Test
+    public void test_GetMediaGrantsForPackages_volume() throws Exception {
+        Long fileId1 = insertFileInResolver(mIsolatedResolver, "test_file1");
+        Long fileId2 = insertFileInResolver(mIsolatedResolver, "test_file2");
+        List<Uri> uris1 = List.of(buildValidPickerUri(fileId1), buildValidPickerUri(fileId2));
+
+        mGrants.addMediaGrantsForPackage(TEST_OWNER_PACKAGE_NAME, uris1, TEST_USER_ID);
+
+        String[] volumes = {"test_volume"};
+        String[] mimeTypes = {PNG_MIME_TYPE};
+
+        List<Uri> fileUris = convertToListOfUri(mGrants.getMediaGrantsForPackages(
+                new String[]{TEST_OWNER_PACKAGE_NAME}, TEST_USER_ID,  mimeTypes, volumes));
+
+        assertTrue(fileUris.isEmpty());
+    }
+
+    @Test
+    public void testRemoveMediaGrantsForPackages() throws Exception {
+        Long fileId1 = insertFileInResolver(mIsolatedResolver, "test_file1");
+        Long fileId2 = insertFileInResolver(mIsolatedResolver, "test_file2");
+        Long fileId3 = insertFileInResolver(mIsolatedResolver, "test_file3");
+        List<Uri> uris1 = List.of(buildValidPickerUri(fileId1), buildValidPickerUri(fileId2));
+        List<Uri> uris2 = List.of(buildValidPickerUri(fileId3));
+
+        // Add grants for 2 different packages.
+        mGrants.addMediaGrantsForPackage(TEST_OWNER_PACKAGE_NAME, uris1, TEST_USER_ID);
+        mGrants.addMediaGrantsForPackage(TEST_OWNER_PACKAGE_NAME2, uris2, TEST_USER_ID);
+
+        String[] mimeTypes = {PNG_MIME_TYPE};
+        String[] volumes = {MediaStore.VOLUME_EXTERNAL_PRIMARY};
+
+        // Verify the grants for the first package were inserted.
+        List<Uri> fileUris = convertToListOfUri(mGrants.getMediaGrantsForPackages(
+                new String[]{TEST_OWNER_PACKAGE_NAME}, TEST_USER_ID,
+                mimeTypes, volumes));
+        List<Long> expectedFileIdsList = List.of(fileId1, fileId2);
+        assertEquals(fileUris.size(), expectedFileIdsList.size());
+        for (Uri uri : fileUris) {
+            assertTrue(expectedFileIdsList.contains(Long.valueOf(ContentUris.parseId(uri))));
+        }
+
+        // Remove one of the 2 grants for TEST_OWNER_PACKAGE_NAME and verify the other grants is
+        // still present.
+        mGrants.removeMediaGrantsForPackage(new String[]{TEST_OWNER_PACKAGE_NAME},
+                List.of(buildValidPickerUri(fileId1)), TEST_USER_ID);
+        List<Uri> fileUris3 = convertToListOfUri(mGrants.getMediaGrantsForPackages(
+                new String[]{TEST_OWNER_PACKAGE_NAME}, TEST_USER_ID,  mimeTypes, volumes));
+        assertEquals(1, fileUris3.size());
+        assertEquals(fileId2, Long.valueOf(ContentUris.parseId(fileUris3.get(0))));
+
+
+        // Verify grants of other packages are unaffected.
+        List<Uri> fileUrisForTestPackage2 = convertToListOfUri(mGrants.getMediaGrantsForPackages(
+                new String[]{TEST_OWNER_PACKAGE_NAME2}, TEST_USER_ID,  mimeTypes, volumes));
+        List<Long> expectedFileIdsList2 = List.of(fileId3);
+        assertEquals(fileUrisForTestPackage2.size(), expectedFileIdsList2.size());
+        for (Uri uri : fileUrisForTestPackage2) {
+            assertTrue(expectedFileIdsList2.contains(Long.valueOf(ContentUris.parseId(uri))));
+        }
+    }
+
+    @Test
+    public void testRemoveMediaGrantsForPackagesLargerDataSet() throws Exception {
+        List<Uri> inputFiles = new ArrayList<>();
+        for (int itr = 1; itr < 110; itr++) {
+            inputFiles.add(buildValidPickerUri(
+                    insertFileInResolver(mIsolatedResolver, "test_file" + itr)));
+        }
+        mGrants.addMediaGrantsForPackage(TEST_OWNER_PACKAGE_NAME, inputFiles, TEST_USER_ID);
+
+        String[] mimeTypes = {PNG_MIME_TYPE};
+        String[] volumes = {MediaStore.VOLUME_EXTERNAL_PRIMARY};
+
+        // The query used inside remove grants is batched by 50 ids, hence having a test like this
+        // would help ensure the batching worked perfectly.
+        mGrants.removeMediaGrantsForPackage(new String[]{TEST_OWNER_PACKAGE_NAME},
+                inputFiles.subList(0, 101), TEST_USER_ID);
+        List<Uri> fileUris3 = convertToListOfUri(mGrants.getMediaGrantsForPackages(
+                new String[]{TEST_OWNER_PACKAGE_NAME}, TEST_USER_ID, mimeTypes, volumes));
+        assertEquals(8, fileUris3.size());
+    }
+    @Test
     public void testAddDuplicateMediaGrants() throws Exception {
 
         Long fileId1 = insertFileInResolver(mIsolatedResolver, "test_file1");
@@ -139,8 +367,9 @@
         assertGrantExistsForPackage(fileId1, TEST_OWNER_PACKAGE_NAME, TEST_USER_ID);
         assertGrantExistsForPackage(fileId2, TEST_OWNER_PACKAGE_NAME, TEST_USER_ID);
 
-        int removed = mGrants.removeAllMediaGrantsForPackage(TEST_OWNER_PACKAGE_NAME, "test",
-                TEST_USER_ID);
+        int removed =
+                mGrants.removeAllMediaGrantsForPackages(
+                        new String[] {TEST_OWNER_PACKAGE_NAME}, "test", TEST_USER_ID);
         assertEquals(2, removed);
 
         try (Cursor c =
@@ -165,11 +394,73 @@
     }
 
     @Test
+    public void removeAllMediaGrantsForMultiplePackages() throws Exception {
+
+        Long fileId1 = insertFileInResolver(mIsolatedResolver, "test_file1");
+        Long fileId2 = insertFileInResolver(mIsolatedResolver, "test_file2");
+        List<Uri> uris = List.of(buildValidPickerUri(fileId1), buildValidPickerUri(fileId2));
+        mGrants.addMediaGrantsForPackage(TEST_OWNER_PACKAGE_NAME, uris, TEST_USER_ID);
+        mGrants.addMediaGrantsForPackage(TEST_OWNER_PACKAGE_NAME2, uris, TEST_USER_ID);
+
+        assertGrantExistsForPackage(fileId1, TEST_OWNER_PACKAGE_NAME, TEST_USER_ID);
+        assertGrantExistsForPackage(fileId2, TEST_OWNER_PACKAGE_NAME, TEST_USER_ID);
+        assertGrantExistsForPackage(fileId1, TEST_OWNER_PACKAGE_NAME2, TEST_USER_ID);
+        assertGrantExistsForPackage(fileId2, TEST_OWNER_PACKAGE_NAME2, TEST_USER_ID);
+
+        int removed =
+                mGrants.removeAllMediaGrantsForPackages(
+                        new String[] {TEST_OWNER_PACKAGE_NAME, TEST_OWNER_PACKAGE_NAME2},
+                        "test",
+                        TEST_USER_ID);
+        assertEquals(4, removed);
+
+        try (Cursor c =
+                mExternalDatabase.runWithTransaction(
+                        (db) ->
+                                db.query(
+                                        MediaGrants.MEDIA_GRANTS_TABLE,
+                                        new String[] {
+                                            MediaGrants.FILE_ID_COLUMN,
+                                            MediaGrants.OWNER_PACKAGE_NAME_COLUMN
+                                        },
+                                        String.format(
+                                                "%s = '%s'",
+                                                MediaGrants.OWNER_PACKAGE_NAME_COLUMN,
+                                                TEST_OWNER_PACKAGE_NAME),
+                                        null,
+                                        null,
+                                        null,
+                                        null))) {
+            assertEquals(0, c.getCount());
+        }
+
+        try (Cursor c =
+                mExternalDatabase.runWithTransaction(
+                        (db) ->
+                                db.query(
+                                        MediaGrants.MEDIA_GRANTS_TABLE,
+                                        new String[] {
+                                            MediaGrants.FILE_ID_COLUMN,
+                                            MediaGrants.OWNER_PACKAGE_NAME_COLUMN
+                                        },
+                                        String.format(
+                                                "%s = '%s'",
+                                                MediaGrants.OWNER_PACKAGE_NAME_COLUMN,
+                                                TEST_OWNER_PACKAGE_NAME2),
+                                        null,
+                                        null,
+                                        null,
+                                        null))) {
+            assertEquals(0, c.getCount());
+        }
+    }
+
+    @Test
     public void removeAllMediaGrantsForPackageRequiresNonEmpty() throws Exception {
         assertThrows(
                 IllegalArgumentException.class,
                 () -> {
-                    mGrants.removeAllMediaGrantsForPackage("", "test", TEST_USER_ID);
+                    mGrants.removeAllMediaGrantsForPackages(new String[]{}, "test", TEST_USER_ID);
                 });
     }
 
@@ -273,4 +564,34 @@
             assertEquals(packageName, ownerValue);
         }
     }
+
+    private List<Uri> convertToListOfUri(Cursor c) {
+        List<Uri> filesUriList = new ArrayList<>(0);
+        while (c.moveToNext()) {
+            final String file_path = c.getString(c.getColumnIndexOrThrow(DATA));
+            final Integer file_id = c.getInt(c.getColumnIndexOrThrow(MediaGrants.FILE_ID_COLUMN));
+            filesUriList.add(getContentUriForPath(
+                    file_path).buildUpon().appendPath(String.valueOf(file_id)).build());
+        }
+        return filesUriList;
+    }
+
+    /**
+     * Modify column value for the fileId passed in the parameters with the modifiedValue.
+     */
+    private void updateFileValues(Long fileId, String columnToBeModified, String modifiedValue) {
+        int numberOfUpdatedRows = mExternalDatabase.runWithTransaction(
+                (db) -> {
+                    ContentValues updatedRowValue = new ContentValues();
+                    updatedRowValue.put(columnToBeModified, modifiedValue);
+                    return db.update(MediaStore.Files.TABLE,
+                            updatedRowValue,
+                            String.format(
+                                    "%s = '%s'",
+                                    MediaStore.Files.FileColumns._ID,
+                                    Long.toString(fileId)),
+                            null);
+                });
+        assertEquals(/* expected */ 1, numberOfUpdatedRows);
+    }
 }
diff --git a/tests/src/com/android/providers/media/MediaProviderForFuseTest.java b/tests/src/com/android/providers/media/MediaProviderForFuseTest.java
index 6ab4950..5f38a62 100644
--- a/tests/src/com/android/providers/media/MediaProviderForFuseTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderForFuseTest.java
@@ -21,6 +21,8 @@
 import static com.android.providers.media.MediaProvider.DIRECTORY_ACCESS_FOR_READ;
 import static com.android.providers.media.MediaProvider.DIRECTORY_ACCESS_FOR_WRITE;
 
+import static org.junit.Assert.fail;
+
 import android.Manifest;
 import android.app.UiAutomation;
 import android.content.ContentResolver;
@@ -182,6 +184,23 @@
         }
     }
 
+    @Test
+    public void test_syntheticPathLookUpWithInvalidUid_throwsSecurityException() throws Exception {
+        try {
+            // Attempt a lookup for path that is synthetic and is a picker uri. Since the test
+            // uid is not the owner of the directory, the lookup should fail in the first step of
+            // the process that is, mContext.checkUriPermission and should throw a security
+            // exception.
+            sMediaProvider.onFileLookupForFuse(
+                    "/storage/emulated/0/.transforms/synthetic/picker/0/com.android.providers"
+                            + ".media.photopicker/media/1000000.jpg", sTestUid /* uid */,
+                    0 /* tid */);
+            fail("This test should throw a security exception");
+        } catch (SecurityException se) {
+            // no-op.
+        }
+    }
+
     private @NonNull File createNomediaFile(@NonNull File dir) throws IOException {
         final File nomediaFile = new File(dir, ".nomedia");
         executeShellCommand("touch " + nomediaFile.getAbsolutePath());
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index 823e901..fe13649 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -73,6 +73,7 @@
 import com.android.providers.media.MediaProvider.VolumeArgumentException;
 import com.android.providers.media.MediaProvider.VolumeNotFoundException;
 import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.data.ItemsProvider;
 import com.android.providers.media.util.FileUtils;
 import com.android.providers.media.util.FileUtilsTest;
 import com.android.providers.media.util.SQLiteQueryBuilder;
@@ -106,6 +107,8 @@
     static final String PERMISSIONLESS_APP = "com.android.providers.media.testapp.withoutperms";
 
     private static Context sIsolatedContext;
+
+    private static ItemsProvider sItemsProvider;
     private static Context sContext;
     private static ContentResolver sIsolatedResolver;
 
@@ -115,6 +118,11 @@
                 .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
                         Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
                         Manifest.permission.READ_DEVICE_CONFIG,
+                        // Adding this to use getUserHandles() api of UserManagerService which
+                        // requires either MANAGE_USERS or CREATE_USERS. Since shell does not have
+                        // MANAGER_USERS permissions, using CREATE_USERS in test. This works with
+                        // MANAGE_USERS permission for MediaProvider module.
+                        Manifest.permission.CREATE_USERS,
                         Manifest.permission.INTERACT_ACROSS_USERS);
 
         resetIsolatedContext();
@@ -338,6 +346,90 @@
 
     }
 
+    @Test
+    public void testGetReadGrantsForPackage() throws Exception {
+        final File dir = Environment
+                .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+        final File testFile = stage(R.raw.lg_g4_iso_800_jpg,
+                new File(dir, "test" + System.nanoTime() + ".jpg"));
+        final Uri uri = MediaStore.scanFile(sIsolatedResolver, testFile);
+        Long fileId = ContentUris.parseId(uri);
+
+        final Uri.Builder builder = Uri.EMPTY.buildUpon();
+        builder.scheme("content");
+        builder.encodedAuthority(MediaStore.AUTHORITY);
+
+        final Uri testUri = builder.appendPath("picker")
+                .appendPath(Integer.toString(UserHandle.myUserId()))
+                .appendPath(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)
+                .appendPath(MediaStore.AUTHORITY)
+                .appendPath(Long.toString(fileId))
+                .build();
+
+        try {
+            String[] mimeTypes = {"image/*"};
+            // Verify empty list with no grants.
+            List<Uri> grantedUris = sItemsProvider.fetchReadGrantedItemsUrisForPackage(
+                    android.os.Process.myUid(), mimeTypes);
+            assertTrue(grantedUris.isEmpty());
+
+            // Grants the READ-GRANT for the testUris for the current package.
+            MediaStore.grantMediaReadForPackage(sIsolatedContext,
+                    android.os.Process.myUid(),
+                    List.of(testUri));
+
+            // Assert that the grant was returned.
+            List<Uri> grantedUris2 = sItemsProvider.fetchReadGrantedItemsUrisForPackage(
+                    android.os.Process.myUid(), mimeTypes);
+            assertEquals(ContentUris.parseId(uri), ContentUris.parseId(grantedUris2.get(0)));
+        } finally {
+            dir.delete();
+            testFile.delete();
+        }
+    }
+
+    @Test
+    public void testRevokeReadGrantsForPackage() throws Exception {
+        final File dir = Environment
+                .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+        final File testFile = stage(R.raw.lg_g4_iso_800_jpg,
+                new File(dir, "test" + System.nanoTime() + ".jpg"));
+        final Uri uri = MediaStore.scanFile(sIsolatedResolver, testFile);
+        Long fileId = ContentUris.parseId(uri);
+
+        final Uri.Builder builder = Uri.EMPTY.buildUpon();
+        builder.scheme("content");
+        builder.encodedAuthority(MediaStore.AUTHORITY);
+
+        final Uri testUri = builder.appendPath("picker")
+                .appendPath(Integer.toString(UserHandle.myUserId()))
+                .appendPath(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)
+                .appendPath(MediaStore.AUTHORITY)
+                .appendPath(Long.toString(fileId))
+                .build();
+
+        try {
+            String[] mimeTypes = {"image/*"};
+            MediaStore.grantMediaReadForPackage(sIsolatedContext,
+                    android.os.Process.myUid(),
+                    List.of(testUri));
+            List<Uri> grantedUris = sItemsProvider.fetchReadGrantedItemsUrisForPackage(
+                    android.os.Process.myUid(), mimeTypes);
+            assertEquals(ContentUris.parseId(uri), ContentUris.parseId(grantedUris.get(0)));
+
+            // Revoked the grant that was provided to testUri and verify that now the current
+            // package has no grants.
+            MediaStore.revokeMediaReadForPackages(sIsolatedContext, android.os.Process.myUid(),
+                    grantedUris);
+            List<Uri> grantedUris2 = sItemsProvider.fetchReadGrantedItemsUrisForPackage(
+                    android.os.Process.myUid(), mimeTypes);
+            assertEquals(0, grantedUris2.size());
+        } finally {
+            dir.delete();
+            testFile.delete();
+        }
+    }
+
     /**
      * We already have solid coverage of this logic in
      * {@code CtsProviderTestCases}, but the coverage system currently doesn't
@@ -1746,5 +1838,6 @@
         sContext = InstrumentationRegistry.getTargetContext();
         sIsolatedContext = new IsolatedContext(sContext, "modern", /*asFuseThread*/ false);
         sIsolatedResolver = sIsolatedContext.getContentResolver();
+        sItemsProvider = new ItemsProvider(sIsolatedContext);
     }
 }
diff --git a/tests/src/com/android/providers/media/PickerProviderMediaGenerator.java b/tests/src/com/android/providers/media/PickerProviderMediaGenerator.java
index b8c2ca6..696300f 100644
--- a/tests/src/com/android/providers/media/PickerProviderMediaGenerator.java
+++ b/tests/src/com/android/providers/media/PickerProviderMediaGenerator.java
@@ -18,6 +18,7 @@
 
 import static android.provider.CloudMediaProviderContract.AlbumColumns;
 import static android.provider.CloudMediaProviderContract.EXTRA_ALBUM_ID;
+import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_SIZE;
 import static android.provider.CloudMediaProviderContract.EXTRA_SYNC_GENERATION;
 import static android.provider.CloudMediaProviderContract.MediaCollectionInfo;
 import static android.provider.CloudMediaProviderContract.MediaColumns;
@@ -94,17 +95,38 @@
         private Intent mAccountConfigurationIntent;
         private int mCursorExtraQueryCount;
         private Bundle mCursorExtra;
+        private Integer mNextPageToken;
 
-        // TODO(b/214592293): Add pagination support for testing purposes.
-        public Cursor getMedia(long generation, String albumId, String[] mimeTypes,
-                long sizeBytes) {
-            final Cursor cursor = getCursor(mMedia, generation, albumId, mimeTypes, sizeBytes,
-                    /* isDeleted */ false);
+        public Cursor getMedia(
+                long generation, String albumId, String[] mimeTypes, long sizeBytes, int pageSize) {
+            return getMedia(generation, albumId, mimeTypes, sizeBytes, null, pageSize);
+        }
+
+        public Cursor getMedia(
+                long generation,
+                String albumId,
+                String[] mimeTypes,
+                long sizeBytes,
+                String pageToken,
+                int pageSize) {
+            final Cursor cursor =
+                    getCursor(
+                            mMedia,
+                            generation,
+                            albumId,
+                            mimeTypes,
+                            sizeBytes,
+                            /* isDeleted */ false,
+                            pageToken);
 
             if (mCursorExtra != null) {
                 cursor.setExtras(mCursorExtra);
             } else {
-                cursor.setExtras(buildCursorExtras(mCollectionId, generation > 0, albumId != null));
+                cursor.setExtras(
+                        buildCursorExtras(
+                                mCollectionId, generation > 0, albumId != null, mNextPageToken,
+                                pageSize > -1));
+                mNextPageToken = null;
             }
 
             if (--mCursorExtraQueryCount == 0) {
@@ -114,12 +136,19 @@
         }
 
         public Cursor getAlbums(String[] mimeTypes, long sizeBytes, boolean isLocal) {
-            final Cursor cursor = getCursor(mAlbums, mimeTypes, sizeBytes, isLocal);
+            return getAlbums(mimeTypes, sizeBytes, isLocal, /* pageToken= */ null);
+        }
+
+        public Cursor getAlbums(
+                String[] mimeTypes, long sizeBytes, boolean isLocal, String pageToken) {
+            final Cursor cursor = getCursor(mAlbums, mimeTypes, sizeBytes, isLocal, pageToken);
 
             if (mCursorExtra != null) {
                 cursor.setExtras(mCursorExtra);
             } else {
-                cursor.setExtras(buildCursorExtras(mCollectionId, false, false));
+                cursor.setExtras(buildCursorExtras(mCollectionId, false, false, mNextPageToken,
+                        false));
+                mNextPageToken = null;
             }
 
             if (--mCursorExtraQueryCount == 0) {
@@ -128,16 +157,21 @@
             return cursor;
         }
 
-        // TODO(b/214592293): Add pagination support for testing purposes.
         public Cursor getDeletedMedia(long generation) {
+            return getDeletedMedia(generation, /* pageToken= */ null);
+        }
+        public Cursor getDeletedMedia(long generation, String pageToken) {
             final Cursor cursor = getCursor(mDeletedMedia, generation, /* albumId */ STRING_DEFAULT,
                     STRING_ARRAY_DEFAULT, /* sizeBytes */ LONG_DEFAULT,
-                    /* isDeleted */ true);
+                    /* isDeleted */ true, pageToken);
 
             if (mCursorExtra != null) {
                 cursor.setExtras(mCursorExtra);
             } else {
-                cursor.setExtras(buildCursorExtras(mCollectionId, generation > 0, false));
+                cursor.setExtras(
+                        buildCursorExtras(mCollectionId, generation > 0, false, mNextPageToken,
+                                false));
+                mNextPageToken = null;
             }
 
             if (--mCursorExtraQueryCount == 0) {
@@ -167,14 +201,23 @@
         }
 
         public void setNextCursorExtras(int queryCount, String mediaCollectionId,
-                boolean honoredSyncGeneration, boolean honoredAlbumId) {
+                boolean honoredSyncGeneration, boolean honoredAlbumId, boolean honouredPageSize) {
             mCursorExtraQueryCount = queryCount;
-            mCursorExtra = buildCursorExtras(mediaCollectionId, honoredSyncGeneration,
-                    honoredAlbumId);
+            mCursorExtra =
+                    buildCursorExtras(
+                            mediaCollectionId,
+                            honoredSyncGeneration,
+                            honoredAlbumId,
+                            mNextPageToken,
+                            honouredPageSize);
         }
 
-        public Bundle buildCursorExtras(String mediaCollectionId, boolean honoredSyncGeneration,
-                boolean honoredAlbumdId) {
+        public Bundle buildCursorExtras(
+                String mediaCollectionId,
+                boolean honoredSyncGeneration,
+                boolean honoredAlbumdId,
+                Integer pageToken,
+                boolean honouredPageSize) {
             final ArrayList<String> honoredArgs = new ArrayList<>();
             if (honoredSyncGeneration) {
                 honoredArgs.add(EXTRA_SYNC_GENERATION);
@@ -183,10 +226,17 @@
                 honoredArgs.add(EXTRA_ALBUM_ID);
             }
 
+            if (honouredPageSize) {
+                honoredArgs.add(EXTRA_PAGE_SIZE);
+            }
+
             final Bundle bundle = new Bundle();
-            bundle.putString(CloudMediaProviderContract.EXTRA_MEDIA_COLLECTION_ID,
-                    mediaCollectionId);
+            bundle.putString(
+                    CloudMediaProviderContract.EXTRA_MEDIA_COLLECTION_ID, mediaCollectionId);
             bundle.putStringArrayList(ContentResolver.EXTRA_HONORED_ARGS, honoredArgs);
+            if (pageToken != null) {
+                bundle.putString(CloudMediaProviderContract.EXTRA_PAGE_TOKEN, pageToken.toString());
+            }
 
             return bundle;
         }
@@ -223,6 +273,7 @@
             mDeletedMedia.clear();
             mAlbums.clear();
             clearCursorExtras();
+            mNextPageToken = null;
         }
 
         public void setMediaCollectionId(String id) {
@@ -241,6 +292,7 @@
             // Increase generation
             return new TestMedia(localId, cloudId, ++mLastSyncGeneration);
         }
+
         private TestMedia createTestAlbumMedia(String localId, String cloudId, String albumId) {
             // Increase generation
             return new TestMedia(localId, cloudId, albumId);
@@ -260,37 +312,90 @@
             return new TestMedia(localId, cloudId, 0);
         }
 
-        private static Cursor getCursor(List<TestMedia> mediaList, long generation,
-                String albumId, String[] mimeTypes, long sizeBytes, boolean isDeleted) {
+        private Cursor getCursor(
+                List<TestMedia> mediaList,
+                long generation,
+                String albumId,
+                String[] mimeTypes,
+                long sizeBytes,
+                boolean isDeleted,
+                String pageToken) {
             final MatrixCursor matrix;
+            final int pageSize = 5;
+
             if (isDeleted) {
                 matrix = new MatrixCursor(DELETED_MEDIA_PROJECTION);
-            } else if(!TextUtils.isEmpty(albumId)) {
+            } else if (!TextUtils.isEmpty(albumId)) {
                 matrix = new MatrixCursor(ALBUM_MEDIA_PROJECTION);
             } else {
                 matrix = new MatrixCursor(MEDIA_PROJECTION);
             }
 
-            for (TestMedia media : mediaList) {
-                if (!TextUtils.isEmpty(albumId) && matchesFilter(media,
-                        albumId, mimeTypes, sizeBytes)) {
-                    matrix.addRow(media.toAlbumMediaArray());
-                } else if (media.generation > generation
-                        && matchesFilter(media, albumId, mimeTypes, sizeBytes)) {
-                    matrix.addRow(media.toArray(isDeleted));
+            int page = 0;
+            if (pageToken != null) {
+                page = Integer.parseInt(pageToken);
+            }
+
+            // Calculate the starting position: pageSize * pageNumber
+            int startPosition = (pageSize * page);
+            // Calculate the end of the page
+            int endPosition = startPosition + pageSize;
+
+            for (int i = startPosition; i < endPosition; i++) {
+
+                try {
+                    TestMedia media = mediaList.get(i);
+                    if (!TextUtils.isEmpty(albumId)
+                            && matchesFilter(media, albumId, mimeTypes, sizeBytes)) {
+                        matrix.addRow(media.toAlbumMediaArray());
+                    } else if (media.generation > generation
+                            && matchesFilter(media, albumId, mimeTypes, sizeBytes)) {
+                        matrix.addRow(media.toArray(isDeleted));
+                    }
+
+                } catch (IndexOutOfBoundsException e) {
+                    // We're at the end of the list, before the end of the page so break the loop.
+                    break;
                 }
             }
+
+            // Set next page token if there is another page.
+            if (mediaList.size() > endPosition) {
+                mNextPageToken = Integer.valueOf(++page);
+            } else {
+                mNextPageToken = null;
+            }
+
             return matrix;
         }
 
         private static Cursor getCursor(List<TestAlbum> albumList, String[] mimeTypes,
-                long sizeBytes, boolean isLocal) {
+                long sizeBytes, boolean isLocal, String pageToken) {
             final MatrixCursor matrix = new MatrixCursor(ALBUM_PROJECTION);
+            final int pageSize = 5;
 
-            for (TestAlbum album : albumList) {
-                final String[] res = album.toArray(mimeTypes, sizeBytes, isLocal);
-                if (res != null) {
-                    matrix.addRow(res);
+            int page = 0;
+            if (pageToken != null) {
+                page = Integer.parseInt(pageToken);
+            }
+
+            // Calculate the starting position: pageSize * pageNumber
+            int startPosition = (pageSize * page);
+            // Calculate the end of the page
+            int endPosition = startPosition + pageSize;
+
+
+            for (int i = startPosition; i < endPosition; i++) {
+
+                try {
+                    TestAlbum album = albumList.get(i);
+                    final String[] res = album.toArray(mimeTypes, sizeBytes, isLocal);
+                    if (res != null) {
+                        matrix.addRow(res);
+                    }
+                } catch (IndexOutOfBoundsException e) {
+                    // We're at the end of the list, before the end of the page so break the loop.
+                    break;
                 }
             }
             return matrix;
diff --git a/tests/src/com/android/providers/media/PickerUriResolverTest.java b/tests/src/com/android/providers/media/PickerUriResolverTest.java
index d84e429..3d8c260 100644
--- a/tests/src/com/android/providers/media/PickerUriResolverTest.java
+++ b/tests/src/com/android/providers/media/PickerUriResolverTest.java
@@ -53,6 +53,7 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.providers.media.photopicker.PickerSyncController;
 import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
 
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
@@ -79,7 +80,7 @@
 
     private static class TestPickerUriResolver extends PickerUriResolver {
         TestPickerUriResolver(Context context) {
-            super(context, new PickerDbFacade(getTargetContext()),
+            super(context, new PickerDbFacade(getTargetContext(), new PickerSyncLockManager()),
                     new ProjectionHelper(Column.class, ExportedSince.class));
         }
 
diff --git a/tests/src/com/android/providers/media/PublicVolumeTest.java b/tests/src/com/android/providers/media/PublicVolumeTest.java
index e2a272f..aaed1f9 100644
--- a/tests/src/com/android/providers/media/PublicVolumeTest.java
+++ b/tests/src/com/android/providers/media/PublicVolumeTest.java
@@ -30,6 +30,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
 import com.android.providers.media.tests.utils.PublicVolumeSetupHelper;
 import com.android.providers.media.util.FileUtils;
 
@@ -43,6 +44,7 @@
 import java.util.List;
 
 @RunWith(AndroidJUnit4.class)
+@RunOnlyOnPostsubmit
 public class PublicVolumeTest {
     static final int POLL_DELAY_MS = 500;
     static final int WAIT_FOR_DEFAULT_FOLDERS_MS = 30000;
diff --git a/tests/src/com/android/providers/media/TestConfigStore.java b/tests/src/com/android/providers/media/TestConfigStore.java
index dddddb2..ca38ecc 100644
--- a/tests/src/com/android/providers/media/TestConfigStore.java
+++ b/tests/src/com/android/providers/media/TestConfigStore.java
@@ -18,9 +18,12 @@
 
 import static java.util.Objects.requireNonNull;
 
+import android.util.Pair;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -32,37 +35,58 @@
  */
 public class TestConfigStore implements ConfigStore {
     private boolean mCloudMediaInPhotoPickerEnabled = false;
-    private @Nullable List<String> mAllowedCloudProviderPackages = null;
+
+    private boolean mPickerChoiceManagedSelectionEnabled = false;
+    private List<String> mAllowedCloudProviderPackages = Collections.emptyList();
     private @Nullable String mDefaultCloudProviderPackage = null;
-    private int mPickerSyncDelayMs = 0;
+    private List<Pair<Executor, Runnable>> mObservers = new ArrayList<>();
 
     public void enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(String... providers) {
         mAllowedCloudProviderPackages = Arrays.asList(providers);
-        enableCloudMediaFeature();
+        mCloudMediaInPhotoPickerEnabled = true;
+        notifyObservers();
     }
 
     public void enableCloudMediaFeature() {
         mCloudMediaInPhotoPickerEnabled = true;
+        notifyObservers();
     }
 
     public void clearAllowedCloudProviderPackagesAndDisableCloudMediaFeature() {
-        mAllowedCloudProviderPackages = null;
+        mAllowedCloudProviderPackages = Collections.emptyList();
         disableCloudMediaFeature();
+        notifyObservers();
     }
 
     public void disableCloudMediaFeature() {
         mCloudMediaInPhotoPickerEnabled = false;
+        notifyObservers();
+    }
+
+    /**
+     * Enables pickerChoiceManagedSelection flag in the test config.
+     */
+    public void enablePickerChoiceManagedSelectionEnabled() {
+        mPickerChoiceManagedSelectionEnabled = true;
     }
 
     @Override
     public @NonNull List<String> getAllowedCloudProviderPackages() {
-        return mAllowedCloudProviderPackages != null ? mAllowedCloudProviderPackages
-                : Collections.emptyList();
+        return mAllowedCloudProviderPackages;
+    }
+
+    public void setAllowedCloudProviderPackages(String... providers) {
+        if (providers.length == 0) {
+            mAllowedCloudProviderPackages = Collections.emptyList();
+        } else {
+            mAllowedCloudProviderPackages = Arrays.asList(providers);
+        }
+        notifyObservers();
     }
 
     @Override
     public boolean isCloudMediaInPhotoPickerEnabled() {
-        return mCloudMediaInPhotoPickerEnabled;
+        return mCloudMediaInPhotoPickerEnabled && !mAllowedCloudProviderPackages.isEmpty();
     }
 
     public void setDefaultCloudProviderPackage(@NonNull String packageName) {
@@ -81,15 +105,6 @@
         return mDefaultCloudProviderPackage;
     }
 
-    @Override
-    public int getPickerSyncDelayMs() {
-        return mPickerSyncDelayMs;
-    }
-
-    public void setPickerSyncDelayMs(int delay) {
-        mPickerSyncDelayMs = delay;
-    }
-
     @NonNull
     @Override
     public List<String> getTranscodeCompatManifest() {
@@ -103,7 +118,24 @@
     }
 
     @Override
+    public boolean isPickerChoiceManagedSelectionEnabled() {
+        return mPickerChoiceManagedSelectionEnabled;
+    }
+
+    @Override
     public void addOnChangeListener(@NonNull Executor executor, @NonNull Runnable listener) {
-        // No-op.
+        Pair p = Pair.create(executor, listener);
+        mObservers.add(p);
+    }
+
+
+    /**
+     * Runs all subscribers to the TestConfigStore.
+     */
+    private void notifyObservers() {
+        for (Pair<Executor, Runnable> observer: mObservers) {
+            // Run tasks in a synchronous manner to avoid test flakes.
+            observer.second.run();
+        }
     }
 }
diff --git a/tests/src/com/android/providers/media/TestDatabaseBackupAndRecovery.java b/tests/src/com/android/providers/media/TestDatabaseBackupAndRecovery.java
index 3bb7a07..fc2dc77 100644
--- a/tests/src/com/android/providers/media/TestDatabaseBackupAndRecovery.java
+++ b/tests/src/com/android/providers/media/TestDatabaseBackupAndRecovery.java
@@ -25,6 +25,7 @@
 import java.io.FileNotFoundException;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 
@@ -74,7 +75,7 @@
     }
 
     @Override
-    protected FuseDaemon getFuseDaemonForFileWithWait(File fuseFilePath, long waitTime)
+    protected FuseDaemon getFuseDaemonForFileWithWait(File fuseFilePath)
             throws FileNotFoundException {
         return null;
     }
@@ -87,4 +88,12 @@
     public void setBackedUpData(Map<String, BackupIdRow> backedUpData) {
         this.mBackedUpData = backedUpData;
     }
+
+    @Override
+    protected void removeRecoveryDataForUserId(int removedUserId) {
+    }
+
+    @Override
+    public void removeRecoveryDataExceptValidUsers(List<String> validUsers) {
+    }
 }
diff --git a/tests/src/com/android/providers/media/cloudproviders/CloudProviderPrimary.java b/tests/src/com/android/providers/media/cloudproviders/CloudProviderPrimary.java
index 39dc32f..8e62baa 100644
--- a/tests/src/com/android/providers/media/cloudproviders/CloudProviderPrimary.java
+++ b/tests/src/com/android/providers/media/cloudproviders/CloudProviderPrimary.java
@@ -16,6 +16,8 @@
 
 package com.android.providers.media.cloudproviders;
 
+import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_TOKEN;
+
 import static com.android.providers.media.PickerProviderMediaGenerator.MediaGenerator;
 
 import android.content.res.AssetFileDescriptor;
@@ -52,8 +54,11 @@
         final CloudProviderQueryExtras queryExtras =
                 CloudProviderQueryExtras.fromCloudMediaBundle(extras);
 
+        String pageToken = extras.getString(EXTRA_PAGE_TOKEN, null);
+
         return mMediaGenerator.getMedia(queryExtras.getGeneration(), queryExtras.getAlbumId(),
-                queryExtras.getMimeTypes(), queryExtras.getSizeBytes());
+                queryExtras.getMimeTypes(), queryExtras.getSizeBytes(), pageToken,
+                queryExtras.getPageSize());
     }
 
     @Override
@@ -61,7 +66,8 @@
         final CloudProviderQueryExtras queryExtras =
                 CloudProviderQueryExtras.fromCloudMediaBundle(extras);
 
-        return mMediaGenerator.getDeletedMedia(queryExtras.getGeneration());
+        String pageToken = extras.getString(EXTRA_PAGE_TOKEN, null);
+        return mMediaGenerator.getDeletedMedia(queryExtras.getGeneration(), pageToken);
     }
 
     @Override
@@ -69,8 +75,9 @@
         final CloudProviderQueryExtras queryExtras =
                 CloudProviderQueryExtras.fromCloudMediaBundle(extras);
 
+        String pageToken = extras.getString(EXTRA_PAGE_TOKEN, null);
         return mMediaGenerator.getAlbums(queryExtras.getMimeTypes(), queryExtras.getSizeBytes(),
-                /* isLocal */ false);
+                /* isLocal */ false, pageToken);
     }
 
     @Override
diff --git a/tests/src/com/android/providers/media/cloudproviders/CloudProviderSecondary.java b/tests/src/com/android/providers/media/cloudproviders/CloudProviderSecondary.java
index a00cbaf..5c3df94 100644
--- a/tests/src/com/android/providers/media/cloudproviders/CloudProviderSecondary.java
+++ b/tests/src/com/android/providers/media/cloudproviders/CloudProviderSecondary.java
@@ -16,6 +16,8 @@
 
 package com.android.providers.media.cloudproviders;
 
+import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_TOKEN;
+
 import static com.android.providers.media.PickerProviderMediaGenerator.MediaGenerator;
 
 import android.content.res.AssetFileDescriptor;
@@ -36,7 +38,7 @@
  * {@link MediaGenerator}
  */
 public class CloudProviderSecondary extends CloudMediaProvider {
-    private static final String AUTHORITY =
+    public static final String AUTHORITY =
             "com.android.providers.media.photopicker.tests.cloud_secondary";
 
     private final MediaGenerator mMediaGenerator =
@@ -52,8 +54,11 @@
         final CloudProviderQueryExtras queryExtras =
                 CloudProviderQueryExtras.fromCloudMediaBundle(extras);
 
+        String pageToken = extras.getString(EXTRA_PAGE_TOKEN, null);
+
         return mMediaGenerator.getMedia(queryExtras.getGeneration(), queryExtras.getAlbumId(),
-                queryExtras.getMimeTypes(), queryExtras.getSizeBytes());
+                queryExtras.getMimeTypes(), queryExtras.getSizeBytes(), pageToken,
+                queryExtras.getPageSize());
     }
 
     @Override
@@ -61,7 +66,8 @@
         final CloudProviderQueryExtras queryExtras =
                 CloudProviderQueryExtras.fromCloudMediaBundle(extras);
 
-        return mMediaGenerator.getDeletedMedia(queryExtras.getGeneration());
+        String pageToken = extras.getString(EXTRA_PAGE_TOKEN, null);
+        return mMediaGenerator.getDeletedMedia(queryExtras.getGeneration(), pageToken);
     }
 
     @Override
@@ -69,8 +75,9 @@
         final CloudProviderQueryExtras queryExtras =
                 CloudProviderQueryExtras.fromCloudMediaBundle(extras);
 
+        String pageToken = extras.getString(EXTRA_PAGE_TOKEN, null);
         return mMediaGenerator.getAlbums(queryExtras.getMimeTypes(), queryExtras.getSizeBytes(),
-                /* isLocal */ false);
+                /* isLocal */ false, pageToken);
     }
 
     @Override
diff --git a/tests/src/com/android/providers/media/cloudproviders/FlakyCloudProvider.java b/tests/src/com/android/providers/media/cloudproviders/FlakyCloudProvider.java
new file mode 100644
index 0000000..2d20574
--- /dev/null
+++ b/tests/src/com/android/providers/media/cloudproviders/FlakyCloudProvider.java
@@ -0,0 +1,170 @@
+/*
+ * 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.cloudproviders;
+
+import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_TOKEN;
+
+import static com.android.providers.media.PickerProviderMediaGenerator.MediaGenerator;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.graphics.Point;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.provider.CloudMediaProvider;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.providers.media.PickerProviderMediaGenerator;
+import com.android.providers.media.photopicker.data.CloudProviderQueryExtras;
+
+import java.io.FileNotFoundException;
+
+/**
+ * Implements a cloud {@link CloudMediaProvider} interface over items generated with {@link
+ * MediaGenerator}
+ *
+ * <p>This provider is intentionally very flaky and will throw a {@link RuntimeException} for two
+ * out of every three requests.
+ */
+public class FlakyCloudProvider extends CloudMediaProvider {
+    private static final String TAG = "FlakyCloudProvider";
+    public static final String AUTHORITY =
+            "com.android.providers.media.photopicker.tests.cloud_flaky";
+    public static final String ACCOUNT_NAME = "test_account@flakyCloudProvider";
+    private static final int INITIAL_REQUEST_COUNT = 0;
+    private static final int REQUEST_COUNT_FOR_NEXT_ONE_TO_FLAKE = 2;
+
+    private final MediaGenerator mMediaGenerator =
+            PickerProviderMediaGenerator.getMediaGenerator(AUTHORITY);
+
+    private int mRequestCount = INITIAL_REQUEST_COUNT;
+
+    /** Determines if the current request should flake. */
+    private boolean shouldFlake() {
+
+        // Always succeed on the first request.
+        if (++mRequestCount < REQUEST_COUNT_FOR_NEXT_ONE_TO_FLAKE) {
+            return false;
+        }
+
+        if (mRequestCount > REQUEST_COUNT_FOR_NEXT_ONE_TO_FLAKE) {
+            mRequestCount = INITIAL_REQUEST_COUNT;
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean onCreate() {
+        mMediaGenerator.setAccountInfo(ACCOUNT_NAME, /* configIntent= */ null);
+        return true;
+    }
+
+    @Override
+    public Cursor onQueryMedia(Bundle extras) {
+        final CloudProviderQueryExtras queryExtras =
+                CloudProviderQueryExtras.fromCloudMediaBundle(extras);
+
+        if (shouldFlake()) {
+            throw new RuntimeException("Simulating a crash in FlakyCloudProvider onQueryMedia");
+        }
+
+        String pageToken = extras.getString(EXTRA_PAGE_TOKEN, null);
+
+        return mMediaGenerator.getMedia(
+                queryExtras.getGeneration(),
+                queryExtras.getAlbumId(),
+                queryExtras.getMimeTypes(),
+                queryExtras.getSizeBytes(),
+                pageToken,
+                queryExtras.getPageSize());
+    }
+
+    @Override
+    public Cursor onQueryDeletedMedia(Bundle extras) {
+        final CloudProviderQueryExtras queryExtras =
+                CloudProviderQueryExtras.fromCloudMediaBundle(extras);
+
+        if (shouldFlake()) {
+            throw new RuntimeException(
+                    "Simulating a crash in FlakyCloudProvider onQueryDeletedMedia");
+        }
+
+        String pageToken = extras.getString(EXTRA_PAGE_TOKEN, null);
+
+        return mMediaGenerator.getDeletedMedia(queryExtras.getGeneration(), pageToken);
+    }
+
+    @Override
+    public Cursor onQueryAlbums(Bundle extras) {
+        final CloudProviderQueryExtras queryExtras =
+                CloudProviderQueryExtras.fromCloudMediaBundle(extras);
+
+        if (shouldFlake()) {
+            throw new RuntimeException("Simulating a crash in FlakyCloudProvider onQueryAlbums");
+        }
+
+        String pageToken = extras.getString(EXTRA_PAGE_TOKEN, null);
+
+        return mMediaGenerator.getAlbums(
+                queryExtras.getMimeTypes(),
+                queryExtras.getSizeBytes(), /* isLocal */
+                false,
+                pageToken);
+    }
+
+    @Override
+    public AssetFileDescriptor onOpenPreview(
+            String mediaId, Point size, Bundle extras, CancellationSignal signal)
+            throws FileNotFoundException {
+        throw new UnsupportedOperationException("onOpenPreview not supported");
+    }
+
+    @Override
+    public ParcelFileDescriptor onOpenMedia(
+            String mediaId, Bundle extras, CancellationSignal signal) throws FileNotFoundException {
+        throw new UnsupportedOperationException("onOpenMedia not supported");
+    }
+
+    @Override
+    public Bundle onGetMediaCollectionInfo(Bundle extras) {
+        if (shouldFlake()) {
+            try {
+                MILLISECONDS.sleep(/* timeout= */ 200L);
+            } catch (InterruptedException e) {
+                Log.d(TAG, "Error while sleep when should flake on get media collection info.", e);
+            }
+        }
+
+        return mMediaGenerator.getMediaCollectionInfo();
+    }
+
+    @VisibleForTesting
+    public void resetToNotFlakeInTheNextRequest() {
+        mRequestCount = INITIAL_REQUEST_COUNT;
+    }
+
+    @VisibleForTesting
+    public void setToFlakeInTheNextRequest() {
+        mRequestCount = REQUEST_COUNT_FOR_NEXT_ONE_TO_FLAKE;
+    }
+}
diff --git a/tests/src/com/android/providers/media/library/RunOnlyOnPostsubmit.java b/tests/src/com/android/providers/media/library/RunOnlyOnPostsubmit.java
new file mode 100644
index 0000000..4bc6072
--- /dev/null
+++ b/tests/src/com/android/providers/media/library/RunOnlyOnPostsubmit.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 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.library;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Tests marked with this annotation will only run on postsubmit and not on presubmit.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface RunOnlyOnPostsubmit {
+}
diff --git a/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java b/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
index 6d8c725..41c5614 100644
--- a/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
+++ b/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
@@ -32,17 +32,22 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import static org.junit.Assert.assertTrue;
+
 import android.Manifest;
 import android.app.Instrumentation;
 import android.app.UiAutomation;
 import android.content.ContentResolver;
+import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.CancellationSignal;
 import android.os.Environment;
+import android.os.OperationCanceledException;
 import android.os.ParcelFileDescriptor;
 import android.provider.CloudMediaProviderContract;
 import android.provider.MediaStore;
@@ -57,7 +62,9 @@
 import com.android.providers.media.TestConfigStore;
 import com.android.providers.media.cloudproviders.CloudProviderPrimary;
 import com.android.providers.media.photopicker.data.ItemsProvider;
+import com.android.providers.media.photopicker.data.PaginationParameters;
 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.google.common.io.ByteStreams;
@@ -75,6 +82,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 public class ItemsProviderTest {
     /**
@@ -102,13 +110,11 @@
     public void setUp() throws Exception {
         final UiAutomation uiAutomation = sInstrumentation.getUiAutomation();
         uiAutomation.adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
-                        Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
-                        Manifest.permission.READ_DEVICE_CONFIG,
-                        Manifest.permission.INTERACT_ACROSS_USERS);
+                Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
+                Manifest.permission.READ_DEVICE_CONFIG,
+                Manifest.permission.INTERACT_ACROSS_USERS);
 
         mConfigStore = new TestConfigStore();
-        // Remove sync delay to avoid flaky tests
-        mConfigStore.setPickerSyncDelayMs(0);
 
         final Context isolatedContext = new IsolatedContext(sTargetContext, /* tag */ "databases",
                 /* asFuseThread */ false, sTargetContext.getUser(), mConfigStore);
@@ -125,12 +131,13 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
-     * about {@link #ALBUM_ID_CAMERA}.
+     * Tests {@link ItemsProvider#getAllCategories(String[], UserId, CancellationSignal)}
+     * to return correct info about {@link AlbumColumns#ALBUM_ID_CAMERA}.
      */
     @Test
     public void testGetCategories_camera() throws Exception {
-        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
+        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null,
+                /* cancellationSignal*/ null);
         assertThat(c.getCount()).isEqualTo(0);
 
         // Create 1 image file in Camera dir to test
@@ -144,12 +151,13 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
-     * about {@link #ALBUM_ID_CAMERA}.
+     * Tests {@link ItemsProvider#getAllCategories(String[], UserId, CancellationSignal)} to return
+     * correct info about {@link AlbumColumns#ALBUM_ID_CAMERA}.
      */
     @Test
     public void testGetCategories_not_camera() throws Exception {
-        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
+        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null,
+                /* cancellationSignal*/ null);
         assertThat(c.getCount()).isEqualTo(0);
 
         // negative test case: image file which should not be returned in Camera category
@@ -163,12 +171,13 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
-     * about {@link #ALBUM_ID_VIDEOS}.
+     * Tests {@link ItemsProvider#getAllCategories(String[], UserId, CancellationSignal)} to return
+     * correct info about {@link AlbumColumns#ALBUM_ID_VIDEOS}.
      */
     @Test
     public void testGetCategories_videos() throws Exception {
-        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
+        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null,
+                /* cancellationSignal*/ null);
         assertThat(c.getCount()).isEqualTo(0);
 
         // Create 1 video file in Movies dir to test
@@ -182,12 +191,13 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
-     * about {@link #ALBUM_ID_VIDEOS}.
+     * Tests {@link ItemsProvider#getAllCategories(String[], UserId, CancellationSignal)} to return
+     * correct info about {@link AlbumColumns#ALBUM_ID_VIDEOS}.
      */
     @Test
     public void testGetCategories_not_videos() throws Exception {
-        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
+        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null,
+                /* cancellationSignal*/ null);
         assertThat(c.getCount()).isEqualTo(0);
 
         // negative test case: image file which should not be returned in Videos category
@@ -201,12 +211,13 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
-     * about {@link #ALBUM_ID_SCREENSHOTS}.
+     * Tests {@link ItemsProvider#getAllCategories(String[], UserId, CancellationSignal)} to return
+     * correct info about {@link AlbumColumns#ALBUM_ID_SCREENSHOTS}.
      */
     @Test
     public void testGetCategories_screenshots() throws Exception {
-        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
+        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null,
+                /* cancellationSignal*/ null);
         assertThat(c.getCount()).isEqualTo(0);
 
         // Create 1 image file in Screenshots dir to test
@@ -241,12 +252,13 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
-     * about {@link #ALBUM_ID_SCREENSHOTS}.
+     * Tests {@link ItemsProvider#getAllCategories(String[], UserId, CancellationSignal)} to return
+     * correct info about {@link AlbumColumns#ALBUM_ID_SCREENSHOTS}.
      */
     @Test
     public void testGetCategories_not_screenshots() throws Exception {
-        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
+        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null,
+                /* cancellationSignal*/ null);
         assertThat(c.getCount()).isEqualTo(0);
 
         // negative test case: image file which should not be returned in Screenshots category
@@ -260,12 +272,13 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
-     * about {@link AlbumColumns#ALBUM_ID_FAVORITES}.
+     * Tests {@link ItemsProvider#getAllCategories(String[], UserId, CancellationSignal)} to return
+     * correct info about {@link AlbumColumns#ALBUM_ID_FAVORITES}.
      */
     @Test
     public void testGetCategories_favorites() throws Exception {
-        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
+        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null,
+                /* cancellationSignal*/ null);
         assertThat(c.getCount()).isEqualTo(0);
 
         // positive test case: image file which should be returned in favorites category
@@ -280,12 +293,13 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
-     * about {@link AlbumColumns#ALBUM_ID_FAVORITES}.
+     * Tests {@link ItemsProvider#getAllCategories(String[], UserId, CancellationSignal)} to return
+     * correct info about {@link AlbumColumns#ALBUM_ID_FAVORITES}.
      */
     @Test
     public void testGetCategories_not_favorites() throws Exception {
-        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
+        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null,
+                /* cancellationSignal*/ null);
         assertThat(c.getCount()).isEqualTo(0);
 
         // negative test case: image file which should not be returned in favorites category
@@ -299,12 +313,13 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
-     * about {@link #ALBUM_ID_DOWNLOADS}.
+     * Tests {@link ItemsProvider#getAllCategories(String[], UserId, CancellationSignal)} to return
+     * correct info about {@link AlbumColumns#ALBUM_ID_DOWNLOADS}.
      */
     @Test
     public void testGetCategories_downloads() throws Exception {
-        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
+        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null,
+                /* cancellationSignal*/ null);
         assertThat(c.getCount()).isEqualTo(0);
 
         // Create 1 image file in Downloads dir to test
@@ -318,12 +333,13 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
-     * about {@link #ALBUM_ID_DOWNLOADS}.
+     * Tests {@link ItemsProvider#getAllCategories(String[], UserId, CancellationSignal)} to return
+     * correct info about {@link AlbumColumns#ALBUM_ID_DOWNLOADS}.
      */
     @Test
     public void testGetCategories_not_downloads() throws Exception {
-        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
+        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null,
+                /* cancellationSignal*/ null);
         assertThat(c.getCount()).isEqualTo(0);
 
         // negative test case: image file which should not be returned in Downloads category
@@ -337,12 +353,13 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
-     * about {@link #ALBUM_ID_VIDEOS}.
+     * Tests {@link ItemsProvider#getAllCategories(String[], UserId, CancellationSignal)} to return
+     * correct info about {@link AlbumColumns#ALBUM_ID_VIDEOS}.
      */
     @Test
     public void testGetCategories_camera_and_videos() throws Exception {
-        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
+        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null,
+                /* cancellationSignal*/ null);
         assertThat(c.getCount()).isEqualTo(0);
 
         // Create 1 video file in Camera dir to test
@@ -359,12 +376,13 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
-     * about {@link AlbumColumns#ALBUM_ID_FAVORITES}.
+     * Tests {@link ItemsProvider#getAllCategories(String[], UserId, CancellationSignal)} to return
+     * correct info about {@link AlbumColumns#ALBUM_ID_FAVORITES}.
      */
     @Test
     public void testGetCategories_screenshots_and_favorites() throws Exception {
-        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
+        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null,
+                /* cancellationSignal*/ null);
         assertThat(c.getCount()).isEqualTo(0);
 
         // Create 1 image file in Screenshots dir to test
@@ -382,12 +400,14 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllCategories(String[], UserId)} to return correct info
-     * about {@link AlbumColumns#ALBUM_ID_DOWNLOADS} and {@link AlbumColumns#ALBUM_ID_FAVORITES}.
+     * Tests {@link ItemsProvider#getAllCategories(String[], UserId, CancellationSignal)} to return
+     * correct info about {@link AlbumColumns#ALBUM_ID_DOWNLOADS} and
+     * {@link AlbumColumns#ALBUM_ID_FAVORITES}.
      */
     @Test
     public void testGetCategories_downloads_and_favorites() throws Exception {
-        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
+        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null,
+                /* cancellationSignal*/ null);
         assertThat(c.getCount()).isEqualTo(0);
 
         // Create 1 image file in Screenshots dir to test
@@ -405,8 +425,10 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} to return all
-     * images and videos.
+     * Tests
+     * {@link ItemsProvider#getAllItems(Category, PaginationParameters, String[], UserId,
+     * CancellationSignal)}
+     * to return all images and videos.
      */
     @Test
     public void testGetItems() throws Exception {
@@ -415,8 +437,9 @@
         File imageFile = assertCreateNewImage();
         File videoFile = assertCreateNewVideo();
         try {
-            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1,
-                    /* mimeType */ null, /* userId */ null);
+            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT,
+                    new PaginationParameters(),
+                    /* mimeType */ null, /* userId */ null, /* cancellationSignal */ null);
             assertThat(res).isNotNull();
             assertThat(res.getCount()).isEqualTo(2);
 
@@ -431,52 +454,235 @@
         }
     }
 
+    /**
+     * Tests
+     * {@link ItemsProvider#getAllItems(Category, PaginationParameters, String[], UserId,
+     * CancellationSignal)}
+     * (Category, int, String[], UserId)} to stop execution when cancellation signal
+     * is triggered before query execution.
+     */
+    @Test(expected = OperationCanceledException.class)
+    public void testGetItems_canceledBeforeQuery_ThrowsImmediately() throws Exception {
+        // Create 1 image and 1 video file to test
+        // Both files should be returned.
+        CancellationSignal cancellationSignal = new CancellationSignal();
+        cancellationSignal.cancel();
+
+        final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT,
+                new PaginationParameters(),
+                /* mimeType */ null, /* userId */ null,
+                /* cancellationSignal */ cancellationSignal);
+    }
+
+    /**
+     * Tests
+     * {@link ItemsProvider#getLocalItems(Category, PaginationParameters, String[], UserId,
+     * CancellationSignal)}
+     * (Category, int, String[], UserId)} to stop execution when cancellation signal
+     * is triggered before query execution.
+     */
+    @Test(expected = OperationCanceledException.class)
+    public void testGetLocalItems_canceledBeforeQuery_ThrowsImmediately() throws Exception {
+        CancellationSignal cancellationSignal = new CancellationSignal();
+        cancellationSignal.cancel();
+
+        mItemsProvider.getLocalItems(Category.DEFAULT,
+                new PaginationParameters(),
+                /* mimeType */ null, /* userId */ null,
+                /* cancellationSignal */ cancellationSignal);
+    }
+
+    /**
+     * Tests
+     * {@link ItemsProvider#getAllCategories(String[], UserId, CancellationSignal)}
+     * (Category, int, String[], UserId)} to stop execution when cancellation signal
+     * is triggered before query execution.
+     */
+    @Test(expected = OperationCanceledException.class)
+    public void testGetCategories_canceledBeforeQuery_ThrowsImmediately() throws Exception {
+        CancellationSignal cancellationSignal = new CancellationSignal();
+        cancellationSignal.cancel();
+
+        mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null, cancellationSignal);
+    }
+
+    /**
+     * Tests
+     * {@link ItemsProvider#getAllCategories(String[], UserId, CancellationSignal)}
+     * (Category, int, String[], UserId)} to stop execution when cancellation signal
+     * is triggered before query execution.
+     */
+    @Test(expected = OperationCanceledException.class)
+    public void testGetLocalCategories_canceledBeforeQuery_ThrowsImmediately() throws Exception {
+        CancellationSignal cancellationSignal = new CancellationSignal();
+        cancellationSignal.cancel();
+
+        mItemsProvider.getLocalCategories(/* mimeType */ null, /* userId */ null,
+                cancellationSignal);
+    }
+
+    /**
+     * Tests
+     * {@link ItemsProvider#getAllItems(Category, PaginationParameters, String[], UserId,
+     * CancellationSignal)}
+     * (Category, int, String[], UserId)} to return all
+     * images and videos.
+     */
     @Test
-    public void testGetItems_sortOrder() throws Exception {
+    public void testGetItems_withLimit() throws Exception {
+        // Create 10 new files.
+        List<File> imageFiles = assertCreateNewImagesWithDifferentDateModifiedTimes(10);
         try {
-            final long timeNow = System.nanoTime() / 1000;
-            final Uri imageFileDateNowPlus1Uri = prepareFileAndGetUri(
-                    new File(getDownloadsDir(),  "latest_" + IMAGE_FILE_NAME), timeNow + 1000);
-            final Uri imageFileDateNowUri
-                    = prepareFileAndGetUri(new File(getDcimDir(), IMAGE_FILE_NAME), timeNow);
-            final Uri videoFileDateNowUri
-                    = prepareFileAndGetUri(new File(getCameraDir(), VIDEO_FILE_NAME), timeNow);
+            // Set the limit and ensure that only that number of items are returned.
+            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT,
+                    new PaginationParameters(/* limit */ 5, /*dateBeforeMs*/ Long.MIN_VALUE, -1),
+                    /* mimeType */ null, /* userId */ null, /* cancellationSignal */ null);
+            assertThat(res).isNotNull();
 
-            // This is the list of uris based on the expected sort order of items returned by
-            // ItemsProvider#getAllItems
-            List<Uri> uris = new ArrayList<>();
-            // This is the latest image file
-            uris.add(imageFileDateNowPlus1Uri);
-            // Video file was scanned after image file, hence has higher _id than image file
-            uris.add(videoFileDateNowUri);
-            uris.add(imageFileDateNowUri);
-
-            try (Cursor cursor = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1,
-                    /* mimeType */ null, /* userId */ null)) {
-                assertThat(cursor).isNotNull();
-
-                final int expectedCount = uris.size();
-                assertThat(cursor.getCount()).isEqualTo(expectedCount);
-
-                int rowNum = 0;
-                assertThat(cursor.moveToFirst()).isTrue();
-                final int idColumnIndex = cursor.getColumnIndexOrThrow(MediaColumns.ID);
-                while (rowNum < expectedCount) {
-                    assertWithMessage("id at row:" + rowNum + " is expected to be"
-                            + " same as id in " + uris.get(rowNum))
-                            .that(String.valueOf(cursor.getLong(idColumnIndex)))
-                            .isEqualTo(uris.get(rowNum).getLastPathSegment());
-                    cursor.moveToNext();
-                    rowNum++;
-                }
-            }
+            // Since the limit was set to 5 only 5 items should be returned.
+            assertThat(res.getCount()).isEqualTo(5);
+            assertThatOnlyImagesVideos(res);
+            // Reset the cursor back. Cursor#moveToPosition(-1) will reset the position to -1,
+            // but since there is no such valid cursor position, it returns false.
+            assertThat(res.moveToPosition(-1)).isFalse();
         } finally {
-            deleteAllFilesNoThrow();
+            for (File file : imageFiles) {
+                file.delete();
+            }
         }
     }
 
     /**
-     * Tests {@link {@link ItemsProvider#getAllItems(Category, int, String[], UserId)}} does not
+     * Tests
+     * {@link ItemsProvider#getAllItems(Category, PaginationParameters, String[], UserId,
+     * CancellationSignal)}
+     * (Category, int, String[], UserId)} to return paginated items.
+     */
+    @Test
+    public void testGetItems_withPagination_sameDateModified() throws Exception {
+        // Create 10 new files, all with same time stamp.
+        List<File> imageFiles = assertCreateNewImagesWithSameDateModifiedTimes(
+                /* number of images */ 10);
+        try {
+            // all files should be returned.
+            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT,
+                    new PaginationParameters(),
+                    /* mimeType */ null, /* userId */ null, /* cancellationSignal */ null);
+            assertThat(res).isNotNull();
+            assertThat(res.getCount()).isEqualTo(10);
+            // create a list from the cursor.
+            List<Item> itemList = new ArrayList<>(10);
+            while (res.moveToNext()) {
+                Item item = Item.fromCursor(res, UserId.CURRENT_USER);
+                itemList.add(item);
+            }
+            res.moveToPosition(0);
+            assertThatOnlyImagesVideos(res);
+
+            // For this test, paginate the above list by returning second half of the items using
+            // the pagingParameters created by the middle item of the above list.
+            PaginationParameters paginationParameters = new PaginationParameters(
+                    /* pageSize */ 5,
+                    /* dateTaken for the last item of the previous page */
+                    itemList.get(4).getDateTaken(),
+                    /* rowId for the last item of the previous page */ itemList.get(4).getRowId());
+
+            // Now set pagination parameters and get items. Since all items have the same time
+            // taken
+            // the pagination would be based on rowIDs.
+            // Files after the middle item should be returned.
+            final Cursor res2 = mItemsProvider.getAllItems(Category.DEFAULT,
+                    paginationParameters, /* mimeType */ null, /* userId */ null,
+                    /* cancellationSignal */ null);
+            assertThat(res2).isNotNull();
+            // Only 5 items should be returned.
+            assertThat(res2.getCount()).isEqualTo(5);
+
+            // Verify that the second half of the expected list has been returned.
+            int itr = 5;
+            while (res2.moveToNext()) {
+                assertThat(Item.fromCursor(res2, UserId.CURRENT_USER).compareTo(
+                        itemList.get(itr))).isEqualTo(0);
+                itr++;
+            }
+            // Ensure all items were verified.
+            assertThat(itr).isEqualTo(10);
+
+            res2.moveToPosition(0);
+            assertThatOnlyImagesVideos(res2);
+        } finally {
+            for (File file : imageFiles) {
+                file.delete();
+            }
+        }
+    }
+
+    /**
+     * Tests {@link ItemsProvider#getAllItems(Category, PaginationParameters, String[], UserId)}
+     * (Category, int, String[], UserId)} to return paginated items.
+     */
+    @Test
+    public void testGetItems_withPagination_differentTimeModified() throws Exception {
+        // Create 10 new files, all with different time taken.
+        List<File> imageFiles = assertCreateNewImagesWithDifferentDateModifiedTimes(
+                /* number of images */ 10);
+        try {
+            // all files should be returned.
+            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT,
+                    new PaginationParameters(),
+                    /* mimeType */ null, /* userId */ null, /* cancellationSignal */ null);
+            assertThat(res).isNotNull();
+            assertThat(res.getCount()).isEqualTo(10);
+            // create a list from the cursor.
+            List<Item> itemList = new ArrayList<>(10);
+            while (res.moveToNext()) {
+                Item item = Item.fromCursor(res, UserId.CURRENT_USER);
+                itemList.add(item);
+            }
+            res.moveToPosition(0);
+            assertThatOnlyImagesVideos(res);
+
+            // For this test, paginate the above list by returning second half of the items using
+            // the pagingParameters created by the middle item of the above list.
+            PaginationParameters paginationParameters = new PaginationParameters(
+                    /* pageSize */ 5,
+                    /* dateTaken for the last item of the previous page */
+                    itemList.get(4).getDateTaken(),
+                    /* rowId for the last item of the previous page */ itemList.get(4).getRowId());
+
+            // Now set pagination parameters and get items.
+            // Files after the middle item should be returned.
+            final Cursor res2 = mItemsProvider.getAllItems(Category.DEFAULT,
+                    paginationParameters, /* mimeType */ null, /* userId */ null,
+                    /* cancellationSignal */ null);
+            assertThat(res2).isNotNull();
+            // Only 5 items should be returned.
+            assertThat(res2.getCount()).isEqualTo(5);
+
+            // Verify that the second half of the expected list has been returned.
+            int itr = 5;
+            while (res2.moveToNext()) {
+                assertThat(Item.fromCursor(res2, UserId.CURRENT_USER).compareTo(
+                        itemList.get(itr))).isEqualTo(0);
+                itr++;
+            }
+            // Ensure all items were verified.
+            assertThat(itr).isEqualTo(10);
+
+            res2.moveToPosition(0);
+            assertThatOnlyImagesVideos(res2);
+        } finally {
+            for (File file : imageFiles) {
+                file.delete();
+            }
+        }
+    }
+
+    /**
+     * Tests
+     * {@link ItemsProvider#getAllItems(Category, PaginationParameters, String[],
+     * UserId)} (Category, PaginationParameters, String[], UserId)} (Category, int, String[],
+     * UserId)}} does not
      * return hidden images/videos.
      */
     @Test
@@ -487,8 +693,9 @@
         File imageFileHidden = assertCreateNewImage(hiddenDir);
         File videoFileHidden = assertCreateNewVideo(hiddenDir);
         try {
-            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1,
-                    /* mimeType */ null, /* userId */ null);
+            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT,
+                    new PaginationParameters(),
+                    /* mimeType */ null, /* userId */ null, /* cancellationSignal */ null);
             assertThat(res).isNotNull();
             assertThat(res.getCount()).isEqualTo(0);
         } finally {
@@ -499,7 +706,10 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} to return all
+     * Tests
+     * {@link ItemsProvider#getAllItems(Category, PaginationParameters, String[], UserId)}
+     * (Category, PaginationParameters, String[], UserId)} (Category, int, String[], UserId)}
+     * to return all
      * images and videos based on the mimeType. Image mimeType should only return images.
      */
     @Test
@@ -509,8 +719,10 @@
         File imageFile = assertCreateNewImage();
         File videoFile = assertCreateNewVideo();
         try {
-            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1,
-                    /* mimeType */ new String[]{ "image/*"}, /* userId */ null);
+            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT,
+                    new PaginationParameters(),
+                    /* mimeType */ new String[]{"image/*"}, /* userId */ null,
+                    /* cancellationSignal */ null);
             assertThat(res).isNotNull();
             assertThat(res.getCount()).isEqualTo(1);
 
@@ -523,7 +735,10 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} to return all
+     * Tests
+     * {@link ItemsProvider#getAllItems(Category, PaginationParameters, String[], UserId)}
+     * (Category, PaginationParameters, String[], UserId)} (Category, int, String[], UserId)}
+     * to return all
      * images and videos based on the mimeType. Image mimeType should only return images.
      */
     @Test
@@ -531,8 +746,10 @@
         // Create a jpg file image. Tests negative use case, this should not be returned below.
         File imageFile = assertCreateNewImage();
         try {
-            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1,
-                    /* mimeType */ new String[]{"image/png"}, /* userId */ null);
+            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT,
+                    new PaginationParameters(),
+                    /* mimeType */ new String[]{"image/png"}, /* userId */ null,
+                    /* cancellationSignal */ null);
             assertThat(res).isNotNull();
             assertThat(res.getCount()).isEqualTo(0);
         } finally {
@@ -541,7 +758,10 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} does not return
+     * Tests
+     * {@link ItemsProvider#getAllItems(Category, PaginationParameters, String[], UserId)}
+     * (Category, PaginationParameters, String[], UserId)} (Category, int, String[], UserId)}
+     * does not return
      * hidden images/videos.
      */
     @Test
@@ -552,8 +772,10 @@
         File imageFileHidden = assertCreateNewImage(hiddenDir);
         File videoFileHidden = assertCreateNewVideo(hiddenDir);
         try {
-            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1,
-                    /* mimeType */ new String[]{"image/*"}, /* userId */ null);
+            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT,
+                    new PaginationParameters(),
+                    /* mimeType */ new String[]{"image/*"}, /* userId */ null,
+                    /* cancellationSignal */ null);
             assertThat(res).isNotNull();
             assertThat(res.getCount()).isEqualTo(0);
         } finally {
@@ -564,7 +786,111 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} to return all
+     * Tests {@link ItemsProvider#getLocalItemsForSelection(Category, List, String[],
+     * UserId, CancellationSignal)} to return only selected items from the media table for ids
+     * defined in the localId selection list.
+     */
+    @Test
+    public void testGetItemsImages_withLocalIdSelection() throws Exception {
+        List<Uri> imageFilesUris = assertCreateNewImagesWithSameDateModifiedTimesAndReturnUri(10);
+        // Put the id of random items from the inserted set. say 4th and 6th item.
+        ArrayList<Long> inputIds = new ArrayList<>(1);
+        inputIds.add(ContentUris.parseId(imageFilesUris.get(4)));
+        inputIds.add(ContentUris.parseId(imageFilesUris.get(6)));
+        ArrayList<Integer> inputIdsAsIntegers =
+                (ArrayList<Integer>) inputIds.stream().map(
+                        (Long id) -> Integer.valueOf(Math.toIntExact(id))).collect(
+                        Collectors.toList());
+        try {
+            // get the item objects for the provided ids.
+            final Cursor res = mItemsProvider.getLocalItemsForSelection(Category.DEFAULT,
+                    /* local id selection list */ inputIdsAsIntegers,
+                    /* mimeType */ new String[]{"image/*"}, /* userId */ null,
+                    /* cancellationSignal */ null);
+
+            // verify that the correct number of items are returned and that they have the correct
+            // ids.
+            assertThat(res).isNotNull();
+            assertThat(res.getCount()).isEqualTo(2);
+            res.moveToPosition(0);
+            while (res.moveToNext()) {
+                Item item = Item.fromCursor(res, UserId.CURRENT_USER);
+                assertTrue(inputIds.contains(Long.parseLong(item.getId())));
+            }
+            assertThatOnlyImages(res);
+        } finally {
+            // clean up.
+            deleteAllFilesNoThrow();
+        }
+    }
+
+    /**
+     * Tests {@link ItemsProvider#getLocalItemsForSelection(Category, List, String[],
+     * UserId, CancellationSignal)} to return only selected items from the media table for ids
+     * defined in the localId selection list.
+     */
+    @Test
+    public void testGetItemsImages_withLocalIdSelection_largeDataSet() throws Exception {
+        List<Uri> imageFilesUris = assertCreateNewImagesWithSameDateModifiedTimesAndReturnUri(200);
+        // Try to fetch all items via selection. 200 items, this will hit the split query and
+        // verify that it is working.
+        List<Integer> inputIdsAsIntegers = imageFilesUris.stream().map(ContentUris::parseId).map(
+                Long::intValue).collect(Collectors.toList());
+        try {
+            // get the item objects for the provided ids.
+            final Cursor res = mItemsProvider.getLocalItemsForSelection(Category.DEFAULT,
+                    /* local id selection list */ inputIdsAsIntegers,
+                    /* mimeType */ new String[]{"image/*"}, /* userId */ null,
+                    /* cancellationSignal */ null);
+
+            // verify that the correct number of items are returned and that they have the correct
+            // ids.
+            assertThat(res).isNotNull();
+            assertThat(res.getCount()).isEqualTo(inputIdsAsIntegers.size());
+            res.moveToPosition(0);
+            while (res.moveToNext()) {
+                Item item = Item.fromCursor(res, UserId.CURRENT_USER);
+                assertTrue(inputIdsAsIntegers.contains(Integer.parseInt(item.getId())));
+            }
+            assertThatOnlyImages(res);
+        } finally {
+            // clean up.
+            deleteAllFilesNoThrow();
+        }
+    }
+
+    /**
+     * Tests {@link ItemsProvider#getLocalItemsForSelection(Category, List, String[],
+     * UserId, CancellationSignal)} to return only selected items from the media table for ids
+     * defined in the localId selection list. Here the list is empty so the parameter is ignored and
+     * the list is returned without any selection.
+     */
+    @Test
+    public void testGetItemsImages_withLocalIdSelectionEmpty() throws Exception {
+        assertCreateNewImagesWithSameDateModifiedTimesAndReturnUri(10);
+        try {
+            // get the item objects for the empty list.
+            final Cursor res = mItemsProvider.getLocalItemsForSelection(Category.DEFAULT,
+                    /* local id selection list */ new ArrayList<>(),
+                    /* mimeType */ new String[]{"image/*"}, /* userId */ null,
+                    /* cancellationSignal */ null);
+
+            assertThat(res).isNotNull();
+            // All images are returned and selection is ignored.
+            assertThat(res.getCount()).isEqualTo(10);
+            assertThatOnlyImages(res);
+        } finally {
+            // clean up.
+            deleteAllFilesNoThrow();
+        }
+    }
+
+
+    /**
+     * Tests
+     * {@link ItemsProvider#getAllItems(Category, PaginationParameters, String[], UserId)}
+     * (Category, PaginationParameters, String[], UserId)} (Category, int, String[], UserId)}
+     * to return all
      * images and videos based on the mimeType. Video mimeType should only return videos.
      */
     @Test
@@ -574,8 +900,10 @@
         File imageFile = assertCreateNewImage();
         File videoFile = assertCreateNewVideo();
         try {
-            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1,
-                    /* mimeType */ new String[]{"video/*"}, /* userId */ null);
+            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT,
+                    new PaginationParameters(),
+                    /* mimeType */ new String[]{"video/*"}, /* userId */ null,
+                    /* cancellationSignal */ null);
             assertThat(res).isNotNull();
             assertThat(res.getCount()).isEqualTo(1);
 
@@ -588,7 +916,10 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} to return all
+     * Tests
+     * {@link ItemsProvider#getAllItems(Category, PaginationParameters, String[], UserId)}
+     * (Category, PaginationParameters, String[], UserId)} (Category, int, String[], UserId)}
+     * to return all
      * images and videos based on the mimeType. Image mimeType should only return images.
      */
     @Test
@@ -596,8 +927,10 @@
         // Create a mp4 video file. Tests positive use case, this should be returned below.
         File videoFile = assertCreateNewVideo();
         try {
-            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1,
-                    /* mimeType */ new String[]{"video/mp4"}, /* userId */ null);
+            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT,
+                    new PaginationParameters(),
+                    /* mimeType */ new String[]{"video/mp4"}, /* userId */ null,
+                    /* cancellationSignal */ null);
             assertThat(res).isNotNull();
             assertThat(res.getCount()).isEqualTo(1);
         } finally {
@@ -606,7 +939,9 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getAllItems(Category, int, String[], UserId)} does not return
+     * Tests
+     * {@link ItemsProvider#getAllItems(Category, PaginationParameters, String[], UserId)}
+     * (Category, PaginationParameters, String[], UserId)} does not return
      * hidden images/videos.
      */
     @Test
@@ -617,8 +952,10 @@
         File imageFileHidden = assertCreateNewImage(hiddenDir);
         File videoFileHidden = assertCreateNewVideo(hiddenDir);
         try {
-            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT, /* limit */ -1,
-                    /* mimeType */ new String[]{"video/*"}, /* userId */ null);
+            final Cursor res = mItemsProvider.getAllItems(Category.DEFAULT,
+                    new PaginationParameters(),
+                    /* mimeType */ new String[]{"video/*"}, /* userId */ null,
+                    /* cancellationSignal */ null);
 
             assertThat(res).isNotNull();
             assertThat(res.getCount()).isEqualTo(0);
@@ -630,23 +967,29 @@
     }
 
     /**
-     * Tests {@link ItemsProvider#getLocalItems(Category, int, String[], UserId)} to returns only
+     * Tests
+     * {@link ItemsProvider#getLocalItems(Category, PaginationParameters, String[], UserId)}
+     * to returns only
      * local content.
      */
     @Test
-    public void testGetLocalItems_withCloud() throws Exception {
+    public void testGetLocalItems_withCloudFeatureOn() throws Exception {
         File videoFile = assertCreateNewVideo();
         try {
             mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(
                     sTargetPackageName);
-            // Init cloud provider and add one item
-            setupCloudProvider((cloudMediaGenerator) -> {
-                cloudMediaGenerator.addMedia(null, "cloud_id1");
-            });
+            // Init cloud provider with no items. We cannot test for cloud items because
+            // getAllItems query does not block on cloud sync.
+            setupCloudProvider((cloudMediaGenerator) -> {});
+            mItemsProvider.initPhotoPickerData(/* albumId */ null,
+                    /* albumAuthority */ null,
+                    /*initLocalOnlyData */ false,
+                    UserId.CURRENT_USER);
 
-            // Verify that getLocalItems includes only local contents
-            try (Cursor c = mItemsProvider.getLocalItems(Category.DEFAULT, -1, new String[]{},
-                    UserId.CURRENT_USER)) {
+            // Verify that getLocalItems includes all local contents
+            try (Cursor c = mItemsProvider.getLocalItems(Category.DEFAULT,
+                    new PaginationParameters(), new String[]{},
+                    UserId.CURRENT_USER, /* cancellationSignal */ null)) {
                 assertThat(c.getCount()).isEqualTo(1);
 
                 assertThat(c.moveToFirst()).isTrue();
@@ -654,21 +997,17 @@
                         .isEqualTo(LOCAL_PICKER_PROVIDER_AUTHORITY);
             }
 
-            // Verify that getAllItems includes cloud items
-            try (Cursor c = mItemsProvider.getAllItems(Category.DEFAULT, -1, new String[]{},
-                    UserId.CURRENT_USER)) {
-                assertThat(c.getCount()).isEqualTo(2);
+            // Verify that getAllItems also includes local items. We cannot check for cloud items
+            // because getAllItems query does not block on cloud sync.
+            try (Cursor c = mItemsProvider.getAllItems(Category.DEFAULT,
+                    new PaginationParameters(), new String[]{},
+                    UserId.CURRENT_USER,
+                    /* cancellationSignal */ null)) {
+                assertThat(c.getCount()).isEqualTo(1);
 
                 // Verify that the first item is cloud item
                 assertThat(c.moveToFirst()).isTrue();
                 assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.AUTHORITY)))
-                        .isEqualTo(CloudProviderPrimary.AUTHORITY);
-                assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.ID))).isEqualTo(
-                        "cloud_id1");
-
-                // Verify that the second item is local item
-                assertThat(c.moveToNext()).isTrue();
-                assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.AUTHORITY)))
                         .isEqualTo(LOCAL_PICKER_PROVIDER_AUTHORITY);
             }
         } finally {
@@ -679,41 +1018,42 @@
     }
 
     @Test
-    public void testGetLocalItems_mergedAlbum_withCloud() throws Exception {
+    public void testGetLocalItems_mergedAlbum_withCloudFeatureOn() throws Exception {
         File videoFile = assertCreateNewVideo();
         Category videoAlbum = new Category(CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS,
                 LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 10, true);
         try {
             mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(
                     sTargetPackageName);
-            // Init cloud provider and add one item
-            setupCloudProvider((cloudMediaGenerator) -> {
-                cloudMediaGenerator.addMedia(null, "cloud_id1", null, "video/mp4", 0, 1024, false);
-            });
+            // Init cloud provider with no items. We cannot test for cloud items because
+            // getAllItems query does not block on cloud sync.
+            setupCloudProvider((cloudMediaGenerator) -> {});
+            mItemsProvider.initPhotoPickerData(/* albumId */ null,
+                    /* albumAuthority */ null,
+                    /*initLocalOnlyData */ false,
+                    UserId.CURRENT_USER);
 
-            // Verify that getLocalItems for merged album "Video" includes only local contents
-            try (Cursor c = mItemsProvider.getLocalItems(videoAlbum, -1, new String[]{},
-                    UserId.CURRENT_USER)) {
+            // Verify that getLocalItems for merged album "Video" includes all local contents
+            try (Cursor c = mItemsProvider.getLocalItems(videoAlbum,
+                    new PaginationParameters(), new String[]{},
+                    UserId.CURRENT_USER, /* cancellationSignal */ null)) {
                 assertThat(c.getCount()).isEqualTo(1);
                 assertThat(c.moveToFirst()).isTrue();
                 assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.AUTHORITY)))
                         .isEqualTo(LOCAL_PICKER_PROVIDER_AUTHORITY);
             }
 
-            // Verify that getAllItems for merged album "Video" also includes cloud contents
-            try (Cursor c = mItemsProvider.getAllItems(videoAlbum, -1, new String[]{},
-                    UserId.CURRENT_USER)) {
-                assertThat(c.getCount()).isEqualTo(2);
+            // Verify that getAllItems for merged album "Video" also includes all local contents.
+            // We cannot check for cloud items because getAllItems query does not block on cloud
+            // sync.
+            try (Cursor c = mItemsProvider.getAllItems(videoAlbum, new PaginationParameters(),
+                    new String[]{},
+                    UserId.CURRENT_USER,
+                    /* cancellationSignal */ null)) {
+                assertThat(c.getCount()).isEqualTo(1);
                 // Verify that the first item is cloud item
                 assertThat(c.moveToFirst()).isTrue();
                 assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.AUTHORITY)))
-                        .isEqualTo(CloudProviderPrimary.AUTHORITY);
-                assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.ID)))
-                        .isEqualTo("cloud_id1");
-
-                // Verify that the second item is local item
-                assertThat(c.moveToNext()).isTrue();
-                assertThat(c.getString(c.getColumnIndexOrThrow(MediaColumns.AUTHORITY)))
                         .isEqualTo(LOCAL_PICKER_PROVIDER_AUTHORITY);
             }
         } finally {
@@ -724,7 +1064,7 @@
     }
 
     @Test
-    public void testGetLocalCategories_withCloud() throws Exception {
+    public void testGetLocalCategories_withCloudFeatureOn() throws Exception {
         File videoFile = assertCreateNewVideo(getMoviesDir());
         File screenshotFile = assertCreateNewImage(getScreenshotsDir());
         final String cloudAlbum = "testAlbum";
@@ -739,10 +1079,14 @@
                         false);
                 cloudMediaGenerator.createAlbum(cloudAlbum);
             });
+            mItemsProvider.initPhotoPickerData(/* albumId */ null,
+                    /* albumAuthority */ null,
+                    /*initLocalOnlyData */ false,
+                    UserId.CURRENT_USER);
 
             // Verify that getLocalCategories only returns local albums
             try (Cursor c = mItemsProvider.getLocalCategories(/* mimeType */ null,
-                    /* userId */ null)) {
+                    /* userId */ null, /* cancellationSignal*/ null)) {
                 assertGetCategoriesMatchMultiple(c, Arrays.asList(
                         Pair.create(ALBUM_ID_VIDEOS, 1),
                         Pair.create(ALBUM_ID_SCREENSHOTS, 1)
@@ -752,8 +1096,9 @@
 
             // Verify that getAllCategories returns local + cloud albums
             try (Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null,
-                    /* userId */ null)) {
+                    /* userId */ null, /* cancellationSignal*/ null)) {
                 assertGetCategoriesMatchMultiple(c, Arrays.asList(
+                        Pair.create(ALBUM_ID_FAVORITES, 0),
                         Pair.create(ALBUM_ID_VIDEOS, 2),
                         Pair.create(ALBUM_ID_SCREENSHOTS, 1),
                         Pair.create(cloudAlbum, 1)
@@ -799,7 +1144,8 @@
             return;
         }
 
-        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null);
+        Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null,
+                /* cancellationSignal*/ null);
         assertThat(c).isNotNull();
         assertThat(c.getCount()).isEqualTo(1);
 
@@ -830,7 +1176,8 @@
     }
 
     private void assertCategoriesNoMatch(String expectedCategoryName) {
-        try (Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null)) {
+        try (Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null,
+                /* cancellationSignal*/ null)) {
             while (c != null && c.moveToNext()) {
                 final int nameColumnIndex = c.getColumnIndexOrThrow(AlbumColumns.DISPLAY_NAME);
                 final String categoryName = c.getString(nameColumnIndex);
@@ -840,7 +1187,8 @@
     }
 
     private void assertGetCategoriesMatchMultiple(List<Pair<String, Integer>> categories) {
-        try (Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null)) {
+        try (Cursor c = mItemsProvider.getAllCategories(/* mimeType */ null, /* userId */ null,
+                /* cancellationSignal*/ null)) {
             assertGetCategoriesMatchMultiple(c, categories);
         }
     }
@@ -960,6 +1308,42 @@
         }
     }
 
+    private List<File> assertCreateNewImagesWithDifferentDateModifiedTimes(int numberOfImages)
+            throws Exception {
+        List<File> imageFiles = new ArrayList<>();
+        for (int itr = 0; itr < numberOfImages; itr++) {
+            String fileName = TAG + "_file_" + String.valueOf(System.nanoTime()) + ".jpg";
+            imageFiles.add(assertCreateNewFileWithLastModifiedTime(getDownloadsDir(), fileName,
+                    System.nanoTime() / 1000));
+        }
+        return imageFiles;
+    }
+
+    private List<File> assertCreateNewImagesWithSameDateModifiedTimes(int numberOfImages)
+            throws Exception {
+        List<File> imageFiles = new ArrayList<>();
+        long currentTime = System.nanoTime() / 1000;
+        for (int itr = 0; itr < numberOfImages; itr++) {
+            String fileName = TAG + "_file_" + String.valueOf(System.nanoTime()) + ".jpg";
+            imageFiles.add(assertCreateNewFileWithLastModifiedTime(getDownloadsDir(), fileName,
+                    currentTime));
+        }
+        return imageFiles;
+    }
+
+
+    private List<Uri> assertCreateNewImagesWithSameDateModifiedTimesAndReturnUri(int numberOfImages)
+            throws Exception {
+        List<Uri> imageFiles = new ArrayList<>();
+        long currentTime = System.nanoTime() / 1000;
+        for (int itr = 0; itr < numberOfImages; itr++) {
+            String fileName = TAG + "_file_" + String.valueOf(System.nanoTime()) + ".jpg";
+            imageFiles.add(assertCreateNewFileWithLastModifiedTimeAndReturnUri(
+                    getDownloadsDir(), fileName, currentTime));
+        }
+        return imageFiles;
+    }
+
     private File assertCreateNewVideo(File dir) throws Exception {
         return assertCreateNewFile(dir, VIDEO_FILE_NAME);
     }
@@ -983,6 +1367,19 @@
         return file;
     }
 
+    private File assertCreateNewFileWithLastModifiedTime(File parentDir, String fileName,
+            long lastModifiedTime) throws Exception {
+        final File file = new File(parentDir, fileName);
+        prepareFileAndGetUri(file, lastModifiedTime);
+        return file;
+    }
+    private Uri assertCreateNewFileWithLastModifiedTimeAndReturnUri(File parentDir, String fileName,
+            long lastModifiedTime) throws Exception {
+        final File file = new File(parentDir, fileName);
+        return prepareFileAndGetUri(file, lastModifiedTime);
+    }
+
+
     private Uri prepareFileAndGetUri(File file, long lastModifiedTime) throws IOException {
         ensureParentExists(file.getParentFile());
 
@@ -1062,8 +1459,8 @@
     private void deleteAllFilesNoThrow() {
         try (Cursor c = mIsolatedResolver.query(
                 MediaStore.Files.getContentUri(VOLUME_EXTERNAL),
-                new String[] {MediaStore.MediaColumns.DATA}, null, null)) {
-            while(c.moveToNext()) {
+                new String[]{MediaStore.MediaColumns.DATA}, null, null)) {
+            while (c.moveToNext()) {
                 (new File(c.getString(
                         c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)))).delete();
             }
diff --git a/tests/src/com/android/providers/media/photopicker/LocalProvider.java b/tests/src/com/android/providers/media/photopicker/LocalProvider.java
index b1c1281..09c82b1 100644
--- a/tests/src/com/android/providers/media/photopicker/LocalProvider.java
+++ b/tests/src/com/android/providers/media/photopicker/LocalProvider.java
@@ -52,7 +52,7 @@
                 CloudProviderQueryExtras.fromCloudMediaBundle(extras);
 
         return mMediaGenerator.getMedia(queryExtras.getGeneration(), queryExtras.getAlbumId(),
-                queryExtras.getMimeTypes(), queryExtras.getSizeBytes());
+                queryExtras.getMimeTypes(), queryExtras.getSizeBytes(), queryExtras.getPageSize());
     }
 
     @Override
diff --git a/tests/src/com/android/providers/media/photopicker/NotificationContentObserverTest.java b/tests/src/com/android/providers/media/photopicker/NotificationContentObserverTest.java
new file mode 100644
index 0000000..7d3084c
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/NotificationContentObserverTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2023 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.net.Uri;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+public class NotificationContentObserverTest {
+    private static final String URI_UPDATE_MEDIA = "content://media/picker_internal/update/media";
+    private static final String URI_UPDATE_ALBUM_CONTENT =
+            "content://media/picker_internal/update/album_content";
+
+    private static final String KEY_MEDIA = "media";
+    private static final String KEY_ALBUM_CONTENT = "album_content";
+    private final NotificationContentObserver.ContentObserverCallback mObserverCallbackA =
+            spy(new TestableContentObserverCallback());
+    private final NotificationContentObserver.ContentObserverCallback mObserverCallbackB =
+            spy(new TestableContentObserverCallback());
+
+    private NotificationContentObserver mObserver;
+
+    @Before
+    public void setUp() {
+        mObserver = new NotificationContentObserver(null);
+    }
+
+    @Test
+    public void registerKeysToObserverCallback_correctKeys_registersCallback() {
+        mObserver.registerKeysToObserverCallback(Arrays.asList(KEY_MEDIA), mObserverCallbackA);
+        mObserver.registerKeysToObserverCallback(
+                Arrays.asList(KEY_ALBUM_CONTENT), mObserverCallbackB);
+
+        assertThat(mObserver.getUrisToCallback()).hasSize(2);
+        assertThat(mObserver.getUrisToCallback())
+                .containsEntry(Arrays.asList(KEY_MEDIA), mObserverCallbackA);
+        assertThat(mObserver.getUrisToCallback())
+                .containsEntry(Arrays.asList(KEY_ALBUM_CONTENT), mObserverCallbackB);
+
+        mObserver.registerKeysToObserverCallback(
+                Arrays.asList(KEY_MEDIA, KEY_ALBUM_CONTENT), mObserverCallbackB);
+
+        assertThat(mObserver.getUrisToCallback()).hasSize(3);
+        assertThat(mObserver.getUrisToCallback()).containsEntry(
+                Arrays.asList(KEY_MEDIA, KEY_ALBUM_CONTENT), mObserverCallbackB);
+    }
+
+    @Test
+    public void registerKeysToObserverCallback_incorrectKey_doesNotRegisterCallback() {
+        mObserver.registerKeysToObserverCallback(Arrays.asList("invalid_key"), mObserverCallbackA);
+
+        assertThat(mObserver.getUrisToCallback()).hasSize(0);
+    }
+
+    @Test
+    public void registerKeysToObserverCallback_atLeastOneValidKey_registersCallback() {
+        mObserver.registerKeysToObserverCallback(
+                Arrays.asList(KEY_ALBUM_CONTENT, "invalid_key"), mObserverCallbackB);
+
+        assertThat(mObserver.getUrisToCallback()).hasSize(1);
+    }
+
+    @Test
+    public void onChange_receivesCorrectMediaUri_invokesCallback() {
+        mObserver.registerKeysToObserverCallback(Arrays.asList(KEY_MEDIA), mObserverCallbackA);
+        String timestamp = "1063";
+
+        mObserver.onChange(false, Uri.parse(URI_UPDATE_MEDIA + "/" + timestamp));
+
+        verify(mObserverCallbackA).onNotificationReceived(timestamp, null);
+    }
+
+    @Test
+    public void onChange_receivesCorrectAlbumContentUri_invokesCallback() {
+        mObserver.registerKeysToObserverCallback(
+                Arrays.asList(KEY_ALBUM_CONTENT), mObserverCallbackB);
+        String albumId = "10";
+        String timestamp = "457801";
+
+        mObserver.onChange(false, Uri.parse(URI_UPDATE_ALBUM_CONTENT
+                + "/" + albumId + "/" + timestamp));
+
+        verify(mObserverCallbackB).onNotificationReceived(timestamp, albumId);
+    }
+
+    @Test
+    public void onChange_receivesIncorrectUri_doesNotInvokeCallback() {
+        mObserver.registerKeysToObserverCallback(
+                Arrays.asList(KEY_ALBUM_CONTENT), mObserverCallbackB);
+        String timestamp = "12345";
+
+        // Missing ablum-id
+        mObserver.onChange(false, Uri.parse(URI_UPDATE_ALBUM_CONTENT + "/" + timestamp));
+
+        verify(mObserverCallbackB, never()).onNotificationReceived(timestamp, null);
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java b/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
index 2524e68..5f6f269 100644
--- a/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
@@ -22,10 +22,16 @@
 import static com.android.providers.media.PickerProviderMediaGenerator.MediaGenerator;
 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LONG_DEFAULT;
 import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_DEFAULT;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.initializeTestWorkManager;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
 import android.content.Context;
 import android.content.Intent;
 import android.database.Cursor;
@@ -35,23 +41,34 @@
 import android.provider.MediaStore;
 import android.util.Pair;
 
+import androidx.annotation.NonNull;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
+import androidx.work.WorkManager;
 
 import com.android.modules.utils.BackgroundThread;
 import com.android.providers.media.PickerProviderMediaGenerator;
 import com.android.providers.media.TestConfigStore;
+import com.android.providers.media.photopicker.data.CloudProviderInfo;
 import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
 import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.data.PickerSyncRequestExtras;
+import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
+import com.android.providers.media.photopicker.sync.PickerSyncManager;
+import com.android.providers.media.util.ForegroundThread;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.io.File;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 @RunWith(AndroidJUnit4.class)
 public class PickerDataLayerTest {
@@ -103,6 +120,7 @@
     private PickerDbFacade mFacade;
     private PickerDataLayer mDataLayer;
     private PickerSyncController mController;
+    private TestConfigStore mConfigStore;
 
     @Before
     public void setUp() {
@@ -120,16 +138,22 @@
         final File dbPath = mContext.getDatabasePath(DB_NAME);
         dbPath.delete();
 
+        final PickerSyncLockManager lockManager = new PickerSyncLockManager();
+
         mDbHelper = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_1);
-        mFacade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY, mDbHelper);
+        mFacade = new PickerDbFacade(mContext, lockManager, LOCAL_PROVIDER_AUTHORITY, mDbHelper);
 
-        final TestConfigStore configStore = new TestConfigStore();
-        configStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(PACKAGE_NAME);
-        configStore.setPickerSyncDelayMs(0);
+        mConfigStore = new TestConfigStore();
+        mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(PACKAGE_NAME);
 
-        mController = new PickerSyncController(
-                mContext, mFacade, configStore, LOCAL_PROVIDER_AUTHORITY);
-        mDataLayer = new PickerDataLayer(mContext, mFacade, mController);
+        mController = PickerSyncController.initialize(
+                mContext, mFacade, mConfigStore, lockManager, LOCAL_PROVIDER_AUTHORITY);
+
+        initializeTestWorkManager(mContext);
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        final PickerSyncManager syncManager = new PickerSyncManager(
+                workManager, mContext, mConfigStore, /* schedulePeriodicSyncs */ false);
+        mDataLayer = new PickerDataLayer(mContext, mFacade, mController, mConfigStore, syncManager);
 
         // Set cloud provider to null to discard
         mFacade.setCloudProvider(null);
@@ -149,6 +173,8 @@
         addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
         addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1);
 
+        final PickerSyncRequestExtras syncRequestExtras = buildDefaultSyncRequestExtras();
+        mDataLayer.initMediaData(syncRequestExtras);
         try (Cursor cr = mDataLayer.fetchAllMedia(buildDefaultQueryArgs())) {
             assertThat(cr.getCount()).isEqualTo(2);
 
@@ -172,6 +198,8 @@
                 MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE, SIZE_BYTES, /* isFavorite */ false);
 
         final Bundle defaultQueryArgs = buildDefaultQueryArgs();
+        final PickerSyncRequestExtras syncRequestExtras = buildDefaultSyncRequestExtras();
+        mDataLayer.initMediaData(syncRequestExtras);
 
         try (Cursor cr = mDataLayer.fetchAllMedia(defaultQueryArgs)) {
             assertThat(cr.getCount()).isEqualTo(4);
@@ -203,6 +231,8 @@
                 MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE, SIZE_BYTES, /* isFavorite */ false);
 
         final Bundle defaultQueryArgs = buildDefaultQueryArgs();
+        final PickerSyncRequestExtras syncRequestExtras = buildDefaultSyncRequestExtras();
+        mDataLayer.initMediaData(syncRequestExtras);
 
         try (Cursor cr = mDataLayer.fetchAllMedia(defaultQueryArgs)) {
             assertThat(cr.getCount()).isEqualTo(4);
@@ -233,6 +263,8 @@
                 MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE, SIZE_BYTES, /* isFavorite */ false);
 
         final Bundle defaultQueryArgs = buildDefaultQueryArgs();
+        final PickerSyncRequestExtras syncRequestExtras = buildDefaultSyncRequestExtras();
+        mDataLayer.initMediaData(syncRequestExtras);
 
         try (Cursor cr = mDataLayer.fetchAllMedia(defaultQueryArgs)) {
             assertThat(cr.getCount()).isEqualTo(4);
@@ -263,6 +295,8 @@
                 MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE, SIZE_BYTES, /* isFavorite */ false);
 
         final Bundle defaultQueryArgs = buildDefaultQueryArgs();
+        final PickerSyncRequestExtras syncRequestExtras = buildDefaultSyncRequestExtras();
+        mDataLayer.initMediaData(syncRequestExtras);
 
         try (Cursor cr = mDataLayer.fetchAllMedia(defaultQueryArgs)) {
             assertThat(cr.getCount()).isEqualTo(4);
@@ -286,6 +320,8 @@
                 MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE, SIZE_BYTES, /* isFavorite */ false);
 
         final Bundle queryArgs = buildQueryArgs(IMAGE_MIME_TYPE, SIZE_BYTES_DEFAULT);
+        final PickerSyncRequestExtras syncRequestExtras = buildDefaultSyncRequestExtras();
+        mDataLayer.initMediaData(syncRequestExtras);
 
         try (Cursor cr = mDataLayer.fetchAllMedia(queryArgs)) {
             assertThat(cr.getCount()).isEqualTo(1);
@@ -305,6 +341,8 @@
                 MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE, SIZE_BYTES, /* isFavorite */ false);
 
         final Bundle queryArgs = buildQueryArgs(IMAGE_MIME_TYPE, SIZE_BYTES - 1);
+        final PickerSyncRequestExtras syncRequestExtras = buildDefaultSyncRequestExtras();
+        mDataLayer.initMediaData(syncRequestExtras);
 
         try (Cursor cr = mDataLayer.fetchAllMedia(queryArgs)) {
             assertThat(cr.getCount()).isEqualTo(1);
@@ -327,6 +365,8 @@
                 MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE, SIZE_BYTES, /* isFavorite */ false);
 
         final Bundle queryArgs = buildQueryArgs(VIDEO_MIME_TYPE, SIZE_BYTES - 1);
+        final PickerSyncRequestExtras syncRequestExtras = buildDefaultSyncRequestExtras();
+        mDataLayer.initMediaData(syncRequestExtras);
 
         try (Cursor cr = mDataLayer.fetchAllMedia(queryArgs)) {
             assertThat(cr.getCount()).isEqualTo(1);
@@ -343,6 +383,9 @@
         addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1);
 
         Bundle queryArgs = buildDefaultQueryArgs();
+        final PickerSyncRequestExtras syncRequestExtras = buildDefaultSyncRequestExtras();
+        mDataLayer.initMediaData(syncRequestExtras);
+
         // Verify that we only see local content
         try (Cursor cr = mDataLayer.fetchLocalMedia(queryArgs)) {
             assertThat(cr.getCount()).isEqualTo(1);
@@ -360,6 +403,7 @@
     }
 
     @Test
+    @Ignore("Enable when b/293112236 is done")
     public void testFetchAlbumMedia() {
         mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
 
@@ -377,6 +421,7 @@
 
         final Bundle defaultQueryArgs = buildDefaultQueryArgs();
 
+        mDataLayer.initMediaData(buildDefaultSyncRequestExtras());
         try (Cursor cr = mDataLayer.fetchAllAlbums(defaultQueryArgs)) {
             assertThat(cr.getCount()).isEqualTo(4);
 
@@ -402,25 +447,26 @@
 
         final Bundle localAlbumQueryArgs = buildQueryArgs(ALBUM_ID_1,
                 LOCAL_PROVIDER_AUTHORITY, MIME_TYPE_DEFAULT, SIZE_BYTES_DEFAULT);
-
-        final Bundle cloudAlbumQueryArgs = buildQueryArgs(ALBUM_ID_2,
-                CLOUD_PRIMARY_PROVIDER_AUTHORITY, MIME_TYPE_DEFAULT, SIZE_BYTES_DEFAULT);
-
-        final Bundle favoriteAlbumQueryArgs = buildQueryArgs(ALBUM_ID_FAVORITES,
-                LOCAL_PROVIDER_AUTHORITY, MIME_TYPE_DEFAULT, SIZE_BYTES_DEFAULT);
-
+        mDataLayer.initMediaData(buildSyncRequestExtras(ALBUM_ID_1, LOCAL_PROVIDER_AUTHORITY));
         try (Cursor cr = mDataLayer.fetchAllMedia(localAlbumQueryArgs)) {
             assertWithMessage("Local album count").that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
         }
 
+        final Bundle cloudAlbumQueryArgs = buildQueryArgs(ALBUM_ID_2,
+                CLOUD_PRIMARY_PROVIDER_AUTHORITY, MIME_TYPE_DEFAULT, SIZE_BYTES_DEFAULT);
+        mDataLayer.initMediaData(
+                buildSyncRequestExtras(ALBUM_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY));
         try (Cursor cr = mDataLayer.fetchAllMedia(cloudAlbumQueryArgs)) {
             assertWithMessage("Cloud album count").that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         }
 
+        final Bundle favoriteAlbumQueryArgs = buildQueryArgs(ALBUM_ID_FAVORITES,
+                LOCAL_PROVIDER_AUTHORITY, MIME_TYPE_DEFAULT, SIZE_BYTES_DEFAULT);
+        mDataLayer.initMediaData(buildDefaultSyncRequestExtras());
         try (Cursor cr = mDataLayer.fetchAllMedia(favoriteAlbumQueryArgs)) {
             assertWithMessage("Favorite album count").that(cr.getCount()).isEqualTo(2);
 
@@ -430,6 +476,7 @@
     }
 
     @Test
+    @Ignore("Enable when b/293112236 is done")
     public void testFetchAlbumMediaMimeTypeFilter() {
         mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
 
@@ -446,26 +493,32 @@
                 MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE, SIZE_BYTES, /* isFavorite */ false);
 
         final Bundle mimeTypeQueryArgs = buildQueryArgs(IMAGE_MIME_TYPE, SIZE_BYTES_DEFAULT);
+        final PickerSyncRequestExtras syncRequestExtras = buildDefaultSyncRequestExtras();
+        mDataLayer.initMediaData(syncRequestExtras);
 
         try (Cursor cr = mDataLayer.fetchAllAlbums(mimeTypeQueryArgs)) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertThat(cr.getCount()).isEqualTo(4);
 
+            // Favorites and Videos merged albums will be always visible
+            assertAlbumCursor(cr, ALBUM_ID_FAVORITES, LOCAL_PROVIDER_AUTHORITY);
+            assertAlbumCursor(cr, ALBUM_ID_VIDEOS, LOCAL_PROVIDER_AUTHORITY);
             assertAlbumCursor(cr, ALBUM_ID_1, LOCAL_PROVIDER_AUTHORITY);
             assertAlbumCursor(cr, ALBUM_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         }
 
         final Bundle localAlbumAndMimeTypeQueryArgs = buildQueryArgs(ALBUM_ID_1,
                 LOCAL_PROVIDER_AUTHORITY, IMAGE_MIME_TYPE, SIZE_BYTES_DEFAULT);
-
-        final Bundle cloudAlbumAndMimeTypeQueryArgs = buildQueryArgs(ALBUM_ID_2,
-                CLOUD_PRIMARY_PROVIDER_AUTHORITY, IMAGE_MIME_TYPE, SIZE_BYTES_DEFAULT);
-
+        mDataLayer.initMediaData(buildSyncRequestExtras(ALBUM_ID_1, LOCAL_PROVIDER_AUTHORITY));
         try (Cursor cr = mDataLayer.fetchAllMedia(localAlbumAndMimeTypeQueryArgs)) {
             assertWithMessage("Local album count").that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
         }
 
+        final Bundle cloudAlbumAndMimeTypeQueryArgs = buildQueryArgs(ALBUM_ID_2,
+                CLOUD_PRIMARY_PROVIDER_AUTHORITY, IMAGE_MIME_TYPE, SIZE_BYTES_DEFAULT);
+        mDataLayer.initMediaData(
+                buildSyncRequestExtras(ALBUM_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY));
         try (Cursor cr = mDataLayer.fetchAllMedia(cloudAlbumAndMimeTypeQueryArgs)) {
             assertWithMessage("Cloud album count").that(cr.getCount()).isEqualTo(1);
 
@@ -474,6 +527,7 @@
     }
 
     @Test
+    @Ignore("Enable when b/293112236 is done")
     public void testFetchAlbumMediaSizeFilter() {
         mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
 
@@ -492,10 +546,14 @@
                 MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE, SIZE_BYTES, /* isFavorite */ false);
 
         final Bundle sizeQueryArgs = buildQueryArgs(MIME_TYPE_DEFAULT, SIZE_BYTES - 1);
+        final PickerSyncRequestExtras syncRequestExtras = buildDefaultSyncRequestExtras();
+        mDataLayer.initMediaData(syncRequestExtras);
 
         try (Cursor cr = mDataLayer.fetchAllAlbums(sizeQueryArgs)) {
-            assertThat(cr.getCount()).isEqualTo(3);
+            assertThat(cr.getCount()).isEqualTo(4);
 
+            // Favorites and Videos merged albums will be always visible
+            assertAlbumCursor(cr, ALBUM_ID_FAVORITES, LOCAL_PROVIDER_AUTHORITY);
             assertAlbumCursor(cr, ALBUM_ID_VIDEOS, LOCAL_PROVIDER_AUTHORITY);
             assertAlbumCursor(cr, ALBUM_ID_1, LOCAL_PROVIDER_AUTHORITY);
             assertAlbumCursor(cr, ALBUM_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -503,16 +561,17 @@
 
         final Bundle localAlbumAndSizeQueryArgs = buildQueryArgs(ALBUM_ID_1,
                 LOCAL_PROVIDER_AUTHORITY, MIME_TYPE_DEFAULT, SIZE_BYTES -1);
-
-        final Bundle cloudAlbumAndSizeQueryArgs = buildQueryArgs(ALBUM_ID_2,
-                CLOUD_PRIMARY_PROVIDER_AUTHORITY, MIME_TYPE_DEFAULT, SIZE_BYTES -1);
-
+        mDataLayer.initMediaData(buildSyncRequestExtras(ALBUM_ID_1, LOCAL_PROVIDER_AUTHORITY));
         try (Cursor cr = mDataLayer.fetchAllMedia(localAlbumAndSizeQueryArgs)) {
             assertWithMessage("Local album count").that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
         }
 
+        final Bundle cloudAlbumAndSizeQueryArgs = buildQueryArgs(ALBUM_ID_2,
+                CLOUD_PRIMARY_PROVIDER_AUTHORITY, MIME_TYPE_DEFAULT, SIZE_BYTES - 1);
+        mDataLayer.initMediaData(
+                buildSyncRequestExtras(ALBUM_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY));
         try (Cursor cr = mDataLayer.fetchAllMedia(cloudAlbumAndSizeQueryArgs)) {
             assertWithMessage("Cloud album count").that(cr.getCount()).isEqualTo(1);
 
@@ -521,6 +580,7 @@
     }
 
     @Test
+    @Ignore("Enable when b/293112236 is done")
     public void testFetchAlbumMediaMimeTypeAndSizeFilter() {
         mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
 
@@ -539,21 +599,27 @@
                 MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE, SIZE_BYTES, /* isFavorite */ false);
 
         final Bundle mimeTypeAndSizeQueryArgs = buildQueryArgs(VIDEO_MIME_TYPE, SIZE_BYTES -1);
-
-        final Bundle cloudAlbumAndMimeTypeQueryArgs = buildQueryArgs(ALBUM_ID_2,
-                CLOUD_PRIMARY_PROVIDER_AUTHORITY, VIDEO_MIME_TYPE, SIZE_BYTES - 1);
+        final PickerSyncRequestExtras syncRequestExtras = buildDefaultSyncRequestExtras();
+        mDataLayer.initMediaData(syncRequestExtras);
 
         try (Cursor cr = mDataLayer.fetchAllAlbums(mimeTypeAndSizeQueryArgs)) {
-            assertWithMessage("Merged and Local album count").that(cr.getCount()).isEqualTo(3);
+            assertWithMessage("Merged and Local album count").that(cr.getCount()).isEqualTo(4);
 
             // Most recent video will be the cover of the Videos album. In this scenario, Videos
             // album cover was generated with cloud authority, so the Videos album authority should
             // be cloud provider authority.
+            // Favorites and Videos album will always be displayed.
+            assertAlbumCursor(cr, ALBUM_ID_FAVORITES, LOCAL_PROVIDER_AUTHORITY);
             assertAlbumCursor(cr, ALBUM_ID_VIDEOS, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
             assertAlbumCursor(cr, ALBUM_ID_1, LOCAL_PROVIDER_AUTHORITY);
             assertAlbumCursor(cr, ALBUM_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         }
 
+        final Bundle cloudAlbumAndMimeTypeQueryArgs = buildQueryArgs(ALBUM_ID_2,
+                CLOUD_PRIMARY_PROVIDER_AUTHORITY, VIDEO_MIME_TYPE, SIZE_BYTES - 1);
+        final PickerSyncRequestExtras cloudSyncRequestExtras =
+                buildSyncRequestExtras(ALBUM_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        mDataLayer.initMediaData(cloudSyncRequestExtras);
         try (Cursor cr = mDataLayer.fetchAllMedia(cloudAlbumAndMimeTypeQueryArgs)) {
             assertWithMessage("Cloud album count").that(cr.getCount()).isEqualTo(1);
 
@@ -562,6 +628,7 @@
     }
 
     @Test
+    @Ignore("Enable when b/293112236 is done")
     public void testFetchAlbumMediaLocalOnly() {
         mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
 
@@ -582,8 +649,10 @@
         // Favorites  - Merged Album - 2 files (1 local + 1 cloud)
 
         final Bundle defaultQueryArgs = buildDefaultQueryArgs();
+        mDataLayer.initMediaData(buildDefaultSyncRequestExtras());
         // Verify that we see both local and cloud albums
         try (Cursor cr = mDataLayer.fetchAllAlbums(defaultQueryArgs)) {
+            // Favorites and Videos merged albums will be always visible
             assertThat(cr.getCount()).isEqualTo(3);
         }
 
@@ -640,6 +709,96 @@
         assertThat(info.accountConfigurationIntent).isEqualTo(expectedIntent);
     }
 
+    @Test
+    public void testInitMediaDataInvalidData() {
+        final Bundle syncExtrasBundle = new Bundle();
+        syncExtrasBundle.putString(MediaStore.EXTRA_ALBUM_ID, "NotMergedAlbum");
+        syncExtrasBundle.putString(MediaStore.EXTRA_ALBUM_AUTHORITY, "NotLocalAuthority");
+        syncExtrasBundle.putBoolean(MediaStore.EXTRA_LOCAL_ONLY, true);
+        final PickerSyncRequestExtras syncExtras =
+                PickerSyncRequestExtras.fromBundle(syncExtrasBundle);
+
+        assertThrows(IllegalStateException.class,
+                () -> mDataLayer.initMediaData(syncExtras));
+    }
+
+    @Test
+    public void testCloudPackageAllowlistListenerRemovesActiveThatIsNowInvalid() {
+        mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        assertThat(mController.getCurrentCloudProviderInfo().packageName).isEqualTo(PACKAGE_NAME);
+
+        // Simulate a DeviceConfig change where the Allowlist is set to empty.
+        mConfigStore.setAllowedCloudProviderPackages(new String[] {});
+
+
+        // The listener uses the ForegroundThread to run the listener, so wait for the
+        // ForegroundThread to complete.
+        ForegroundThread.waitForIdle();
+
+        assertThat(mController.getCurrentCloudProviderInfo()).isEqualTo(CloudProviderInfo.EMPTY);
+    }
+
+    @Test
+    public void testCloudPackageAllowlistListenerDoesNotChangeAllowedProvider() {
+        mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        assertThat(mController.getCurrentCloudProviderInfo().packageName).isEqualTo(PACKAGE_NAME);
+
+        // Simulate a DeviceConfig change where the Allowlist adds a new provider, but the current
+        // provider is still permitted.
+        final String newlyAddedProviderPackage = "com.hooli.super.awesome.cloud.provider";
+        mConfigStore.setAllowedCloudProviderPackages(
+                new String[] {PACKAGE_NAME, newlyAddedProviderPackage});
+
+        // The listener uses the ForegroundThread to run the listener, so wait for the
+        // ForegroundThread to complete.
+        ForegroundThread.waitForIdle();
+
+        // Ensure nothing was changed.
+        assertThat(mController.getCurrentCloudProviderInfo().packageName).isEqualTo(PACKAGE_NAME);
+    }
+
+    @Test
+    public void testWaitForSyncWhenSyncFutureIsComplete()
+            throws ExecutionException, InterruptedException {
+        final CompletableFuture<Void> completableFuture = new CompletableFuture<>();
+        completableFuture.complete(null);
+
+        final int inputRetryCount = 3;
+        assertThat(mDataLayer
+                .waitForSync(completableFuture, "work-name", inputRetryCount))
+                .isEqualTo(inputRetryCount);
+    }
+
+    @Test
+    public void testWaitForSyncWhenSyncFutureNeverCompletes()
+            throws ExecutionException, InterruptedException, TimeoutException {
+        final PickerSyncManager mockSyncManager = mock(PickerSyncManager.class);
+        final PickerDataLayer dataLayer = new PickerDataLayer(mContext, mFacade, mController,
+                mConfigStore, mockSyncManager);
+        final CompletableFuture<Void> completableFuture = new CompletableFuture<>();
+        doReturn(true).when(mockSyncManager).isUniqueWorkPending(any());
+
+        final int inputRetryCount = 3;
+        assertThat(dataLayer
+                .waitForSync(completableFuture, "work-name", inputRetryCount))
+                .isEqualTo(0);
+    }
+
+    @Test
+    public void testWaitForSyncWhenWorkerFails()
+            throws ExecutionException, InterruptedException, TimeoutException {
+        final PickerSyncManager mockSyncManager = mock(PickerSyncManager.class);
+        final PickerDataLayer dataLayer = new PickerDataLayer(mContext, mFacade, mController,
+                mConfigStore, mockSyncManager);
+        final CompletableFuture<Void> completableFuture = new CompletableFuture<>();
+        doReturn(false).when(mockSyncManager).isUniqueWorkPending(any());
+
+        final int inputRetryCount = 3;
+        assertThat(dataLayer
+                .waitForSync(completableFuture, "work-name", inputRetryCount))
+                .isEqualTo(inputRetryCount);
+    }
+
     private static void waitForIdle() {
         final CountDownLatch latch = new CountDownLatch(1);
         BackgroundThread.getExecutor().execute(() -> {
@@ -678,6 +837,30 @@
         return queryArgs;
     }
 
+    @NonNull
+    private static PickerSyncRequestExtras buildDefaultSyncRequestExtras() {
+        return PickerSyncRequestExtras.fromBundle(buildDefaultSyncRequestBundle());
+    }
+
+    @NonNull
+    private static PickerSyncRequestExtras buildSyncRequestExtras(@NonNull String albumId,
+            @NonNull String albumAuthority) {
+        final Bundle syncRequestExtras = buildDefaultSyncRequestBundle();
+        syncRequestExtras.putString(MediaStore.EXTRA_ALBUM_ID, albumId);
+        syncRequestExtras.putString(MediaStore.EXTRA_ALBUM_AUTHORITY, albumAuthority);
+
+        return PickerSyncRequestExtras
+                .fromBundle(syncRequestExtras);
+    }
+
+    @NonNull
+    private static Bundle buildDefaultSyncRequestBundle() {
+        final Bundle syncRequestExtras = new Bundle();
+        syncRequestExtras.putBoolean(MediaStore.EXTRA_LOCAL_ONLY, false);
+
+        return syncRequestExtras;
+    }
+
     private static void addMedia(MediaGenerator generator, Pair<String, String> media) {
         generator.addMedia(media.first, media.second);
     }
diff --git a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
index 77f3bca..7408e4b 100644
--- a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
@@ -17,34 +17,43 @@
 package com.android.providers.media.photopicker;
 
 import static com.android.providers.media.PickerProviderMediaGenerator.MediaGenerator;
+import static com.android.providers.media.PickerUriResolver.REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI;
+import static com.android.providers.media.photopicker.NotificationContentObserver.MEDIA;
 
-import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Resources;
+import android.database.ContentObserver;
 import android.database.Cursor;
+import android.os.Handler;
 import android.os.Process;
-import android.os.SystemClock;
 import android.os.storage.StorageManager;
 import android.provider.CloudMediaProviderContract.MediaColumns;
 import android.util.Pair;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.modules.utils.BackgroundThread;
 import com.android.providers.media.PickerProviderMediaGenerator;
 import com.android.providers.media.TestConfigStore;
 import com.android.providers.media.photopicker.data.CloudProviderInfo;
 import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
 import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
+import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
 
 import org.junit.After;
 import org.junit.Before;
@@ -52,6 +61,7 @@
 import org.junit.runner.RunWith;
 
 import java.io.File;
+import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -60,6 +70,8 @@
 public class PickerSyncControllerTest {
     private static final String LOCAL_PROVIDER_AUTHORITY =
             "com.android.providers.media.photopicker.tests.local";
+    private static final String FLAKY_CLOUD_PROVIDER_AUTHORITY =
+            "com.android.providers.media.photopicker.tests.cloud_flaky";
     private static final String CLOUD_PRIMARY_PROVIDER_AUTHORITY =
             "com.android.providers.media.photopicker.tests.cloud_primary";
     private static final String CLOUD_SECONDARY_PROVIDER_AUTHORITY =
@@ -72,6 +84,8 @@
             PickerProviderMediaGenerator.getMediaGenerator(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
     private final MediaGenerator mCloudSecondaryMediaGenerator =
             PickerProviderMediaGenerator.getMediaGenerator(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
+    private final MediaGenerator mCloudFlakyMediaGenerator =
+            PickerProviderMediaGenerator.getMediaGenerator(FLAKY_CLOUD_PROVIDER_AUTHORITY);
 
     private static final String LOCAL_ID_1 = "1";
     private static final String LOCAL_ID_2 = "2";
@@ -79,6 +93,17 @@
     private static final String CLOUD_ID_1 = "1";
     private static final String CLOUD_ID_2 = "2";
     private static final String CLOUD_ID_3 = "3";
+    private static final String CLOUD_ID_4 = "4";
+    private static final String CLOUD_ID_5 = "5";
+    private static final String CLOUD_ID_6 = "6";
+    private static final String CLOUD_ID_7 = "7";
+    private static final String CLOUD_ID_8 = "8";
+    private static final String CLOUD_ID_9 = "9";
+    private static final String CLOUD_ID_10 = "10";
+    private static final String CLOUD_ID_11 = "11";
+    private static final String CLOUD_ID_12 = "12";
+    private static final String CLOUD_ID_13 = "13";
+    private static final String CLOUD_ID_14 = "14";
 
     private static final String ALBUM_ID_1 = "1";
     private static final String ALBUM_ID_2 = "2";
@@ -88,14 +113,23 @@
     private static final Pair<String, String> CLOUD_ONLY_1 = Pair.create(null, CLOUD_ID_1);
     private static final Pair<String, String> CLOUD_ONLY_2 = Pair.create(null, CLOUD_ID_2);
     private static final Pair<String, String> CLOUD_ONLY_3 = Pair.create(null, CLOUD_ID_3);
+    private static final Pair<String, String> CLOUD_ONLY_4 = Pair.create(null, CLOUD_ID_4);
+    private static final Pair<String, String> CLOUD_ONLY_5 = Pair.create(null, CLOUD_ID_5);
+    private static final Pair<String, String> CLOUD_ONLY_6 = Pair.create(null, CLOUD_ID_6);
+    private static final Pair<String, String> CLOUD_ONLY_7 = Pair.create(null, CLOUD_ID_7);
+    private static final Pair<String, String> CLOUD_ONLY_8 = Pair.create(null, CLOUD_ID_8);
+    private static final Pair<String, String> CLOUD_ONLY_9 = Pair.create(null, CLOUD_ID_9);
+    private static final Pair<String, String> CLOUD_ONLY_10 = Pair.create(null, CLOUD_ID_10);
+    private static final Pair<String, String> CLOUD_ONLY_11 = Pair.create(null, CLOUD_ID_11);
+    private static final Pair<String, String> CLOUD_ONLY_12 = Pair.create(null, CLOUD_ID_12);
+    private static final Pair<String, String> CLOUD_ONLY_13 = Pair.create(null, CLOUD_ID_13);
+    private static final Pair<String, String> CLOUD_ONLY_14 = Pair.create(null, CLOUD_ID_14);
     private static final Pair<String, String> CLOUD_AND_LOCAL_1
             = Pair.create(LOCAL_ID_1, CLOUD_ID_1);
 
     private static final String COLLECTION_1 = "1";
     private static final String COLLECTION_2 = "2";
 
-    private static final int SYNC_DELAY_MS = 1000;
-
     private static final int DB_VERSION_1 = 1;
     private static final int DB_VERSION_2 = 2;
     private static final String DB_NAME = "test_db";
@@ -104,32 +138,36 @@
     private TestConfigStore mConfigStore;
     private PickerDbFacade mFacade;
     private PickerSyncController mController;
+    private PickerSyncLockManager mLockManager;
 
     @Before
     public void setUp() {
         mLocalMediaGenerator.resetAll();
         mCloudPrimaryMediaGenerator.resetAll();
         mCloudSecondaryMediaGenerator.resetAll();
+        mCloudFlakyMediaGenerator.resetAll();
 
         mLocalMediaGenerator.setMediaCollectionId(COLLECTION_1);
         mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_1);
         mCloudSecondaryMediaGenerator.setMediaCollectionId(COLLECTION_1);
+        mCloudFlakyMediaGenerator.setMediaCollectionId(COLLECTION_1);
 
-        mContext = InstrumentationRegistry.getTargetContext();
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
 
         // Delete db so it's recreated on next access and previous test state is cleared
         final File dbPath = mContext.getDatabasePath(DB_NAME);
         dbPath.delete();
 
+        mLockManager = new PickerSyncLockManager();
+
         PickerDatabaseHelper dbHelper = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_1);
-        mFacade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY, dbHelper);
+        mFacade = new PickerDbFacade(mContext, mLockManager, LOCAL_PROVIDER_AUTHORITY, dbHelper);
 
         mConfigStore = new TestConfigStore();
         mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(PACKAGE_NAME);
-        mConfigStore.setPickerSyncDelayMs(0);
 
-        mController = new PickerSyncController(
-                mContext, mFacade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+        mController = PickerSyncController.initialize(
+                mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
 
         // Set cloud provider to null to avoid trying to sync it during other tests
         // that might be using an IsolatedContext
@@ -153,6 +191,81 @@
     }
 
     @Test
+    public void testInitCloudProviderOnDeviceConfigChange() {
+
+        TestConfigStore configStore = new TestConfigStore();
+        configStore.disableCloudMediaFeature();
+
+        PickerSyncController controller =
+                PickerSyncController.initialize(mContext, mFacade, configStore, mLockManager);
+        assertWithMessage(
+                "CloudProviderInfo should have been EMPTY when CloudMediaFeature is disabled.")
+                .that(controller.getCurrentCloudProviderInfo()).isEqualTo(CloudProviderInfo.EMPTY);
+        configStore.setDefaultCloudProviderPackage(PACKAGE_NAME);
+        configStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(PACKAGE_NAME);
+
+        // Ensure the cloud provider is set to something. (The test package name here actually
+        // has multiple cloud providers in it, so just ensure something got set.)
+        assertWithMessage("Failed to set cloud provider on config change.")
+                .that(controller.getCurrentCloudProviderInfo().authority).isNotNull();
+
+        configStore.clearAllowedCloudProviderPackagesAndDisableCloudMediaFeature();
+
+        // Ensure the cloud provider is correctly nulled out when the config changes again.
+        assertWithMessage("Failed to nullify cloud provider on config change.")
+                .that(controller.getCurrentCloudProviderInfo().authority).isNull();
+    }
+
+    @Test
+    public void testSyncIsCancelledIfCloudProviderIsChanged() throws UnableToAcquireLockException {
+
+        PickerSyncController controller = spy(mController);
+
+        // Ensure we return the appropriate authority until we actually enter the sync process,
+        // and then return a different authority than what the sync was started with to simulate
+        // a cloud provider changing.
+        doReturn(CLOUD_PRIMARY_PROVIDER_AUTHORITY,
+                CLOUD_SECONDARY_PROVIDER_AUTHORITY)
+                .when(controller)
+                .getCloudProviderWithTimeout();
+
+        // Add local only media, we expect these to be successfully sync'd from the local provider.
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_2);
+        mLocalMediaGenerator.setNextCursorExtras(
+                /* queryCount */ 2,
+                /* mediaCollectionId */ COLLECTION_1,
+                /* honoredSyncGeneration */ true,
+                /* honoredAlbumId */ false,
+                /* honoredPageSize */ true);
+
+        // Add cloud media, we should try to sync these, but not actually commit them since the
+        // cloud provider will be changed before the transaction can be committed.
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2);
+        mCloudPrimaryMediaGenerator.setNextCursorExtras(
+                /* queryCount */ 2,
+                /* mediaCollectionId */ COLLECTION_1,
+                /* honoredSyncGeneration */ true,
+                /* honoredAlbumId */ false,
+                /* honoredPageSize */ true);
+
+        controller.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        controller.syncAllMedia();
+
+        // The cursor should only contain the items from the local provider. (Even though we've
+        // added a total of 4 items to the linked providers.)
+        try (Cursor cr = queryMedia()) {
+            assertWithMessage("Cursor should only contain the items from the local provider.")
+                    .that(cr.getCount()).isEqualTo(2);
+
+            assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
+            assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
+        }
+
+    }
+
+    @Test
     public void testSyncAllMediaNoCloud() {
         // 1. Do nothing
         mController.syncAllMedia();
@@ -164,7 +277,9 @@
 
         mController.syncAllMedia();
         try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after adding two local only media.")
+                    .that(cr.getCount()).isEqualTo(2);
 
             assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
             assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
@@ -175,7 +290,10 @@
         mController.syncAllMedia();
 
         try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after deleting one local-only "
+                            + "media.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
         }
@@ -185,7 +303,10 @@
         mController.syncAllMedia();
 
         try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after resetting media without "
+                            + "version bump.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
         }
@@ -210,7 +331,10 @@
         mController.syncAlbumMedia(ALBUM_ID_1, true);
 
         try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, true)) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of album medias in album albumId = "
+                            + ALBUM_ID_1)
+                    .that(cr.getCount()).isEqualTo(2);
 
             assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
             assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
@@ -222,7 +346,10 @@
         mController.syncAlbumMedia(ALBUM_ID_1, true);
 
         try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, true)) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of album medias in album albumId = "
+                            + ALBUM_ID_1)
+                    .that(cr.getCount()).isEqualTo(2);
 
             assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
             assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
@@ -232,7 +359,10 @@
         mController.syncAlbumMedia(ALBUM_ID_2, true);
 
         try (Cursor cr = queryAlbumMedia(ALBUM_ID_2, true)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of album medias in album albumId = "
+                            + ALBUM_ID_2)
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
         }
@@ -243,7 +373,10 @@
 
         assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_1, true);
         try (Cursor cr = queryAlbumMedia(ALBUM_ID_2, true)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of album medias in album albumId = "
+                            + ALBUM_ID_2)
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
         }
@@ -269,7 +402,9 @@
         setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
 
         try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after syncing all media")
+                    .that(cr.getCount()).isEqualTo(2);
 
             assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
             assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -282,7 +417,9 @@
         // 5. Set primary cloud provider once again
         setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after second sync of all media.")
+                    .that(cr.getCount()).isEqualTo(2);
 
             assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
             assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -299,7 +436,9 @@
         addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1);
         setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after syncing all media.")
+                    .that(cr.getCount()).isEqualTo(1);
             assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         }
 
@@ -310,10 +449,12 @@
         // 3. Add another media in primary cloud provider
         addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2);
 
-        mController.syncAllMediaFromLocalProvider();
+        mController.syncAllMediaFromLocalProvider(/* cancellationSignal=*/ null);
         // Verify that the sync only synced local items
         try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(3);
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after local sync")
+                    .that(cr.getCount()).isEqualTo(3);
 
             assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
             assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
@@ -322,29 +463,6 @@
     }
 
     @Test
-    public void testSyncAllMediaResetsAlbumMedia() {
-        // 1. Set primary cloud provider
-        setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
-        assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_1, false);
-
-        // 2. Add album_media
-        addAlbumMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1.first, CLOUD_ONLY_1.second,
-                ALBUM_ID_1);
-        addAlbumMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2.first, CLOUD_ONLY_2.second,
-                ALBUM_ID_1);
-        mController.syncAlbumMedia(ALBUM_ID_1, false);
-
-        // 3. Assert non-empty album_media
-        try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
-            assertThat(cr.getCount()).isEqualTo(2);
-        }
-
-        // 4. Sync all media and assert empty album_media
-        mController.syncAllMedia();
-        assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_1, false);
-    }
-
-    @Test
     public void testSyncAllAlbumMediaCloudOnly() {
         // 1. Add media before setting primary cloud provider
         addAlbumMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1.first, CLOUD_ONLY_1.second,
@@ -364,7 +482,10 @@
         mController.syncAlbumMedia(ALBUM_ID_1, false);
 
         try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of album medias on queryAlbumMedia() after setting cloud "
+                            + "providers and syncing cloud album media")
+                    .that(cr.getCount()).isEqualTo(2);
 
             assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
             assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -379,7 +500,10 @@
         setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         mController.syncAlbumMedia(ALBUM_ID_1, false);
         try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of album medias on queryAlbumMedia() after setting cloud "
+                            + "providers and syncing cloud album media for the second time")
+                    .that(cr.getCount()).isEqualTo(2);
 
             assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
             assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -414,7 +538,10 @@
 
         mController.syncAlbumMedia(ALBUM_ID_1, false);
         try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of album media on queryAlbumMedia() after syncing first "
+                            + "album from cloud provider")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         }
@@ -431,7 +558,10 @@
         // 4a. Sync the first album and query local albums
         mController.syncAlbumMedia(ALBUM_ID_1, true);
         try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, true)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of album media on queryAlbumMedia() after syncing first "
+                            + "album from local provider")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
         }
@@ -439,7 +569,10 @@
         // 4b. Sync the second album
         mController.syncAlbumMedia(ALBUM_ID_2, true);
         try (Cursor cr = queryAlbumMedia(ALBUM_ID_2, true)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of album media on queryAlbumMedia() after syncing second "
+                            + "album from local provider")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
         }
@@ -447,7 +580,10 @@
         // 5. Sync and query cloud albums
         mController.syncAlbumMedia(ALBUM_ID_1, false);
         try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of album media on queryAlbumMedia() after syncing first "
+                            + "album from cloud provider")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         }
@@ -470,7 +606,9 @@
 
         mController.syncAllMedia();
         try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after syncing all media")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         }
@@ -485,7 +623,10 @@
         mController.syncAllMedia();
 
         try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after setting valid cloud version"
+                            + " and syncing all media.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         }
@@ -506,7 +647,10 @@
 
         mController.syncAlbumMedia(ALBUM_ID_1, false);
         try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of album media on queryAlbumMedia() after syncing album "
+                            + "from cloud provider")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         }
@@ -522,7 +666,10 @@
 
         mController.syncAlbumMedia(ALBUM_ID_1, false);
         try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of album media on queryAlbumMedia() after cloud provider "
+                            + "reset and syncing album from cloud provider")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         }
@@ -545,7 +692,9 @@
         setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
 
         try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after syncing all media")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
         }
@@ -555,7 +704,9 @@
         mController.syncAllMedia();
 
         try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after deleting local-only item.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         }
@@ -565,7 +716,9 @@
         mController.syncAllMedia();
 
         try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after re-adding local-only item.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
         }
@@ -575,7 +728,9 @@
         mController.syncAllMedia();
 
         try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after deleting cloud+local item.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
         }
@@ -590,64 +745,124 @@
     @Test
     public void testSetCloudProvider() {
         //1. Get local provider assertion out of the way
-        assertThat(mController.getLocalProvider()).isEqualTo(LOCAL_PROVIDER_AUTHORITY);
+        assertWithMessage("Unexpected local provider.")
+                .that(mController.getLocalProvider()).isEqualTo(LOCAL_PROVIDER_AUTHORITY);
 
         // Assert that no cloud provider set on facade
-        assertThat(mFacade.getCloudProvider()).isNull();
+        assertWithMessage("Facade cloud provider should have been null.")
+                .that(mFacade.getCloudProvider()).isNull();
 
         // 2. Can set cloud provider
-        assertThat(mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
-        assertThat(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        assertWithMessage("Failed to set cloud provider. ")
+                .that(mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
+        assertWithMessage("Unexpected cloud provider.")
+                .that(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
 
         // Assert that setting cloud provider clears facade cloud provider
         // And after syncing, the latest provider is set on the facade
-        assertThat(mFacade.getCloudProvider()).isNull();
+        assertWithMessage("Setting cloud provider failed to clear facade cloud provider.")
+                .that(mFacade.getCloudProvider()).isNull();
         mController.syncAllMedia();
-        assertThat(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        assertWithMessage("Failed to set latest provider on the facade post sync.")
+                .that(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
 
         // 3. Can clear cloud provider
-        assertThat(setCloudProviderAndSyncAllMedia(null)).isTrue();
-        assertThat(mController.getCloudProvider()).isNull();
+        assertWithMessage("Failed to clear cloud provider.")
+                .that(setCloudProviderAndSyncAllMedia(null)).isTrue();
+        assertWithMessage("Cloud provider should have been null.")
+                .that(mController.getCloudProvider()).isNull();
 
         // Assert that setting cloud provider clears facade cloud provider
         // And after syncing, the latest provider is set on the facade
-        assertThat(mFacade.getCloudProvider()).isNull();
+        assertWithMessage("Setting cloud provider failed to clear facade cloud provider.")
+                .that(mFacade.getCloudProvider()).isNull();
         mController.syncAllMedia();
-        assertThat(mFacade.getCloudProvider()).isNull();
+        assertWithMessage("Facade Cloud provider should have been null post sync.")
+                .that(mFacade.getCloudProvider()).isNull();
 
         // 4. Can set cloud proivder
-        assertThat(mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
-        assertThat(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        assertWithMessage("Failed to set cloud provider. ")
+                .that(mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
+        assertWithMessage("Unexpected cloud provider.")
+                .that(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
 
         // Assert that setting cloud provider clears facade cloud provider
         // And after syncing, the latest provider is set on the facade
-        assertThat(mFacade.getCloudProvider()).isNull();
+        assertWithMessage("Setting cloud provider failed to clear facade cloud provider.")
+                .that(mFacade.getCloudProvider()).isNull();
         mController.syncAllMedia();
-        assertThat(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        assertWithMessage("Failed to set latest provider on the facade post sync.")
+                .that(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
 
         // Invalid cloud provider is ignored
-        assertThat(setCloudProviderAndSyncAllMedia(LOCAL_PROVIDER_AUTHORITY)).isFalse();
-        assertThat(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        assertWithMessage("Setting invalid cloud provider should have failed.")
+                .that(setCloudProviderAndSyncAllMedia(LOCAL_PROVIDER_AUTHORITY)).isFalse();
+        assertWithMessage("Unexpected cloud provider.")
+                .that(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
 
         // Assert that unsuccessfully setting cloud provider doesn't clear facade cloud provider
         // And after syncing, nothing changes
-        assertThat(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        assertWithMessage(
+                "Unsuccessfully setting cloud provider should have failed to clear facade cloud "
+                        + "provider.")
+                .that(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         mController.syncAllMedia();
-        assertThat(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        assertWithMessage("Unexpected facade cloud provider post sync.")
+                .that(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
     }
 
     @Test
+    public void testEnableCloudQueriesAfterMPRestart() {
+        //1. Get local provider assertion out of the way
+        assertWithMessage("Unexpected local provider.")
+                .that(mController.getLocalProvider()).isEqualTo(LOCAL_PROVIDER_AUTHORITY);
+
+        // Assert that no cloud provider set on facade
+        assertWithMessage("Facade cloud provider should have been null.")
+                .that(mFacade.getCloudProvider()).isNull();
+
+        // 2. Can set cloud provider
+        assertWithMessage("Failed to set cloud provider.")
+                .that(mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
+        assertWithMessage("Unexpected cloud provider.")
+                .that(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        // Assert that setting cloud provider clears facade cloud provider
+        // And after syncing, the latest provider is set on the facade
+        assertWithMessage("Setting cloud provider failed to clear facade cloud provider.")
+                .that(mFacade.getCloudProvider()).isNull();
+        mController.syncAllMedia();
+        assertWithMessage("Unexpected facade cloud provider post sync.")
+                .that(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        // 3. Clear facade cloud provider to simulate MP restart.
+        mFacade.setCloudProvider(null);
+
+        // 4. Assert that latest provider is set in the facade after sync even when no sync was
+        // required.
+        mController.syncAllMedia();
+        assertWithMessage("Failed to set latest provider in the facade after MP restart.")
+                .that(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+    }
+
+
+    @Test
     public void testGetSupportedCloudProviders() {
         List<CloudProviderInfo> providers = mController.getAvailableCloudProviders();
 
-        CloudProviderInfo primaryInfo = new CloudProviderInfo(CLOUD_PRIMARY_PROVIDER_AUTHORITY,
-                PACKAGE_NAME,
-                Process.myUid());
-        CloudProviderInfo secondaryInfo = new CloudProviderInfo(CLOUD_SECONDARY_PROVIDER_AUTHORITY,
-                PACKAGE_NAME,
-                Process.myUid());
+        final CloudProviderInfo primaryInfo =
+                new CloudProviderInfo(
+                        CLOUD_PRIMARY_PROVIDER_AUTHORITY, PACKAGE_NAME, Process.myUid());
+        final CloudProviderInfo secondaryInfo =
+                new CloudProviderInfo(
+                        CLOUD_SECONDARY_PROVIDER_AUTHORITY, PACKAGE_NAME, Process.myUid());
+        final CloudProviderInfo flakyInfo =
+                new CloudProviderInfo(
+                        FLAKY_CLOUD_PROVIDER_AUTHORITY, PACKAGE_NAME, Process.myUid());
 
-        assertThat(providers).containsExactly(primaryInfo, secondaryInfo);
+        assertWithMessage(
+                "Unexpected cloud provider in the list returned by getAvailableCloudProviders().")
+                .that(providers).containsExactly(primaryInfo, secondaryInfo, flakyInfo);
     }
 
     @Test
@@ -656,36 +871,54 @@
                 CLOUD_PRIMARY_PROVIDER_AUTHORITY, PACKAGE_NAME, Process.myUid());
         final CloudProviderInfo secondaryInfo = new CloudProviderInfo(
                 CLOUD_SECONDARY_PROVIDER_AUTHORITY, PACKAGE_NAME, Process.myUid());
+        final CloudProviderInfo flakyInfo = new CloudProviderInfo(FLAKY_CLOUD_PROVIDER_AUTHORITY,
+                PACKAGE_NAME,
+                Process.myUid());
 
         mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(PACKAGE_NAME);
-        final PickerSyncController controller = new PickerSyncController(
-                mContext, mFacade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+        final PickerSyncController controller = PickerSyncController.initialize(
+                mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
         final List<CloudProviderInfo> providers = controller.getAvailableCloudProviders();
-        assertThat(providers).containsExactly(primaryInfo, secondaryInfo);
+        assertWithMessage(
+                "Unexpected cloud provider in the list returned by getAvailableCloudProviders() "
+                        + "when using allowList.")
+                .that(providers).containsExactly(primaryInfo, secondaryInfo, flakyInfo);
     }
 
     @Test
     public void testNotifyPackageRemoval_NoDefaultCloudProviderPackage() {
         mConfigStore.clearDefaultCloudProviderPackage();
 
-        assertThat(mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
-        assertThat(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        assertWithMessage("Failed to set cloud provider.")
+                .that(mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
+        assertWithMessage("Unexpected cloud provider.")
+                .that(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
 
         // Assert passing wrong package name doesn't clear the current cloud provider
         mController.notifyPackageRemoval(PACKAGE_NAME + "invalid");
-        assertThat(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        assertWithMessage(
+                "Unexpected cloud provider, passing wrong package shouldn't have cleared the "
+                        + "current cloud provider.")
+                .that(mController.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
 
         // Assert passing the current cloud provider package name clears the current cloud provider
         mController.notifyPackageRemoval(PACKAGE_NAME);
-        assertThat(mController.getCloudProvider()).isNull();
+        assertWithMessage(
+                "Unexpected cloud provider, passing current package should have cleared the "
+                        + "current cloud provider.")
+                .that(mController.getCloudProvider()).isNull();
 
         // Assert that the cloud provider state was not UNSET after the last cloud provider removal
         mConfigStore.setDefaultCloudProviderPackage(PACKAGE_NAME);
 
-        mController =
-                new PickerSyncController(mContext, mFacade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+        mController = PickerSyncController.initialize(mContext, mFacade, mConfigStore,
+                mLockManager, LOCAL_PROVIDER_AUTHORITY);
 
-        assertThat(mController.getCurrentCloudProviderInfo().packageName).isEqualTo(PACKAGE_NAME);
+        assertWithMessage(
+                "Unexpected cloud provider, cloud provider state got UNSET after the last cloud "
+                        + "provider removal")
+                .that(mController.getCurrentCloudProviderInfo().packageName).isEqualTo(
+                        PACKAGE_NAME);
     }
 
     // TODO(b/278687585): Add test for PickerSyncController#notifyPackageRemoval with a different
@@ -694,77 +927,64 @@
     @Test
     public void testSelectDefaultCloudProvider_NoDefaultAuthority() {
         PickerSyncController controller = createControllerWithDefaultProvider(null);
-        assertThat(controller.getCloudProvider()).isNull();
+        assertWithMessage("Default provider was set to null.")
+                .that(controller.getCloudProvider()).isNull();
     }
 
     @Test
     public void testSelectDefaultCloudProvider_defaultAuthoritySet() {
         PickerSyncController controller = createControllerWithDefaultProvider(PACKAGE_NAME);
-        assertThat(controller.getCurrentCloudProviderInfo().packageName).isEqualTo(PACKAGE_NAME);
+        assertWithMessage("Default provider was set to " + PACKAGE_NAME)
+                .that(controller.getCurrentCloudProviderInfo().packageName).isEqualTo(PACKAGE_NAME);
     }
 
     @Test
     public void testIsProviderAuthorityEnabled() {
-        assertThat(mController.isProviderEnabled(LOCAL_PROVIDER_AUTHORITY)).isTrue();
-        assertThat(mController.isProviderEnabled(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isFalse();
-        assertThat(mController.isProviderEnabled(CLOUD_SECONDARY_PROVIDER_AUTHORITY)).isFalse();
+        assertWithMessage("Expected " + LOCAL_PROVIDER_AUTHORITY + " to be enabled.")
+                .that(mController.isProviderEnabled(LOCAL_PROVIDER_AUTHORITY)).isTrue();
+        assertWithMessage("Expected " + CLOUD_PRIMARY_PROVIDER_AUTHORITY + " to be disabled")
+                .that(mController.isProviderEnabled(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isFalse();
+        assertWithMessage("Expected " + CLOUD_SECONDARY_PROVIDER_AUTHORITY + " to be disabled")
+                .that(mController.isProviderEnabled(CLOUD_SECONDARY_PROVIDER_AUTHORITY)).isFalse();
 
         setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
 
-        assertThat(mController.isProviderEnabled(LOCAL_PROVIDER_AUTHORITY)).isTrue();
-        assertThat(mController.isProviderEnabled(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
-        assertThat(mController.isProviderEnabled(CLOUD_SECONDARY_PROVIDER_AUTHORITY)).isFalse();
+        assertWithMessage("Expected " + LOCAL_PROVIDER_AUTHORITY + " to be enabled.")
+                .that(mController.isProviderEnabled(LOCAL_PROVIDER_AUTHORITY)).isTrue();
+        assertWithMessage("Expected " + CLOUD_PRIMARY_PROVIDER_AUTHORITY + " to be enabled.")
+                .that(mController.isProviderEnabled(CLOUD_PRIMARY_PROVIDER_AUTHORITY)).isTrue();
+        assertWithMessage("Expected " + CLOUD_SECONDARY_PROVIDER_AUTHORITY + " to be disabled.")
+                .that(mController.isProviderEnabled(CLOUD_SECONDARY_PROVIDER_AUTHORITY)).isFalse();
     }
 
     @Test
     public void testIsProviderUidEnabled() {
-        assertThat(mController.isProviderEnabled(LOCAL_PROVIDER_AUTHORITY, Process.myUid()))
+        assertWithMessage("Expected " + LOCAL_PROVIDER_AUTHORITY + " uid = " + Process.myUid()
+                + " to be enabled.")
+                .that(mController.isProviderEnabled(LOCAL_PROVIDER_AUTHORITY, Process.myUid()))
                 .isTrue();
-        assertThat(mController.isProviderEnabled(LOCAL_PROVIDER_AUTHORITY, 1000)).isFalse();
-    }
-
-    @Test
-    public void testNotifyMediaEvent() {
-        mConfigStore.clearAllowedCloudProviderPackagesAndDisableCloudMediaFeature();
-        mConfigStore.setPickerSyncDelayMs(SYNC_DELAY_MS);
-
-        final PickerSyncController controller = new PickerSyncController(
-                mContext, mFacade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
-
-        // 1. Add media and notify
-        addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
-        controller.notifyMediaEvent();
-        waitForIdle();
-        assertEmptyCursorFromMediaQuery();
-
-        // 2. Sleep for delay
-        SystemClock.sleep(SYNC_DELAY_MS);
-        waitForIdle();
-
-        try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(1);
-
-            assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
-        }
+        assertWithMessage(
+                "Expected " + LOCAL_PROVIDER_AUTHORITY + " uid = 1000" + " to be disabled.")
+                .that(mController.isProviderEnabled(LOCAL_PROVIDER_AUTHORITY, 1000)).isFalse();
     }
 
     @Test
     public void testSyncAfterDbCreate() {
         mConfigStore.clearAllowedCloudProviderPackagesAndDisableCloudMediaFeature();
-        mConfigStore.setPickerSyncDelayMs(0);
 
         final PickerDatabaseHelper dbHelper = new PickerDatabaseHelper(
                 mContext, DB_NAME, DB_VERSION_1);
-        PickerDbFacade facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
+        PickerDbFacade facade = new PickerDbFacade(mContext, mLockManager, LOCAL_PROVIDER_AUTHORITY,
                 dbHelper);
-        PickerSyncController controller = new PickerSyncController(
-                mContext, facade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+        PickerSyncController controller = PickerSyncController.initialize(
+                mContext, facade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
 
         addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
         controller.syncAllMedia();
 
         try (Cursor cr = queryMedia(facade)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage("Unexpected number of media after adding one local-only media.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
         }
@@ -775,20 +995,22 @@
         final File dbPath = mContext.getDatabasePath(DB_NAME);
         dbPath.delete();
 
-        facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY, dbHelper);
-        controller = new PickerSyncController(
-                mContext, facade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+        facade = new PickerDbFacade(mContext, mLockManager, LOCAL_PROVIDER_AUTHORITY, dbHelper);
+        controller = PickerSyncController.initialize(
+                mContext, facade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
 
         // Initially empty db
         try (Cursor cr = queryMedia(facade)) {
-            assertThat(cr.getCount()).isEqualTo(0);
+            assertWithMessage("Unexpected number of media after deleting and recreating the db.")
+                    .that(cr.getCount()).isEqualTo(0);
         }
 
         controller.syncAllMedia();
 
         // Fully synced db
         try (Cursor cr = queryMedia(facade)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage("Unexpected number of media after fully syncing the recreated db.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
         }
@@ -797,19 +1019,19 @@
     @Test
     public void testSyncAfterDbUpgrade() {
         mConfigStore.clearAllowedCloudProviderPackagesAndDisableCloudMediaFeature();
-        mConfigStore.setPickerSyncDelayMs(SYNC_DELAY_MS);
 
         PickerDatabaseHelper dbHelperV1 = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_1);
-        PickerDbFacade facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
+        PickerDbFacade facade = new PickerDbFacade(mContext, mLockManager, LOCAL_PROVIDER_AUTHORITY,
                 dbHelperV1);
-        PickerSyncController controller = new PickerSyncController(
-                mContext, facade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+        PickerSyncController controller = PickerSyncController.initialize(
+                mContext, facade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
 
         addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
         controller.syncAllMedia();
 
         try (Cursor cr = queryMedia(facade)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage("Unexpected number of media after adding one local-only media.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
         }
@@ -817,20 +1039,22 @@
         // Upgrade db version
         dbHelperV1.close();
         PickerDatabaseHelper dbHelperV2 = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_2);
-        facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY, dbHelperV2);
-        controller = new PickerSyncController(
-                mContext, facade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+        facade = new PickerDbFacade(mContext, mLockManager, LOCAL_PROVIDER_AUTHORITY, dbHelperV2);
+        controller = PickerSyncController.initialize(
+                mContext, facade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
 
         // Initially empty db
         try (Cursor cr = queryMedia(facade)) {
-            assertThat(cr.getCount()).isEqualTo(0);
+            assertWithMessage("Unexpected number of media after upgrading the db version.")
+                    .that(cr.getCount()).isEqualTo(0);
         }
 
         controller.syncAllMedia();
 
         // Fully synced db
         try (Cursor cr = queryMedia(facade)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage("Unexpected number of media after fully syncing the upgraded db.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
         }
@@ -839,19 +1063,19 @@
     @Test
     public void testSyncAfterDbDowngrade() {
         mConfigStore.clearAllowedCloudProviderPackagesAndDisableCloudMediaFeature();
-        mConfigStore.setPickerSyncDelayMs(SYNC_DELAY_MS);
 
         PickerDatabaseHelper dbHelperV2 = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_2);
-        PickerDbFacade facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
+        PickerDbFacade facade = new PickerDbFacade(mContext, mLockManager, LOCAL_PROVIDER_AUTHORITY,
                 dbHelperV2);
-        PickerSyncController controller = new PickerSyncController(
-                mContext, facade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+        PickerSyncController controller = PickerSyncController.initialize(
+                mContext, facade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
 
         addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
         controller.syncAllMedia();
 
         try (Cursor cr = queryMedia(facade)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage("Unexpected number of media after adding one local-only media.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
         }
@@ -859,21 +1083,23 @@
         // Downgrade db version
         dbHelperV2.close();
         PickerDatabaseHelper dbHelperV1 = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_1);
-        facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
+        facade = new PickerDbFacade(mContext, mLockManager, LOCAL_PROVIDER_AUTHORITY,
                 dbHelperV1);
-        controller = new PickerSyncController(
-                mContext, facade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+        controller = PickerSyncController.initialize(
+                mContext, facade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
 
         // Initially empty db
         try (Cursor cr = queryMedia(facade)) {
-            assertThat(cr.getCount()).isEqualTo(0);
+            assertWithMessage("Unexpected number of media after downgrading the db version.")
+                    .that(cr.getCount()).isEqualTo(0);
         }
 
         controller.syncAllMedia();
 
         // Fully synced db
         try (Cursor cr = queryMedia(facade)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage("Unexpected number of media after fully syncing the downgraded db.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
         }
@@ -886,7 +1112,7 @@
 
         // 2. Force the next 2 syncs (including retry) to have correct extra_media_collection_id
         mCloudPrimaryMediaGenerator.setNextCursorExtras(2, COLLECTION_1,
-                /* honoredSyncGeneration */ true, /* honoredAlbumId */ false);
+                /* honoredSyncGeneration */ true, /* honoredAlbumId */ false, true);
 
         // 4. Add cloud media
         addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1);
@@ -894,20 +1120,26 @@
         // 5. Sync and verify media
         mController.syncAllMedia();
         try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media on syncing all media with correct "
+                            + "extra_media_collection_id")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         }
 
         // 6. Force the next sync (without retry) to have incorrect extra_media_collection_id
         mCloudPrimaryMediaGenerator.setNextCursorExtras(1, COLLECTION_2,
-                /* honoredSyncGeneration */ true, /* honoredAlbumId */ false);
+                /* honoredSyncGeneration */ true, /* honoredAlbumId */ false, true);
         addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2);
 
         // 7. Sync and verify media after retry succeeded
         mController.syncAllMedia();
         try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of media on syncing all media with incorrect "
+                            + "extra_media_collection_id")
+                    .that(cr.getCount()).isEqualTo(2);
 
             assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
             assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -915,7 +1147,7 @@
 
         // 8. Force the next 2 syncs (including retry) to have incorrect extra_media_collection_id
         mCloudPrimaryMediaGenerator.setNextCursorExtras(2, COLLECTION_2,
-                /* honoredSyncGeneration */ true, /* honoredAlbumId */ false);
+                /* honoredSyncGeneration */ true, /* honoredAlbumId */ false, true);
         addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_3);
 
         // 9. Sync and verify media was reset
@@ -928,9 +1160,9 @@
         // 1. Set cloud provider
         setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
 
-        // 2. Force the next 2 syncs (including retry) to have correct extra_media_collection_id
+        // 2. Force the next 2 syncs (including retry) to have correct extra_honored_args
         mCloudPrimaryMediaGenerator.setNextCursorExtras(2, COLLECTION_1,
-                /* honoredSyncGeneration */ true, /* honoredAlbumId */ false);
+                /* honoredSyncGeneration */ true, /* honoredAlbumId */ false, true);
 
         // 3. Add cloud media
         addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1);
@@ -938,20 +1170,50 @@
         // 4. Sync and verify media
         mController.syncAllMedia();
         try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media on syncing all media with correct "
+                            + "extra_honored_args")
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         }
 
         // 5. Force the next sync (without retry) to have incorrect extra_honored_args
         mCloudPrimaryMediaGenerator.setNextCursorExtras(1, COLLECTION_1,
-                /* honoredSyncGeneration */ false, /* honoredAlbumId */ false);
+                /* honoredSyncGeneration */ false, /* honoredAlbumId */ false, true);
         addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2);
 
         // 6. Sync and verify media after retry succeeded
         mController.syncAllMedia();
         try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of media on syncing all media with incorrect "
+                            + "extra_honored_args")
+                    .that(cr.getCount()).isEqualTo(2);
+
+            assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        }
+    }
+
+    @Test
+    public void testSyncAllMedia_missingOptionalHonoredArgs_displaysCloud() {
+        // 1. Set cloud provider
+        setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        // 2. Add media before syncing again with the cloud provider
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2);
+
+        // 3. Force next sync to not honour page size
+        mCloudPrimaryMediaGenerator.setNextCursorExtras(2, COLLECTION_1,
+                /* honoredSyncGeneration */ true, /* honoredAlbumId */ false, false);
+
+        // 4. Sync and verify media
+        mController.syncAllMedia();
+        try (Cursor cr = queryMedia()) {
+            assertWithMessage("Unexpected number of media")
+                    .that(cr.getCount()).isEqualTo(/* expected= */ 2);
 
             assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
             assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -965,7 +1227,7 @@
 
         // 2. Force the next sync to have correct extra_media_collection_id
         mCloudPrimaryMediaGenerator.setNextCursorExtras(1, COLLECTION_1,
-                /* honoredSyncGeneration */ false, /* honoredAlbumId */ true);
+                /* honoredSyncGeneration */ false, /* honoredAlbumId */ true, true);
 
         // 3. Add cloud album_media
         addAlbumMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1.first, CLOUD_ONLY_1.second,
@@ -974,14 +1236,16 @@
         // 4. Sync and verify album_media
         mController.syncAlbumMedia(ALBUM_ID_1, false);
         try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of album media from album with albumId = " + ALBUM_ID_1)
+                    .that(cr.getCount()).isEqualTo(1);
 
             assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         }
 
         // 5. Force the next sync to have incorrect extra_album_id
         mCloudPrimaryMediaGenerator.setNextCursorExtras(1, COLLECTION_1,
-                /* honoredSyncGeneration */ false, /* honoredAlbumId */ false);
+                /* honoredSyncGeneration */ false, /* honoredAlbumId */ false, true);
 
         // 6. Sync and verify album_media is empty
         mController.syncAlbumMedia(ALBUM_ID_1, false);
@@ -991,26 +1255,27 @@
     @Test
     public void testUserPrefsAfterDbUpgrade() {
         mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(PACKAGE_NAME);
-        mConfigStore.setPickerSyncDelayMs(SYNC_DELAY_MS);
 
         PickerDatabaseHelper dbHelperV1 = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_1);
-        PickerDbFacade facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
+        PickerDbFacade facade = new PickerDbFacade(mContext, mLockManager, LOCAL_PROVIDER_AUTHORITY,
                 dbHelperV1);
-        PickerSyncController controller =
-                new PickerSyncController(mContext, facade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+        PickerSyncController controller = PickerSyncController.initialize(
+                mContext, facade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
 
         controller.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
-        assertThat(controller.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        assertWithMessage("Unexpected cloud provider on db set up.")
+                .that(controller.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
 
         // Downgrade db version
         dbHelperV1.close();
         PickerDatabaseHelper dbHelperV2 = new PickerDatabaseHelper(mContext, DB_NAME, DB_VERSION_2);
-        facade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY,
+        facade = new PickerDbFacade(mContext, mLockManager, LOCAL_PROVIDER_AUTHORITY,
                 dbHelperV2);
-        controller = new PickerSyncController(
-                mContext, facade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+        controller = PickerSyncController.initialize(
+                mContext, facade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
 
-        assertThat(controller.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        assertWithMessage("Unexpected cloud provider after db version downgrade.")
+                .that(controller.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
     }
 
     @Test
@@ -1020,43 +1285,511 @@
 
         // Test the default NOT_SET state
         mController =
-                new PickerSyncController(mContext, mFacade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+                PickerSyncController.initialize(
+                        mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
 
-        assertThat(mController.getCurrentCloudProviderInfo().packageName).isEqualTo(PACKAGE_NAME);
+        assertWithMessage("Unexpected cloud provider on testing the default NOT_SET state.")
+                .that(mController.getCurrentCloudProviderInfo().packageName).isEqualTo(
+                        PACKAGE_NAME);
 
         // Set and test the UNSET state
         mController.setCloudProvider(/* authority */ null);
 
         mController =
-                new PickerSyncController(mContext, mFacade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+                PickerSyncController.initialize(
+                        mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
 
-        assertThat(mController.getCloudProvider()).isNull();
+        assertWithMessage("Unexpected cloud provider on setting and testing the NOT_SET state.")
+                .that(mController.getCloudProvider()).isNull();
 
         // Set and test the SET state
         mController.setCloudProvider(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
 
         mController =
-                new PickerSyncController(mContext, mFacade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+                PickerSyncController.initialize(
+                        mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
 
-        assertThat(mController.getCloudProvider()).isEqualTo(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
+        assertWithMessage("Unexpected cloud provider on setting and testing the SET state.")
+                .that(mController.getCloudProvider()).isEqualTo(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
     }
 
     @Test
     public void testAvailableCloudProviders_CloudFeatureDisabled() {
-        assertThat(mController.getAvailableCloudProviders()).isNotEmpty();
+        assertWithMessage("Empty list returned by getAvailableCloudProviders().")
+                .that(mController.getAvailableCloudProviders()).isNotEmpty();
         mConfigStore.disableCloudMediaFeature();
-        assertThat(mController.getAvailableCloudProviders()).isEmpty();
+        assertWithMessage(
+                "Non-empty list returned by getAvailableCloudProviders() after disabling the "
+                        + "cloud media feature.")
+                .that(mController.getAvailableCloudProviders()).isEmpty();
     }
 
-    private static void waitForIdle() {
-        final CountDownLatch latch = new CountDownLatch(1);
-        BackgroundThread.getExecutor().execute(latch::countDown);
-        try {
-            latch.await(30, TimeUnit.SECONDS);
-        } catch (InterruptedException e) {
-            throw new IllegalStateException(e);
+    @Test
+    public void testSyncWithMultiplePages() {
+
+        // First Page
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_3);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_4);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_5);
+        // Second Page
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_6);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_7);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_8);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_9);
+
+        setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        try (Cursor cr = queryMedia()) {
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after adding 9 cloud-only media.")
+                    .that(cr.getCount()).isEqualTo(9);
+            assertCursor(cr, CLOUD_ID_9, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_8, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_7, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_6, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_5, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_4, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_3, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        }
+    }
+
+    @Test
+    public void testSyncDeletedItemsWithMultiplePages() {
+
+        // First Page
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_3);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_4);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_5);
+        // Second Page
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_6);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_7);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_8);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_9);
+
+        setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        try (Cursor cr = queryMedia()) {
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after adding 9 cloud-only media.")
+                    .that(cr.getCount()).isEqualTo(9);
+            assertCursor(cr, CLOUD_ID_9, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_8, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_7, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_6, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_5, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_4, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_3, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
         }
 
+        deleteMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1);
+        deleteMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2);
+        deleteMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_3);
+        deleteMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_4);
+        deleteMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_5);
+        deleteMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_6);
+        deleteMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_7);
+        deleteMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_8);
+
+        mController.syncAllMedia();
+
+        try (Cursor cr = queryMedia()) {
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after deleting 8 out the 9 "
+                            + "cloud-only media.")
+                    .that(cr.getCount()).isEqualTo(1);
+            assertCursor(cr, CLOUD_ID_9, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        }
+
+    }
+
+    @Test
+    public void testResumableIncrementalSyncOperation() {
+        // First Page
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_1);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_2);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_3);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_4);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_5);
+
+        // Complete a full sync since it hasn't synced before.
+        setCloudProviderAndSyncAllMedia(FLAKY_CLOUD_PROVIDER_AUTHORITY);
+        mController.syncAllMedia();
+        mController.syncAllMedia();
+
+        try (Cursor cr = queryMedia()) {
+            // Should only have the first page since the sync is flaky
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after adding 5 cloud-only media.")
+                    .that(cr.getCount()).isEqualTo(5);
+            assertCursor(cr, CLOUD_ID_5, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_4, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_3, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_2, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_1, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+        }
+
+        // Add some more data
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_6);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_7);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_8);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_9);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_10);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_11);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_12);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_13);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_14);
+
+        // FlakyCloudMediaProvider will throw errors on 2 out of 3 requests, so we need to sync
+        // a few times to ensure we resume the mid-sync failure.
+        mController.syncAllMedia();
+        mController.syncAllMedia();
+        mController.syncAllMedia();
+
+        try (Cursor cr = queryMedia()) {
+            // Should have all pages now
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after adding 9 cloud-only media, "
+                            + "in addition to previously added 5 cloud-only media.")
+                    .that(cr.getCount()).isEqualTo(14);
+            assertCursor(cr, CLOUD_ID_14, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_13, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_12, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_11, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_10, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_9, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_8, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_7, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_6, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_5, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_4, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_3, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_2, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_1, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+        }
+    }
+
+    @Test
+    public void testResumableFullSyncOperation() {
+        // First Page of data
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_1);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_2);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_3);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_4);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_5);
+        // Second Page of data
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_6);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_7);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_8);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_9);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_10);
+        // Third Page of data
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_11);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_12);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_13);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_14);
+
+        mController.setCloudProvider(FLAKY_CLOUD_PROVIDER_AUTHORITY);
+        try (Cursor cr = queryMedia()) {
+            // Db should be empty since we haven't synced yet.
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() before sync.")
+                    .that(cr.getCount()).isEqualTo(0);
+        }
+
+        // FlakyCloudMediaProvider will throw errors on 2 out of 3 requests, if we sync once, it
+        // should not be able to complete the sync.
+        mController.syncAllMedia();
+
+        try (Cursor cr = queryMedia()) {
+            // Assert that the sync is not complete.
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia().")
+                    .that(cr.getCount()).isLessThan(14);
+        }
+
+        // Resume sync and complete it. It will take a few sync calls to complete the sync.
+        mController.syncAllMedia();
+        mController.syncAllMedia();
+        mController.syncAllMedia();
+        mController.syncAllMedia();
+
+        try (Cursor cr = queryMedia()) {
+            // Should have all pages now
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after adding 14 cloud-only media.")
+                    .that(cr.getCount()).isEqualTo(14);
+            assertCursor(cr, CLOUD_ID_14, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_13, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_12, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_11, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_10, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_9, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_8, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_7, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_6, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_5, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_4, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_3, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_2, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_1, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+        }
+    }
+
+    @Test
+    public void testFullSyncWithCollectionIdChange() {
+        mController.setCloudProvider(FLAKY_CLOUD_PROVIDER_AUTHORITY);
+        mCloudFlakyMediaGenerator.setMediaCollectionId(COLLECTION_1);
+
+        // First Page of data
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_1);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_2);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_3);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_4);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_5);
+        // Second Page of data
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_6);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_7);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_8);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_9);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_10);
+        // Third Page of data
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_11);
+
+        // FlakyCloudMediaProvider will throw errors on 2 out of 3 requests, if we sync once, it
+        // should not be able to complete the sync.
+        mController.syncAllMedia();
+
+        try (Cursor cr = queryMedia()) {
+            // Assert that the sync is not complete.
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia().")
+                    .that(cr.getCount()).isLessThan(11);
+        }
+
+        // Reset data and change collection id.
+        mCloudFlakyMediaGenerator.resetAll();
+        mCloudFlakyMediaGenerator.setMediaCollectionId(COLLECTION_2);
+
+        // First Page of data
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_12);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_13);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_14);
+
+        // FlakyCloudMediaProvider will throw errors on 2 out of 3 requests. It will take a few
+        // tries to complete the sync.
+        mController.syncAllMedia();
+        mController.syncAllMedia();
+        mController.syncAllMedia();
+
+        try (Cursor cr = queryMedia()) {
+            // Db should be empty since we haven't synced yet.
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after adding 3 cloud-only media.")
+                    .that(cr.getCount()).isEqualTo(3);
+            assertCursor(cr, CLOUD_ID_14, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_13, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_12, FLAKY_CLOUD_PROVIDER_AUTHORITY);
+        }
+    }
+
+    @Test
+    public void testFullSyncWithCloudProviderChange() {
+        mController.setCloudProvider(FLAKY_CLOUD_PROVIDER_AUTHORITY);
+
+        // First Page of data
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_1);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_2);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_3);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_4);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_5);
+        // Second Page of data
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_6);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_7);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_8);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_9);
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_10);
+        // Third Page of data
+        addMedia(mCloudFlakyMediaGenerator, CLOUD_ONLY_11);
+
+        // FlakyCloudMediaProvider will throw errors on 2 out of 3 requests, if we sync once, it
+        // should not be able to complete the sync.
+        mController.syncAllMedia();
+
+        try (Cursor cr = queryMedia()) {
+            // Assert that the sync is not complete.
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia().")
+                    .that(cr.getCount()).isLessThan(11);
+        }
+
+        mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        // First Page of data
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_12);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_13);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_14);
+
+        mController.syncAllMedia();
+
+        try (Cursor cr = queryMedia()) {
+            // Db should be empty since we haven't synced yet.
+            assertWithMessage(
+                    "Unexpected number of media on queryMedia() after adding 3 cloud-only media.")
+                    .that(cr.getCount()).isEqualTo(3);
+            assertCursor(cr, CLOUD_ID_14, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_13, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_12, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        }
+    }
+
+    @Test
+    public void testContentAddNotifications() throws Exception {
+        NotificationContentObserver observer = new NotificationContentObserver(null);
+        observer.register(mContext.getContentResolver());
+
+        setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_1);
+
+        final CountDownLatch latch = new CountDownLatch(1);
+        final NotificationContentObserver.ContentObserverCallback callback =
+                spy(new TestableContentObserverCallback(latch));
+        observer.registerKeysToObserverCallback(Arrays.asList(MEDIA), callback);
+
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_3);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_4);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_5);
+        mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_2);
+
+        mController.syncAllMedia();
+
+        // Wait until the callback has received the notification.
+        latch.await(5, TimeUnit.SECONDS);
+
+        try (Cursor cr = queryMedia()) {
+            cr.moveToFirst();
+            verify(callback)
+                    .onNotificationReceived(
+                            cr.getString(cr.getColumnIndex(MediaColumns.DATE_TAKEN_MILLIS)), null);
+        } finally {
+            observer.unregister(mContext.getContentResolver());
+        }
+    }
+
+    @Test
+    public void testContentDeleteNotifications() throws Exception {
+        NotificationContentObserver observer = new NotificationContentObserver(null);
+        observer.register(mContext.getContentResolver());
+
+        setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        CountDownLatch latch = new CountDownLatch(1);
+        NotificationContentObserver.ContentObserverCallback callback =
+                spy(new TestableContentObserverCallback(latch));
+        observer.registerKeysToObserverCallback(Arrays.asList(MEDIA), callback);
+
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1);
+        mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_1);
+        mController.syncAllMedia();
+        latch.await(2, TimeUnit.SECONDS);
+        verify(callback).onNotificationReceived(any(), any());
+
+        latch = new CountDownLatch(1);
+        callback = spy(new TestableContentObserverCallback(latch));
+        observer.registerKeysToObserverCallback(Arrays.asList(MEDIA), callback);
+
+        deleteMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1);
+        mController.syncAllMedia();
+        latch.await(2, TimeUnit.SECONDS);
+        verify(callback).onNotificationReceived(any(), any());
+
+        observer.unregister(mContext.getContentResolver());
+    }
+
+    @Test
+    public void testCollectionIdChangeResetsUi() throws InterruptedException {
+        final ContentResolver contentResolver = mContext.getContentResolver();
+        final TestContentObserver refreshUiNotificationObserver = new TestContentObserver(null);
+        try {
+            setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_1);
+
+            // Simulate a UI session begins listening.
+            contentResolver.registerContentObserver(REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI,
+                    /* notifyForDescendants */ false, refreshUiNotificationObserver);
+
+            mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_2);
+
+            mController.syncAllMedia();
+
+            assertWithMessage("Refresh ui notification should have been received.")
+                    .that(refreshUiNotificationObserver.mNotificationReceived).isTrue();
+        } finally {
+            contentResolver.unregisterContentObserver(refreshUiNotificationObserver);
+        }
+    }
+
+    @Test
+    public void testRefreshUiNotifications() throws InterruptedException {
+        final ContentResolver contentResolver = mContext.getContentResolver();
+        final TestContentObserver refreshUiNotificationObserver = new TestContentObserver(null);
+        try {
+            contentResolver.registerContentObserver(REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI,
+                    /* notifyForDescendants */ false, refreshUiNotificationObserver);
+
+            assertWithMessage("Refresh ui notification should have not been received.")
+                    .that(refreshUiNotificationObserver.mNotificationReceived).isFalse();
+
+            mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(PACKAGE_NAME);
+            mConfigStore.setDefaultCloudProviderPackage(PACKAGE_NAME);
+
+            // The cloud provider is changed on PickerSyncController construction
+            mController = PickerSyncController
+                    .initialize(mContext, mFacade, mConfigStore, mLockManager);
+            TimeUnit.MILLISECONDS.sleep(100);
+            assertWithMessage(
+                    "Failed to receive refresh ui notification on change in cloud provider.")
+                    .that(refreshUiNotificationObserver.mNotificationReceived).isTrue();
+
+            refreshUiNotificationObserver.mNotificationReceived = false;
+
+            // The SET_CLOUD_PROVIDER is called using a different cloud provider from before
+            mController.setCloudProvider(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
+            TimeUnit.MILLISECONDS.sleep(100);
+            assertWithMessage(
+                    "Failed to receive refresh ui notification on change in cloud provider.")
+                    .that(refreshUiNotificationObserver.mNotificationReceived).isTrue();
+
+            refreshUiNotificationObserver.mNotificationReceived = false;
+
+            // The cloud provider remains unchanged on PickerSyncController construction
+            mController = PickerSyncController
+                    .initialize(mContext, mFacade, mConfigStore, mLockManager);
+            TimeUnit.MILLISECONDS.sleep(100);
+            assertWithMessage(
+                    "Refresh ui notification should have not been received when cloud provider "
+                            + "remains unchanged.")
+                    .that(refreshUiNotificationObserver.mNotificationReceived).isFalse();
+
+            // The SET_CLOUD_PROVIDER is called using the same cloud provider as before
+            mController.setCloudProvider(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
+            TimeUnit.MILLISECONDS.sleep(100);
+            assertWithMessage(
+                    "Refresh ui notification should have not been received when setCloudProvider "
+                            + "is called using the same cloud provider as before.")
+                    .that(refreshUiNotificationObserver.mNotificationReceived).isFalse();
+        } finally {
+            contentResolver.unregisterContentObserver(refreshUiNotificationObserver);
+        }
     }
 
     private static void addMedia(MediaGenerator generator, Pair<String, String> media) {
@@ -1098,13 +1831,15 @@
 
     private void assertEmptyCursorFromMediaQuery() {
         try (Cursor cr = queryMedia()) {
-            assertThat(cr.getCount()).isEqualTo(0);
+            assertWithMessage("Cursor should have been empty.")
+                    .that(cr.getCount()).isEqualTo(0);
         }
     }
 
     private void assertEmptyCursorFromAlbumMediaQuery(String albumId, boolean isLocal) {
         try (Cursor cr = queryAlbumMedia(albumId, isLocal)) {
-            assertThat(cr.getCount()).isEqualTo(0);
+            assertWithMessage("Cursor from queryAlbumMedia should have been empty.")
+                    .that(cr.getCount()).isEqualTo(0);
         }
     }
 
@@ -1127,17 +1862,31 @@
         } else {
             mConfigStore.clearDefaultCloudProviderPackage();
         }
-        mConfigStore.setPickerSyncDelayMs(SYNC_DELAY_MS);
 
-        return new PickerSyncController(
-                mockContext, mFacade, mConfigStore, LOCAL_PROVIDER_AUTHORITY);
+        return PickerSyncController.initialize(
+                mockContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
     }
 
     private static void assertCursor(Cursor cursor, String id, String expectedAuthority) {
         cursor.moveToNext();
-        assertThat(cursor.getString(cursor.getColumnIndex(MediaColumns.ID)))
+        assertWithMessage("Unexpected value of MediaColumns.ID in the cursor.")
+                .that(cursor.getString(cursor.getColumnIndex(MediaColumns.ID)))
                 .isEqualTo(id);
-        assertThat(cursor.getString(cursor.getColumnIndex( MediaColumns.AUTHORITY)))
+        assertWithMessage("Unexpected value of MediaColumns.AUTHORITY in the cursor.")
+                .that(cursor.getString(cursor.getColumnIndex(MediaColumns.AUTHORITY)))
                 .isEqualTo(expectedAuthority);
     }
+
+    private static class TestContentObserver extends ContentObserver {
+        boolean mNotificationReceived;
+
+        TestContentObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            mNotificationReceived = true;
+        }
+    }
 }
diff --git a/tests/src/com/android/providers/media/photopicker/SafetyProtectionUtilsTest.java b/tests/src/com/android/providers/media/photopicker/SafetyProtectionUtilsTest.java
index f176325..745acc0 100644
--- a/tests/src/com/android/providers/media/photopicker/SafetyProtectionUtilsTest.java
+++ b/tests/src/com/android/providers/media/photopicker/SafetyProtectionUtilsTest.java
@@ -34,7 +34,6 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -74,7 +73,6 @@
         });
     }
 
-    @Ignore("Enable once b/269874157 is fixed")
     @Test
     public void testWhetherShouldUseSafetyProtectionResourcesWhenTOrAboveAndFeatureFlagOn() {
         assumeTrue(SdkLevel.isAtLeastT());
diff --git a/tests/src/com/android/providers/media/photopicker/TEST_MAPPING b/tests/src/com/android/providers/media/photopicker/TEST_MAPPING
new file mode 100644
index 0000000..7bcae5d
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/TEST_MAPPING
@@ -0,0 +1,42 @@
+{
+  "mainline-presubmit": [
+    {
+      "name": "CtsPhotoPickerTest[com.google.android.mediaprovider.apex]",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.LargeTest"
+        }
+      ]
+    },
+    {
+      "name": "MediaProviderTests[com.google.android.mediaprovider.apex]",
+      "options": [
+        {
+          // For changes in Photopicker tests we want to run all of the photopicker
+          // tests in the given package regardless of @RunOnlyOnPostsubmit annotation
+          "include-filter": "com.android.providers.media.photopicker"
+        }
+      ]
+    }
+  ],
+  "presubmit": [
+    {
+      "name": "CtsPhotoPickerTest",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.LargeTest"
+        }
+      ]
+    },
+    {
+      "name": "MediaProviderTests",
+      "options": [
+        {
+          // For changes in Photopicker tests we want to run all of the photopicker
+          // tests in the given package regardless of @RunOnlyOnPostsubmit annotation
+          "include-filter": "com.android.providers.media.photopicker"
+        }
+      ]
+    }
+  ]
+}
diff --git a/tests/src/com/android/providers/media/photopicker/TestableContentObserverCallback.java b/tests/src/com/android/providers/media/photopicker/TestableContentObserverCallback.java
new file mode 100644
index 0000000..cf24aef
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/TestableContentObserverCallback.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2023 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;
+
+import androidx.annotation.Nullable;
+
+import java.util.concurrent.CountDownLatch;
+
+public class TestableContentObserverCallback
+        implements NotificationContentObserver.ContentObserverCallback {
+
+    @Nullable private CountDownLatch mLatch;
+
+    public TestableContentObserverCallback() {}
+
+    public TestableContentObserverCallback(CountDownLatch latch) {
+        this.mLatch = latch;
+    }
+
+    @Override
+    public void onNotificationReceived(String dateTakenMs, String albumId) {
+        // do nothing
+        if (mLatch != null) {
+            mLatch.countDown();
+        }
+    }
+}
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 0a82af8..c032844 100644
--- a/tests/src/com/android/providers/media/photopicker/data/ExternalDbFacadeTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/ExternalDbFacadeTest.java
@@ -22,16 +22,20 @@
 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS;
 import static android.provider.CloudMediaProviderContract.EXTRA_ALBUM_ID;
 import static android.provider.CloudMediaProviderContract.EXTRA_MEDIA_COLLECTION_ID;
+import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_SIZE;
+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.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_GIF;
 import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_NONE;
+import static android.provider.MediaStore.MediaColumns.DATE_TAKEN;
 
 import static com.android.providers.media.photopicker.data.ExternalDbFacade.COLUMN_OLD_ID;
 import static com.android.providers.media.photopicker.data.ExternalDbFacade.TABLE_DELETED_MEDIA;
 import static com.android.providers.media.photopicker.data.ExternalDbFacade.TABLE_FILES;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -79,10 +83,12 @@
     private static final long DATE_TAKEN_MS3 = 1624886050568L;
     private static final long DATE_TAKEN_MS4 = 1624886050569L;
     private static final long DATE_TAKEN_MS5 = 1624886050570L;
+    private static final long DATE_MODIFIED_MS1 = 1625000011L;
+    private static final long DATE_MODIFIED_MS2 = 1625000012L;
+    private static final long DATE_MODIFIED_MS3 = 1625000013L;
     private static final long GENERATION_MODIFIED1 = 1;
     private static final long GENERATION_MODIFIED2 = 2;
     private static final long GENERATION_MODIFIED3 = 3;
-    private static final long GENERATION_MODIFIED4 = 4;
     private static final long GENERATION_MODIFIED5 = 5;
     private static final long SIZE = 8000;
     private static final long HEIGHT = 500;
@@ -109,50 +115,82 @@
             ExternalDbFacade facade = new ExternalDbFacade(sIsolatedContext, helper,
                     mock(VolumeCache.class));
 
-            assertThat(facade.addDeletedMedia(ID1)).isTrue();
-            assertThat(facade.addDeletedMedia(ID2)).isTrue();
+            if (!facade.addDeletedMedia(ID1)) {
+                assertWithMessage("Adding item with ID %d failed",
+                        ID1).fail();
+            }
+            if (!facade.addDeletedMedia(ID2)) {
+                assertWithMessage("Adding item with ID %d failed",
+                        ID2).fail();
+            }
 
             try (Cursor cursor = facade.queryDeletedMedia(/* generation */ 0)) {
-                assertThat(cursor.getCount()).isEqualTo(2);
-
+                assertWithMessage(
+                        "Number of rows in the deleted_media table with generation greater than 0"
+                                + " was")
+                        .that(cursor.getCount()).isEqualTo(2);
                 ArrayList<Long> ids = new ArrayList<>();
                 while (cursor.moveToNext()) {
                     ids.add(cursor.getLong(0));
                 }
-
-                assertThat(ids).contains(ID1);
-                assertThat(ids).contains(ID2);
+                assertWithMessage("The list of ids from delete_media table")
+                        .that(ids).contains(ID1);
+                assertWithMessage("The list of ids from delete_media table")
+                        .that(ids).contains(ID2);
             }
 
             // Filter by generation should only return ID2
             try (Cursor cursor = facade.queryDeletedMedia(/* generation */ 1)) {
-                assertThat(cursor.getCount()).isEqualTo(1);
+                assertWithMessage(
+                        "Number of rows in the deleted_media table with generation greater than 1"
+                                + " is")
+                        .that(cursor.getCount()).isEqualTo(1);
 
                 cursor.moveToFirst();
-                assertThat(cursor.getLong(0)).isEqualTo(ID2);
+                assertWithMessage("ID fro row having generation greater than 1")
+                        .that(cursor.getLong(0)).isEqualTo(ID2);
             }
 
             // Adding ids again should succeed but bump generation_modified of ID1 and ID2
-            assertThat(facade.addDeletedMedia(ID1)).isTrue();
-            assertThat(facade.addDeletedMedia(ID2)).isTrue();
+            if (!facade.addDeletedMedia(ID1)) {
+                assertWithMessage("Adding item with ID %d failed",
+                        ID1).fail();
+            }
+            if (!facade.addDeletedMedia(ID2)) {
+                assertWithMessage("Adding item with ID %d failed",
+                        ID2).fail();
+            }
 
             // Filter by generation again, now returns both ids since their generation_modified was
             // bumped
             try (Cursor cursor = facade.queryDeletedMedia(/* generation */ 1)) {
-                assertThat(cursor.getCount()).isEqualTo(2);
+                assertWithMessage(
+                        "Number of rows in the deleted_media table with generation greater than 1"
+                                + " is")
+                        .that(cursor.getCount()).isEqualTo(2);
             }
 
             // Remove ID2 should succeed
-            assertThat(facade.removeDeletedMedia(ID2)).isTrue();
+            if (!facade.removeDeletedMedia(ID2)) {
+                assertWithMessage("Removing item with ID %d failed", ID2).fail();
+            }
             // Remove ID2 again should fail
-            assertThat(facade.removeDeletedMedia(ID2)).isFalse();
+            if (facade.removeDeletedMedia(ID2)) {
+                assertWithMessage("Removing item with ID %d should have failed", ID2).fail();
+            }
 
             // Verify only ID1 left
             try (Cursor cursor = facade.queryDeletedMedia(/* generation */ 0)) {
-                assertThat(cursor.getCount()).isEqualTo(1);
+                assertWithMessage(
+                        "Number of rows in the deleted_media table with generation greater than 0"
+                                + " is")
+                        .that(cursor.getCount()).isEqualTo(1);
 
                 cursor.moveToFirst();
-                assertThat(cursor.getLong(0)).isEqualTo(ID1);
+                assertWithMessage(
+                        "ID of the item left in the deleted_media table after deleting row with "
+                                + "id=ID2 is")
+                        .that(cursor.getLong(0)).isEqualTo(ID1);
             }
         }
     }
@@ -163,18 +201,33 @@
             ExternalDbFacade facade = new ExternalDbFacade(sIsolatedContext, helper,
                     mock(VolumeCache.class));
 
-            assertThat(facade.onFileInserted(FileColumns.MEDIA_TYPE_VIDEO, /* isPending */ false))
-                    .isTrue();
-            assertThat(facade.onFileInserted(FileColumns.MEDIA_TYPE_IMAGE, /* isPending */ false))
-                    .isTrue();
+            if (!facade.onFileInserted(FileColumns.MEDIA_TYPE_VIDEO, /* isPending */ false)) {
+                assertWithMessage(
+                        "Expected to return true but returned false on Insert of "
+                                + "MEDIA_TYPE_VIDEO").fail();
+            }
+            if (!facade.onFileInserted(FileColumns.MEDIA_TYPE_IMAGE, /* isPending */ false)) {
+                assertWithMessage(
+                        "Expected to return true but returned false on Insert of "
+                                + "MEDIA_TYPE_IMAGE").fail();
+            }
             assertDeletedMediaEmpty(facade);
 
-            assertThat(facade.onFileInserted(FileColumns.MEDIA_TYPE_AUDIO, /* isPending */ false))
-                    .isFalse();
-            assertThat(facade.onFileInserted(FileColumns.MEDIA_TYPE_NONE, /* isPending */ false))
-                    .isFalse();
-            assertThat(facade.onFileInserted(FileColumns.MEDIA_TYPE_IMAGE, /* isPending */ true))
-                    .isFalse();
+            if (facade.onFileInserted(FileColumns.MEDIA_TYPE_AUDIO, /* isPending */ false)) {
+                assertWithMessage(
+                        "Expected to return false but returned true on Insert of "
+                                + "MEDIA_TYPE_AUDIO").fail();
+            }
+            if (facade.onFileInserted(FileColumns.MEDIA_TYPE_NONE, /* isPending */ false)) {
+                assertWithMessage(
+                        "Expected to return false but returned true on Insert of "
+                                + "MEDIA_TYPE_NONE").fail();
+            }
+            if (facade.onFileInserted(FileColumns.MEDIA_TYPE_IMAGE, /* isPending */ true)) {
+                assertWithMessage(
+                        "Expected to return false but returned true on Insert of "
+                                + " MEDIA_TYPE_IMAGE with isPending true").fail();
+            }
             assertDeletedMediaEmpty(facade);
         }
     }
@@ -186,53 +239,73 @@
                     mock(VolumeCache.class));
 
             // Non-media -> non-media: no-op
-            assertThat(facade.onFileUpdated(ID1,
-                            FileColumns.MEDIA_TYPE_NONE, FileColumns.MEDIA_TYPE_NONE,
-                            /* oldIsTrashed */ false, /* newIsTrashed */ false,
-                            /* oldIsPending */ false, /* newIsPending */ false,
-                            /* oldIsFavorite */ false, /* newIsFavorite */ false,
-                            /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
-                            /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)).isFalse();
+            if (facade.onFileUpdated(ID1,
+                    FileColumns.MEDIA_TYPE_NONE, FileColumns.MEDIA_TYPE_NONE,
+                    /* oldIsTrashed */ false, /* newIsTrashed */ false,
+                    /* oldIsPending */ false, /* newIsPending */ false,
+                    /* oldIsFavorite */ false, /* newIsFavorite */ false,
+                    /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)) {
+                assertWithMessage(
+                        "Expected to return false but returned true on Update from "
+                                + "MEDIA_TYPE_NONE to MEDIA_TYPE_NONE").fail();
+            }
             assertDeletedMediaEmpty(facade);
 
             // Media -> non-media: added to deleted_media
-            assertThat(facade.onFileUpdated(ID1,
-                            FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_NONE,
-                            /* oldIsTrashed */ false, /* newIsTrashed */ false,
-                            /* oldIsPending */ false, /* newIsPending */ false,
-                            /* oldIsFavorite */ false, /* newIsFavorite */ false,
-                            /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
-                            /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)).isTrue();
+            if (!facade.onFileUpdated(ID1,
+                    FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_NONE,
+                    /* oldIsTrashed */ false, /* newIsTrashed */ false,
+                    /* oldIsPending */ false, /* newIsPending */ false,
+                    /* oldIsFavorite */ false, /* newIsFavorite */ false,
+                    /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)) {
+                assertWithMessage(
+                        "Expected to return true but returned false on Update from "
+                                + "MEDIA_TYPE_IMAGE to MEDIA_TYPE_NONE").fail();
+            }
             assertDeletedMedia(facade, ID1);
 
             // Non-media -> non-media: no-op
-            assertThat(facade.onFileUpdated(ID1,
-                            FileColumns.MEDIA_TYPE_NONE, FileColumns.MEDIA_TYPE_NONE,
-                            /* oldIsTrashed */ false, /* newIsTrashed */ false,
-                            /* oldIsPending */ false, /* newIsPending */ false,
-                            /* oldIsFavorite */ false, /* newIsFavorite */ false,
-                            /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
-                            /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)).isFalse();
+            if (facade.onFileUpdated(ID1,
+                    FileColumns.MEDIA_TYPE_NONE, FileColumns.MEDIA_TYPE_NONE,
+                    /* oldIsTrashed */ false, /* newIsTrashed */ false,
+                    /* oldIsPending */ false, /* newIsPending */ false,
+                    /* oldIsFavorite */ false, /* newIsFavorite */ false,
+                    /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)) {
+                assertWithMessage(
+                        "Expected to return false but returned true on Update from "
+                                + "MEDIA_TYPE_NONE to MEDIA_TYPE_NONE").fail();
+            }
             assertDeletedMedia(facade, ID1);
 
             // Non-media -> media: remove from deleted_media
-            assertThat(facade.onFileUpdated(ID1,
-                            FileColumns.MEDIA_TYPE_NONE, FileColumns.MEDIA_TYPE_IMAGE,
-                            /* oldIsTrashed */ false, /* newIsTrashed */ false,
-                            /* oldIsPending */ false, /* newIsPending */ false,
-                            /* oldIsFavorite */ false, /* newIsFavorite */ false,
-                            /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
-                            /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)).isTrue();
+            if (!facade.onFileUpdated(ID1,
+                    FileColumns.MEDIA_TYPE_NONE, FileColumns.MEDIA_TYPE_IMAGE,
+                    /* oldIsTrashed */ false, /* newIsTrashed */ false,
+                    /* oldIsPending */ false, /* newIsPending */ false,
+                    /* oldIsFavorite */ false, /* newIsFavorite */ false,
+                    /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)) {
+                assertWithMessage(
+                        "Expected to return true but returned false on Update from "
+                                + "MEDIA_TYPE_NONE to MEDIA_TYPE_IMAGE").fail();
+            }
             assertDeletedMediaEmpty(facade);
 
-            // Non-media -> media: no-op
-            assertThat(facade.onFileUpdated(ID1,
-                            FileColumns.MEDIA_TYPE_NONE, FileColumns.MEDIA_TYPE_NONE,
-                            /* oldIsTrashed */ false, /* newIsTrashed */ false,
-                            /* oldIsPending */ false, /* newIsPending */ false,
-                            /* oldIsFavorite */ false, /* newIsFavorite */ false,
-                            /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
-                            /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)).isFalse();
+            // Non-media -> Non-media: no-op
+            if (facade.onFileUpdated(ID1,
+                    FileColumns.MEDIA_TYPE_NONE, FileColumns.MEDIA_TYPE_NONE,
+                    /* oldIsTrashed */ false, /* newIsTrashed */ false,
+                    /* oldIsPending */ false, /* newIsPending */ false,
+                    /* oldIsFavorite */ false, /* newIsFavorite */ false,
+                    /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)) {
+                assertWithMessage(
+                        "Expected to return false but returned true on Update from "
+                                + "MEDIA_TYPE_NONE to MEDIA_TYPE_NONE").fail();
+            }
             assertDeletedMediaEmpty(facade);
         }
     }
@@ -244,33 +317,47 @@
                     mock(VolumeCache.class));
 
             // Was trashed but is now neither trashed nor pending
-            assertThat(facade.onFileUpdated(ID1,
-                            FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
-                            /* oldIsTrashed */ true, /* newIsTrashed */ false,
-                            /* oldIsPending */ false, /* newIsPending */ false,
-                            /* oldIsFavorite */ false, /* newIsFavorite */ false,
-                            /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
-                            /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)).isTrue();
+            if (!facade.onFileUpdated(ID1,
+                    FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
+                    /* oldIsTrashed */ true, /* newIsTrashed */ false,
+                    /* oldIsPending */ false, /* newIsPending */ false,
+                    /* oldIsFavorite */ false, /* newIsFavorite */ false,
+                    /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)) {
+                assertWithMessage(
+                        "Expected to return true but returned false on update, when the oldMedia "
+                                + "was trashed but the newMedia is neither trashed nor pending.")
+                        .fail();
+            }
             assertDeletedMediaEmpty(facade);
 
             // Was not trashed but is now trashed
-            assertThat(facade.onFileUpdated(ID1,
-                            FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
-                            /* oldIsTrashed */ false, /* newIsTrashed */ true,
-                            /* oldIsPending */ false, /* newIsPending */ false,
-                            /* oldIsFavorite */ false, /* newIsFavorite */ false,
-                            /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
-                            /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)).isTrue();
+            if (!facade.onFileUpdated(ID1,
+                    FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
+                    /* oldIsTrashed */ false, /* newIsTrashed */ true,
+                    /* oldIsPending */ false, /* newIsPending */ false,
+                    /* oldIsFavorite */ false, /* newIsFavorite */ false,
+                    /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)) {
+                assertWithMessage(
+                        "Expected to return true but returned false on update, when the oldMedia "
+                                + "was not trashed but the newMedia is trashed.").fail();
+            }
             assertDeletedMedia(facade, ID1);
 
             // Was trashed but is now neither trashed nor pending
-            assertThat(facade.onFileUpdated(ID1,
-                            FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
-                            /* oldIsTrashed */ true, /* newIsTrashed */ false,
-                            /* oldIsPending */ false, /* newIsPending */ false,
-                            /* oldIsFavorite */ false, /* newIsFavorite */ false,
-                            /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
-                            /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)).isTrue();
+            if (!facade.onFileUpdated(ID1,
+                    FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
+                    /* oldIsTrashed */ true, /* newIsTrashed */ false,
+                    /* oldIsPending */ false, /* newIsPending */ false,
+                    /* oldIsFavorite */ false, /* newIsFavorite */ false,
+                    /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)) {
+                assertWithMessage(
+                        "Expected to return true but returned false on update, when the oldMedia "
+                                + "was trashed but the newMedia is neither trashed nor pending.")
+                        .fail();
+            }
             assertDeletedMediaEmpty(facade);
         }
     }
@@ -282,33 +369,47 @@
                     mock(VolumeCache.class));
 
             // Was pending but is now neither trashed nor pending
-            assertThat(facade.onFileUpdated(ID1,
-                            FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
-                            /* oldIsTrashed */ false, /* newIsTrashed */ false,
-                            /* oldIsPending */ true, /* newIsPending */ false,
-                            /* oldIsFavorite */ false, /* newIsFavorite */ false,
-                            /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
-                            /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)).isTrue();
+            if (!facade.onFileUpdated(ID1,
+                    FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
+                    /* oldIsTrashed */ false, /* newIsTrashed */ false,
+                    /* oldIsPending */ true, /* newIsPending */ false,
+                    /* oldIsFavorite */ false, /* newIsFavorite */ false,
+                    /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)) {
+                assertWithMessage(
+                        "Expected to return true but returned false on update, when the oldMedia "
+                                + "was pending but the newMedia is neither trashed nor pending.")
+                        .fail();
+            }
             assertDeletedMediaEmpty(facade);
 
             // Was not pending but is now pending
-            assertThat(facade.onFileUpdated(ID1,
-                            FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
-                            /* oldIsTrashed */ false, /* newIsTrashed */ false,
-                            /* oldIsPending */ false, /* newIsPending */ true,
-                            /* oldIsFavorite */ false, /* newIsFavorite */ false,
-                            /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
-                            /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)).isTrue();
+            if (!facade.onFileUpdated(ID1,
+                    FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
+                    /* oldIsTrashed */ false, /* newIsTrashed */ false,
+                    /* oldIsPending */ false, /* newIsPending */ true,
+                    /* oldIsFavorite */ false, /* newIsFavorite */ false,
+                    /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)) {
+                assertWithMessage(
+                        "Expected to return true but returned false on update, when the oldMedia "
+                                + "was not pending but the newMedia is pending.").fail();
+            }
             assertDeletedMedia(facade, ID1);
 
             // Was pending but is now neither trashed nor pending
-            assertThat(facade.onFileUpdated(ID1,
-                            FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
-                            /* oldIsTrashed */ false, /* newIsTrashed */ false,
-                            /* oldIsPending */ true, /* newIsPending */ false,
-                            /* oldIsFavorite */ false, /* newIsFavorite */ false,
-                            /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
-                            /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)).isTrue();
+            if (!facade.onFileUpdated(ID1,
+                    FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
+                    /* oldIsTrashed */ false, /* newIsTrashed */ false,
+                    /* oldIsPending */ true, /* newIsPending */ false,
+                    /* oldIsFavorite */ false, /* newIsFavorite */ false,
+                    /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)) {
+                assertWithMessage(
+                        "Expected to return true but returned false on update, when the oldMedia "
+                                + "was pending but the newMedia is neither trashed nor pending.")
+                        .fail();
+            }
             assertDeletedMediaEmpty(facade);
         }
     }
@@ -320,22 +421,32 @@
                     mock(VolumeCache.class));
 
             // Was favorite but is now not favorited
-            assertThat(facade.onFileUpdated(ID1,
-                            FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
-                            /* oldIsTrashed */ false, /* newIsTrashed */ false,
-                            /* oldIsPending */ false, /* newIsPending */ false,
-                            /* oldIsFavorite */ true, /* newIsFavorite */ false,
-                            /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
-                            /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)).isTrue();
+            if (!facade.onFileUpdated(ID1,
+                    FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
+                    /* oldIsTrashed */ false, /* newIsTrashed */ false,
+                    /* oldIsPending */ false, /* newIsPending */ false,
+                    /* oldIsFavorite */ true, /* newIsFavorite */ false,
+                    /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)) {
+                assertWithMessage(
+                        "Expected to return true but returned false on update with visible "
+                                + "favorite, when the oldMedia "
+                                + "was favorite but the newMedia is not favorite.").fail();
+            }
 
             // Was not favorite but is now favorited
-            assertThat(facade.onFileUpdated(ID1,
-                            FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
-                            /* oldIsTrashed */ false, /* newIsTrashed */ false,
-                            /* oldIsPending */ false, /* newIsPending */ false,
-                            /* oldIsFavorite */ false, /* newIsFavorite */ true,
-                            /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
-                            /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)).isTrue();
+            if (!facade.onFileUpdated(ID1,
+                    FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
+                    /* oldIsTrashed */ false, /* newIsTrashed */ false,
+                    /* oldIsPending */ false, /* newIsPending */ false,
+                    /* oldIsFavorite */ false, /* newIsFavorite */ true,
+                    /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)) {
+                assertWithMessage(
+                        "Expected to return true but returned false on update with visible "
+                                + "favorite, when the oldMedia "
+                                + "was not favorite but the newMedia is favorite.").fail();
+            }
         }
     }
 
@@ -346,22 +457,32 @@
                     mock(VolumeCache.class));
 
             // Was favorite but is now not favorited
-            assertThat(facade.onFileUpdated(ID1,
-                            FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
-                            /* oldIsTrashed */ true, /* newIsTrashed */ true,
-                            /* oldIsPending */ false, /* newIsPending */ false,
-                            /* oldIsFavorite */ true, /* newIsFavorite */ false,
-                            /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
-                            /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)).isFalse();
+            if (facade.onFileUpdated(ID1,
+                    FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
+                    /* oldIsTrashed */ true, /* newIsTrashed */ true,
+                    /* oldIsPending */ false, /* newIsPending */ false,
+                    /* oldIsFavorite */ true, /* newIsFavorite */ false,
+                    /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)) {
+                assertWithMessage(
+                        "Expected to return true but returned false on update with hidden "
+                                + "favorite, when the oldMedia was favorite but the newMedia is "
+                                + "not favorite.").fail();
+            }
 
             // Was not favorite but is now favorited
-            assertThat(facade.onFileUpdated(ID1,
-                            FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
-                            /* oldIsTrashed */ false, /* newIsTrashed */ false,
-                            /* oldIsPending */ true, /* newIsPending */ true,
-                            /* oldIsFavorite */ false, /* newIsFavorite */ true,
-                            /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
-                            /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)).isFalse();
+            if (facade.onFileUpdated(ID1,
+                    FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
+                    /* oldIsTrashed */ false, /* newIsTrashed */ false,
+                    /* oldIsPending */ true, /* newIsPending */ true,
+                    /* oldIsFavorite */ false, /* newIsFavorite */ true,
+                    /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)) {
+                assertWithMessage(
+                        "Expected to return false but returned true on update with hidden "
+                                + "favorite, when the oldMedia was not favorite but the newMedia "
+                                + "is favorite.").fail();
+            }
         }
     }
 
@@ -372,22 +493,32 @@
                     mock(VolumeCache.class));
 
             // Was _SPECIAL_FORMAT_NONE but is now _SPECIAL_FORMAT_GIF
-            assertThat(facade.onFileUpdated(ID1,
+            if (!facade.onFileUpdated(ID1,
                     FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
                     /* oldIsTrashed */ false, /* newIsTrashed */ false,
                     /* oldIsPending */ false, /* newIsPending */ false,
                     /* oldIsFavorite */ false, /* newIsFavorite */ false,
                     /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
-                    /* newSpecialFormat */ _SPECIAL_FORMAT_GIF)).isTrue();
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_GIF)) {
+                assertWithMessage(
+                        "Expected to return true but returned false on update with visible "
+                                + "special format, when the oldSpecialFormat was NONE but the "
+                                + "newSpecialFormat is GIF.").fail();
+            }
 
             // Was _SPECIAL_FORMAT_GIF but is now _SPECIAL_FORMAT_NONE
-            assertThat(facade.onFileUpdated(ID1,
+            if (!facade.onFileUpdated(ID1,
                     FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
                     /* oldIsTrashed */ false, /* newIsTrashed */ false,
                     /* oldIsPending */ false, /* newIsPending */ false,
                     /* oldIsFavorite */ false, /* newIsFavorite */ false,
                     /* oldSpecialFormat */ _SPECIAL_FORMAT_GIF,
-                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)).isTrue();
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)) {
+                assertWithMessage(
+                        "Expected to return true but returned false on update with visible "
+                                + "special format, when the oldSpecialFormat was GIF but the "
+                                + "newSpecialFormat is NONE.").fail();
+            }
         }
     }
 
@@ -398,22 +529,32 @@
                     mock(VolumeCache.class));
 
             // Was _SPECIAL_FORMAT_NONE but is now _SPECIAL_FORMAT_GIF
-            assertThat(facade.onFileUpdated(ID1,
+            if (facade.onFileUpdated(ID1,
                     FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
                     /* oldIsTrashed */ true, /* newIsTrashed */ true,
                     /* oldIsPending */ false, /* newIsPending */ false,
                     /* oldIsFavorite */ false, /* newIsFavorite */ false,
                     /* oldSpecialFormat */ _SPECIAL_FORMAT_NONE,
-                    /* newSpecialFormat */ _SPECIAL_FORMAT_GIF)).isFalse();
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_GIF)) {
+                assertWithMessage(
+                        "Expected to return false but returned true on update with hidden special"
+                                + " format, when the oldSpecialFormat was NONE but the "
+                                + "newSpecialFormat is GIF.").fail();
+            }
 
-            // Was _SPECIAL_FORMAT_NONE but is now _SPECIAL_FORMAT_GIF
-            assertThat(facade.onFileUpdated(ID1,
+            // Was _SPECIAL_FORMAT_GIF but is now _SPECIAL_FORMAT_NONE
+            if (facade.onFileUpdated(ID1,
                     FileColumns.MEDIA_TYPE_IMAGE, FileColumns.MEDIA_TYPE_IMAGE,
                     /* oldIsTrashed */ false, /* newIsTrashed */ false,
                     /* oldIsPending */ true, /* newIsPending */ true,
                     /* oldIsFavorite */ false, /* newIsFavorite */ false,
                     /* oldSpecialFormat */ _SPECIAL_FORMAT_GIF,
-                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)).isFalse();
+                    /* newSpecialFormat */ _SPECIAL_FORMAT_NONE)) {
+                assertWithMessage(
+                        "Expected to return false but returned true on update with hidden special"
+                                + " format, when the oldSpecialFormat was GIF but the "
+                                + "newSpecialFormat is NONE.").fail();
+            }
         }
     }
 
@@ -423,13 +564,25 @@
             ExternalDbFacade facade = new ExternalDbFacade(sIsolatedContext, helper,
                     mock(VolumeCache.class));
 
-            assertThat(facade.onFileDeleted(ID1, FileColumns.MEDIA_TYPE_NONE)).isFalse();
+            if (facade.onFileDeleted(ID1, FileColumns.MEDIA_TYPE_NONE)) {
+                assertWithMessage(
+                        "Expected to return false when the mediaType is NONE, but returned true "
+                                + "on delete.").fail();
+            }
             assertDeletedMediaEmpty(facade);
 
-            assertThat(facade.onFileDeleted(ID1, FileColumns.MEDIA_TYPE_IMAGE)).isTrue();
+            if (!facade.onFileDeleted(ID1, FileColumns.MEDIA_TYPE_IMAGE)) {
+                assertWithMessage(
+                        "Expected to return true when the mediaType is IMAGE, but returned false "
+                                + "on delete.").fail();
+            }
             assertDeletedMedia(facade, ID1);
 
-            assertThat(facade.onFileDeleted(ID1, FileColumns.MEDIA_TYPE_NONE)).isFalse();
+            if (facade.onFileDeleted(ID1, FileColumns.MEDIA_TYPE_NONE)) {
+                assertWithMessage(
+                        "Expected to return false when the mediaType is NONE, but returned true "
+                                + "on delete.").fail();
+            }
             assertDeletedMedia(facade, ID1);
         }
     }
@@ -452,7 +605,9 @@
             helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv));
 
             try (Cursor cursor = queryAllMedia(facade)) {
-                assertThat(cursor.getCount()).isEqualTo(2);
+                assertWithMessage("Number of rows on querying TABLE_FILES for all media is")
+                        .that(cursor.getCount())
+                        .isEqualTo(2);
                 assertCursorExtras(cursor);
 
                 cursor.moveToFirst();
@@ -463,9 +618,17 @@
             }
 
             try (Cursor cursor = facade.queryMedia(GENERATION_MODIFIED1,
-                            /* albumId */ null, /* mimeType */ null)) {
-                assertThat(cursor.getCount()).isEqualTo(1);
-                assertCursorExtras(cursor, EXTRA_SYNC_GENERATION);
+                    /* albumId */ null, /* mimeType */ null, /* pageSize*/ 10,
+                    /*pageToken */ null)) {
+                assertWithMessage(
+                        "Number of rows on querying TABLE_FILES for (generation: "
+                                + "GENERATION_MODIFIED1, albumId: null, mimeType: null, pageSize:"
+                                + " 10) is")
+                        .that(cursor.getCount())
+                        .isEqualTo(1);
+                //PAGE_TOKEN will also be set since pageSize is not -1.
+                assertCursorExtras(cursor, EXTRA_SYNC_GENERATION, EXTRA_PAGE_SIZE,
+                        EXTRA_PAGE_TOKEN);
 
                 cursor.moveToFirst();
                 assertMediaColumns(facade, cursor, ID2, DATE_TAKEN_MS1);
@@ -489,7 +652,10 @@
             helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cvTrashed));
 
             try (Cursor cursor = queryAllMedia(facade)) {
-                assertThat(cursor.getCount()).isEqualTo(0);
+                assertWithMessage(
+                        "Number of rows on querying TABLES_FILES with cvPending and cvTrashed "
+                                + "inserted is")
+                        .that(cursor.getCount()).isEqualTo(0);
             }
         }
     }
@@ -504,7 +670,7 @@
             // Intentionally associate <dateModifiedSeconds2 with generation_modifed1>
             // and <dateModifiedSeconds1 with generation_modifed2> below.
             // This allows us verify that the sort order from queryMediaGeneration
-            // is based on date_taken and not generation_modified.
+            // is based on date_taken and _id and not generation_modified.
             ContentValues cv = getContentValues(DATE_TAKEN_MS2, GENERATION_MODIFIED1);
             cv.remove(MediaColumns.DATE_TAKEN);
             cv.put(MediaColumns.DATE_MODIFIED, dateModifiedSeconds2);
@@ -515,18 +681,29 @@
             helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv));
 
             try (Cursor cursor = queryAllMedia(facade)) {
-                assertThat(cursor.getCount()).isEqualTo(2);
+                assertWithMessage(
+                        "Number of rows on querying TABLES_FILES with modified date for all media"
+                                + " is")
+                        .that(cursor.getCount())
+                        .isEqualTo(2);
 
                 cursor.moveToFirst();
-                assertMediaColumns(facade, cursor, ID1, dateModifiedSeconds2 * 1000);
+                assertMediaColumns(facade, cursor, ID2, dateModifiedSeconds2 * 1000);
 
                 cursor.moveToNext();
-                assertMediaColumns(facade, cursor, ID2, dateModifiedSeconds1 * 1000);
+                assertMediaColumns(facade, cursor, ID1, dateModifiedSeconds1 * 1000);
             }
 
             try (Cursor cursor = facade.queryMedia(GENERATION_MODIFIED1,
-                            /* albumId */ null, /* mimeType */ null)) {
-                assertThat(cursor.getCount()).isEqualTo(1);
+                    /* albumId */ null, /* mimeType */ null, /* pageSize*/ -1,
+                    /*pageToken */ null)) {
+                assertWithMessage(
+                        "Number of rows on querying TABLE_FILES with modified date for "
+                                + "(generation: "
+                                + "GENERATION_MODIFIED1, albumId: null, mimeType: null, pageSize:"
+                                + " -1) is")
+                        .that(cursor.getCount())
+                        .isEqualTo(1);
 
                 cursor.moveToFirst();
                 assertMediaColumns(facade, cursor, ID2, dateModifiedSeconds1 * 1000);
@@ -545,20 +722,30 @@
             helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv));
 
             try (Cursor cursor = queryAllMedia(facade)) {
-                assertThat(cursor.getCount()).isEqualTo(1);
+                assertWithMessage("Number of rows on querying TABLES_FILES for all media is")
+                        .that(cursor.getCount())
+                        .isEqualTo(1);
 
                 cursor.moveToFirst();
                 assertMediaColumns(facade, cursor, ID1, DATE_TAKEN_MS1);
             }
 
             try (Cursor cursor = facade.queryMedia(/* generation */ 0,
-                            /* albumId */ null, VIDEO_MIME_TYPES_QUERY)) {
-                assertThat(cursor.getCount()).isEqualTo(0);
+                    /* albumId */ null, VIDEO_MIME_TYPES_QUERY, /* pageSize*/ -1,
+                    /* pageToken*/ null)) {
+                assertWithMessage(
+                        "Number of rows on querying TABLES_FILES for media with mime type VIDEO is")
+                        .that(cursor.getCount())
+                        .isEqualTo(0);
             }
 
             try (Cursor cursor = facade.queryMedia(/* generation */ 0,
-                            /* albumId */ null, IMAGE_MIME_TYPES_QUERY)) {
-                assertThat(cursor.getCount()).isEqualTo(1);
+                    /* albumId */ null, IMAGE_MIME_TYPES_QUERY, /* pageSize*/ -1,
+                    /* pageToken*/ null)) {
+                assertWithMessage(
+                        "Number of rows on querying TABLES_FILES for media with mime type IMAGE is")
+                        .that(cursor.getCount())
+                        .isEqualTo(1);
 
                 cursor.moveToFirst();
                 assertMediaColumns(facade, cursor, ID1, DATE_TAKEN_MS1);
@@ -575,21 +762,33 @@
             initMediaInAllAlbums(helper);
 
             try (Cursor cursor = queryAllMedia(facade)) {
-                assertThat(cursor.getCount()).isEqualTo(3);
+                assertWithMessage("Number of rows on querying TABLES_FILES for all media is")
+                        .that(cursor.getCount())
+                        .isEqualTo(3);
             }
 
             try (Cursor cursor = facade.queryMedia(/* generation */ -1,
-                            ALBUM_ID_CAMERA, /* mimeType */ null)) {
-                assertThat(cursor.getCount()).isEqualTo(1);
-                assertCursorExtras(cursor, EXTRA_ALBUM_ID);
+                    ALBUM_ID_CAMERA, /* mimeType */ null, /* pageSize*/ 20,
+                    /* pageToken*/ null)) {
+                assertWithMessage(
+                        "Number of rows on querying TABLES_FILES for media with ALBUM_ID_CAMERA is")
+                        .that(cursor.getCount())
+                        .isEqualTo(1);
+                //PAGE_TOKEN will also be set since pageSize is not -1.
+                assertCursorExtras(cursor, EXTRA_ALBUM_ID, EXTRA_PAGE_SIZE, EXTRA_PAGE_TOKEN);
 
                 cursor.moveToFirst();
                 assertMediaColumns(facade, cursor, ID1, DATE_TAKEN_MS1);
             }
 
             try (Cursor cursor = facade.queryMedia(/* generation */ -1,
-                            ALBUM_ID_SCREENSHOTS, /* mimeType */ null)) {
-                assertThat(cursor.getCount()).isEqualTo(1);
+                    ALBUM_ID_SCREENSHOTS, /* mimeType */ null, /* pageSize*/ -1,
+                    /* pageToken*/ null)) {
+                assertWithMessage(
+                        "Number of rows on querying TABLES_FILES for media with "
+                                + "ALBUM_ID_SCREENSHOTS is")
+                        .that(cursor.getCount())
+                        .isEqualTo(1);
                 assertCursorExtras(cursor, EXTRA_ALBUM_ID);
 
                 cursor.moveToFirst();
@@ -597,9 +796,15 @@
             }
 
             try (Cursor cursor = facade.queryMedia(/* generation */ -1,
-                            ALBUM_ID_DOWNLOADS, /* mimeType */ null)) {
-                assertThat(cursor.getCount()).isEqualTo(1);
-                assertCursorExtras(cursor, EXTRA_ALBUM_ID);
+                    ALBUM_ID_DOWNLOADS, /* mimeType */ null, /* pageSize*/ 10,
+                    /* pageToken*/ null)) {
+                assertWithMessage(
+                        "Number of rows on querying TABLES_FILES for media with "
+                                + "ALBUM_ID_DOWNLOADS is")
+                        .that(cursor.getCount())
+                        .isEqualTo(1);
+                //PAGE_TOKEN will also be set since pageSize is not -1.
+                assertCursorExtras(cursor, EXTRA_ALBUM_ID, EXTRA_PAGE_SIZE, EXTRA_PAGE_TOKEN);
 
                 cursor.moveToFirst();
                 assertMediaColumns(facade, cursor, ID3, DATE_TAKEN_MS3);
@@ -619,29 +824,169 @@
             helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv));
 
             try (Cursor cursor = queryAllMedia(facade)) {
-                assertThat(cursor.getCount()).isEqualTo(1);
+                assertWithMessage("Number of rows on querying TABLES_FILES for all media is")
+                        .that(cursor.getCount())
+                        .isEqualTo(1);
 
                 cursor.moveToFirst();
                 assertMediaColumns(facade, cursor, ID1, DATE_TAKEN_MS1);
             }
 
             try (Cursor cursor = facade.queryMedia(/* generation */ 0,
-                            ALBUM_ID_SCREENSHOTS, IMAGE_MIME_TYPES_QUERY)) {
-                assertThat(cursor.getCount()).isEqualTo(0);
+                    ALBUM_ID_SCREENSHOTS, IMAGE_MIME_TYPES_QUERY, /* pageSize*/ -1,
+                    /* pageToken*/ null)) {
+                assertWithMessage(
+                        "Number of rows on querying TABLES_FILES for media with "
+                                + "ALBUM_ID_SCREENSHOTS and IMAGE_MIME_TYPES_QUERY is")
+                        .that(cursor.getCount())
+                        .isEqualTo(0);
             }
 
             try (Cursor cursor = facade.queryMedia(/* generation */ 0,
-                            ALBUM_ID_CAMERA, VIDEO_MIME_TYPES_QUERY)) {
-                assertThat(cursor.getCount()).isEqualTo(0);
+                    ALBUM_ID_CAMERA, VIDEO_MIME_TYPES_QUERY, /* pageSize*/ -1,
+                    /* pageToken*/ null)) {
+                assertWithMessage(
+                        "Number of rows on querying TABLES_FILES for media with ALBUM_ID_CAMERA "
+                                + "and VIDEO_MIME_TYPES_QUERY is")
+                        .that(cursor.getCount())
+                        .isEqualTo(0);
+
             }
 
             try (Cursor cursor = facade.queryMedia(/* generation */ 0,
-                            ALBUM_ID_CAMERA, IMAGE_MIME_TYPES_QUERY)) {
+                    ALBUM_ID_CAMERA, IMAGE_MIME_TYPES_QUERY, /* pageSize*/ -1,
+                    /* pageToken*/ null)) {
+                assertWithMessage(
+                        "Number of rows on querying TABLES_FILES for media with ALBUM_ID_CAMERA "
+                                + "and IMAGE_MIME_TYPES_QUERY is")
+                        .that(cursor.getCount())
+                        .isEqualTo(1);
+
+                cursor.moveToFirst();
+                assertMediaColumns(facade, cursor, ID1, DATE_TAKEN_MS1);
+            }
+        }
+    }
+
+    @Test
+    public void testQueryMedia_withPageSize_returnsCorrectSortOrder() throws Exception {
+        try (DatabaseHelper helper = new TestDatabaseHelper(sIsolatedContext)) {
+            ExternalDbFacade facade = new ExternalDbFacade(sIsolatedContext, helper,
+                    mock(VolumeCache.class));
+
+            // Insert 5 images with date_taken non-null
+            ContentValues cv = getContentValues(DATE_TAKEN_MS1, GENERATION_MODIFIED1);
+            helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv));
+
+            cv.put(MediaColumns.DATE_TAKEN, DATE_TAKEN_MS2);
+            helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv));
+
+            cv.put(MediaColumns.DATE_TAKEN, DATE_TAKEN_MS3);
+            helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv));
+
+            cv.put(MediaColumns.DATE_TAKEN, DATE_TAKEN_MS4);
+            helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv));
+
+            cv.put(MediaColumns.DATE_TAKEN, DATE_TAKEN_MS5);
+            helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv));
+
+            // Verify that media returned in descending order of date_taken, _id
+            try (Cursor cursor = facade.queryMedia(/* generation */ 0,
+                    /* albumId */ null, /* mimeType */ null, /* pageSize*/ 2,
+                    /* pageToken*/ null)) {
+                assertThat(cursor.getCount()).isEqualTo(2);
+
+                cursor.moveToFirst();
+                assertMediaColumns(facade, cursor, ID5, DATE_TAKEN_MS5);
+
+                cursor.moveToNext();
+                assertMediaColumns(facade, cursor, ID4, DATE_TAKEN_MS4);
+            }
+
+            try (Cursor cursor = facade.queryMedia(/* generation */ 0,
+                    /* albumId */ null, /* mimeType */ null, /* pageSize*/ 3,
+                    /* pageToken*/ DATE_TAKEN_MS4 + "|" + ID4)) {
+                assertThat(cursor.getCount()).isEqualTo(3);
+
+                cursor.moveToFirst();
+                assertMediaColumns(facade, cursor, ID3, DATE_TAKEN_MS3);
+
+                cursor.moveToNext();
+                assertMediaColumns(facade, cursor, ID2, DATE_TAKEN_MS2);
+
+                cursor.moveToNext();
+                assertMediaColumns(facade, cursor, ID1, DATE_TAKEN_MS1);
+            }
+        }
+    }
+
+    @Test
+    public void testQueryMedia_withPageSizeMissingPageToken_returnsCorrectSortOrder()
+            throws Exception {
+        try (DatabaseHelper helper = new TestDatabaseHelper(sIsolatedContext)) {
+            ExternalDbFacade facade = new ExternalDbFacade(sIsolatedContext, helper,
+                    mock(VolumeCache.class));
+
+            // Insert 5 images, 2 with date_taken non-null and 3 with date_taken null
+            ContentValues cv = getContentValues(DATE_TAKEN_MS1, GENERATION_MODIFIED1);
+            helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv));
+
+            cv.put(MediaColumns.DATE_TAKEN, DATE_TAKEN_MS2);
+            helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv));
+
+            cv.remove(DATE_TAKEN);
+
+            cv.put(MediaColumns.DATE_MODIFIED, DATE_MODIFIED_MS1);
+            helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv));
+
+            cv.put(MediaColumns.DATE_MODIFIED, DATE_MODIFIED_MS2);
+            helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv));
+
+            cv.put(MediaColumns.DATE_MODIFIED, DATE_MODIFIED_MS3);
+            helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv));
+
+            // Verify that media returned in descending order of date_taken, _id
+            try (Cursor cursor = facade.queryMedia(/* generation */ 0,
+                    /* albumId */ null, /* mimeType */ null, /* pageSize*/ 2,
+                    /* pageToken*/ null)) {
+                assertThat(cursor.getCount()).isEqualTo(2);
+
+                cursor.moveToFirst();
+                assertMediaColumns(facade, cursor, ID5, Long.valueOf(DATE_MODIFIED_MS3) * 1000);
+
+                cursor.moveToNext();
+                assertMediaColumns(facade, cursor, ID4, Long.valueOf(DATE_MODIFIED_MS2) * 1000);
+            }
+
+            String pageToken =  Long.valueOf(DATE_MODIFIED_MS2) * 1000 + "|" + ID4;
+            try (Cursor cursor = facade.queryMedia(/* generation */ 0,
+                    /* albumId */ null, /* mimeType */ null, /* pageSize*/ 2,
+                    /* pageToken*/ pageToken)) {
+                assertThat(cursor.getCount()).isEqualTo(2);
+
+                cursor.moveToFirst();
+                assertMediaColumns(facade, cursor, ID3, Long.valueOf(DATE_MODIFIED_MS1) * 1000);
+
+                cursor.moveToNext();
+                assertMediaColumns(facade, cursor, ID2, DATE_TAKEN_MS2);
+            }
+
+            pageToken =  DATE_TAKEN_MS2 + "|" + ID2;
+            try (Cursor cursor = facade.queryMedia(/* generation */ 0,
+                    /* albumId */ null, /* mimeType */ null, /* pageSize*/ 2,
+                    /* pageToken*/ pageToken)) {
                 assertThat(cursor.getCount()).isEqualTo(1);
 
                 cursor.moveToFirst();
                 assertMediaColumns(facade, cursor, ID1, DATE_TAKEN_MS1);
             }
+
+            pageToken = DATE_MODIFIED_MS1 + "|" + ID1;
+            try (Cursor cursor = facade.queryMedia(/* generation */ 0,
+                    /* albumId */ null, /* mimeType */ null, /* pageSize*/ 2,
+                    /* pageToken*/ pageToken)) {
+                assertThat(cursor.getCount()).isEqualTo(0);
+            }
         }
     }
 
@@ -688,7 +1033,8 @@
             final String mediaCollectionId = bundle.getString(
                     MediaCollectionInfo.MEDIA_COLLECTION_ID);
 
-            assertThat(mediaCollectionId).isEqualTo(expectedMediaCollectionId);
+            assertWithMessage("The mediaCollectionId is")
+                    .that(mediaCollectionId).isEqualTo(expectedMediaCollectionId);
         }
     }
 
@@ -721,11 +1067,15 @@
             helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv));
 
             try (Cursor cursor = queryAllMedia(facade)) {
-                assertThat(cursor.getCount()).isEqualTo(1);
+                assertWithMessage("Number of rows on querying TABLES_FILES with for all media is")
+                        .that(cursor.getCount())
+                        .isEqualTo(1);
             }
 
             try (Cursor cursor = facade.queryAlbums(/* mimeType */ null)) {
-                assertThat(cursor.getCount()).isEqualTo(0);
+                assertWithMessage("Number of rows on querying TABLES_FILES for albums is")
+                        .that(cursor.getCount())
+                        .isEqualTo(0);
             }
         }
     }
@@ -779,11 +1129,17 @@
             helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv2));
 
             try (Cursor cursor = queryAllMedia(facade)) {
-                assertThat(cursor.getCount()).isEqualTo(2);
+                assertWithMessage("Number of rows on querying TABLES_FILES for all media")
+                        .that(cursor.getCount())
+                        .isEqualTo(2);
             }
 
             try (Cursor cursor = facade.queryAlbums(IMAGE_MIME_TYPES_QUERY)) {
-                assertThat(cursor.getCount()).isEqualTo(1);
+                assertWithMessage(
+                        "Number of rows on querying TABLES_FILES for albums with "
+                                + "IMAGE_MIME_TYPES_QUERY")
+                        .that(cursor.getCount())
+                        .isEqualTo(1);
 
                 // We verify the order of the albums only the image in camera is shown
                 cursor.moveToNext();
@@ -795,10 +1151,14 @@
     @Test
     public void testOrderOfLocalAlbumIds() {
         // Camera, ScreenShots, Downloads
-        assertThat(ExternalDbFacade.LOCAL_ALBUM_IDS[0]).isEqualTo(ALBUM_ID_CAMERA);
-        assertThat(ExternalDbFacade.LOCAL_ALBUM_IDS[1])
+        assertWithMessage("Local album at 0th index is")
+                .that(ExternalDbFacade.LOCAL_ALBUM_IDS[0])
+                .isEqualTo(ALBUM_ID_CAMERA);
+        assertWithMessage("Local album at 1st index is")
+                .that(ExternalDbFacade.LOCAL_ALBUM_IDS[1])
                 .isEqualTo(ALBUM_ID_SCREENSHOTS);
-        assertThat(ExternalDbFacade.LOCAL_ALBUM_IDS[2])
+        assertWithMessage("Local album at 2nd index is")
+                .that(ExternalDbFacade.LOCAL_ALBUM_IDS[2])
                 .isEqualTo(ALBUM_ID_DOWNLOADS);
     }
 
@@ -808,14 +1168,14 @@
         cv1.put(MediaColumns.RELATIVE_PATH, ExternalDbFacade.RELATIVE_PATH_CAMERA);
         helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv1));
 
-        // Insert in screenshots ablum
+        // Insert in screenshots album
         ContentValues cv2 = getContentValues(DATE_TAKEN_MS2, GENERATION_MODIFIED2);
         cv2.put(
                 MediaColumns.RELATIVE_PATH,
                 Environment.DIRECTORY_PICTURES + "/" + Environment.DIRECTORY_SCREENSHOTS + "/");
         helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv2));
 
-        // Insert in download ablum
+        // Insert in download album
         ContentValues cv3 = getContentValues(DATE_TAKEN_MS3, GENERATION_MODIFIED3);
         cv3.put(MediaColumns.IS_DOWNLOAD, 1);
         helper.runWithTransaction(db -> db.insert(TABLE_FILES, null, cv3));
@@ -823,18 +1183,25 @@
 
     private static void assertDeletedMediaEmpty(ExternalDbFacade facade) {
         try (Cursor cursor = facade.queryDeletedMedia(/* generation */ 0)) {
-            assertThat(cursor.getCount()).isEqualTo(0);
+            assertWithMessage(
+                    "Number of rows in the deleted_media table is")
+                    .that(cursor.getCount()).isEqualTo(0);
         }
     }
 
     private static void assertDeletedMedia(ExternalDbFacade facade, long id) {
         try (Cursor cursor = facade.queryDeletedMedia(/* generation */ 0)) {
-            assertThat(cursor.getCount()).isEqualTo(1);
+            assertWithMessage("Number of rows in the deleted_media table is")
+                    .that(cursor.getCount())
+                    .isEqualTo(1);
 
             cursor.moveToFirst();
-            assertThat(cursor.getLong(0)).isEqualTo(id);
-            assertThat(cursor.getColumnName(0)).isEqualTo(
-                    CloudMediaProviderContract.MediaColumns.ID);
+            assertWithMessage("Row id for the deleted media is")
+                    .that(cursor.getLong(0))
+                    .isEqualTo(id);
+            assertWithMessage("Name of the column at index 0 is")
+                    .that(cursor.getColumnName(0))
+                    .isEqualTo(CloudMediaProviderContract.MediaColumns.ID);
         }
     }
 
@@ -865,24 +1232,44 @@
         int orientationIndex = cursor.getColumnIndex(
                 CloudMediaProviderContract.MediaColumns.ORIENTATION);
 
-        assertThat(cursor.getLong(idIndex)).isEqualTo(id);
-        assertThat(cursor.getLong(dateTakenIndex)).isEqualTo(dateTakenMs);
-        assertThat(cursor.getLong(sizeIndex)).isEqualTo(SIZE);
-        assertThat(cursor.getString(mimeTypeIndex)).isEqualTo(mimeType);
-        assertThat(cursor.getLong(durationIndex)).isEqualTo(DURATION_MS);
-        assertThat(cursor.getInt(isFavoriteIndex)).isEqualTo(isFavorite);
-        assertThat(cursor.getInt(heightIndex)).isEqualTo(HEIGHT);
-        assertThat(cursor.getInt(widthIndex)).isEqualTo(WIDTH);
-        assertThat(cursor.getInt(orientationIndex)).isEqualTo(ORIENTATION);
+        assertWithMessage("MediaColumns.ID is")
+                .that(cursor.getLong(idIndex))
+                .isEqualTo(id);
+        assertWithMessage("MediaColumns.DATE_TAKEN_MILLIS is")
+                .that(cursor.getLong(dateTakenIndex))
+                .isEqualTo(dateTakenMs);
+        assertWithMessage("MediaColumns.SIZE_BYTES is")
+                .that(cursor.getLong(sizeIndex))
+                .isEqualTo(SIZE);
+        assertWithMessage("MediaColumns.MIME_TYPE is")
+                .that(cursor.getString(mimeTypeIndex))
+                .isEqualTo(mimeType);
+        assertWithMessage("MediaColumns.DURATION_MILLIS is")
+                .that(cursor.getLong(durationIndex))
+                .isEqualTo(DURATION_MS);
+        assertWithMessage("MediaColumns.IS_FAVORITE is")
+                .that(cursor.getInt(isFavoriteIndex))
+                .isEqualTo(isFavorite);
+        assertWithMessage("MediaColumns.HEIGHT is")
+                .that(cursor.getInt(heightIndex))
+                .isEqualTo(HEIGHT);
+        assertWithMessage("MediaColumns.WIDTH is")
+                .that(cursor.getInt(widthIndex))
+                .isEqualTo(WIDTH);
+        assertWithMessage("MediaColumns.ORIENTATION is")
+                .that(cursor.getInt(orientationIndex))
+                .isEqualTo(ORIENTATION);
     }
 
     private static void assertCursorExtras(Cursor cursor, String... honoredArg) {
         final Bundle bundle = cursor.getExtras();
 
-        assertThat(bundle.getString(EXTRA_MEDIA_COLLECTION_ID))
+        assertWithMessage("Cursor extras is")
+                .that(bundle.getString(EXTRA_MEDIA_COLLECTION_ID))
                 .isEqualTo(MediaStore.getVersion(sIsolatedContext));
         if (honoredArg != null) {
-            assertThat(bundle.getStringArrayList(EXTRA_HONORED_ARGS))
+            assertWithMessage("Honored args are")
+                    .that(bundle.getStringArrayList(EXTRA_HONORED_ARGS))
                     .containsExactlyElementsIn(Arrays.asList(honoredArg));
         }
     }
@@ -896,10 +1283,14 @@
                 CloudMediaProviderContract.AlbumColumns.DATE_TAKEN_MILLIS);
         int countIndex = cursor.getColumnIndex(CloudMediaProviderContract.AlbumColumns.MEDIA_COUNT);
 
-        assertThat(cursor.getString(displayNameIndex)).isEqualTo(displayName);
-        assertThat(cursor.getString(idIndex)).isNotNull();
-        assertThat(cursor.getLong(dateTakenIndex)).isEqualTo(dateTakenMs);
-        assertThat(cursor.getLong(countIndex)).isEqualTo(count);
+        assertWithMessage("AlbumColumns.DISPLAY_NAME is")
+                .that(cursor.getString(displayNameIndex)).isEqualTo(displayName);
+        assertWithMessage("AlbumColumns.MEDIA_COVER_ID is")
+                .that(cursor.getString(idIndex)).isNotNull();
+        assertWithMessage("AlbumColumns.DATE_TAKEN_MILLIS is")
+                .that(cursor.getLong(dateTakenIndex)).isEqualTo(dateTakenMs);
+        assertWithMessage("AlbumColumns.MEDIA_COUNT is")
+                .that(cursor.getLong(countIndex)).isEqualTo(count);
     }
 
     private static void assertMediaCollectionInfo(ExternalDbFacade facade, Bundle bundle,
@@ -907,13 +1298,15 @@
         long generation = bundle.getLong(MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION);
         String mediaCollectionId = bundle.getString(MediaCollectionInfo.MEDIA_COLLECTION_ID);
 
-        assertThat(generation).isEqualTo(expectedGeneration);
-        assertThat(mediaCollectionId).isEqualTo(MediaStore.getVersion(sIsolatedContext));
+        assertWithMessage("LAST_MEDIA_SYNC_GENERATION is")
+                .that(generation).isEqualTo(expectedGeneration);
+        assertWithMessage("MEDIA_COLLECTION_ID is")
+                .that(mediaCollectionId).isEqualTo(MediaStore.getVersion(sIsolatedContext));
     }
 
     private static Cursor queryAllMedia(ExternalDbFacade facade) {
         return facade.queryMedia(/* generation */ -1, /* albumId */ null,
-                /* mimeType */ null);
+                /* mimeType */ null, /* pageSize*/ -1, /* pageToken*/ null);
     }
 
     private static ContentValues getContentValues(long dateTakenMs, long generation) {
diff --git a/tests/src/com/android/providers/media/photopicker/data/PickerDatabaseHelperTest.java b/tests/src/com/android/providers/media/photopicker/data/PickerDatabaseHelperTest.java
index 9d4eac2..6d1d4de 100644
--- a/tests/src/com/android/providers/media/photopicker/data/PickerDatabaseHelperTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/PickerDatabaseHelperTest.java
@@ -364,7 +364,7 @@
             values = getBasicContentValues();
             values.put(KEY_DATE_TAKEN_MS, -1);
             values.put(KEY_CLOUD_ID, CLOUD_ID);
-            assertThat(db.insert(MEDIA_TABLE, null, values)).isEqualTo(-1);
+            assertThat(db.insert(MEDIA_TABLE, null, values)).isEqualTo(1);
 
             // date_taken_ms=NULL for Album Media
             values = getBasicContentValues();
@@ -376,7 +376,7 @@
             values = getBasicContentValues();
             values.put(KEY_DATE_TAKEN_MS, -1);
             values.put(KEY_CLOUD_ID, CLOUD_ID);
-            assertThat(db.insert(ALBUM_MEDIA_TABLE, null, values)).isEqualTo(-1);
+            assertThat(db.insert(ALBUM_MEDIA_TABLE, null, values)).isEqualTo(1);
         }
     }
 
diff --git a/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java b/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
index f38afe8..d7838af 100644
--- a/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
@@ -22,8 +22,11 @@
 import static com.android.providers.media.util.MimeUtils.getExtensionFromMimeType;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.MockitoAnnotations.initMocks;
 
 import android.content.ContentValues;
 import android.content.Context;
@@ -35,17 +38,23 @@
 import android.provider.ExportedSince;
 import android.provider.MediaStore.PickerMediaColumns;
 
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.providers.media.ProjectionHelper;
+import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
+import com.android.providers.media.photopicker.sync.SyncTracker;
+import com.android.providers.media.photopicker.sync.SyncTrackerRegistry;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
 
 import java.io.File;
+import java.util.Collections;
+import java.util.concurrent.CompletableFuture;
 
 @RunWith(AndroidJUnit4.class)
 public class PickerDbFacadeTest {
@@ -86,14 +95,25 @@
     private Context mContext;
     private ProjectionHelper mProjectionHelper;
 
+    @Mock
+    private SyncTracker mMockLocalSyncTracker;
+    @Mock
+    private SyncTracker mMockCloudSyncTracker;
+
     @Before
     public void setUp() {
-        mContext = InstrumentationRegistry.getTargetContext();
+        initMocks(this);
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
         File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
         dbPath.delete();
-        mFacade = new PickerDbFacade(mContext, LOCAL_PROVIDER);
+        mFacade = new PickerDbFacade(mContext, new PickerSyncLockManager(), LOCAL_PROVIDER);
         mFacade.setCloudProvider(CLOUD_PROVIDER);
         mProjectionHelper = new ProjectionHelper(Column.class, ExportedSince.class);
+
+
+        // Inject mock trackers
+        SyncTrackerRegistry.setLocalSyncTracker(mMockLocalSyncTracker);
+        SyncTrackerRegistry.setCloudSyncTracker(mMockCloudSyncTracker);
     }
 
     @After
@@ -101,6 +121,10 @@
         if (mFacade != null) {
             mFacade.setCloudProvider(null);
         }
+
+        // Reset mock trackers
+        SyncTrackerRegistry.setLocalSyncTracker(new SyncTracker());
+        SyncTrackerRegistry.setCloudSyncTracker(new SyncTracker());
     }
 
     @Test
@@ -111,7 +135,10 @@
         assertAddMediaOperation(LOCAL_PROVIDER, cursor1, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after addMediaOperation with cursor1 "
+                            + "on LOCAL_PROVIDER.")
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID, DATE_TAKEN_MS + 1);
         }
@@ -120,7 +147,10 @@
         assertAddMediaOperation(LOCAL_PROVIDER, cursor2, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after trying to update the same row with cursor2 "
+                            + "on LOCAL_PROVIDER.")
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID, DATE_TAKEN_MS + 2);
         }
@@ -133,7 +163,9 @@
         assertAddMediaOperation(CLOUD_PROVIDER, cursor, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after addMediaOperation on CLOUD_PROVIDER.")
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, CLOUD_ID, DATE_TAKEN_MS);
         }
@@ -147,7 +179,10 @@
         assertAddMediaOperation(CLOUD_PROVIDER, cursor1, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after addMediaOperation with cursor1 on "
+                            + "CLOUD_PROVIDER.")
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, CLOUD_ID, DATE_TAKEN_MS + 1);
         }
@@ -156,7 +191,10 @@
         assertAddMediaOperation(CLOUD_PROVIDER, cursor2, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after trying to update the same row with cursor2 "
+                            + "on CLOUD_PROVIDER.")
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, CLOUD_ID, DATE_TAKEN_MS + 2);
         }
@@ -171,7 +209,11 @@
         assertAddMediaOperation(CLOUD_PROVIDER, cloudCursor, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after addMediaOperation with:\nlocalCursor having "
+                            + "localId = " + LOCAL_ID + ", followed by\ncloudCursor having "
+                            + "localId = " + LOCAL_ID + ", cloudId = " + CLOUD_ID)
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID, DATE_TAKEN_MS);
         }
@@ -186,13 +228,48 @@
         assertAddMediaOperation(LOCAL_PROVIDER, localCursor, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after addMediaOperation with:\ncloudCursor having "
+                            + "localId = " + LOCAL_ID + ", cloudId = " + CLOUD_ID + ", followed by"
+                            + "\ncloudCursor having localId = " + LOCAL_ID)
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID, DATE_TAKEN_MS + 1);
         }
     }
 
     @Test
+    public void testMediaSortOrder() {
+        final Cursor cursor1 = getLocalMediaCursor(LOCAL_ID_1, DATE_TAKEN_MS);
+        final Cursor cursor2 = getCloudMediaCursor(CLOUD_ID_1, null, DATE_TAKEN_MS);
+        final Cursor cursor3 = getLocalMediaCursor(LOCAL_ID_2, DATE_TAKEN_MS + 1);
+
+        assertAddMediaOperation(LOCAL_PROVIDER, cursor1, 1);
+        assertAddMediaOperation(CLOUD_PROVIDER, cursor2, 1);
+        assertAddMediaOperation(LOCAL_PROVIDER, cursor3, 1);
+
+        try (Cursor cr = queryMediaAll()) {
+            assertWithMessage(
+                    "Unexpected number of media on queryMediaAll() after adding 2 "
+                            + "localMediaCursor and 1 cloudMediaCursor to "
+                            + LOCAL_PROVIDER + " and " + CLOUD_PROVIDER + " respectively.")
+                    .that(cr.getCount()).isEqualTo(/* expected= */ 3);
+
+            cr.moveToFirst();
+            // Latest items should show up first.
+            assertCloudMediaCursor(cr, LOCAL_ID_2, DATE_TAKEN_MS + 1);
+
+            cr.moveToNext();
+            // If the date taken is the same for 2 or more items, they should be sorted in the order
+            // of their insertion in the database with the latest row inserted first.
+            assertCloudMediaCursor(cr, CLOUD_ID_1, DATE_TAKEN_MS);
+
+            cr.moveToNext();
+            assertCloudMediaCursor(cr, LOCAL_ID_1, DATE_TAKEN_MS);
+        }
+    }
+
+    @Test
     public void testAddLocalAlbumMedia() {
         Cursor cursor1 = getAlbumMediaCursor(LOCAL_ID, /* cloud id */ null, DATE_TAKEN_MS + 1);
         Cursor cursor2 = getAlbumMediaCursor(LOCAL_ID, /* cloud id */ null, DATE_TAKEN_MS + 2);
@@ -200,7 +277,11 @@
         assertAddAlbumMediaOperation(LOCAL_PROVIDER, cursor1, 1, ALBUM_ID);
 
         try (Cursor cr = queryAlbumMedia(ALBUM_ID, true)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of albumMedia after adding albumMediaCursor having localId"
+                            + " = "
+                            + LOCAL_ID + " cloudId = " + null + " to " + LOCAL_PROVIDER)
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID, DATE_TAKEN_MS + 1);
         }
@@ -210,7 +291,11 @@
         assertAddAlbumMediaOperation(LOCAL_PROVIDER, cursor2, 1, ALBUM_ID);
 
         try (Cursor cr = queryAlbumMedia(ALBUM_ID, true)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of albumMedia after resetting and updating the same row "
+                            + "with albumMediaCursor having localId = "
+                            + LOCAL_ID + " cloudId = " + null)
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID, DATE_TAKEN_MS + 2);
         }
@@ -224,7 +309,11 @@
         assertAddAlbumMediaOperation(CLOUD_PROVIDER, cursor1, 1, ALBUM_ID);
 
         try (Cursor cr = queryAlbumMedia(ALBUM_ID, false)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of albumMedia after adding albumMediaCursor having localId"
+                            + " = "
+                            + null + " cloudId = " + CLOUD_ID + " to " + CLOUD_PROVIDER)
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, CLOUD_ID, DATE_TAKEN_MS + 1);
         }
@@ -234,13 +323,50 @@
         assertAddAlbumMediaOperation(CLOUD_PROVIDER, cursor2, 1, ALBUM_ID);
 
         try (Cursor cr = queryAlbumMedia(ALBUM_ID, false)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of albumMedia after resetting and updating the same row "
+                    + "with albumMediaCursor having localId = "
+                            + null + " cloudId = " + CLOUD_PROVIDER)
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, CLOUD_ID, DATE_TAKEN_MS + 2);
         }
     }
 
     @Test
+    public void testAddCloudAlbumMediaWhileCloudSyncIsRunning() {
+
+
+        doReturn(Collections.singletonList(new CompletableFuture<>()))
+                .when(mMockCloudSyncTracker)
+                .pendingSyncFutures();
+
+        Cursor cursor1 = getAlbumMediaCursor(/* local id */ null, CLOUD_ID, DATE_TAKEN_MS + 1);
+
+        assertAddAlbumMediaOperation(CLOUD_PROVIDER, cursor1, 1, ALBUM_ID);
+
+        try (Cursor cr = queryAlbumMedia(ALBUM_ID, false)) {
+            assertWithMessage(
+                    "Unexpected number of albumMedia after adding albumMediaCursor having localId"
+                    + " = "
+                            + null + " cloudId = " + CLOUD_ID + " to " + CLOUD_PROVIDER)
+                    .that(cr.getCount()).isEqualTo(1);
+            cr.moveToFirst();
+            assertCloudMediaCursor(cr, CLOUD_ID, DATE_TAKEN_MS + 1);
+        }
+
+        // These files should also be in the media table since we're pretending that
+        // we have a cloud sync running.
+        try (Cursor cr = queryMediaAll()) {
+            assertWithMessage(
+                    "Unexpected number of media on querying all media with cloud sync running.")
+                    .that(cr.getCount()).isEqualTo(1);
+            cr.moveToFirst();
+            assertCloudMediaCursor(cr, CLOUD_ID, DATE_TAKEN_MS + 1);
+        }
+    }
+
+    @Test
     public void testAddCloudAlbumMediaAvailableOnDevice() {
         // Add local row for a media item in media table.
         final Cursor localCursor = getLocalMediaCursor(LOCAL_ID, DATE_TAKEN_MS);
@@ -254,7 +380,9 @@
         // Assert that preference was given to the local media item over cloud media item at the
         // time of insertion in album_media table.
         try (Cursor albumCursor = queryAlbumMedia(ALBUM_ID, false)) {
-            assertThat(albumCursor.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of albumMedia on querying " + ALBUM_ID)
+                    .that(albumCursor.getCount()).isEqualTo(1);
             albumCursor.moveToFirst();
             assertCloudMediaCursor(albumCursor, LOCAL_ID, DATE_TAKEN_MS);
         }
@@ -271,26 +399,64 @@
         // Assert that cloud media metadata was inserted in the database as local_id points to a
         // deleted item.
         try (Cursor albumCursor = queryAlbumMedia(ALBUM_ID, false)) {
-            assertThat(albumCursor.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of albumMedia on querying " + ALBUM_ID)
+                    .that(albumCursor.getCount()).isEqualTo(1);
             albumCursor.moveToFirst();
             assertCloudMediaCursor(albumCursor, CLOUD_ID, DATE_TAKEN_MS);
         }
     }
 
     @Test
+    public void testAlbumMediaSortOrder() {
+        final Cursor cursor1 = getAlbumMediaCursor(null, CLOUD_ID_1, DATE_TAKEN_MS);
+        final Cursor cursor2 = getAlbumMediaCursor(LOCAL_ID_1, null, DATE_TAKEN_MS);
+        final Cursor cursor3 = getAlbumMediaCursor(null, CLOUD_ID_2, DATE_TAKEN_MS + 1);
+
+        assertAddAlbumMediaOperation(CLOUD_PROVIDER, cursor1, 1, ALBUM_ID);
+        assertAddAlbumMediaOperation(LOCAL_PROVIDER, cursor2, 1, ALBUM_ID);
+        assertAddAlbumMediaOperation(CLOUD_PROVIDER, cursor3, 1, ALBUM_ID);
+
+        try (Cursor cr = queryAlbumMedia(ALBUM_ID, false)) {
+            assertWithMessage(
+                    "Unexpected number of media on queryMediaAll() after adding 2 "
+                            + "cloudAlbumMediaCursor and 1 localAlbumMediaCursor to "
+                            + CLOUD_PROVIDER + " and " + LOCAL_PROVIDER + " respectively.")
+                    .that(cr.getCount()).isEqualTo(/* expected= */ 3);
+
+            cr.moveToFirst();
+            // Latest items should show up first.
+            assertCloudMediaCursor(cr, CLOUD_ID_2, DATE_TAKEN_MS + 1);
+
+            cr.moveToNext();
+            // If the date taken is the same for 2 or more items, they should be sorted in the order
+            // of their insertion in the database with the latest row inserted first.
+            assertCloudMediaCursor(cr, LOCAL_ID_1, DATE_TAKEN_MS);
+
+            cr.moveToNext();
+            assertCloudMediaCursor(cr, CLOUD_ID_1, DATE_TAKEN_MS);
+        }
+    }
+
+    @Test
     public void testRemoveLocal() throws Exception {
         Cursor localCursor = getLocalMediaCursor(LOCAL_ID, DATE_TAKEN_MS);
 
         assertAddMediaOperation(LOCAL_PROVIDER, localCursor, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after addMediaOperation with local media cursor "
+                            + "localCursor.")
+                    .that(cr.getCount()).isEqualTo(1);
         }
 
         assertRemoveMediaOperation(LOCAL_PROVIDER, getDeletedMediaCursor(LOCAL_ID), 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(0);
+            assertWithMessage(
+                    "Unexpected number of media after removeMediaOperation on local provider.")
+                    .that(cr.getCount()).isEqualTo(0);
         }
     }
 
@@ -303,7 +469,12 @@
         assertAddMediaOperation(CLOUD_PROVIDER, cloudCursor, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after addMediaOperation with one localCursor and "
+                            + "one cloudCursor where "
+                            + "\nlocalCursor has localId = " + LOCAL_ID
+                            + "\ncloudCursor has localId = " + LOCAL_ID + ", cloudId = " + CLOUD_ID)
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID, DATE_TAKEN_MS);
         }
@@ -311,7 +482,9 @@
         assertRemoveMediaOperation(LOCAL_PROVIDER, getDeletedMediaCursor(LOCAL_ID), 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after removeMediaOperation on local provider.")
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, CLOUD_ID, DATE_TAKEN_MS);
         }
@@ -324,13 +497,18 @@
         assertAddMediaOperation(CLOUD_PROVIDER, cloudCursor, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after addMediaOperation with cloud media cursor "
+                            + "cloudCursor.")
+                    .that(cr.getCount()).isEqualTo(1);
         }
 
         assertRemoveMediaOperation(CLOUD_PROVIDER, getDeletedMediaCursor(CLOUD_ID), 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(0);
+            assertWithMessage(
+                    "Unexpected number of media after removeMediaOperation on cloud provider.")
+                    .that(cr.getCount()).isEqualTo(0);
         }
     }
 
@@ -347,7 +525,14 @@
         }
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after addMediaOperation with two cloudCursor where "
+                            + "\ncloudCursor1 has localId = " + LOCAL_ID + ", cloudId = " + CLOUD_ID
+                            + "1"
+                            + "\ncloudCursor2 has localId = " + LOCAL_ID + ", cloudId = " + CLOUD_ID
+                            + "2"
+            )
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, CLOUD_ID + "1", DATE_TAKEN_MS + 1);
         }
@@ -355,7 +540,9 @@
         assertRemoveMediaOperation(CLOUD_PROVIDER, getDeletedMediaCursor(CLOUD_ID + "1"), 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after removeMediaOperation on cloud provider.")
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, CLOUD_ID + "2", DATE_TAKEN_MS + 2);
         }
@@ -370,7 +557,12 @@
         assertAddMediaOperation(LOCAL_PROVIDER, localCursor, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after addMediaOperation with one localCursor and "
+                            + "one cloudCursor where "
+                            + "\nlocalCursor has localId = " + LOCAL_ID
+                            + "\ncloudCursor has localId = " + LOCAL_ID + ", cloudId = " + CLOUD_ID)
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID, DATE_TAKEN_MS);
         }
@@ -378,7 +570,9 @@
         assertRemoveMediaOperation(CLOUD_PROVIDER, getDeletedMediaCursor(CLOUD_ID), 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after removeMediaOperation on cloud provider.")
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID, DATE_TAKEN_MS);
         }
@@ -398,7 +592,11 @@
         }
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after addMediaOperation with two localCursor where "
+                            + "\nlocalCursor1 has localId = " + LOCAL_ID
+                            + "\nlocalCursor2 has localId = " + LOCAL_ID)
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID, DATE_TAKEN_MS + 2);
         }
@@ -406,7 +604,9 @@
         assertRemoveMediaOperation(LOCAL_PROVIDER, getDeletedMediaCursor(LOCAL_ID), 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(0);
+            assertWithMessage(
+                    "Unexpected number of media after removeMediaOperation on local provider.")
+                    .that(cr.getCount()).isEqualTo(0);
         }
     }
 
@@ -419,7 +619,12 @@
         assertAddMediaOperation(CLOUD_PROVIDER, cloudCursor2, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after addMediaOperation with two cloudCursor where "
+                            + "\ncloudCursor1 has localId = " + LOCAL_ID + ", cloudId = " + CLOUD_ID
+                            + "\ncloudCursor2 has localId = " + LOCAL_ID + ", cloudId = " + CLOUD_ID
+            )
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, CLOUD_ID, DATE_TAKEN_MS + 2);
         }
@@ -427,7 +632,9 @@
         assertRemoveMediaOperation(CLOUD_PROVIDER, getDeletedMediaCursor(CLOUD_ID), 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(0);
+            assertWithMessage(
+                    "Unexpected number of media after removeMediaOperation on cloud provider.")
+                    .that(cr.getCount()).isEqualTo(0);
         }
     }
 
@@ -442,7 +649,13 @@
         assertAddMediaOperation(CLOUD_PROVIDER, cloudCursor2, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after addMediaOperation with one localCursor and "
+                            + "two cloudCursor, where \nlocalCursor has localId = "
+                            + LOCAL_ID + "\ncloudCursor1 has localId = " + LOCAL_ID + ", cloudId = "
+                            + CLOUD_ID + "\ncloudCursor1 has localId = " + LOCAL_ID + ", cloudId = "
+                            + CLOUD_ID)
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID, DATE_TAKEN_MS);
         }
@@ -450,7 +663,11 @@
         assertRemoveMediaOperation(LOCAL_PROVIDER, getDeletedMediaCursor(LOCAL_ID), 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after removeMediaOperation deleting media with "
+                            + "localId ="
+                            + LOCAL_ID + " from local provider.")
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, CLOUD_ID, DATE_TAKEN_MS + 2);
         }
@@ -458,7 +675,59 @@
         assertRemoveMediaOperation(CLOUD_PROVIDER, getDeletedMediaCursor(CLOUD_ID), 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(0);
+            assertWithMessage(
+                    "Unexpected number of media after removeMediaOperation deleting media with "
+                            + "cloudId ="
+                            + CLOUD_ID + " from cloud provider.")
+                    .that(cr.getCount()).isEqualTo(0);
+        }
+    }
+
+    @Test
+    public void testRemoveMedia_withLatestDateTakenMillis() {
+        Cursor localCursor = getLocalMediaCursor(LOCAL_ID, DATE_TAKEN_MS);
+        Cursor cloudCursor1 = getCloudMediaCursor(CLOUD_ID, LOCAL_ID, DATE_TAKEN_MS + 1);
+
+        assertAddMediaOperation(LOCAL_PROVIDER, localCursor, 1);
+        assertAddMediaOperation(CLOUD_PROVIDER, cloudCursor1, 1);
+
+        try (Cursor cr = queryMediaAll()) {
+            assertWithMessage(
+                    "Unexpected number of media after addMediaOperation with one localCursor and "
+                            + "one cloudCursor where "
+                            + "\nlocalCursor has localId = " + LOCAL_ID
+                            + "\ncloudCursor1 has localId = " + LOCAL_ID + ", cloudId = "
+                            + CLOUD_ID)
+                    .that(cr.getCount()).isEqualTo(1);
+            cr.moveToFirst();
+            assertCloudMediaCursor(cr, LOCAL_ID, DATE_TAKEN_MS);
+        }
+
+        try (PickerDbFacade.DbWriteOperation operation =
+                     mFacade.beginRemoveMediaOperation(CLOUD_PROVIDER)) {
+            assertWriteOperation(operation, getDeletedMediaCursor(CLOUD_ID), /* writeCount */ 1);
+            assertWithMessage(
+                    "Unexpected value for the firstDateTakenMillis in the columns affected by DB "
+                            + "write operation.")
+                    .that(operation.getFirstDateTakenMillis()).isEqualTo(DATE_TAKEN_MS + 1);
+            operation.setSuccess();
+        }
+
+        try (PickerDbFacade.DbWriteOperation operation =
+                     mFacade.beginRemoveMediaOperation(LOCAL_PROVIDER)) {
+            assertWriteOperation(operation, getDeletedMediaCursor(LOCAL_ID), /* writeCount */ 1);
+            assertWithMessage(
+                    "Unexpected value for the FirstDateTakenMillis in the columns affected by DB "
+                            + "write operation.")
+                    .that(operation.getFirstDateTakenMillis()).isEqualTo(DATE_TAKEN_MS);
+            operation.setSuccess();
+        }
+
+        try (Cursor cr = queryMediaAll()) {
+            assertWithMessage(
+                    "Unexpected number of media after removeMediaOperation on cloud provider then"
+                            + " on local provider.")
+                    .that(cr.getCount()).isEqualTo(0);
         }
     }
 
@@ -475,7 +744,14 @@
         assertAddMediaOperation(CLOUD_PROVIDER, cloudCursor2, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after addMediaOperation with one localCursor and "
+                            + "two cloudCursor, where \nlocalCursor has localId = " + LOCAL_ID
+                            + "\ncloudCursor1 has localId = " + LOCAL_ID + ", cloudId = " + CLOUD_ID
+                            + "1"
+                            + "\ncloudCursor1 has localId = " + LOCAL_ID + ", cloudId = " + CLOUD_ID
+                            + "2")
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID, DATE_TAKEN_MS);
         }
@@ -483,12 +759,15 @@
         assertResetMediaOperation(LOCAL_PROVIDER, null, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after resetMediaOperation on local provider.")
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
 
             // Verify that local_id was deleted and either of cloudCursor1 or cloudCursor2
             // was promoted
-            assertThat(cr.getString(1)).isNotNull();
+            assertWithMessage("Failed to delete local_Id.")
+                    .that(cr.getString(1)).isNotNull();
         }
     }
 
@@ -501,7 +780,12 @@
         assertAddMediaOperation(CLOUD_PROVIDER, cloudCursor, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after addMediaOperation with one localCursor and "
+                            + "one cloudCursor where "
+                            + "\nlocalCursor has localId = " + LOCAL_ID
+                            + "\ncloudCursor has localId = " + LOCAL_ID + ", cloudId = " + CLOUD_ID)
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID, DATE_TAKEN_MS);
         }
@@ -509,7 +793,9 @@
         assertResetMediaOperation(CLOUD_PROVIDER, null, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after resetMediaOperation on cloud provider.")
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID, DATE_TAKEN_MS);
         }
@@ -524,21 +810,30 @@
         assertAddMediaOperation(CLOUD_PROVIDER, cloudCursor, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media after addMediaOperation with one localCursor and "
+                            + "one cloudCursor where "
+                            + "\nlocalCursor has localId = " + LOCAL_ID
+                            + "\ncloudCursor has localId = " + LOCAL_ID + ", cloudId = " + CLOUD_ID)
+                    .that(cr.getCount()).isEqualTo(1);
         }
 
         PickerDbFacade.QueryFilterBuilder qfbBefore = new PickerDbFacade.QueryFilterBuilder(5);
         qfbBefore.setDateTakenBeforeMs(DATE_TAKEN_MS - 1);
         qfbBefore.setId(5);
         try (Cursor cr = mFacade.queryMediaForUi(qfbBefore.build())) {
-            assertThat(cr.getCount()).isEqualTo(0);
+            assertWithMessage(
+                    "Unexpected number of media with dateTakenBeforeMs set to DATE_TAKEN_MS - 1.")
+                    .that(cr.getCount()).isEqualTo(0);
         }
 
         PickerDbFacade.QueryFilterBuilder qfbAfter = new PickerDbFacade.QueryFilterBuilder(5);
         qfbAfter.setDateTakenAfterMs(DATE_TAKEN_MS + 1);
         qfbAfter.setId(5);
         try (Cursor cr = mFacade.queryMediaForUi(qfbAfter.build())) {
-            assertThat(cr.getCount()).isEqualTo(0);
+            assertWithMessage(
+                    "Unexpected number of media with dateTakenAfterMs set to DATE_TAKEN_MS + 1.")
+                    .that(cr.getCount()).isEqualTo(0);
         }
     }
 
@@ -558,7 +853,8 @@
         qfbBefore.setDateTakenBeforeMs(DATE_TAKEN_MS);
         qfbBefore.setId(2);
         try (Cursor cr = mFacade.queryMediaForUi(qfbBefore.build())) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage("Unexpected number of media with Id set to 2.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID + "1", DATE_TAKEN_MS);
@@ -568,7 +864,8 @@
         qfbAfter.setDateTakenAfterMs(DATE_TAKEN_MS);
         qfbAfter.setId(1);
         try (Cursor cr = mFacade.queryMediaForUi(qfbAfter.build())) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage("Unexpected number of media with Id set to 1.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID + "2", DATE_TAKEN_MS);
@@ -589,7 +886,10 @@
         qfbBefore.setDateTakenBeforeMs(DATE_TAKEN_MS + 1);
         qfbBefore.setId(0);
         try (Cursor cr = mFacade.queryMediaForUi(qfbBefore.build())) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media with limit set to 1 and dateTakenBeforeMs set to "
+                            + "DATE_TAKEN_MS + 1.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID + "3", DATE_TAKEN_MS);
@@ -599,7 +899,10 @@
         qfbAfter.setDateTakenAfterMs(DATE_TAKEN_MS - 1);
         qfbAfter.setId(0);
         try (Cursor cr = mFacade.queryMediaForUi(qfbAfter.build())) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media with limit set to 1 and dateTakenAfterMs set to "
+                            + "DATE_TAKEN_MS - 1.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID + "3", DATE_TAKEN_MS);
@@ -607,7 +910,8 @@
 
         try (Cursor cr = mFacade.queryMediaForUi(
                 new PickerDbFacade.QueryFilterBuilder(1).build())) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage("Unexpected number of media with limit set to 1.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID + "3", DATE_TAKEN_MS);
@@ -630,12 +934,14 @@
         PickerDbFacade.QueryFilterBuilder qfbAll = new PickerDbFacade.QueryFilterBuilder(1000);
         qfbAll.setSizeBytes(10);
         try (Cursor cr = mFacade.queryMediaForUi(qfbAll.build())) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage("Unexpected number of media with sizeBytes set to 10.")
+                    .that(cr.getCount()).isEqualTo(2);
         }
 
         qfbAll.setSizeBytes(1);
         try (Cursor cr = mFacade.queryMediaForUi(qfbAll.build())) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage("Unexpected number of media with sizeBytes set to 1.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID, MP4_VIDEO_MIME_TYPE);
@@ -647,14 +953,20 @@
         qfbAfter.setId(0);
         qfbAfter.setSizeBytes(10);
         try (Cursor cr = mFacade.queryMediaForUi(qfbAfter.build())) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of media with sizeBytes set to 10 and dateTakenAfterMs set"
+                            + " to DATE_TAKEN_MS - 1.")
+                    .that(cr.getCount()).isEqualTo(2);
         }
 
         qfbAfter.setDateTakenAfterMs(DATE_TAKEN_MS - 1);
         qfbAfter.setId(0);
         qfbAfter.setSizeBytes(1);
         try (Cursor cr = mFacade.queryMediaForUi(qfbAfter.build())) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media with sizeBytes set to 1 and dateTakenAfterMs set "
+                            + "to DATE_TAKEN_MS - 1.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID, MP4_VIDEO_MIME_TYPE);
@@ -666,14 +978,20 @@
         qfbBefore.setId(0);
         qfbBefore.setSizeBytes(10);
         try (Cursor cr = mFacade.queryMediaForUi(qfbBefore.build())) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of media with sizeBytes set to 10 and dateTakenBeforeMs "
+                            + "set to DATE_TAKEN_MS + 1.")
+                    .that(cr.getCount()).isEqualTo(2);
         }
 
         qfbBefore.setDateTakenBeforeMs(DATE_TAKEN_MS + 1);
         qfbBefore.setId(0);
         qfbBefore.setSizeBytes(1);
         try (Cursor cr = mFacade.queryMediaForUi(qfbBefore.build())) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of media with sizeBytes set to 1 and dateTakenBeforeMs set"
+                            + " to DATE_TAKEN_MS + 1.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID, MP4_VIDEO_MIME_TYPE);
@@ -712,26 +1030,34 @@
         PickerDbFacade.QueryFilterBuilder qfbAll = new PickerDbFacade.QueryFilterBuilder(1000);
         qfbAll.setMimeTypes(new String[]{"*/*"});
         try (Cursor cr = mFacade.queryMediaForUi(qfbAll.build())) {
-            assertThat(cr.getCount()).isEqualTo(6);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to {\"*/*\"}")
+                    .that(cr.getCount()).isEqualTo(6);
         }
 
         qfbAll.setMimeTypes(new String[]{"image/*"});
         try (Cursor cr = mFacade.queryMediaForUi(qfbAll.build())) {
-            assertThat(cr.getCount()).isEqualTo(4);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to {\"image/*\"}")
+                    .that(cr.getCount()).isEqualTo(4);
 
-            assertAllMediaCursor(cr, new String[] {CLOUD_ID_2, CLOUD_ID_1, LOCAL_ID_2,
-                    CLOUD_ID_3}, new long[] {DATE_TAKEN_MS, DATE_TAKEN_MS, DATE_TAKEN_MS,
-                        DATE_TAKEN_MS - 1}, new String[] {GIF_IMAGE_MIME_TYPE, PNG_IMAGE_MIME_TYPE,
+            assertAllMediaCursor(cr,
+                    new String[]{CLOUD_ID_2, CLOUD_ID_1, LOCAL_ID_2, CLOUD_ID_3},
+                    new long[]{DATE_TAKEN_MS, DATE_TAKEN_MS, DATE_TAKEN_MS, DATE_TAKEN_MS - 1},
+                    new String[]{GIF_IMAGE_MIME_TYPE, PNG_IMAGE_MIME_TYPE,
                             JPEG_IMAGE_MIME_TYPE, PNG_IMAGE_MIME_TYPE});
         }
 
         qfbAll.setMimeTypes(new String[]{"video/*"});
         try (Cursor cr = mFacade.queryMediaForUi(qfbAll.build())) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to {\"video/*\"}")
+                    .that(cr.getCount()).isEqualTo(2);
 
-            assertAllMediaCursor(cr, new String[] {LOCAL_ID_3, LOCAL_ID_1}, new long[]
-                    {DATE_TAKEN_MS + 1, DATE_TAKEN_MS}, new String[] {MP4_VIDEO_MIME_TYPE,
-                        WEBM_VIDEO_MIME_TYPE});
+            assertAllMediaCursor(cr,
+                    new String[]{LOCAL_ID_3, LOCAL_ID_1},
+                    new long[]{DATE_TAKEN_MS + 1, DATE_TAKEN_MS},
+                    new String[]{MP4_VIDEO_MIME_TYPE, WEBM_VIDEO_MIME_TYPE});
         }
 
         // Verify after
@@ -740,29 +1066,40 @@
         qfbAfter.setId(0);
         qfbAfter.setMimeTypes(new String[]{"image/*"});
         try (Cursor cr = mFacade.queryMediaForUi(qfbAfter.build())) {
-            assertThat(cr.getCount()).isEqualTo(3);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to {\"image/*\"} "
+                            + "and date taken after set to DATE_TAKEN_MS")
+                    .that(cr.getCount()).isEqualTo(3);
         }
 
         qfbAfter.setDateTakenAfterMs(DATE_TAKEN_MS - 1);
         qfbAfter.setId(0);
         qfbAfter.setMimeTypes(new String[]{PNG_IMAGE_MIME_TYPE});
         try (Cursor cr = mFacade.queryMediaForUi(qfbAfter.build())) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to "
+                            + "{PNG_IMAGE_MIME_TYPE} and date taken after set to DATE_TAKEN_MS - 1")
+                    .that(cr.getCount()).isEqualTo(2);
 
-            assertAllMediaCursor(cr, new String[] {CLOUD_ID_1, CLOUD_ID_3}, new long[]
-                    {DATE_TAKEN_MS, DATE_TAKEN_MS - 1}, new String[] {PNG_IMAGE_MIME_TYPE,
-                        PNG_IMAGE_MIME_TYPE});
+            assertAllMediaCursor(cr,
+                    new String[]{CLOUD_ID_1, CLOUD_ID_3},
+                    new long[]{DATE_TAKEN_MS, DATE_TAKEN_MS - 1},
+                    new String[]{PNG_IMAGE_MIME_TYPE, PNG_IMAGE_MIME_TYPE});
         }
 
         qfbAfter.setDateTakenAfterMs(DATE_TAKEN_MS - 1);
         qfbAfter.setId(0);
         qfbAfter.setMimeTypes(new String[]{"video/*"});
         try (Cursor cr = mFacade.queryMediaForUi(qfbAfter.build())) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to {\"video/*\"} "
+                            + "and date taken after set to DATE_TAKEN_MS - 1")
+                    .that(cr.getCount()).isEqualTo(2);
 
-            assertAllMediaCursor(cr, new String[] {LOCAL_ID_3, LOCAL_ID_1}, new long[]
-                    {DATE_TAKEN_MS + 1, DATE_TAKEN_MS}, new String[] {MP4_VIDEO_MIME_TYPE,
-                        WEBM_VIDEO_MIME_TYPE});
+            assertAllMediaCursor(cr,
+                    new String[]{LOCAL_ID_3, LOCAL_ID_1},
+                    new long[]{DATE_TAKEN_MS + 1, DATE_TAKEN_MS},
+                    new String[]{MP4_VIDEO_MIME_TYPE, WEBM_VIDEO_MIME_TYPE});
         }
 
         // Verify before
@@ -771,14 +1108,20 @@
         qfbBefore.setId(0);
         qfbBefore.setMimeTypes(new String[]{"*/*"});
         try (Cursor cr = mFacade.queryMediaForUi(qfbBefore.build())) {
-            assertThat(cr.getCount()).isEqualTo(5);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to {\"*/*\"} and "
+                            + "date taken before set to DATE_TAKEN_MS + 1")
+                    .that(cr.getCount()).isEqualTo(5);
         }
 
         qfbBefore.setDateTakenBeforeMs(DATE_TAKEN_MS + 1);
         qfbBefore.setId(0);
         qfbBefore.setMimeTypes(new String[]{"video/*"});
         try (Cursor cr = mFacade.queryMediaForUi(qfbBefore.build())) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to {\"video/*\"} "
+                            + "and date taken before set to DATE_TAKEN_MS + 1")
+                    .that(cr.getCount()).isEqualTo(1);
 
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID_1, DATE_TAKEN_MS, WEBM_VIDEO_MIME_TYPE);
@@ -788,22 +1131,31 @@
         qfbBefore.setId(0);
         qfbBefore.setMimeTypes(new String[]{"video/*"});
         try (Cursor cr = mFacade.queryMediaForUi(qfbBefore.build())) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to {\"video/*\"} "
+                            + "and date taken before set to DATE_TAKEN_MS + 2")
+                    .that(cr.getCount()).isEqualTo(2);
 
-            assertAllMediaCursor(cr, new String[] {LOCAL_ID_3, LOCAL_ID_1}, new long[]
-                    {DATE_TAKEN_MS + 1, DATE_TAKEN_MS}, new String[] {MP4_VIDEO_MIME_TYPE,
-                        WEBM_VIDEO_MIME_TYPE});
+            assertAllMediaCursor(cr,
+                    new String[]{LOCAL_ID_3, LOCAL_ID_1},
+                    new long[]{DATE_TAKEN_MS + 1, DATE_TAKEN_MS},
+                    new String[]{MP4_VIDEO_MIME_TYPE, WEBM_VIDEO_MIME_TYPE});
         }
 
         qfbBefore.setDateTakenBeforeMs(DATE_TAKEN_MS + 1);
         qfbBefore.setId(0);
         qfbBefore.setMimeTypes(new String[]{PNG_IMAGE_MIME_TYPE});
         try (Cursor cr = mFacade.queryMediaForUi(qfbBefore.build())) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to "
+                            + "{PNG_IMAGE_MIME_TYPE} and date taken before set to DATE_TAKEN_MS +"
+                            + " 1")
+                    .that(cr.getCount()).isEqualTo(2);
 
-            assertAllMediaCursor(cr, new String[] {CLOUD_ID_1, CLOUD_ID_3}, new long[]
-                    {DATE_TAKEN_MS, DATE_TAKEN_MS - 1}, new String[] {PNG_IMAGE_MIME_TYPE ,
-                        PNG_IMAGE_MIME_TYPE});
+            assertAllMediaCursor(cr,
+                    new String[]{CLOUD_ID_1, CLOUD_ID_3},
+                    new long[]{DATE_TAKEN_MS, DATE_TAKEN_MS - 1},
+                    new String[]{PNG_IMAGE_MIME_TYPE, PNG_IMAGE_MIME_TYPE});
         }
     }
 
@@ -847,21 +1199,29 @@
         PickerDbFacade.QueryFilterBuilder qfbAll = new PickerDbFacade.QueryFilterBuilder(1000);
         qfbAll.setMimeTypes(new String[]{"*/*"});
         try (Cursor cr = mFacade.queryMediaForUi(qfbAll.build())) {
-            assertThat(cr.getCount()).isEqualTo(8);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to {\"*/*\"}")
+                    .that(cr.getCount()).isEqualTo(8);
         }
 
         qfbAll.setMimeTypes(new String[]{"image/*", PNG_IMAGE_MIME_TYPE, MP4_VIDEO_MIME_TYPE});
         try (Cursor cr = mFacade.queryMediaForUi(qfbAll.build())) {
-            assertThat(cr.getCount()).isEqualTo(6);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to {\"image/*\","
+                            + "PNG_IMAGE_MIME_TYPE ,PNG_IMAGE_MIME_TYPE}")
+                    .that(cr.getCount()).isEqualTo(6);
         }
 
         qfbAll.setMimeTypes(new String[]{GIF_IMAGE_MIME_TYPE, MPEG_VIDEO_MIME_TYPE,
                 WEBM_VIDEO_MIME_TYPE});
         try (Cursor cr = mFacade.queryMediaForUi(qfbAll.build())) {
-            assertThat(cr.getCount()).isEqualTo(3);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to "
+                            + "{GIF_IMAGE_MIME_TYPE, MPEG_VIDEO_MIME_TYPE, WEBM_VIDEO_MIME_TYPE}")
+                    .that(cr.getCount()).isEqualTo(3);
 
-            assertAllMediaCursor(cr, new String[] {CLOUD_ID_3, CLOUD_ID_2, LOCAL_ID_1},
-                    new long[] {DATE_TAKEN_MS, DATE_TAKEN_MS, DATE_TAKEN_MS}, new String[] {
+            assertAllMediaCursor(cr, new String[]{CLOUD_ID_3, CLOUD_ID_2, LOCAL_ID_1},
+                    new long[]{DATE_TAKEN_MS, DATE_TAKEN_MS, DATE_TAKEN_MS}, new String[]{
                             MPEG_VIDEO_MIME_TYPE, GIF_IMAGE_MIME_TYPE, WEBM_VIDEO_MIME_TYPE});
         }
 
@@ -872,7 +1232,10 @@
         qfbAfter.setId(0);
         qfbAfter.setMimeTypes(new String[]{"video/*"});
         try (Cursor cr = mFacade.queryMediaForUi(qfbAfter.build())) {
-            assertThat(cr.getCount()).isEqualTo(4);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to {\"video/*\"} "
+                            + "and date taken after set to DATE_TAKEN_MS - 1")
+                    .that(cr.getCount()).isEqualTo(4);
         }
 
         qfbAfter.setDateTakenAfterMs(DATE_TAKEN_MS - 1);
@@ -880,10 +1243,14 @@
         qfbAfter.setMimeTypes(new String[]{GIF_IMAGE_MIME_TYPE,
                 MPEG_VIDEO_MIME_TYPE, WEBM_VIDEO_MIME_TYPE, M4V_VIDEO_MIME_TYPE});
         try (Cursor cr = mFacade.queryMediaForUi(qfbAfter.build())) {
-            assertThat(cr.getCount()).isEqualTo(3);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to "
+                            + "{GIF_IMAGE_MIME_TYPE, MPEG_VIDEO_MIME_TYPE, WEBM_VIDEO_MIME_TYPE, "
+                            + "M4V_VIDEO_MIME_TYPE} and date taken after set to DATE_TAKEN_MS - 1")
+                    .that(cr.getCount()).isEqualTo(3);
 
-            assertAllMediaCursor(cr, new String[] {CLOUD_ID_3, CLOUD_ID_2, LOCAL_ID_1},
-                    new long[] {DATE_TAKEN_MS, DATE_TAKEN_MS, DATE_TAKEN_MS}, new String[] {
+            assertAllMediaCursor(cr, new String[]{CLOUD_ID_3, CLOUD_ID_2, LOCAL_ID_1},
+                    new long[]{DATE_TAKEN_MS, DATE_TAKEN_MS, DATE_TAKEN_MS}, new String[]{
                             MPEG_VIDEO_MIME_TYPE, GIF_IMAGE_MIME_TYPE, WEBM_VIDEO_MIME_TYPE});
         }
 
@@ -891,7 +1258,11 @@
         qfbAfter.setId(0);
         qfbAfter.setMimeTypes(new String[]{GIF_IMAGE_MIME_TYPE, MP4_VIDEO_MIME_TYPE});
         try (Cursor cr = mFacade.queryMediaForUi(qfbAfter.build())) {
-            assertThat(cr.getCount()).isEqualTo(3);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to "
+                            + "{GIF_IMAGE_MIME_TYPE, MP4_VIDEO_MIME_TYPE} and date taken after "
+                            + "set to DATE_TAKEN_MS - 1")
+                    .that(cr.getCount()).isEqualTo(3);
         }
 
         // Verify before
@@ -900,14 +1271,20 @@
         qfbBefore.setId(0);
         qfbBefore.setMimeTypes(new String[]{"*/*"});
         try (Cursor cr = mFacade.queryMediaForUi(qfbBefore.build())) {
-            assertThat(cr.getCount()).isEqualTo(7);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to {\"*/*\"} and "
+                            + "date taken before set to DATE_TAKEN_MS + 1")
+                    .that(cr.getCount()).isEqualTo(7);
         }
 
         qfbBefore.setDateTakenBeforeMs(DATE_TAKEN_MS);
         qfbBefore.setId(0);
         qfbBefore.setMimeTypes(new String[]{"image/*"});
         try (Cursor cr = mFacade.queryMediaForUi(qfbBefore.build())) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to {\"image/*\"} "
+                            + "and date taken before set to DATE_TAKEN_MS")
+                    .that(cr.getCount()).isEqualTo(1);
 
             cr.moveToFirst();
             assertCloudMediaCursor(cr, CLOUD_ID_4, DATE_TAKEN_MS - 1, PNG_IMAGE_MIME_TYPE);
@@ -917,10 +1294,14 @@
         qfbBefore.setId(0);
         qfbBefore.setMimeTypes(new String[]{MP4_VIDEO_MIME_TYPE, GIF_IMAGE_MIME_TYPE});
         try (Cursor cr = mFacade.queryMediaForUi(qfbBefore.build())) {
-            assertThat(cr.getCount()).isEqualTo(3);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to "
+                            + "{MP4_VIDEO_MIME_TYPE, GIF_IMAGE_MIME_TYPE} and date taken before "
+                            + "set to DATE_TAKEN_MS + 2")
+                    .that(cr.getCount()).isEqualTo(3);
 
-            assertAllMediaCursor(cr, new String[] {LOCAL_ID_4, CLOUD_ID_2, LOCAL_ID_3},
-                    new long[] {DATE_TAKEN_MS + 1, DATE_TAKEN_MS, DATE_TAKEN_MS}, new String[] {
+            assertAllMediaCursor(cr, new String[]{LOCAL_ID_4, CLOUD_ID_2, LOCAL_ID_3},
+                    new long[]{DATE_TAKEN_MS + 1, DATE_TAKEN_MS, DATE_TAKEN_MS}, new String[]{
                             MP4_VIDEO_MIME_TYPE, GIF_IMAGE_MIME_TYPE, MP4_VIDEO_MIME_TYPE});
         }
     }
@@ -942,14 +1323,20 @@
         qfbAll.setMimeTypes(new String[]{"*/*"});
         qfbAll.setSizeBytes(10);
         try (Cursor cr = mFacade.queryMediaForUi(qfbAll.build())) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to {\"*/*\"} and size "
+                            + "filter set to 10 bytes")
+                    .that(cr.getCount()).isEqualTo(2);
         }
 
         // mime_type and size filter matches none
         qfbAll.setMimeTypes(new String[]{WEBM_VIDEO_MIME_TYPE});
         qfbAll.setSizeBytes(1);
         try (Cursor cr = mFacade.queryMediaForUi(qfbAll.build())) {
-            assertThat(cr.getCount()).isEqualTo(0);
+            assertWithMessage(
+                    "Unexpected number of rows with mime_type filter set to "
+                            + "{WEBM_VIDEO_MIME_TYPE} and size filter set to 1 byte")
+                    .that(cr.getCount()).isEqualTo(0);
         }
     }
 
@@ -973,20 +1360,22 @@
         }
 
         // Assert one projection column
-        final String[] oneProjection = new String[] { PickerMediaColumns.DATE_TAKEN };
+        final String[] oneProjection = new String[]{PickerMediaColumns.DATE_TAKEN};
 
         try (Cursor cr = mFacade.queryMediaIdForApps(CLOUD_PROVIDER, CLOUD_ID,
                 oneProjection)) {
             assertThat(cr.getCount()).isEqualTo(1);
 
             cr.moveToFirst();
-            assertThat(cr.getLong(cr.getColumnIndex(PickerMediaColumns.DATE_TAKEN)))
+            assertWithMessage(
+                    "Unexpected value of PickerMediaColumns.DATE_TAKEN with cloud provider.")
+                    .that(cr.getLong(cr.getColumnIndex(PickerMediaColumns.DATE_TAKEN)))
                     .isEqualTo(DATE_TAKEN_MS);
         }
 
         // Assert invalid projection column
         final String invalidColumn = "testInvalidColumn";
-        final String[] invalidProjection = new String[] {
+        final String[] invalidProjection = new String[]{
                 PickerMediaColumns.DATE_TAKEN,
                 invalidColumn
         };
@@ -996,9 +1385,13 @@
             assertThat(cr.getCount()).isEqualTo(1);
 
             cr.moveToFirst();
-            assertThat(cr.getLong(cr.getColumnIndex(invalidColumn)))
+            assertWithMessage(
+                    "Unexpected value of the invalidColumn with cloud provider.")
+                    .that(cr.getLong(cr.getColumnIndex(invalidColumn)))
                     .isEqualTo(0);
-            assertThat(cr.getLong(cr.getColumnIndex(PickerMediaColumns.DATE_TAKEN)))
+            assertWithMessage(
+                    "Unexpected value of PickerMediaColumns.DATE_TAKEN with cloud provider.")
+                    .that(cr.getLong(cr.getColumnIndex(PickerMediaColumns.DATE_TAKEN)))
                     .isEqualTo(DATE_TAKEN_MS);
         }
     }
@@ -1020,7 +1413,9 @@
                 new PickerDbFacade.QueryFilterBuilder(/* limit */ 1000);
         try (Cursor cr = mFacade.queryMediaForUi(qfb.build())) {
 
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of rows on queryMediaForUi.")
+                    .that(cr.getCount()).isEqualTo(2);
             cr.moveToFirst();
             assertThrows(
                     IllegalArgumentException.class,
@@ -1063,7 +1458,9 @@
         try (Cursor cr =
                      mFacade.queryAlbumMediaForUi(
                              localQfb.setAlbumId(ALBUM_ID).build(), LOCAL_PROVIDER)) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of rows on queryAlbumMediaForUi with local provider.")
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertThrows(
                     IllegalArgumentException.class,
@@ -1081,7 +1478,9 @@
         try (Cursor cr =
                      mFacade.queryAlbumMediaForUi(
                              cloudQfb.setAlbumId(ALBUM_ID).build(), CLOUD_PROVIDER)) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of rows on queryAlbumMediaForUi with cloud provider.")
+                    .that(cr.getCount()).isEqualTo(2);
             cr.moveToFirst();
             assertThrows(
                     IllegalArgumentException.class,
@@ -1104,7 +1503,10 @@
         assertAddMediaOperation(CLOUD_PROVIDER, cloudCursor, 1);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of rows on queryMediaAll with both local and cloud "
+                            + "provider.")
+                    .that(cr.getCount()).isEqualTo(2);
 
             cr.moveToFirst();
             assertCloudMediaCursor(cr, CLOUD_ID, DATE_TAKEN_MS);
@@ -1117,7 +1519,9 @@
         mFacade.setCloudProvider(null);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(1);
+            assertWithMessage(
+                    "Unexpected number of rows on queryMediaAll after hiding cloud provider.")
+                    .that(cr.getCount()).isEqualTo(1);
 
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID, DATE_TAKEN_MS);
@@ -1127,7 +1531,9 @@
         mFacade.setCloudProvider(CLOUD_PROVIDER);
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of rows on queryMediaAll after un-hiding cloud provider.")
+                    .that(cr.getCount()).isEqualTo(2);
 
             cr.moveToFirst();
             assertCloudMediaCursor(cr, CLOUD_ID, DATE_TAKEN_MS);
@@ -1168,12 +1574,17 @@
         PickerDbFacade.QueryFilterBuilder qfb =
                 new PickerDbFacade.QueryFilterBuilder(/* limit */ 1000);
         try (Cursor cr = mFacade.queryMediaForUi(qfb.build())) {
-            assertThat(cr.getCount()).isEqualTo(4);
+            assertWithMessage(
+                    "Unexpected number of rows on queryMediaForUi with no filter.")
+                    .that(cr.getCount()).isEqualTo(4);
         }
 
         qfb.setIsFavorite(true);
         try (Cursor cr = mFacade.queryMediaForUi(qfb.build())) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of rows on queryMediaForUi with isFavorite filter set to "
+                            + "true.")
+                    .that(cr.getCount()).isEqualTo(2);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, CLOUD_ID + 1, DATE_TAKEN_MS);
 
@@ -1213,11 +1624,15 @@
         PickerDbFacade.QueryFilterBuilder qfb =
                 new PickerDbFacade.QueryFilterBuilder(/* limit */ 1000);
         try (Cursor cr = mFacade.queryMediaForUi(qfb.build())) {
-            assertThat(cr.getCount()).isEqualTo(4);
+            assertWithMessage("Unexpected number of rows on queryMediaForUi without any filter.")
+                    .that(cr.getCount()).isEqualTo(4);
         }
 
-        try (Cursor cr = mFacade.getMergedAlbums(qfb.build())) {
-            assertThat(cr.getCount()).isEqualTo(2);
+        try (Cursor cr = mFacade.getMergedAlbums(qfb.build(), CLOUD_PROVIDER)) {
+            assertWithMessage(
+                    "Unexpected number of rows on getMergedAlbums without any filter for cloud "
+                            + "provider.")
+                    .that(cr.getCount()).isEqualTo(2);
             cr.moveToFirst();
             assertCloudAlbumCursor(cr,
                     ALBUM_ID_FAVORITES,
@@ -1236,6 +1651,100 @@
     }
 
     @Test
+    public void testGetVideosAlbumWithMimeTypesFilter() throws Exception {
+        Cursor localCursor1 = getMediaCursor(LOCAL_ID + "1", DATE_TAKEN_MS, GENERATION_MODIFIED,
+                /* mediaStoreUri */ null, SIZE_BYTES, MP4_VIDEO_MIME_TYPE,
+                STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+        Cursor localCursor2 = getMediaCursor(LOCAL_ID + "2", DATE_TAKEN_MS, GENERATION_MODIFIED,
+                /* mediaStoreUri */ null, SIZE_BYTES, JPEG_IMAGE_MIME_TYPE,
+                STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ true);
+        Cursor cloudCursor1 = getMediaCursor(CLOUD_ID + "1", DATE_TAKEN_MS, GENERATION_MODIFIED,
+                /* mediaStoreUri */ null, SIZE_BYTES, JPEG_IMAGE_MIME_TYPE,
+                STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+        Cursor cloudCursor2 = getMediaCursor(CLOUD_ID + "2", DATE_TAKEN_MS, GENERATION_MODIFIED,
+                /* mediaStoreUri */ null, SIZE_BYTES, MP4_VIDEO_MIME_TYPE,
+                STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+
+        try (PickerDbFacade.DbWriteOperation operation =
+                     mFacade.beginAddMediaOperation(LOCAL_PROVIDER)) {
+            assertWriteOperation(operation, localCursor1, 1);
+            assertWriteOperation(operation, localCursor2, 1);
+            operation.setSuccess();
+        }
+        try (PickerDbFacade.DbWriteOperation operation =
+                     mFacade.beginAddMediaOperation(CLOUD_PROVIDER)) {
+            assertWriteOperation(operation, cloudCursor1, 1);
+            assertWriteOperation(operation, cloudCursor2, 1);
+            operation.setSuccess();
+        }
+
+        PickerDbFacade.QueryFilterBuilder qfb =
+                new PickerDbFacade.QueryFilterBuilder(/* limit */ 1000);
+        try (Cursor cr = mFacade.queryMediaForUi(qfb.build())) {
+            assertWithMessage("Unexpected number of rows on queryMediaForUi without any filter.")
+                    .that(cr.getCount()).isEqualTo(4);
+        }
+
+        try (Cursor cr = mFacade.getMergedAlbums(qfb.build(), CLOUD_PROVIDER)) {
+            assertWithMessage(
+                    "Unexpected number of rows on getMergedAlbums without any filter for cloud "
+                            + "provider.")
+                    .that(cr.getCount()).isEqualTo(2);
+            cr.moveToFirst();
+            assertCloudAlbumCursor(cr,
+                    ALBUM_ID_FAVORITES,
+                    ALBUM_ID_FAVORITES,
+                    LOCAL_ID + "2",
+                    DATE_TAKEN_MS,
+                    /* count */ 1);
+            cr.moveToNext();
+            assertCloudAlbumCursor(cr,
+                    ALBUM_ID_VIDEOS,
+                    ALBUM_ID_VIDEOS,
+                    LOCAL_ID + "1",
+                    DATE_TAKEN_MS,
+                    /* count */ 2);
+        }
+
+        qfb.setMimeTypes(new String[]{MP4_VIDEO_MIME_TYPE, JPEG_IMAGE_MIME_TYPE});
+        try (Cursor cr = mFacade.getMergedAlbums(qfb.build(), /* cloudProvider*/ CLOUD_PROVIDER)) {
+            assertWithMessage(
+                    "Unexpected number of rows on getMergedAlbums without any filter for cloud "
+                            + "provider.")
+                    .that(cr.getCount()).isEqualTo(2);
+            cr.moveToFirst();
+            assertCloudAlbumCursor(cr,
+                    ALBUM_ID_FAVORITES,
+                    ALBUM_ID_FAVORITES,
+                    LOCAL_ID + "2",
+                    DATE_TAKEN_MS,
+                    /* count */ 1);
+            cr.moveToNext();
+            assertCloudAlbumCursor(cr,
+                    ALBUM_ID_VIDEOS,
+                    ALBUM_ID_VIDEOS,
+                    LOCAL_ID + "1",
+                    DATE_TAKEN_MS,
+                    /* count */ 2);
+        }
+
+        qfb.setMimeTypes(new String[]{GIF_IMAGE_MIME_TYPE, JPEG_IMAGE_MIME_TYPE});
+        try (Cursor cr = mFacade.getMergedAlbums(qfb.build(), /* cloudProvider*/ CLOUD_PROVIDER)) {
+            assertWithMessage(
+                    "Unexpected number of rows on getMergedAlbums with mime type filter set to "
+                            + "{GIF_IMAGE_MIME_TYPE, JPEG_IMAGE_MIME_TYPE} for cloud provider.")
+                    .that(cr.getCount()).isEqualTo(1);
+            cr.moveToFirst();
+            assertCloudAlbumCursor(cr,
+                    ALBUM_ID_FAVORITES,
+                    ALBUM_ID_FAVORITES,
+                    LOCAL_ID + "2",
+                    DATE_TAKEN_MS,
+                    /* count */ 1);
+        }
+    }
+
+    @Test
     public void testGetFavoritesAlbumWithMimeTypesFilter() throws Exception {
         Cursor localCursor1 = getMediaCursor(LOCAL_ID + "1", DATE_TAKEN_MS, GENERATION_MODIFIED,
                 /* mediaStoreUri */ null, SIZE_BYTES, MP4_VIDEO_MIME_TYPE,
@@ -1266,11 +1775,15 @@
         PickerDbFacade.QueryFilterBuilder qfb =
                 new PickerDbFacade.QueryFilterBuilder(/* limit */ 1000);
         try (Cursor cr = mFacade.queryMediaForUi(qfb.build())) {
-            assertThat(cr.getCount()).isEqualTo(4);
+            assertWithMessage("Unexpected number of rows on queryMediaForUi without any filter.")
+                    .that(cr.getCount()).isEqualTo(4);
         }
 
-        try (Cursor cr = mFacade.getMergedAlbums(qfb.build())) {
-            assertThat(cr.getCount()).isEqualTo(2);
+        try (Cursor cr = mFacade.getMergedAlbums(qfb.build(), CLOUD_PROVIDER)) {
+            assertWithMessage(
+                    "Unexpected number of rows on getMergedAlbums without any filter for cloud "
+                            + "provider.")
+                    .that(cr.getCount()).isEqualTo(2);
             cr.moveToFirst();
             assertCloudAlbumCursor(cr,
                     ALBUM_ID_FAVORITES,
@@ -1288,8 +1801,25 @@
         }
 
         qfb.setMimeTypes(IMAGE_MIME_TYPES_QUERY);
-        try (Cursor cr = mFacade.getMergedAlbums(qfb.build())) {
-            assertThat(cr.getCount()).isEqualTo(1);
+        try (Cursor cr = mFacade.getMergedAlbums(qfb.build(), /* cloudProvider*/ null)) {
+            assertWithMessage(
+                    "Unexpected number of rows on getMergedAlbums with mime type filter set to "
+                            + "IMAGE_MIME_TYPES_QUERY and cloudProvider set to null.")
+                    .that(cr.getCount()).isEqualTo(1);
+            cr.moveToFirst();
+            assertCloudAlbumCursor(cr,
+                    ALBUM_ID_FAVORITES,
+                    ALBUM_ID_FAVORITES,
+                    CLOUD_ID + "1",
+                    DATE_TAKEN_MS,
+                    /* count */ 1);
+        }
+
+        try (Cursor cr = mFacade.getMergedAlbums(qfb.build(), CLOUD_PROVIDER)) {
+            assertWithMessage(
+                    "Unexpected number of rows on getMergedAlbums with mime type filter set to "
+                            + "{IMAGE_MIME_TYPES_QUERY} with cloudProvider.")
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudAlbumCursor(cr,
                     ALBUM_ID_FAVORITES,
@@ -1300,8 +1830,11 @@
         }
 
         qfb.setMimeTypes(VIDEO_MIME_TYPES_QUERY);
-        try (Cursor cr = mFacade.getMergedAlbums(qfb.build())) {
-            assertThat(cr.getCount()).isEqualTo(2);
+        try (Cursor cr = mFacade.getMergedAlbums(qfb.build(), CLOUD_PROVIDER)) {
+            assertWithMessage(
+                    "Unexpected number of rows on getMergedAlbums with mime type filter set to "
+                            + "VIDEO_MIME_TYPES_QUERY with cloudProvider.")
+                    .that(cr.getCount()).isEqualTo(2);
             cr.moveToFirst();
             assertCloudAlbumCursor(cr,
                     ALBUM_ID_FAVORITES,
@@ -1319,8 +1852,11 @@
         }
 
         qfb.setMimeTypes(new String[]{"foo"});
-        try (Cursor cr = mFacade.getMergedAlbums(qfb.build())) {
-            assertThat(cr.getCount()).isEqualTo(0);
+        try (Cursor cr = mFacade.getMergedAlbums(qfb.build(), CLOUD_PROVIDER)) {
+            assertWithMessage(
+                    "Unexpected number of rows on getMergedAlbums with mime type filter set to "
+                            + "{\"foo\"} and not null cloudProvider.")
+                    .that(cr.getCount()).isEqualTo(1);
         }
     }
 
@@ -1362,24 +1898,33 @@
                 new PickerDbFacade.QueryFilterBuilder(/* limit */ 1000);
         // Verify that we see all(local + cloud) items.
         try (Cursor cr = mFacade.queryMediaForUi(qfb.build())) {
-            assertThat(cr.getCount()).isEqualTo(4);
+            assertWithMessage("Unexpected number of rows on queryMediaForUi without any filter.")
+                    .that(cr.getCount()).isEqualTo(4);
         }
 
         // Verify that we only see local items with isLocalOnly=true
         qfb.setIsLocalOnly(true);
         try (Cursor cr = mFacade.queryMediaForUi(qfb.build())) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage(
+                    "Unexpected number of rows on queryMediaForUi with isLocalOnly set to true.")
+                    .that(cr.getCount()).isEqualTo(2);
 
             cr.moveToNext();
-            assertThat(cr.getString(cr.getColumnIndex(MediaColumns.ID))).isEqualTo(LOCAL_ID + "2");
+            assertWithMessage("Unexpected value of MediaColumns.ID at cursor.")
+                    .that(cr.getString(cr.getColumnIndex(MediaColumns.ID))).isEqualTo(
+                            LOCAL_ID + "2");
             cr.moveToNext();
-            assertThat(cr.getString(cr.getColumnIndex(MediaColumns.ID))).isEqualTo(LOCAL_ID + "1");
+            assertWithMessage("Unexpected value of MediaColumns.ID at cursor.")
+                    .that(cr.getString(cr.getColumnIndex(MediaColumns.ID))).isEqualTo(
+                            LOCAL_ID + "1");
         }
 
         // Verify that we see all available merged albums and their respective media count
         qfb.setIsLocalOnly(false);
-        try (Cursor cr = mFacade.getMergedAlbums(qfb.build())) {
-            assertThat(cr.getCount()).isEqualTo(2);
+        try (Cursor cr = mFacade.getMergedAlbums(qfb.build(), CLOUD_PROVIDER)) {
+            assertWithMessage(
+                    "Unexpected number of rows on getMergedAlbums with isLocalOnly set to false.")
+                    .that(cr.getCount()).isEqualTo(2);
             cr.moveToFirst();
             assertCloudAlbumCursor(cr,
                     ALBUM_ID_FAVORITES,
@@ -1398,8 +1943,11 @@
 
         qfb.setIsLocalOnly(true);
         // Verify that with isLocalOnly=true, we only see one album with only one local item.
-        try (Cursor cr = mFacade.getMergedAlbums(qfb.build())) {
-            assertThat(cr.getCount()).isEqualTo(1);
+        try (Cursor cr = mFacade.getMergedAlbums(qfb.build(), /* cloudProvider */ null)) {
+            assertWithMessage(
+                    "Unexpected number of rows on getMergedAlbums with isLocalOnly set to true "
+                            + "and cloudProvider set to null.")
+                    .that(cr.getCount()).isEqualTo(1);
             cr.moveToFirst();
             assertCloudAlbumCursor(cr,
                     ALBUM_ID_FAVORITES,
@@ -1427,7 +1975,8 @@
         }
 
         try (Cursor cr = queryMediaAll()) {
-            assertThat(cr.getCount()).isEqualTo(2);
+            assertWithMessage("Unexpected number of rows on queryMediaForUi.")
+                    .that(cr.getCount()).isEqualTo(2);
             cr.moveToFirst();
             assertCloudMediaCursor(cr, LOCAL_ID + 1, MP4_VIDEO_MIME_TYPE);
 
@@ -1468,17 +2017,20 @@
             ContentValues values = new ContentValues();
             values.put(PickerDbFacade.KEY_STANDARD_MIME_TYPE_EXTENSION,
                     MediaColumns.STANDARD_MIME_TYPE_EXTENSION_ANIMATED_WEBP);
-            assertThat(operation.execute(LOCAL_ID, values)).isTrue();
+            assertWithMessage("Failed to update media with LOCAL_ID.")
+                    .that(operation.execute(LOCAL_ID, values)).isTrue();
             operation.setSuccess();
         }
 
         try (Cursor cursor = queryMediaAll()) {
-            assertThat(cursor.getCount()).isEqualTo(1);
+            assertWithMessage("Unexpected number of rows after update operation.")
+                    .that(cursor.getCount()).isEqualTo(1);
 
             // Assert that STANDARD_MIME_TYPE_EXTENSION has been updated
             cursor.moveToFirst();
-            assertThat(cursor.getInt(cursor.getColumnIndex(
-                    MediaColumns.STANDARD_MIME_TYPE_EXTENSION)))
+            assertWithMessage("Failed to update STANDARD_MIME_TYPE_EXTENSION.")
+                    .that(cursor.getInt(cursor.getColumnIndex(
+                            MediaColumns.STANDARD_MIME_TYPE_EXTENSION)))
                     .isEqualTo(MediaColumns.STANDARD_MIME_TYPE_EXTENSION_ANIMATED_WEBP);
         }
     }
@@ -1499,17 +2051,20 @@
             ContentValues values = new ContentValues();
             values.put(PickerDbFacade.KEY_STANDARD_MIME_TYPE_EXTENSION,
                     MediaColumns.STANDARD_MIME_TYPE_EXTENSION_ANIMATED_WEBP);
-            assertThat(operation.execute(CLOUD_ID, values)).isFalse();
+            assertWithMessage("Unexpected, should have failed to update media with CLOUD_ID.")
+                    .that(operation.execute(CLOUD_ID, values)).isFalse();
             operation.setSuccess();
         }
 
         try (Cursor cursor = queryMediaAll()) {
-            assertThat(cursor.getCount()).isEqualTo(1);
+            assertWithMessage("Unexpected number of rows after update operation.")
+                    .that(cursor.getCount()).isEqualTo(1);
 
             // Assert that STANDARD_MIME_TYPE_EXTENSION is same as before
             cursor.moveToFirst();
-            assertThat(cursor.getInt(cursor.getColumnIndex(
-                    MediaColumns.STANDARD_MIME_TYPE_EXTENSION)))
+            assertWithMessage("Unexpected STANDARD_MIME_TYPE_EXTENSION, not same as before.")
+                    .that(cursor.getInt(cursor.getColumnIndex(
+                            MediaColumns.STANDARD_MIME_TYPE_EXTENSION)))
                     .isEqualTo(STANDARD_MIME_TYPE_EXTENSION);
         }
     }
@@ -1571,21 +2126,22 @@
     private static void assertWriteOperation(PickerDbFacade.DbWriteOperation operation,
             Cursor cursor, int expectedWriteCount) {
         final int writeCount = operation.execute(cursor);
-        assertThat(writeCount).isEqualTo(expectedWriteCount);
+        assertWithMessage("Unexpected write count on operation.execute(cursor).")
+                .that(writeCount).isEqualTo(expectedWriteCount);
     }
 
     // TODO(b/190713331): s/id/CloudMediaProviderContract#MediaColumns#ID/
     private static Cursor getDeletedMediaCursor(String id) {
         MatrixCursor c =
-                new MatrixCursor(new String[] {"id"});
-        c.addRow(new String[] {id});
+                new MatrixCursor(new String[]{"id"});
+        c.addRow(new String[]{id});
         return c;
     }
 
     private static Cursor getMediaCursor(String id, long dateTakenMs, long generationModified,
             String mediaStoreUri, long sizeBytes, String mimeType, int standardMimeTypeExtension,
             boolean isFavorite) {
-        String[] projectionKey = new String[] {
+        String[] projectionKey = new String[]{
                 MediaColumns.ID,
                 MediaColumns.MEDIA_STORE_URI,
                 MediaColumns.DATE_TAKEN_MILLIS,
@@ -1600,7 +2156,7 @@
                 MediaColumns.ORIENTATION,
         };
 
-        String[] projectionValue = new String[] {
+        String[] projectionValue = new String[]{
                 id,
                 mediaStoreUri,
                 String.valueOf(dateTakenMs),
@@ -1629,7 +2185,7 @@
             String mimeType,
             int standardMimeTypeExtension) {
         String[] projectionKey =
-                new String[] {
+                new String[]{
                         MediaColumns.ID,
                         MediaColumns.MEDIA_STORE_URI,
                         MediaColumns.DATE_TAKEN_MILLIS,
@@ -1641,7 +2197,7 @@
                 };
 
         String[] projectionValue =
-                new String[] {
+                new String[]{
                         id,
                         mediaStoreUri,
                         String.valueOf(dateTakenMs),
@@ -1694,15 +2250,21 @@
 
     private static void assertCloudAlbumCursor(Cursor cursor, String albumId, String displayName,
             String mediaCoverId, long dateTakenMs, long mediaCount) {
-        assertThat(cursor.getString(cursor.getColumnIndex(AlbumColumns.ID)))
+        assertWithMessage("Unexpected value of AlbumColumns.ID for cloud album cursor.")
+                .that(cursor.getString(cursor.getColumnIndex(AlbumColumns.ID)))
                 .isEqualTo(albumId);
-        assertThat(cursor.getString(cursor.getColumnIndex(AlbumColumns.DISPLAY_NAME)))
+        assertWithMessage("Unexpected value of AlbumColumns.DISPLAY_NAME for cloud album cursor.")
+                .that(cursor.getString(cursor.getColumnIndex(AlbumColumns.DISPLAY_NAME)))
                 .isEqualTo(displayName);
-        assertThat(cursor.getString(cursor.getColumnIndex(AlbumColumns.MEDIA_COVER_ID)))
+        assertWithMessage("Unexpected value of AlbumColumns.MEDIA_COVER_ID for cloud album cursor.")
+                .that(cursor.getString(cursor.getColumnIndex(AlbumColumns.MEDIA_COVER_ID)))
                 .isEqualTo(mediaCoverId);
-        assertThat(cursor.getLong(cursor.getColumnIndex(AlbumColumns.DATE_TAKEN_MILLIS)))
+        assertWithMessage(
+                "Unexpected value of AlbumColumns.DATE_TAKEN_MILLIS for cloud album cursor.")
+                .that(cursor.getLong(cursor.getColumnIndex(AlbumColumns.DATE_TAKEN_MILLIS)))
                 .isEqualTo(dateTakenMs);
-        assertThat(cursor.getLong(cursor.getColumnIndex(AlbumColumns.MEDIA_COUNT)))
+        assertWithMessage("Unexpected value of AlbumColumns.MEDIA_COUNT for cloud album cursor.")
+                .that(cursor.getLong(cursor.getColumnIndex(AlbumColumns.MEDIA_COUNT)))
                 .isEqualTo(mediaCount);
     }
 
@@ -1711,28 +2273,43 @@
         final String localData = getData(LOCAL_PROVIDER, displayName);
         final String cloudData = getData(CLOUD_PROVIDER, displayName);
 
-        assertThat(cursor.getString(cursor.getColumnIndex(MediaColumns.ID)))
+        assertWithMessage("Unexpected value of MediaColumns.ID for the cloud media cursor.")
+                .that(cursor.getString(cursor.getColumnIndex(MediaColumns.ID)))
                 .isEqualTo(id);
-        assertThat(cursor.getString(cursor.getColumnIndex(MediaColumns.AUTHORITY)))
+        assertWithMessage("Unexpected value of MediaColumns.AUTHORITY for the cloud media cursor.")
+                .that(cursor.getString(cursor.getColumnIndex(MediaColumns.AUTHORITY)))
                 .isEqualTo(id.startsWith(LOCAL_ID) ? LOCAL_PROVIDER : CLOUD_PROVIDER);
-        assertThat(cursor.getString(cursor.getColumnIndex(MediaColumns.DATA)))
+        assertWithMessage("Unexpected value of MediaColumns.DATA for the cloud media cursor.")
+                .that(cursor.getString(cursor.getColumnIndex(MediaColumns.DATA)))
                 .isEqualTo(id.startsWith(LOCAL_ID) ? localData : cloudData);
     }
 
     private static void assertCloudMediaCursor(Cursor cursor, String id, long dateTakenMs) {
         assertCloudMediaCursor(cursor, id, MP4_VIDEO_MIME_TYPE);
 
-        assertThat(cursor.getString(cursor.getColumnIndex(MediaColumns.MIME_TYPE)))
+        assertWithMessage("Unexpected value of MediaColumns.MIME_TYPE for the cloud media cursor.")
+                .that(cursor.getString(cursor.getColumnIndex(MediaColumns.MIME_TYPE)))
                 .isEqualTo(MP4_VIDEO_MIME_TYPE);
-        assertThat(cursor.getInt(cursor.getColumnIndex(MediaColumns.STANDARD_MIME_TYPE_EXTENSION)))
+        assertWithMessage(
+                "Unexpected value of MediaColumns.STANDARD_MIME_TYPE_EXTENSION for the cloud "
+                        + "media cursor.")
+                .that(cursor.getInt(
+                        cursor.getColumnIndex(MediaColumns.STANDARD_MIME_TYPE_EXTENSION)))
                 .isEqualTo(STANDARD_MIME_TYPE_EXTENSION);
-        assertThat(cursor.getLong(cursor.getColumnIndex(MediaColumns.DATE_TAKEN_MILLIS)))
+        assertWithMessage(
+                "Unexpected value of MediaColumns.DATE_TAKEN_MILLIS for the cloud media cursor.")
+                .that(cursor.getLong(cursor.getColumnIndex(MediaColumns.DATE_TAKEN_MILLIS)))
                 .isEqualTo(dateTakenMs);
-        assertThat(cursor.getLong(cursor.getColumnIndex(MediaColumns.SYNC_GENERATION)))
+        assertWithMessage(
+                "Unexpected value of MediaColumns.SYNC_GENERATION for the cloud media cursor.")
+                .that(cursor.getLong(cursor.getColumnIndex(MediaColumns.SYNC_GENERATION)))
                 .isEqualTo(GENERATION_MODIFIED);
-        assertThat(cursor.getLong(cursor.getColumnIndex(MediaColumns.SIZE_BYTES)))
+        assertWithMessage("Unexpected value of MediaColumns.SIZE_BYTES for the cloud media cursor.")
+                .that(cursor.getLong(cursor.getColumnIndex(MediaColumns.SIZE_BYTES)))
                 .isEqualTo(SIZE_BYTES);
-        assertThat(cursor.getLong(cursor.getColumnIndex(MediaColumns.DURATION_MILLIS)))
+        assertWithMessage(
+                "Unexpected value of MediaColumns.DURATION_MILLIS for the cloud media cursor.")
+                .that(cursor.getLong(cursor.getColumnIndex(MediaColumns.DURATION_MILLIS)))
                 .isEqualTo(DURATION_MS);
     }
 
@@ -1740,17 +2317,30 @@
             Cursor cursor, String id, long dateTakenMs, String mimeType) {
         assertCloudMediaCursor(cursor, id, mimeType);
 
-        assertThat(cursor.getString(cursor.getColumnIndex(MediaColumns.MIME_TYPE)))
+        assertWithMessage("Unexpected value for MediaColumns.MIME_TYPE for the cloud media cursor.")
+                .that(cursor.getString(cursor.getColumnIndex(MediaColumns.MIME_TYPE)))
                 .isEqualTo(mimeType);
-        assertThat(cursor.getInt(cursor.getColumnIndex(MediaColumns.STANDARD_MIME_TYPE_EXTENSION)))
+        assertWithMessage(
+                "Unexpected value for MediaColumns.STANDARD_MIME_TYPE_EXTENSION for the cloud "
+                        + "media cursor.")
+                .that(cursor.getInt(
+                        cursor.getColumnIndex(MediaColumns.STANDARD_MIME_TYPE_EXTENSION)))
                 .isEqualTo(STANDARD_MIME_TYPE_EXTENSION);
-        assertThat(cursor.getLong(cursor.getColumnIndex(MediaColumns.DATE_TAKEN_MILLIS)))
+        assertWithMessage(
+                "Unexpected value for MediaColumns.DATE_TAKEN_MILLIS for the cloud media cursor.")
+                .that(cursor.getLong(cursor.getColumnIndex(MediaColumns.DATE_TAKEN_MILLIS)))
                 .isEqualTo(dateTakenMs);
-        assertThat(cursor.getLong(cursor.getColumnIndex(MediaColumns.SYNC_GENERATION)))
+        assertWithMessage(
+                "Unexpected value for MediaColumns.SYNC_GENERATION for the cloud media cursor.")
+                .that(cursor.getLong(cursor.getColumnIndex(MediaColumns.SYNC_GENERATION)))
                 .isEqualTo(GENERATION_MODIFIED);
-        assertThat(cursor.getLong(cursor.getColumnIndex(MediaColumns.SIZE_BYTES)))
+        assertWithMessage(
+                "Unexpected value for MediaColumns.SIZE_BYTES for the cloud media cursor.")
+                .that(cursor.getLong(cursor.getColumnIndex(MediaColumns.SIZE_BYTES)))
                 .isEqualTo(SIZE_BYTES);
-        assertThat(cursor.getLong(cursor.getColumnIndex(MediaColumns.DURATION_MILLIS)))
+        assertWithMessage(
+                "Unexpected value for MediaColumns.DURATION_MILLIS for the cloud media cursor.")
+                .that(cursor.getLong(cursor.getColumnIndex(MediaColumns.DURATION_MILLIS)))
                 .isEqualTo(DURATION_MS);
     }
 
@@ -1758,8 +2348,11 @@
             Cursor cursor, String[] mediaIds, long[] dateTakenMs, String[] mimeTypes) {
         int mediaCount = cursor.getCount();
         for (int mediaNo = 0; mediaNo < mediaCount; mediaNo = mediaNo + 1) {
-            if (mediaNo == 0) cursor.moveToFirst();
-            else cursor.moveToNext();
+            if (mediaNo == 0) {
+                cursor.moveToFirst();
+            } else {
+                cursor.moveToNext();
+            }
             assertCloudMediaCursor(cursor, mediaIds[mediaNo], dateTakenMs[mediaNo],
                     mimeTypes[mediaNo]);
         }
@@ -1770,23 +2363,42 @@
         final String localData = getData(LOCAL_PROVIDER, displayName);
         final String cloudData = getData(CLOUD_PROVIDER, displayName);
 
-        assertThat(cursor.getString(cursor.getColumnIndex(PickerMediaColumns.DISPLAY_NAME)))
+        assertWithMessage(
+                "Unexpected value for PickerMediaColumns.DISPLAY_NAME for the media store cursor.")
+                .that(cursor.getString(cursor.getColumnIndex(PickerMediaColumns.DISPLAY_NAME)))
                 .isEqualTo(displayName);
-        assertThat(cursor.getString(cursor.getColumnIndex(PickerMediaColumns.DATA)))
+        assertWithMessage(
+                "Unexpected value for PickerMediaColumns.DATA for the media store cursor.")
+                .that(cursor.getString(cursor.getColumnIndex(PickerMediaColumns.DATA)))
                 .isEqualTo(id.startsWith(LOCAL_ID) ? localData : cloudData);
-        assertThat(cursor.getString(cursor.getColumnIndex(PickerMediaColumns.MIME_TYPE)))
+        assertWithMessage(
+                "Unexpected value for PickerMediaColumns.MIME_TYPE for the media store cursor.")
+                .that(cursor.getString(cursor.getColumnIndex(PickerMediaColumns.MIME_TYPE)))
                 .isEqualTo(MP4_VIDEO_MIME_TYPE);
-        assertThat(cursor.getLong(cursor.getColumnIndex(PickerMediaColumns.DATE_TAKEN)))
+        assertWithMessage(
+                "Unexpected value for PickerMediaColumns.DATE_TAKEN for the media store cursor.")
+                .that(cursor.getLong(cursor.getColumnIndex(PickerMediaColumns.DATE_TAKEN)))
                 .isEqualTo(dateTakenMs);
-        assertThat(cursor.getLong(cursor.getColumnIndex(PickerMediaColumns.SIZE)))
+        assertWithMessage(
+                "Unexpected value for PickerMediaColumns.SIZE for the media store cursor.")
+                .that(cursor.getLong(cursor.getColumnIndex(PickerMediaColumns.SIZE)))
                 .isEqualTo(SIZE_BYTES);
-        assertThat(cursor.getLong(cursor.getColumnIndex(PickerMediaColumns.DURATION_MILLIS)))
+        assertWithMessage(
+                "Unexpected value for PickerMediaColumns.DURATION_MILLIS for the media store "
+                        + "cursor.")
+                .that(cursor.getLong(cursor.getColumnIndex(PickerMediaColumns.DURATION_MILLIS)))
                 .isEqualTo(DURATION_MS);
-        assertThat(cursor.getInt(cursor.getColumnIndex(PickerMediaColumns.HEIGHT)))
+        assertWithMessage(
+                "Unexpected value for PickerMediaColumns.HEIGHT for the media store cursor.")
+                .that(cursor.getInt(cursor.getColumnIndex(PickerMediaColumns.HEIGHT)))
                 .isEqualTo(HEIGHT);
-        assertThat(cursor.getInt(cursor.getColumnIndex(PickerMediaColumns.WIDTH)))
+        assertWithMessage(
+                "Unexpected value for PickerMediaColumns.WIDTH for the media store cursor.")
+                .that(cursor.getInt(cursor.getColumnIndex(PickerMediaColumns.WIDTH)))
                 .isEqualTo(WIDTH);
-        assertThat(cursor.getInt(cursor.getColumnIndex(PickerMediaColumns.ORIENTATION)))
+        assertWithMessage(
+                "Unexpected value for PickerMediaColumns.ORIENTATION for the media store cursor.")
+                .that(cursor.getInt(cursor.getColumnIndex(PickerMediaColumns.ORIENTATION)))
                 .isEqualTo(ORIENTATION);
     }
-}
\ No newline at end of file
+}
diff --git a/tests/src/com/android/providers/media/photopicker/data/SelectionTest.java b/tests/src/com/android/providers/media/photopicker/data/SelectionTest.java
index bce370b..189f122 100644
--- a/tests/src/com/android/providers/media/photopicker/data/SelectionTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/SelectionTest.java
@@ -62,6 +62,22 @@
     }
 
     @Test
+    public void testAddSelectedItem_orderedSelection() {
+        try {
+            enableOrderedSelection();
+            final Item item1 = generateFakeImageItem("1");
+            final Item item2 = generateFakeImageItem("2");
+
+            mSelection.addSelectedItem(item1);
+            mSelection.addSelectedItem(item2);
+            assertThat(mSelection.getSelectedItemOrder(item1).getValue().intValue()).isEqualTo(1);
+            assertThat(mSelection.getSelectedItemOrder(item2).getValue().intValue()).isEqualTo(2);
+        } finally {
+            disableOrderedSelection();
+        }
+    }
+
+    @Test
     public void testDeleteSelectedItem() {
         final String id = "1";
         final Item item = generateFakeImageItem(id);
@@ -76,6 +92,56 @@
     }
 
     @Test
+    public void testDeleteSelectedItem_orderedSelection() {
+        try {
+            enableOrderedSelection();
+            final Item item1 = generateFakeImageItem("1");
+            final Item item2 = generateFakeImageItem("2");
+            final Item item3 = generateFakeImageItem("3");
+
+            mSelection.addSelectedItem(item1);
+            mSelection.addSelectedItem(item2);
+            mSelection.addSelectedItem(item3);
+
+            assertThat(mSelection.getSelectedItemOrder(item1).getValue().intValue()).isEqualTo(1);
+            assertThat(mSelection.getSelectedItemOrder(item2).getValue().intValue()).isEqualTo(2);
+            assertThat(mSelection.getSelectedItemOrder(item3).getValue().intValue()).isEqualTo(3);
+
+            mSelection.removeSelectedItem(item1);
+
+            assertThat(mSelection.getSelectedItemOrder(item2).getValue().intValue()).isEqualTo(1);
+            assertThat(mSelection.getSelectedItemOrder(item3).getValue().intValue()).isEqualTo(2);
+
+            mSelection.removeSelectedItem(item3);
+
+            assertThat(mSelection.getSelectedItemOrder(item2).getValue().intValue()).isEqualTo(1);
+        } finally {
+            disableOrderedSelection();
+        }
+    }
+
+    @Test
+    public void testGetSelectedItems_orderedSelection() {
+        try {
+            enableOrderedSelection();
+            final Item item1 = generateFakeImageItem("1");
+            final Item item2 = generateFakeImageItem("2");
+            final Item item3 = generateFakeImageItem("3");
+
+            mSelection.addSelectedItem(item1);
+            mSelection.addSelectedItem(item2);
+            mSelection.addSelectedItem(item3);
+
+            List<Item> itemsSorted = mSelection.getSelectedItems();
+            assertThat(itemsSorted.get(0).getId()).isEqualTo("1");
+            assertThat(itemsSorted.get(1).getId()).isEqualTo("2");
+            assertThat(itemsSorted.get(2).getId()).isEqualTo("3");
+        } finally {
+            disableOrderedSelection();
+        }
+    }
+
+    @Test
     public void testClearSelectedItem() {
         final String id = "1";
         final Item item = generateFakeImageItem(id);
@@ -164,6 +230,39 @@
     }
 
     @Test
+    public void testParseValuesFromIntent_orderedSelection() {
+        final Intent intent = new Intent();
+        intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, true);
+
+        mSelection.parseSelectionValuesFromIntent(intent);
+
+        assertThat(mSelection.isSelectionOrdered()).isTrue();
+    }
+
+    @Test
+    public void testParseValuesFromIntent_InvalidOrderedSelectionGetContent_throwsException() {
+        final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+        intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, true);
+
+        try {
+            mSelection.parseSelectionValuesFromIntent(intent);
+            fail("Ordered selection not allowed for GET_CONTENT");
+        } catch (IllegalArgumentException expected) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testParseValuesFromIntent_OrderedSelectionDisabledInPermissionMode() {
+        final Intent intent = new Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP);
+        intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, true);
+
+        mSelection.parseSelectionValuesFromIntent(intent);
+
+        assertThat(mSelection.isSelectionOrdered()).isFalse();
+    }
+
+    @Test
     public void testParseValuesFromIntent_allowMultipleNotSupported() {
         final Intent intent = new Intent();
         intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
@@ -276,4 +375,16 @@
 
         return generateJpegItem(id, dateTakenMs, /* generationModified */ 1L);
     }
+
+    private void enableOrderedSelection() {
+        final Intent intent = new Intent();
+        intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, true);
+        mSelection.parseSelectionValuesFromIntent(intent);
+    }
+
+    private void disableOrderedSelection() {
+        final Intent intent = new Intent();
+        intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, false);
+        mSelection.parseSelectionValuesFromIntent(intent);
+    }
 }
\ No newline at end of file
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/ActiveProfileButtonTest.java b/tests/src/com/android/providers/media/photopicker/espresso/ActiveProfileButtonTest.java
index e96d45c..ef6c885 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/ActiveProfileButtonTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/ActiveProfileButtonTest.java
@@ -25,7 +25,6 @@
 import static androidx.test.espresso.matcher.ViewMatchers.isSelected;
 import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
 import static androidx.test.espresso.matcher.ViewMatchers.withId;
-import static androidx.test.espresso.matcher.ViewMatchers.withParent;
 import static androidx.test.espresso.matcher.ViewMatchers.withText;
 
 import static com.android.providers.media.photopicker.espresso.RecyclerViewTestUtils.assertItemNotSelected;
@@ -39,12 +38,15 @@
 import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
 
 import com.android.providers.media.R;
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
+import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger.PhotoPickerEvent;
 
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@RunOnlyOnPostsubmit
 @RunWith(AndroidJUnit4ClassRunner.class)
 public class ActiveProfileButtonTest extends PhotoPickerBaseTest {
     private static final int PROFILE_BUTTON = R.id.profile_button;
@@ -127,11 +129,19 @@
         // Check the text on the button. It should be "Switch to work"
         onView(withText(R.string.picker_work_profile)).check(matches(isDisplayed()));
 
+        // Verify log
+        UiEventLoggerTestUtils.verifyLogWithInstanceId(
+                mRule, PhotoPickerEvent.PHOTO_PICKER_PROFILE_SWITCH_BUTTON_ENABLED);
+
         // verify clicking it does not open error dialog
         onView(withId(PROFILE_BUTTON)).check(matches(isDisplayed())).perform(click());
         onView(withText(R.string.picker_profile_admin_title)).check(doesNotExist());
         onView(withText(R.string.picker_profile_work_paused_title)).check(doesNotExist());
 
+        // Verify log
+        UiEventLoggerTestUtils.verifyLogWithInstanceId(
+                mRule, PhotoPickerEvent.PHOTO_PICKER_PROFILE_SWITCH_BUTTON_CLICK);
+
         // Clicking the button, it takes a few ms to change the string.
         // Wait 100ms to be sure.
         // TODO(b/201982046): Replace with more stable workaround using Espresso idling resources
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 1f4f306..5cc870d 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/AlbumsTabTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/AlbumsTabTest.java
@@ -17,6 +17,7 @@
 package com.android.providers.media.photopicker.espresso;
 
 import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.Espresso.pressBack;
 import static androidx.test.espresso.action.ViewActions.click;
 import static androidx.test.espresso.assertion.ViewAssertions.matches;
 import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
@@ -28,6 +29,7 @@
 import static com.android.providers.media.photopicker.espresso.OverflowMenuUtils.assertOverflowMenuNotShown;
 import static com.android.providers.media.photopicker.espresso.RecyclerViewMatcher.withRecyclerView;
 import static com.android.providers.media.photopicker.espresso.RecyclerViewTestUtils.assertItemDisplayed;
+import static com.android.providers.media.photopicker.espresso.RecyclerViewTestUtils.assertItemNotDisplayed;
 
 import static org.hamcrest.Matchers.allOf;
 
@@ -36,12 +38,14 @@
 import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
 
 import com.android.providers.media.R;
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
+import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger.PhotoPickerEvent;
 
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@RunOnlyOnPostsubmit
 @RunWith(AndroidJUnit4ClassRunner.class)
 public class AlbumsTabTest extends PhotoPickerBaseTest {
 
@@ -51,7 +55,6 @@
     public ActivityScenarioRule<PhotoPickerTestActivity> mRule =
             new ActivityScenarioRule<>(PhotoPickerBaseTest.getMultiSelectionIntent());
 
-    @Ignore("b/227478958 Odd failure to verify Downloads album")
     @Test
     public void testAlbumGrid() {
         // Goto Albums page
@@ -78,10 +81,16 @@
         onView(withId(PICKER_TAB_RECYCLERVIEW_ID))
                 .check(new RecyclerViewItemCountAssertion(expectedAlbumCount));
 
+        // Verify albums tab click and albums loaded UI events
+        UiEventLoggerTestUtils.verifyLogWithInstanceId(
+                mRule, PhotoPickerEvent.PHOTO_PICKER_TAB_ALBUMS_OPEN);
+        UiEventLoggerTestUtils.verifyLogWithInstanceIdAndPosition(
+                mRule, PhotoPickerEvent.PHOTO_PICKER_UI_LOADED_ALBUMS, expectedAlbumCount);
+
         // First album is Camera
-        assertItemContentInAlbumList(/* position */ 0, R.string.picker_category_videos);
+        assertItemContentInAlbumList(/* position */ 0, R.string.picker_category_camera);
         // Second album is Videos
-        assertItemContentInAlbumList(/* position */ 1, R.string.picker_category_camera);
+        assertItemContentInAlbumList(/* position */ 1, R.string.picker_category_videos);
         // Third album is Downloads
         assertItemContentInAlbumList(/* position */ 2, R.string.picker_category_downloads);
 
@@ -94,21 +103,39 @@
     private void assertItemContentInAlbumList(int position, int albumNameResId) {
         // Verify the components are shown on the album item
         assertItemDisplayed(PICKER_TAB_RECYCLERVIEW_ID, position, R.id.album_name);
-        assertItemDisplayed(PICKER_TAB_RECYCLERVIEW_ID, position, R.id.item_count);
+        // As per the current requirements , hiding album's item count.
+        // In case if in future we need to show album's item count , we also have to assert its
+        // correct count with the visibility of album's item count block.
+        assertItemNotDisplayed(PICKER_TAB_RECYCLERVIEW_ID, position, R.id.item_count);
         assertItemDisplayed(PICKER_TAB_RECYCLERVIEW_ID, position, R.id.icon_thumbnail);
 
         // Verify we have the album in the list
         onView(allOf(withText(albumNameResId), isDescendantOfA(withId(PICKER_TAB_RECYCLERVIEW_ID))))
                 .check(matches(isDisplayed()));
 
-        // Verify the position of the album name matches the correct order
+        // Verify the position of the album name matches the correct order AND click the album
         onView(withRecyclerView(PICKER_TAB_RECYCLERVIEW_ID)
                 .atPositionOnView(position, R.id.album_name))
-                .check(matches(withText(albumNameResId)));
+                .check(matches(withText(albumNameResId)))
+                .perform(click());
 
-        // Verify the item count is correct
-        onView(withRecyclerView(PICKER_TAB_RECYCLERVIEW_ID)
-                .atPositionOnView(position, R.id.item_count))
-                .check(matches(withText("1 item")));
+        // Verify album click UI event
+        UiEventLoggerTestUtils.verifyLogWithInstanceId(mRule, getUiEventForAlbumId(albumNameResId));
+
+        // Go back to the Albums tab
+        pressBack();
+    }
+
+    private PhotoPickerEvent getUiEventForAlbumId(int albumNameResId) {
+        switch (albumNameResId) {
+            case R.string.picker_category_videos:
+                return PhotoPickerEvent.PHOTO_PICKER_ALBUM_VIDEOS_OPEN;
+            case R.string.picker_category_camera:
+                return PhotoPickerEvent.PHOTO_PICKER_ALBUM_CAMERA_OPEN;
+            case R.string.picker_category_downloads:
+                return PhotoPickerEvent.PHOTO_PICKER_ALBUM_DOWNLOADS_OPEN;
+            default:
+                throw new IllegalArgumentException("Unexpected albumNameResId: " + albumNameResId);
+        }
     }
 }
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/BlockedByAdminProfileButtonTest.java b/tests/src/com/android/providers/media/photopicker/espresso/BlockedByAdminProfileButtonTest.java
index 5ad647b..f06963a 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/BlockedByAdminProfileButtonTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/BlockedByAdminProfileButtonTest.java
@@ -27,12 +27,15 @@
 import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
 
 import com.android.providers.media.R;
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
+import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger.PhotoPickerEvent;
 
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@RunOnlyOnPostsubmit
 @RunWith(AndroidJUnit4ClassRunner.class)
 public class BlockedByAdminProfileButtonTest extends PhotoPickerBaseTest {
     @BeforeClass
@@ -53,11 +56,19 @@
         // Check the text on the button. It should be "Switch to personal"
         onView(withText(R.string.picker_personal_profile)).check(matches(isDisplayed()));
 
+        // Verify log
+        UiEventLoggerTestUtils.verifyLogWithInstanceId(
+                mRule, PhotoPickerEvent.PHOTO_PICKER_PROFILE_SWITCH_BUTTON_DISABLED);
+
         // Verify onClick shows a dialog
         onView(withId(profileButtonId)).check(matches(isDisplayed())).perform(click());
         onView(withText(R.string.picker_profile_admin_title)).check(matches(isDisplayed()));
         onView(withText(R.string.picker_profile_admin_msg_from_work)).check(matches(isDisplayed()));
 
+        // Verify log
+        UiEventLoggerTestUtils.verifyLogWithInstanceId(
+                mRule, PhotoPickerEvent.PHOTO_PICKER_PROFILE_SWITCH_BUTTON_CLICK);
+
         onView(withText(android.R.string.ok)).check(matches(isDisplayed())).perform(click());
     }
 }
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/BottomSheetIdlingResource.java b/tests/src/com/android/providers/media/photopicker/espresso/BottomSheetIdlingResource.java
index 693b445..a36531f 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/BottomSheetIdlingResource.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/BottomSheetIdlingResource.java
@@ -99,7 +99,8 @@
      *     given {@link ActivityScenarioRule}.
      * @param scenario
      */
-    public static BottomSheetIdlingResource register(ActivityScenario scenario) {
+    public static <T extends PhotoPickerTestActivity> BottomSheetIdlingResource register(
+            ActivityScenario<T> scenario) {
         final BottomSheetIdlingResource[] idlingResources = new BottomSheetIdlingResource[1];
         scenario.onActivity(
                 (activity -> {
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/DisabledAccessibilityTest.java b/tests/src/com/android/providers/media/photopicker/espresso/DisabledAccessibilityTest.java
new file mode 100644
index 0000000..3e17e2f
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/espresso/DisabledAccessibilityTest.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2023 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.espresso;
+
+import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_NONE;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.isNotSelected;
+import static androidx.test.espresso.matcher.ViewMatchers.isSelected;
+import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import static com.android.providers.media.photopicker.espresso.BottomSheetTestUtils.assertBottomSheetState;
+import static com.android.providers.media.photopicker.espresso.CustomSwipeAction.customSwipeDownPartialScreen;
+import static com.android.providers.media.photopicker.espresso.CustomSwipeAction.swipeLeftAndWait;
+import static com.android.providers.media.photopicker.espresso.CustomSwipeAction.swipeRightAndWait;
+import static com.android.providers.media.photopicker.espresso.OrientationUtils.setLandscapeOrientation;
+import static com.android.providers.media.photopicker.espresso.OrientationUtils.setPortraitOrientation;
+import static com.android.providers.media.photopicker.espresso.OverflowMenuUtils.assertOverflowMenuNotShown;
+import static com.android.providers.media.photopicker.espresso.RecyclerViewMatcher.withRecyclerView;
+import static com.android.providers.media.photopicker.espresso.RecyclerViewTestUtils.longClickItem;
+
+import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED;
+import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.not;
+
+import android.app.Activity;
+
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.espresso.IdlingRegistry;
+import androidx.test.espresso.action.ViewActions;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+
+import com.android.providers.media.R;
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
+import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * {@link DisabledAccessibilityTest} tests the
+ * {@link com.android.providers.media.photopicker.PhotoPickerActivity} behaviors that require it to
+ * launch in partial screen.
+ */
+@RunOnlyOnPostsubmit
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class DisabledAccessibilityTest extends PhotoPickerBaseTest {
+
+    private ActivityScenario<PhotoPickerAccessibilityDisabledTestActivity> mScenario;
+
+    /**
+     * Note - {@link ActivityScenario#launchActivityForResult(Class)} launches the activity with the
+     * intent action {@link android.content.Intent#ACTION_MAIN}.
+     */
+    @Before
+    public void launchActivity() {
+        mScenario = ActivityScenario.launchActivityForResult(
+                PhotoPickerAccessibilityDisabledTestActivity.class);
+    }
+
+    @After
+    public void closeActivity() {
+        if (mScenario != null) {
+            mScenario.close();
+        }
+    }
+
+    @Test
+    @Ignore("b/313489524")
+    // TODO(b/313489524): Fix flaky orientation change in the photo picker espresso tests
+    public void testBottomSheetState() {
+        // Bottom sheet assertions are different based on the orientation
+        setPortraitOrientation(mScenario);
+
+        // Register bottom sheet idling resource so that we don't read bottom sheet state when
+        // in between changing states
+        final BottomSheetIdlingResource bottomSheetIdlingResource =
+                BottomSheetIdlingResource.register(mScenario);
+
+        try {
+            // Single select PhotoPicker is launched in partial screen mode
+            bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
+            onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
+            onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
+            mScenario.onActivity(
+                    activity -> {
+                        assertBottomSheetState(activity, STATE_COLLAPSED);
+                    });
+
+            // Swipe up and check that the PhotoPicker is in full screen mode
+            bottomSheetIdlingResource.setExpectedState(STATE_EXPANDED);
+            onView(withId(PRIVACY_TEXT_ID)).perform(ViewActions.swipeUp());
+            mScenario.onActivity(
+                    activity -> {
+                        assertBottomSheetState(activity, STATE_EXPANDED);
+                    });
+
+            // Swipe down and check that the PhotoPicker is in partial screen mode
+            bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
+            onView(withId(PRIVACY_TEXT_ID)).perform(ViewActions.swipeDown());
+            mScenario.onActivity(
+                    activity -> {
+                        assertBottomSheetState(activity, STATE_COLLAPSED);
+                    });
+
+            // Swiping down on drag bar is not strong enough as closing the bottomsheet requires a
+            // stronger downward swipe using espresso.
+            // Simply swiping down on R.id.bottom_sheet throws an error from espresso, as the view
+            // is only 60% visible, but downward swipe is only successful on an element which is 90%
+            // visible.
+            onView(withId(R.id.bottom_sheet)).perform(customSwipeDownPartialScreen());
+        } finally {
+            IdlingRegistry.getInstance().unregister(bottomSheetIdlingResource);
+        }
+        assertThat(mScenario.getResult().getResultCode()).isEqualTo(Activity.RESULT_CANCELED);
+    }
+
+    @Test
+    @Ignore("b/313489524")
+    // TODO(b/313489524): Fix flaky orientation change in the photo picker espresso tests
+    public void testBottomSheetStateInLandscapeMode() {
+        // Bottom sheet assertions are different based on the orientation
+        setLandscapeOrientation(mScenario);
+
+        // Register bottom sheet idling resource so that we don't read bottom sheet state when
+        // in between changing states
+        final BottomSheetIdlingResource bottomSheetIdlingResource =
+                BottomSheetIdlingResource.register(mScenario);
+
+        try {
+            // Single select PhotoPicker is launched in full screen mode in Landscape orientation
+            mScenario.onActivity(
+                    activity -> {
+                        assertBottomSheetState(activity, STATE_EXPANDED);
+                    });
+
+            // Swiping down on drag bar / privacy text is not strong enough as closing the
+            // bottomsheet requires a stronger downward swipe using espresso.
+            onView(withId(R.id.bottom_sheet)).perform(ViewActions.swipeDown());
+        } finally {
+            IdlingRegistry.getInstance().unregister(bottomSheetIdlingResource);
+        }
+        assertThat(mScenario.getResult().getResultCode()).isEqualTo(Activity.RESULT_CANCELED);
+    }
+
+    @Test
+    public void testTabSwiping() throws Exception {
+        // Bottom sheet assertions are different based on the orientation
+        setPortraitOrientation(mScenario);
+
+        onView(withId(TAB_LAYOUT_ID)).check(matches(isDisplayed()));
+
+        // If we want to swipe the viewPager2 of tabContainerFragment in Espresso tests, at least 90
+        // percent of the view's area is displayed to the user. Swipe up the bottom Sheet to make
+        // sure it is in full Screen mode.
+        // Register bottom sheet idling resource so that we don't read bottom sheet state when
+        // in between changing states
+        final BottomSheetIdlingResource bottomSheetIdlingResource =
+                BottomSheetIdlingResource.register(mScenario);
+
+        try {
+            // Single select PhotoPicker is launched in partial screen mode
+            bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
+            mScenario.onActivity(activity -> {
+                assertBottomSheetState(activity, STATE_COLLAPSED);
+            });
+
+            // Swipe up and check that the PhotoPicker is in full screen mode.
+            onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
+            onView(withId(PRIVACY_TEXT_ID)).perform(ViewActions.swipeUp());
+            bottomSheetIdlingResource.setExpectedState(STATE_EXPANDED);
+            mScenario.onActivity(
+                    activity -> {
+                        assertBottomSheetState(activity, STATE_EXPANDED);
+                    });
+        } finally {
+            IdlingRegistry.getInstance().unregister(bottomSheetIdlingResource);
+        }
+
+        try (ViewPager2IdlingResource idlingResource =
+                     ViewPager2IdlingResource.register(mScenario, TAB_VIEW_PAGER_ID)) {
+            // Swipe left, we should see albums tab
+            swipeLeftAndWait(TAB_VIEW_PAGER_ID);
+
+            onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+                    .check(matches(isSelected()));
+            onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+                    .check(matches(isNotSelected()));
+            // Verify Camera album is shown, we are in albums tab
+            onView(allOf(withText(R.string.picker_category_camera),
+                    isDescendantOfA(withId(PICKER_TAB_RECYCLERVIEW_ID)))).check(
+                    matches(isDisplayed()));
+
+            // Swipe right, we should see photos tab
+            swipeRightAndWait(TAB_VIEW_PAGER_ID);
+
+            onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+                    .check(matches(isSelected()));
+            onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+                    .check(matches(isNotSelected()));
+            // Verify first item is recent header, we are in photos tab
+            onView(withRecyclerView(PICKER_TAB_RECYCLERVIEW_ID)
+                    .atPositionOnView(0, R.id.date_header_title))
+                    .check(matches(withText(R.string.recent)));
+        }
+    }
+
+    @Test
+    public void testPreview_singleSelect_image() throws Exception {
+        // Bottom sheet assertions are different based on the orientation
+        setPortraitOrientation(mScenario);
+
+        onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
+
+        final BottomSheetIdlingResource bottomSheetIdlingResource =
+                BottomSheetIdlingResource.register(mScenario);
+
+        try {
+            bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
+            onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
+            onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
+            mScenario.onActivity(activity -> {
+                assertBottomSheetState(activity, STATE_COLLAPSED);
+            });
+
+            // Navigate to preview
+            longClickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
+
+            UiEventLoggerTestUtils.verifyLogWithInstanceIdAndPosition(mScenario,
+                    PhotoPickerUiEventLogger.PhotoPickerEvent.PHOTO_PICKER_PREVIEW_ITEM_MAIN_GRID,
+                    _SPECIAL_FORMAT_NONE, JPEG_IMAGE_MIME_TYPE, IMAGE_1_POSITION);
+
+            try (ViewPager2IdlingResource idlingResource =
+                         ViewPager2IdlingResource.register(mScenario, PREVIEW_VIEW_PAGER_ID)) {
+                // No dragBar in preview
+                bottomSheetIdlingResource.setExpectedState(STATE_EXPANDED);
+                onView(withId(DRAG_BAR_ID)).check(matches(not(isDisplayed())));
+                // No privacy text in preview
+                onView(withId(PRIVACY_TEXT_ID)).check(matches(not(isDisplayed())));
+                mScenario.onActivity(activity -> {
+                    assertBottomSheetState(activity, STATE_EXPANDED);
+                });
+
+                // Verify image is previewed
+                PreviewFragmentAssertionUtils.assertSingleSelectCommonLayoutMatches();
+                onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
+                // Verify no special format icon is previewed
+                onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+                onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
+                // Verify the overflow menu is not shown for PICK_IMAGES intent
+                assertOverflowMenuNotShown();
+            }
+            // Navigate back to Photo grid
+            onView(withContentDescription("Navigate up")).perform(click());
+
+            onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
+            onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
+            onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
+
+            bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
+            // Shows dragBar and privacy text after we are back to Photos tab
+            mScenario.onActivity(activity -> {
+                assertBottomSheetState(activity, STATE_COLLAPSED);
+            });
+        } finally {
+            IdlingRegistry.getInstance().unregister(bottomSheetIdlingResource);
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/MaxSelectionTest.java b/tests/src/com/android/providers/media/photopicker/espresso/MaxSelectionTest.java
index c025cc0..3d3870c 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/MaxSelectionTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/MaxSelectionTest.java
@@ -17,6 +17,7 @@
 package com.android.providers.media.photopicker.espresso;
 
 import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.Espresso.pressBack;
 import static androidx.test.espresso.action.ViewActions.click;
 import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
 import static androidx.test.espresso.assertion.ViewAssertions.matches;
@@ -28,6 +29,9 @@
 import static com.android.providers.media.photopicker.espresso.RecyclerViewTestUtils.assertItemNotSelected;
 import static com.android.providers.media.photopicker.espresso.RecyclerViewTestUtils.assertItemSelected;
 import static com.android.providers.media.photopicker.espresso.RecyclerViewTestUtils.clickItem;
+import static com.android.providers.media.photopicker.espresso.RecyclerViewTestUtils.longClickItem;
+
+import static org.hamcrest.Matchers.not;
 
 import android.view.View;
 
@@ -37,10 +41,13 @@
 import androidx.test.ext.junit.rules.ActivityScenarioRule;
 import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
 
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@RunOnlyOnPostsubmit
 @RunWith(AndroidJUnit4ClassRunner.class)
 public class MaxSelectionTest extends PhotoPickerBaseTest {
     private static final int MAX_SELECTION_COUNT = 2;
@@ -57,10 +64,36 @@
         clickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
         assertItemSelected(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_CHECK_ID);
 
-        // Select second image item thumbnail and verify select icon is selected
-        clickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_2_POSITION, ICON_THUMBNAIL_ID);
+        // Assert that when the max selection is not yet reached, the select button is visible on
+        // long click preview of an unselected item (the second image item in this case).
+        // Then select this item (the second image item) by clicking the select button.
+        longClickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_2_POSITION, ICON_THUMBNAIL_ID);
+        onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID))
+                .check(matches(isDisplayed()))
+                .perform(click());
+
+        // Go back to the photos grid
+        pressBack();
+
+        // Verify that the select icon is selected for the second image item
         assertItemSelected(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_2_POSITION, ICON_CHECK_ID);
 
+        // Assert that when the max selection is reached, the select button is not visible on long
+        // click preview of an unselected item (the video item in this case).
+        longClickItem(PICKER_TAB_RECYCLERVIEW_ID, VIDEO_POSITION, ICON_THUMBNAIL_ID);
+        onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(not(isDisplayed())));
+
+        // Go back to the photos grid
+        pressBack();
+
+        // Assert that the deselect button is always visible on long click preview of a selected
+        // item (any of the 2 image items in this case), irrespective of the max selection
+        longClickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
+        onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(isDisplayed()));
+
+        // Go back to the photos grid
+        pressBack();
+
         // Click Video item thumbnail and verify select icon is not selected. Because we set the
         // max selection is 2.
         clickItem(PICKER_TAB_RECYCLERVIEW_ID, VIDEO_POSITION, ICON_THUMBNAIL_ID);
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/MimeTypeFilterTest.java b/tests/src/com/android/providers/media/photopicker/espresso/MimeTypeFilterTest.java
index ce0c612..1905838 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/MimeTypeFilterTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/MimeTypeFilterTest.java
@@ -22,33 +22,46 @@
 import static androidx.test.espresso.assertion.ViewAssertions.matches;
 import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.isSelected;
 import static androidx.test.espresso.matcher.ViewMatchers.withId;
 import static androidx.test.espresso.matcher.ViewMatchers.withParent;
 import static androidx.test.espresso.matcher.ViewMatchers.withText;
 
 import static org.hamcrest.Matchers.allOf;
 
-import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.core.app.ActivityScenario;
 import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
 
 import com.android.providers.media.R;
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
 
-import org.junit.Rule;
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@RunOnlyOnPostsubmit
 @RunWith(AndroidJUnit4ClassRunner.class)
 public class MimeTypeFilterTest extends PhotoPickerBaseTest {
 
     private static final String IMAGE_MIME_TYPE = "image/*";
+    private static final String VIDEO_MIME_TYPE = "video/*";
+    public ActivityScenario<PhotoPickerTestActivity> mScenario;
 
-    @Rule
-    public ActivityScenarioRule<PhotoPickerTestActivity> mRule = new ActivityScenarioRule<>(
-            PhotoPickerBaseTest.getSingleSelectMimeTypeFilterIntent(IMAGE_MIME_TYPE));
+    @Before
+    public void launchActivity() {
+        mScenario =
+                ActivityScenario.launchActivityForResult(
+                        PhotoPickerBaseTest.getSingleSelectMimeTypeFilterIntent(IMAGE_MIME_TYPE));
+    }
+
+    @After
+    public void closeActivity() {
+        mScenario.close();
+    }
 
     @Test
     public void testPhotosTabOnlyImageItems() {
-
         onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
 
         // Two image items and one recent date header
@@ -90,4 +103,21 @@
         onView(allOf(withId(itemCountId),
                 withParent(withId(PICKER_TAB_RECYCLERVIEW_ID)))).check(doesNotExist());
     }
+
+    @Test
+    public void testPickerTabTitleText_forVariousMimeTypeFilters() {
+        onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+                .check(matches(isSelected()));
+
+        mScenario = ActivityScenario.launchActivityForResult(
+                PhotoPickerBaseTest.getSingleSelectMimeTypeFilterIntent(VIDEO_MIME_TYPE));
+        onView(allOf(withText(PICKER_VIDEOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+                .check(matches(isSelected()));
+
+        mScenario = ActivityScenario.launchActivityForResult(
+                PhotoPickerBaseTest.getSingleSelectionIntent());
+        onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+                .check(matches(isSelected()));
+
+    }
 }
\ No newline at end of file
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/MultiSelectTest.java b/tests/src/com/android/providers/media/photopicker/espresso/MultiSelectTest.java
index ce55a83..aed2d96 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/MultiSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/MultiSelectTest.java
@@ -51,18 +51,17 @@
 import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
 
 import com.android.providers.media.R;
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@RunOnlyOnPostsubmit
 @RunWith(AndroidJUnit4ClassRunner.class)
 public class MultiSelectTest extends PhotoPickerBaseTest {
 
-    private static final int TAB_VIEW_PAGER_ID = R.id.picker_tab_viewpager;
-
     private ActivityScenario<PhotoPickerTestActivity> mScenario;
 
     @Before
@@ -78,11 +77,6 @@
     }
 
     @Test
-    public void testMultiSelectDoesNotShowProfileButton() {
-        assertProfileButtonNotShown();
-    }
-
-    @Test
     public void testMultiselect_showDragBar() {
         onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
     }
@@ -254,7 +248,6 @@
     }
 
     @Test
-    @Ignore("Enable after b/228574741 is fixed")
     public void testMultiSelectTabSwiping() throws Exception {
         onView(withId(TAB_LAYOUT_ID)).check(matches(isDisplayed()));
 
@@ -287,7 +280,6 @@
     }
 
     @Test
-    @Ignore("Enable after b/222013536 is fixed")
     public void testMultiSelectScrollDownToClose() {
         final BottomSheetIdlingResource bottomSheetIdlingResource =
                 BottomSheetIdlingResource.register(mScenario);
@@ -300,15 +292,6 @@
                 assertBottomSheetState(activity, STATE_EXPANDED);
             });
 
-            // Shows dragBar and privacy text after we are back to Photos tab
-            onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
-            onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
-            mScenario.onActivity(activity -> {
-                assertBottomSheetState(activity, STATE_EXPANDED);
-            });
-
-            // Swiping down on drag bar or toolbar is not closing the bottom sheet as closing the
-            // bottomsheet requires a stronger downward swipe.
             onView(withId(R.id.bottom_sheet)).perform(ViewActions.swipeDown());
         } finally {
             IdlingRegistry.getInstance().unregister(bottomSheetIdlingResource);
@@ -317,29 +300,4 @@
         assertThat(mScenario.getResult().getResultCode()).isEqualTo(
                 Activity.RESULT_CANCELED);
     }
-
-
-    private void assertProfileButtonNotShown() {
-        // Partial screen does not show profile button
-        onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
-
-        // Navigate to Albums tab
-        onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
-                .perform(click());
-        onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
-
-        final int cameraStringId = R.string.picker_category_camera;
-        // Navigate to photos in Camera album
-        onView(allOf(withText(cameraStringId),
-                isDescendantOfA(withId(PICKER_TAB_RECYCLERVIEW_ID)))).perform(click());
-        onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
-
-        // Click back button
-        onView(withContentDescription("Navigate up")).perform(click());
-
-        // on clicking back button we are back to Album grid
-        onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
-                .check(matches(isSelected()));
-        onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
-    }
 }
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/NoItemsTest.java b/tests/src/com/android/providers/media/photopicker/espresso/NoItemsTest.java
index 9697077..277f1dd 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/NoItemsTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/NoItemsTest.java
@@ -22,7 +22,6 @@
 import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
 import static androidx.test.espresso.matcher.ViewMatchers.withId;
-import static androidx.test.espresso.matcher.ViewMatchers.withParent;
 import static androidx.test.espresso.matcher.ViewMatchers.withText;
 
 import static org.hamcrest.Matchers.allOf;
@@ -34,12 +33,13 @@
 import android.provider.MediaStore;
 
 import com.android.providers.media.R;
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
 
-import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@RunOnlyOnPostsubmit
 @RunWith(AndroidJUnit4ClassRunner.class)
 public class NoItemsTest extends PhotoPickerBaseTest {
 
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/OrientationUtils.java b/tests/src/com/android/providers/media/photopicker/espresso/OrientationUtils.java
index 9d0be47..2cc03f6 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/OrientationUtils.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/OrientationUtils.java
@@ -28,16 +28,18 @@
 import androidx.test.core.app.ActivityScenario;
 
 class OrientationUtils {
-    public static void setLandscapeOrientation(ActivityScenario<PhotoPickerTestActivity> scenario) {
+    public static <T extends PhotoPickerTestActivity> void setLandscapeOrientation(
+            ActivityScenario<T> scenario) {
         changeOrientation(scenario, SCREEN_ORIENTATION_LANDSCAPE, ORIENTATION_LANDSCAPE);
     }
 
-    public static void setPortraitOrientation(ActivityScenario<PhotoPickerTestActivity> scenario) {
+    public static <T extends PhotoPickerTestActivity> void setPortraitOrientation(
+            ActivityScenario<T> scenario) {
         changeOrientation(scenario, SCREEN_ORIENTATION_PORTRAIT, ORIENTATION_PORTRAIT);
     }
 
-    private static void changeOrientation(
-            ActivityScenario<PhotoPickerTestActivity> scenario,
+    private static <T extends PhotoPickerTestActivity> void changeOrientation(
+            ActivityScenario<T> scenario,
             int screenOrientation,
             int configOrientation) {
         scenario.onActivity(
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerAccessibilityDisabledTestActivity.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerAccessibilityDisabledTestActivity.java
new file mode 100644
index 0000000..26d722e
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerAccessibilityDisabledTestActivity.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 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.espresso;
+
+/**
+ * In espresso tests, the default accessibility mode, evaluated by
+ * {@link android.view.accessibility.AccessibilityManager#isEnabled()}, is enabled.
+ *
+ * {@link PhotoPickerAccessibilityDisabledTestActivity} is used to cover the code that requires the
+ * accessibility to be disabled.
+ *
+ * This {@link android.app.Activity} is launched using the {@link android.content.Intent}
+ * {@link android.content.Intent#ACTION_MAIN}.
+ */
+public class PhotoPickerAccessibilityDisabledTestActivity extends PhotoPickerTestActivity {
+    @Override
+    protected boolean isAccessibilityEnabled() {
+        return false;
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerActivityTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerActivityTest.java
index b76acbb..3d0bdc1 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerActivityTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerActivityTest.java
@@ -25,18 +25,16 @@
 import static androidx.test.espresso.matcher.ViewMatchers.isSelected;
 import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
 import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withParent;
 import static androidx.test.espresso.matcher.ViewMatchers.withText;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.providers.media.PickerUriResolver.REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI;
 import static com.android.providers.media.photopicker.espresso.BottomSheetTestUtils.assertBottomSheetState;
-import static com.android.providers.media.photopicker.espresso.CustomSwipeAction.customSwipeDownPartialScreen;
-import static com.android.providers.media.photopicker.espresso.CustomSwipeAction.swipeLeftAndWait;
-import static com.android.providers.media.photopicker.espresso.CustomSwipeAction.swipeRightAndWait;
 import static com.android.providers.media.photopicker.espresso.OrientationUtils.setLandscapeOrientation;
-import static com.android.providers.media.photopicker.espresso.OrientationUtils.setPortraitOrientation;
 import static com.android.providers.media.photopicker.espresso.OverflowMenuUtils.assertOverflowMenuNotShown;
 import static com.android.providers.media.photopicker.espresso.RecyclerViewMatcher.withRecyclerView;
 
-import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED;
 import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED;
 import static com.google.common.truth.Truth.assertThat;
 
@@ -44,6 +42,7 @@
 import static org.hamcrest.Matchers.not;
 
 import android.app.Activity;
+import android.content.ContentResolver;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.core.app.ActivityScenario;
@@ -53,6 +52,7 @@
 import androidx.viewpager2.widget.ViewPager2;
 
 import com.android.providers.media.R;
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
 
 import org.junit.After;
 import org.junit.Before;
@@ -60,11 +60,12 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.concurrent.TimeUnit;
+
+@RunOnlyOnPostsubmit
 @RunWith(AndroidJUnit4ClassRunner.class)
 public class PhotoPickerActivityTest extends PhotoPickerBaseTest {
 
-    private static final int TAB_VIEW_PAGER_ID = R.id.picker_tab_viewpager;
-
     public ActivityScenario<PhotoPickerTestActivity> mScenario;
 
     @Before
@@ -88,7 +89,8 @@
         onView(withId(R.id.fragment_container)).check(matches(isDisplayed()));
         onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
         onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
-        // Partial screen does not show profile button
+        // Assuming by default, the tests run without a managed user
+        // Single user mode does not show profile button
         onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
         onView(withId(android.R.id.empty)).check(matches(not(isDisplayed())));
 
@@ -100,75 +102,29 @@
     }
 
     @Test
-    public void testDoesNotShowProfileButton_partialScreen() {
-        assertProfileButtonNotShown();
-    }
+    public void testProfileButtonHiddenInSingleUserMode() {
+        // Assuming that the test runs without a managed user
 
-    @Test
-    @Ignore("Enable after b/222013536 is fixed")
-    public void testDoesNotShowProfileButton_fullScreen() {
-        // Bottomsheet assertions are different for landscape mode
-        setPortraitOrientation(mScenario);
-
-        // Partial screen does not show profile button
+        // Single user mode does not show profile button in the main grid
         onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
 
-        BottomSheetTestUtils.swipeUp(mScenario);
+        onView(withId(TAB_LAYOUT_ID)).check(matches(isDisplayed()));
 
-        assertProfileButtonNotShown();
+        // On clicking albums tab item, we should see albums tab
+        onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+                .perform(click());
+        onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+                .check(matches(isSelected()));
+        onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+                .check(matches(isNotSelected()));
+
+        // Single user mode does not show profile button in the albums grid
+        onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
     }
 
     @Test
-    @Ignore("Enable after b/222013536 is fixed")
-    public void testBottomSheetState() {
-        // Bottom sheet assertions are different for landscape mode
-        setPortraitOrientation(mScenario);
-
-        // Register bottom sheet idling resource so that we don't read bottom sheet state when
-        // in between changing states
-        final BottomSheetIdlingResource bottomSheetIdlingResource =
-                BottomSheetIdlingResource.register(mScenario);
-
-        try {
-            // Single select PhotoPicker is launched in partial screen mode
-            bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
-            onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
-            onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
-            mScenario.onActivity(
-                    activity -> {
-                        assertBottomSheetState(activity, STATE_COLLAPSED);
-                    });
-
-            // Swipe up and check that the PhotoPicker is in full screen mode
-            bottomSheetIdlingResource.setExpectedState(STATE_EXPANDED);
-            onView(withId(PRIVACY_TEXT_ID)).perform(ViewActions.swipeUp());
-            mScenario.onActivity(
-                    activity -> {
-                        assertBottomSheetState(activity, STATE_EXPANDED);
-                    });
-
-            // Swipe down and check that the PhotoPicker is in partial screen mode
-            bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
-            onView(withId(PRIVACY_TEXT_ID)).perform(ViewActions.swipeDown());
-            mScenario.onActivity(
-                    activity -> {
-                        assertBottomSheetState(activity, STATE_COLLAPSED);
-                    });
-
-            // Swiping down on drag bar is not strong enough as closing the bottomsheet requires a
-            // stronger downward swipe using espresso.
-            // Simply swiping down on R.id.bottom_sheet throws an error from espresso, as the view
-            // is only 60% visible, but downward swipe is only successful on an element which is 90%
-            // visible.
-            onView(withId(R.id.bottom_sheet)).perform(customSwipeDownPartialScreen());
-        } finally {
-            IdlingRegistry.getInstance().unregister(bottomSheetIdlingResource);
-        }
-        assertThat(mScenario.getResult().getResultCode()).isEqualTo(Activity.RESULT_CANCELED);
-    }
-
-    @Test
-    @Ignore("Enable after b/222013536 is fixed")
+    @Ignore("b/313489524")
+    // TODO(b/313489524): Fix flaky orientation change in the photo picker espresso tests
     public void testBottomSheetStateInLandscapeMode() {
         // Bottom sheet assertions are different for landscape mode
         setLandscapeOrientation(mScenario);
@@ -246,89 +202,37 @@
     }
 
     @Test
-    @Ignore("Enable after b/222013536 is fixed")
-    public void testTabSwiping() throws Exception {
-        onView(withId(TAB_LAYOUT_ID)).check(matches(isDisplayed()));
+    public void testResetOnCloudProviderChange() throws InterruptedException {
+        // Enable cloud media feature for the activity through the test config store
+        mScenario.onActivity(
+                activity ->
+                        activity.getConfigStore()
+                                .enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(
+                                        "com.hooli.super.awesome.cloud.provider"));
 
-        // If we want to swipe the viewPager2 of tabContainerFragment in Espresso tests, at least 90
-        // percent of the view's area is displayed to the user. Swipe up the bottom Sheet to make
-        // sure it is in full Screen mode.
-        // Register bottom sheet idling resource so that we don't read bottom sheet state when
-        // in between changing states
-        final BottomSheetIdlingResource bottomSheetIdlingResource =
-                BottomSheetIdlingResource.register(mScenario);
-
-        try {
-
-            // When accessibility is enabled, we always launch the photo picker in full screen mode.
-            // Accessibility is enabled in Espresso test, so we can't check the COLLAPSED state.
-            //            // Single select PhotoPicker is launched in partial screen mode
-            //            bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
-            //            mScenario.onActivity(activity -> {
-            //                assertBottomSheetState(activity, STATE_COLLAPSED);
-            //            });
-
-            // Swipe up and check that the PhotoPicker is in full screen mode.
-            //            onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
-            //            onView(withId(PRIVACY_TEXT_ID)).perform(ViewActions.swipeUp());
-            bottomSheetIdlingResource.setExpectedState(STATE_EXPANDED);
-            mScenario.onActivity(
-                    activity -> {
-                        assertBottomSheetState(activity, STATE_EXPANDED);
-                    });
-        } finally {
-            IdlingRegistry.getInstance().unregister(bottomSheetIdlingResource);
-        }
-
-        try (ViewPager2IdlingResource idlingResource =
-                ViewPager2IdlingResource.register(mScenario, TAB_VIEW_PAGER_ID)) {
-            // Swipe left, we should see albums tab
-            swipeLeftAndWait(TAB_VIEW_PAGER_ID);
-
-            onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
-                    .check(matches(isSelected()));
-            onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
-                    .check(matches(isNotSelected()));
-            // Verify Camera album is shown, we are in albums tab
-            onView(allOf(withText(R.string.picker_category_camera),
-                    isDescendantOfA(withId(PICKER_TAB_RECYCLERVIEW_ID)))).check(
-                    matches(isDisplayed()));
-
-            // Swipe right, we should see photos tab
-            swipeRightAndWait(TAB_VIEW_PAGER_ID);
-
-            onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
-                    .check(matches(isSelected()));
-            onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
-                    .check(matches(isNotSelected()));
-            // Verify first item is recent header, we are in photos tab
-            onView(withRecyclerView(PICKER_TAB_RECYCLERVIEW_ID)
-                    .atPositionOnView(0, R.id.date_header_title))
-                    .check(matches(withText(R.string.recent)));
-        }
-    }
-
-    private void assertProfileButtonNotShown() {
-        // Partial screen does not show profile button
-        onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
-
-        // Navigate to Albums tab
+        // Switch to the albums tab
         onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
                 .perform(click());
-        onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
-
-        final int cameraStringId = R.string.picker_category_camera;
-        // Navigate to photos in Camera album
-        onView(allOf(withText(cameraStringId),
-                isDescendantOfA(withId(PICKER_TAB_RECYCLERVIEW_ID)))).perform(click());
-        onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
-
-        // Click back button
-        onView(withContentDescription("Navigate up")).perform(click());
-
-        // on clicking back button we are back to Album grid
         onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
                 .check(matches(isSelected()));
-        onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
+
+        // Navigate to the photos in the Camera album
+        final int cameraStringId = R.string.picker_category_camera;
+        onView(allOf(withText(cameraStringId), isDescendantOfA(withId(PICKER_TAB_RECYCLERVIEW_ID))))
+                .perform(click());
+        onView(allOf(withText(cameraStringId), withParent(withId(R.id.toolbar))))
+                .check(matches(isDisplayed()));
+
+        // Notify refresh ui
+        final ContentResolver contentResolver =
+                getInstrumentation().getTargetContext().getContentResolver();
+        contentResolver.notifyChange(
+                REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI, /* observer= */ null);
+
+        TimeUnit.MILLISECONDS.sleep(/* timeout= */ 100);
+
+        // Verify activity reset to the initial launch state (Photos tab)
+        onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+                .check(matches(isSelected()));
     }
 }
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
index 3569efd..f0b6b03 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
@@ -29,12 +29,15 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Environment;
+import android.os.Process;
 import android.provider.MediaStore;
 import android.system.ErrnoException;
 import android.system.Os;
 
 import androidx.core.util.Supplier;
+import androidx.lifecycle.MutableLiveData;
 import androidx.test.InstrumentationRegistry;
+import androidx.work.testing.WorkManagerTestInitHelper;
 
 import com.android.providers.media.IsolatedContext;
 import com.android.providers.media.R;
@@ -54,9 +57,11 @@
 
 public class PhotoPickerBaseTest {
     protected static final int PICKER_TAB_RECYCLERVIEW_ID = R.id.picker_tab_recyclerview;
+    protected static final int TAB_VIEW_PAGER_ID = R.id.picker_tab_viewpager;
     protected static final int TAB_LAYOUT_ID = R.id.tab_layout;
     protected static final int PICKER_PHOTOS_STRING_ID = R.string.picker_photos;
     protected static final int PICKER_ALBUMS_STRING_ID = R.string.picker_albums;
+    protected static final int PICKER_VIDEOS_STRING_ID = R.string.picker_videos;
     protected static final int PREVIEW_VIEW_PAGER_ID = R.id.preview_viewPager;
     protected static final int ICON_CHECK_ID = R.id.icon_check;
     protected static final int ICON_THUMBNAIL_ID = R.id.icon_thumbnail;
@@ -67,6 +72,12 @@
     protected static final int PREVIEW_MOTION_PHOTO_ID = R.id.preview_motion_photo;
     protected static final int PREVIEW_ADD_OR_SELECT_BUTTON_ID = R.id.preview_add_or_select_button;
     protected static final int PRIVACY_TEXT_ID = R.id.privacy_text;
+    protected static final String GIF_IMAGE_MIME_TYPE = "image/gif";
+    protected static final String ANIMATED_WEBP_MIME_TYPE = "image/webp";
+    protected static final String JPEG_IMAGE_MIME_TYPE = "image/jpeg";
+    protected static final String MP4_VIDEO_MIME_TYPE = "video/mp4";
+
+    protected static final String MANAGED_SELECTION_ENABLED_EXTRA = "MANAGED_SELECTION_ENABLE";
 
     protected static final int DIMEN_PREVIEW_ADD_OR_SELECT_WIDTH
             = R.dimen.preview_add_or_select_width;
@@ -112,11 +123,22 @@
         sUserSelectImagesForAppIntent = new Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP);
         sUserSelectImagesForAppIntent.addCategory(Intent.CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST);
         Bundle extras = new Bundle();
-        extras.putInt(Intent.EXTRA_UID, 1234);
+        extras.putInt(Intent.EXTRA_UID, Process.myUid());
         sUserSelectImagesForAppIntent.putExtras(extras);
     }
 
-    private static final File IMAGE_1_FILE = new File(Environment.getExternalStorageDirectory(),
+    private static final Intent sPickerChoiceManagedSelectionIntent;
+    static {
+        sPickerChoiceManagedSelectionIntent = new Intent(
+                MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP);
+        sPickerChoiceManagedSelectionIntent.addCategory(
+                Intent.CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST);
+        Bundle extras = new Bundle();
+        extras.putInt(Intent.EXTRA_UID, Process.myUid());
+        extras.putBoolean(MANAGED_SELECTION_ENABLED_EXTRA, true);
+        sPickerChoiceManagedSelectionIntent.putExtras(extras);
+    }
+    public static final File IMAGE_1_FILE = new File(Environment.getExternalStorageDirectory(),
             Environment.DIRECTORY_DCIM + "/Camera"
                     + "/image_" + System.currentTimeMillis() + ".jpeg");
     private static final File IMAGE_2_FILE = new File(Environment.getExternalStorageDirectory(),
@@ -148,6 +170,9 @@
         return sUserSelectImagesForAppIntent;
     }
 
+    public static Intent getPickerChoiceManagedSelectionIntent() {
+        return sPickerChoiceManagedSelectionIntent;
+    }
     public static Intent getMultiSelectionIntent(int max) {
         final Intent intent = new Intent(sMultiSelectionIntent);
         Bundle extras = new Bundle();
@@ -182,6 +207,8 @@
         sUserIdManager = mock(UserIdManager.class);
         when(sUserIdManager.getCurrentUserProfileId()).thenReturn(UserId.CURRENT_USER);
 
+        WorkManagerTestInitHelper.initializeTestWorkManager(sIsolatedContext);
+
         createFiles();
     }
 
@@ -283,6 +310,7 @@
             updateIsManagedUserSelected(/* isManagedUserSelected */ true);
             return null;
         }).when(sUserIdManager).setManagedAsCurrentUserProfile();
+        when(sUserIdManager.getCrossProfileAllowed()).thenReturn(new MutableLiveData<>(true));
     }
 
     /**
@@ -307,6 +335,7 @@
         when(sUserIdManager.isWorkProfileOff()).thenReturn(false);
         when(sUserIdManager.isCrossProfileAllowed()).thenReturn(false);
         when(sUserIdManager.isManagedUserSelected()).thenReturn(true);
+        when(sUserIdManager.getCrossProfileAllowed()).thenReturn(new MutableLiveData<>(false));
     }
 
     private static void updateIsManagedUserSelected(boolean isManagedUserSelected) {
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerTestActivity.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerTestActivity.java
index 062bf3a..5026235 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerTestActivity.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerTestActivity.java
@@ -16,17 +16,52 @@
 
 package com.android.providers.media.photopicker.espresso;
 
+import static com.android.providers.media.photopicker.espresso.PhotoPickerBaseTest.MANAGED_SELECTION_ENABLED_EXTRA;
+
+import static org.mockito.Mockito.RETURNS_SMART_NULLS;
+import static org.mockito.Mockito.mock;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.UiEventLogger;
+import com.android.providers.media.TestConfigStore;
 import com.android.providers.media.photopicker.PhotoPickerActivity;
 import com.android.providers.media.photopicker.data.ItemsProvider;
+import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger;
 import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
 
 public class PhotoPickerTestActivity extends PhotoPickerActivity {
+    private final TestConfigStore mConfigStore = new TestConfigStore();
+    private final UiEventLogger mLogger = mock(UiEventLogger.class, RETURNS_SMART_NULLS);
+    private InstanceId mInstanceId;
+
     @Override
+    @NonNull
     protected PickerViewModel getOrCreateViewModel() {
-        PickerViewModel pickerViewModel = super.getOrCreateViewModel();
+        final PickerViewModel pickerViewModel = super.getOrCreateViewModel();
+        if (getIntent().getExtras() != null && getIntent().getExtras().getBoolean(
+                MANAGED_SELECTION_ENABLED_EXTRA)) {
+            mConfigStore.enablePickerChoiceManagedSelectionEnabled();
+        }
+        pickerViewModel.setConfigStore(mConfigStore);
         pickerViewModel.setItemsProvider(new ItemsProvider(
                 PhotoPickerBaseTest.getIsolatedContext()));
         pickerViewModel.setUserIdManager(PhotoPickerBaseTest.getMockUserIdManager());
+        pickerViewModel.setLogger(new PhotoPickerUiEventLogger(mLogger));
+        mInstanceId = pickerViewModel.getInstanceId();
         return pickerViewModel;
     }
+
+    TestConfigStore getConfigStore() {
+        return mConfigStore;
+    }
+
+    UiEventLogger getLogger() {
+        return mLogger;
+    }
+
+    InstanceId getInstanceId() {
+        return mInstanceId;
+    }
 }
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerUserSelectActivityTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerUserSelectActivityTest.java
index 3d1a13d..8c39da5 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerUserSelectActivityTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerUserSelectActivityTest.java
@@ -16,11 +16,13 @@
 
 package com.android.providers.media.photopicker.espresso;
 
+import static androidx.test.InstrumentationRegistry.getTargetContext;
 import static androidx.test.espresso.Espresso.onView;
 import static androidx.test.espresso.action.ViewActions.click;
 import static androidx.test.espresso.assertion.ViewAssertions.matches;
 import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.isNotSelected;
 import static androidx.test.espresso.matcher.ViewMatchers.isSelected;
 import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
 import static androidx.test.espresso.matcher.ViewMatchers.withId;
@@ -31,25 +33,40 @@
 import static com.android.providers.media.photopicker.ui.TabAdapter.ITEM_TYPE_BANNER;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static junit.framework.Assert.fail;
 
 import static org.hamcrest.Matchers.allOf;
 import static org.hamcrest.Matchers.not;
 
 import android.app.Activity;
+import android.content.ContentUris;
 import android.content.Intent;
+import android.net.Uri;
 import android.provider.MediaStore;
 
+import androidx.lifecycle.ViewModelProvider;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.core.app.ActivityScenario;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
 
 import com.android.providers.media.R;
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
+import com.android.providers.media.photopicker.DataLoaderThread;
+import com.android.providers.media.photopicker.data.Selection;
+import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
 
 import org.junit.After;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunOnlyOnPostsubmit
 @SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
 @RunWith(AndroidJUnit4ClassRunner.class)
 public class PhotoPickerUserSelectActivityTest extends PhotoPickerBaseTest {
@@ -85,7 +102,7 @@
     @Test
     public void testActivityProfileButtonNotShown() {
         launchValidActivity();
-        // Partial screen does not show profile button
+        // User select mode does not show profile button
         onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
 
         // Navigate to Albums tab
@@ -130,6 +147,34 @@
     }
 
     @Test
+    public void testAddButtonIsShowsAllowNone() {
+        launchValidActivityWithManagedSelectionEnabled();
+        final int bottomBarId = R.id.picker_bottom_bar;
+        final int viewSelectedId = R.id.button_view_selected;
+        final int addButtonId = R.id.button_add;
+
+        // Default view, no item selected.
+        onView(withId(bottomBarId)).check(matches(isDisplayed()));
+        onView(withId(viewSelectedId)).check(matches(not(isDisplayed())));
+        onView(withId(addButtonId)).check(matches(isDisplayed()));
+        // verify that 'Allow none' is displayed in this case.
+        onView(withId(addButtonId)).check(
+                matches(withText(R.string.picker_add_button_allow_none_option)));
+
+        clickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
+
+        onView(withId(bottomBarId)).check(matches(isDisplayed()));
+        onView(withId(viewSelectedId)).check(matches(isDisplayed()));
+
+        onView(withId(addButtonId)).check(matches(withText("Allow (1)")));
+        onView(withId(addButtonId)).check(matches(isDisplayed()));
+
+
+        onView(withId(VIEW_SELECTED_BUTTON_ID)).perform(click());
+        onView(withId(addButtonId)).check(matches(withText("Allow (1)")));
+    }
+
+    @Test
     public void testNoCloudSettingsAndBanners() {
         launchValidActivity();
 
@@ -142,6 +187,134 @@
     }
 
     @Test
+    public void testPreview_deselectAll_showAllowNone() throws Exception {
+        launchValidActivityWithManagedSelectionEnabled();
+        onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
+
+        // Select first and second image
+        clickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
+        // Navigate to preview
+        onView(withId(VIEW_SELECTED_BUTTON_ID)).perform(click());
+        try (ViewPager2IdlingResource idlingResource =
+                     ViewPager2IdlingResource.register(mScenario, PREVIEW_VIEW_PAGER_ID)) {
+            final int previewAddButtonId = R.id.preview_add_button;
+            final int previewSelectButtonId = R.id.preview_selected_check_button;
+            final String selectedString =
+                    getTargetContext().getResources().getString(R.string.selected);
+            // Verify that, initially, we show "selected" check button
+            onView(withId(previewSelectButtonId)).check(matches(isSelected()));
+            onView(withId(previewSelectButtonId)).check(matches(withText(selectedString)));
+            // Verify that the text in Add button matches "Allow (1)"
+            onView(withId(previewAddButtonId))
+                    .check(matches(withText("Allow (1)")));
+
+            // Deselect item in preview
+            onView(withId(previewSelectButtonId)).perform(click());
+            onView(withId(previewSelectButtonId)).check(matches(isNotSelected()));
+            onView(withId(previewSelectButtonId)).check(matches(withText(R.string.deselected)));
+            // Verify that the text in Add button now changes to "Allow none"
+            onView(withId(previewAddButtonId))
+                    .check(matches(withText("Allow none")));
+            // Verify that we have 0 items in selected items
+            mScenario.onActivity(activity -> {
+                Selection selection =
+                        new ViewModelProvider(activity).get(PickerViewModel.class).getSelection();
+                assertThat(selection.getSelectedItemCount().getValue()).isEqualTo(0);
+            });
+
+            // Select the item again
+            onView(withId(previewSelectButtonId)).perform(click());
+            onView(withId(previewSelectButtonId)).check(matches(isSelected()));
+            onView(withId(previewSelectButtonId)).check(matches(withText(selectedString)));
+            // Verify that the text in Add button now changes back to "Allow (1)"
+            onView(withId(previewAddButtonId))
+                    .check(matches(withText("Allow (1)")));
+            // Verify that we have 1 item in selected items
+            mScenario.onActivity(activity -> {
+                Selection selection =
+                        new ViewModelProvider(activity).get(PickerViewModel.class).getSelection();
+                assertThat(selection.getSelectedItemCount().getValue()).isEqualTo(1);
+            });
+        }
+    }
+
+    @Test
+    public void testPreview_showsOnlyAlreadyLoadedGrantItems() throws Exception {
+        launchValidActivityWithManagedSelectionEnabled();
+        onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
+
+        final Uri uri = MediaStore.scanFile(getIsolatedContext().getContentResolver(),
+                IMAGE_1_FILE);
+        MediaStore.waitForIdle(getIsolatedContext().getContentResolver());
+        mScenario.onActivity(activity -> {
+            // Add an item id to the pre-granted set, so that when preview fragment gets opened up
+            // there is something to load as a remaining item.
+            Selection selection =
+                    new ViewModelProvider(activity).get(PickerViewModel.class).getSelection();
+            selection.setTotalNumberOfPreGrantedItems(1);
+            selection.setPreGrantedItemSet(Set.of(String.valueOf(ContentUris.parseId(uri))));
+
+            // Verify that we don't have anything to preview
+            selection.prepareSelectedItemsForPreviewAll();
+            assertWithMessage("Expected preview-able item list to be empty")
+                    .that(selection.getSelectedItemsForPreview()).isEmpty();
+        });
+
+        // Block the DataLoader thread by posting a conditional wait. This will block fetching of
+        // pregranted items in preview
+        final CountDownLatch latch = new CountDownLatch(1);
+        DataLoaderThread.waitForIdle();
+        DataLoaderThread.getHandler().postDelayed(() -> {
+            // Wait for 5 seconds if we don't receive a countdown
+            try {
+                assertWithMessage("Expected the test to send countdown before 5s")
+                        .that(latch.await(5, TimeUnit.SECONDS)).isTrue();
+            } catch (InterruptedException e) {
+                fail("Unexpected excepetion : " + e.getMessage());
+            }
+        }, DataLoaderThread.TOKEN, 0);
+
+        // Navigate to preview
+        onView(withId(VIEW_SELECTED_BUTTON_ID)).perform(click());
+
+        // Verify that UI shows no selected / deselected button
+        try (ViewPager2IdlingResource idlingResource =
+                     ViewPager2IdlingResource.register(mScenario, PREVIEW_VIEW_PAGER_ID)) {
+            final int previewAddButtonId = R.id.preview_add_button;
+            final int previewSelectButtonId = R.id.preview_selected_check_button;
+            // Verify that, initially, we show "selected" check button
+            onView(withId(previewSelectButtonId)).check(matches(not(isDisplayed())));
+            onView(withId(previewAddButtonId)).check(matches(isDisplayed()));
+            // Verify that the text in Add button matches "Allow (1)"
+            onView(withId(previewAddButtonId)).check(matches(withText("Allow (1)")));
+        }
+
+        // Free DataLoaderThread so that it can load pregranted items
+        latch.countDown();
+        DataLoaderThread.waitForIdle();
+
+        // Verify that UI now shows selected button
+        try (ViewPager2IdlingResource idlingResource =
+                     ViewPager2IdlingResource.register(mScenario, PREVIEW_VIEW_PAGER_ID)) {
+            final int previewAddButtonId = R.id.preview_add_button;
+            final int previewSelectButtonId = R.id.preview_selected_check_button;
+            final String selectedString =
+                    getTargetContext().getResources().getString(R.string.selected);
+            // Verify that, initially, we show "selected" check button
+            onView(withId(previewSelectButtonId)).check(matches(isSelected()));
+            onView(withId(previewSelectButtonId)).check(matches(withText(selectedString)));
+            // Verify that the text in Add button matches "Allow (1)"
+            onView(withId(previewAddButtonId))
+                    .check(matches(withText("Allow (1)")));
+            mScenario.onActivity(activity -> {
+                Selection selection =
+                        new ViewModelProvider(activity).get(PickerViewModel.class).getSelection();
+                assertThat(selection.getSelectedItemCount().getValue()).isEqualTo(1);
+            });
+        }
+    }
+
+    @Test
     public void testUserSelectCorrectHeaderTextIsShown() {
         launchValidActivity();
         onView(withText(R.string.picker_header_permissions)).check(matches(isDisplayed()));
@@ -153,4 +326,10 @@
                 ActivityScenario.launchActivityForResult(
                         PhotoPickerBaseTest.getUserSelectImagesForAppIntent());
     }
+
+    /** Test helper to launch a valid test activity. */
+    private void launchValidActivityWithManagedSelectionEnabled() {
+        mScenario = ActivityScenario.launchActivityForResult(
+                PhotoPickerBaseTest.getPickerChoiceManagedSelectionIntent());
+    }
 }
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotosTabTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotosTabTest.java
index b59ceb5..265b871 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotosTabTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotosTabTest.java
@@ -40,12 +40,15 @@
 import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
 
 import com.android.providers.media.R;
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
+import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger.PhotoPickerEvent;
 import com.android.providers.media.photopicker.util.DateTimeUtils;
 
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@RunOnlyOnPostsubmit
 @RunWith(AndroidJUnit4ClassRunner.class)
 public class PhotosTabTest extends PhotoPickerBaseTest {
     private static final int ICON_GIF_ID = R.id.icon_gif;
@@ -64,6 +67,10 @@
         // check the count of items
         onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(new RecyclerViewItemCountAssertion(4));
 
+        // Verify log
+        UiEventLoggerTestUtils.verifyLogWithInstanceIdAndPosition(
+                mRule, PhotoPickerEvent.PHOTO_PICKER_UI_LOADED_PHOTOS, /* countOfMediaItems */ 3);
+
         // Verify first item is recent header
         onView(withRecyclerView(PICKER_TAB_RECYCLERVIEW_ID)
                 .atPositionOnView(0, R.id.date_header_title))
@@ -137,6 +144,10 @@
         // Verify that drag bar is shown
         onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
 
+        // Verify log
+        UiEventLoggerTestUtils.verifyLogWithInstanceIdAndPosition(mRule,
+                PhotoPickerEvent.PHOTO_PICKER_UI_LOADED_ALBUM_CONTENTS, /* countOfMediaItems */ 1);
+
         final int dateHeaderTitleId = R.id.date_header_title;
         final int recentHeaderPosition = 0;
         // Verify that first item is not a recent header
@@ -171,4 +182,19 @@
         onView(allOf(withText(cameraStringId),
                 isDescendantOfA(withId(PICKER_TAB_RECYCLERVIEW_ID)))).check(matches(isDisplayed()));
     }
+
+    @Test
+    public void testSwitchToPhotosGrid() {
+        // Goto Albums page
+        onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+                .perform(click());
+
+        // Goto Photos page
+        onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+                .perform(click());
+
+        // Verify log
+        UiEventLoggerTestUtils.verifyLogWithInstanceId(
+                mRule, PhotoPickerEvent.PHOTO_PICKER_TAB_PHOTOS_OPEN);
+    }
 }
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PreviewFragmentAssertionUtils.java b/tests/src/com/android/providers/media/photopicker/espresso/PreviewFragmentAssertionUtils.java
new file mode 100644
index 0000000..fefc641
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PreviewFragmentAssertionUtils.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2023 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.espresso;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import static org.hamcrest.Matchers.not;
+
+import com.android.providers.media.R;
+
+class PreviewFragmentAssertionUtils {
+    private static final int PREVIEW_ADD_OR_SELECT_BUTTON_ID = R.id.preview_add_or_select_button;
+
+    static void assertSingleSelectCommonLayoutMatches() {
+        onView(withId(R.id.preview_viewPager)).check(matches(isDisplayed()));
+        onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(isDisplayed()));
+        // Verify that the text in Add button
+        onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(withText(R.string.add)));
+
+        onView(withId(R.id.preview_selected_check_button)).check(matches(not(isDisplayed())));
+        onView(withId(R.id.preview_add_button)).check(matches(not(isDisplayed())));
+    }
+}
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 49f8562..e8d89a7 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectLongPressTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectLongPressTest.java
@@ -41,6 +41,7 @@
 import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
 
 import com.android.providers.media.R;
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
 import com.android.providers.media.photopicker.data.Selection;
 import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
 
@@ -48,6 +49,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@RunOnlyOnPostsubmit
 @RunWith(AndroidJUnit4ClassRunner.class)
 public class PreviewMultiSelectLongPressTest extends PhotoPickerBaseTest {
     private static final int ICON_THUMBNAIL_ID = R.id.icon_thumbnail;
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 e9aa40d..c6befda 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectTest.java
@@ -53,7 +53,9 @@
 import androidx.viewpager2.widget.ViewPager2;
 
 import com.android.providers.media.R;
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
 import com.android.providers.media.photopicker.data.Selection;
+import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger.PhotoPickerEvent;
 import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
 
 import org.hamcrest.Description;
@@ -64,6 +66,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@RunOnlyOnPostsubmit
 @RunWith(AndroidJUnit4ClassRunner.class)
 public class PreviewMultiSelectTest extends PhotoPickerBaseTest {
     private static final int VIDEO_PREVIEW_THUMBNAIL_ID = R.id.preview_video_image;
@@ -80,8 +83,16 @@
 
         // Select two items and Navigate to preview
         clickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
+        UiEventLoggerTestUtils.verifyLogWithInstanceIdAndPosition(
+                mRule, PhotoPickerEvent.PHOTO_PICKER_SELECTED_ITEM_MAIN_GRID, IMAGE_1_POSITION);
+
         clickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_2_POSITION, ICON_THUMBNAIL_ID);
+        UiEventLoggerTestUtils.verifyLogWithInstanceIdAndPosition(
+                mRule, PhotoPickerEvent.PHOTO_PICKER_SELECTED_ITEM_MAIN_GRID, IMAGE_2_POSITION);
+
         onView(withId(VIEW_SELECTED_BUTTON_ID)).perform(click());
+        UiEventLoggerTestUtils.verifyLogWithInstanceIdAndPosition(mRule,
+                PhotoPickerEvent.PHOTO_PICKER_PREVIEW_ALL_SELECTED, /* selectedItemCount */ 2);
 
         try (ViewPager2IdlingResource idlingResource =
                 ViewPager2IdlingResource.register(mRule.getScenario(), PREVIEW_VIEW_PAGER_ID)) {
@@ -353,6 +364,9 @@
         clickItem(PICKER_TAB_RECYCLERVIEW_ID, 1, ICON_THUMBNAIL_ID);
         assertItemSelected(PICKER_TAB_RECYCLERVIEW_ID, /* position */ 1, ICON_THUMBNAIL_ID);
 
+        UiEventLoggerTestUtils.verifyLogWithInstanceIdAndPosition(
+                mRule, PhotoPickerEvent.PHOTO_PICKER_SELECTED_ITEM_ALBUM, /* position= */ 1);
+
         // Navigate to preview
         onView(withId(VIEW_SELECTED_BUTTON_ID)).perform(click());
 
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 241d842..3de3575 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java
@@ -16,6 +16,8 @@
 
 package com.android.providers.media.photopicker.espresso;
 
+import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_NONE;
+
 import static androidx.test.espresso.Espresso.onView;
 import static androidx.test.espresso.action.ViewActions.click;
 import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
@@ -27,13 +29,11 @@
 import static androidx.test.espresso.matcher.ViewMatchers.withParent;
 import static androidx.test.espresso.matcher.ViewMatchers.withText;
 
-import static com.android.providers.media.photopicker.espresso.BottomSheetTestUtils.assertBottomSheetState;
 import static com.android.providers.media.photopicker.espresso.OrientationUtils.setLandscapeOrientation;
 import static com.android.providers.media.photopicker.espresso.OrientationUtils.setPortraitOrientation;
 import static com.android.providers.media.photopicker.espresso.OverflowMenuUtils.assertOverflowMenuNotShown;
 import static com.android.providers.media.photopicker.espresso.RecyclerViewTestUtils.longClickItem;
 
-import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED;
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.hamcrest.Matchers.allOf;
@@ -45,16 +45,18 @@
 import android.view.View;
 
 import androidx.appcompat.widget.Toolbar;
-import androidx.test.espresso.IdlingRegistry;
 import androidx.test.ext.junit.rules.ActivityScenarioRule;
 import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
 
 import com.android.providers.media.R;
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
+import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger.PhotoPickerEvent;
 
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@RunOnlyOnPostsubmit
 @RunWith(AndroidJUnit4ClassRunner.class)
 public class PreviewSingleSelectTest extends PhotoPickerBaseTest {
 
@@ -63,79 +65,19 @@
             = new ActivityScenarioRule<>(PhotoPickerBaseTest.getSingleSelectionIntent());
 
     @Test
-    public void testPreview_singleSelect_image() throws Exception {
-        onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
-
-        // Bottomsheet assertions are different for landscape mode
-        setPortraitOrientation(mRule.getScenario());
-
-        final BottomSheetIdlingResource bottomSheetIdlingResource =
-                BottomSheetIdlingResource.register(mRule.getScenario());
-
-        try {
-            // TODO(b/226318844): When accessibility is enabled, we always launch the photo picker
-            // in full screen mode. Accessibility is enabled in Espresso test, we can't check the
-            // COLLAPSED state.
-//            bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
-//            onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
-//            onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
-//            mRule.getScenario().onActivity(activity -> {
-//                assertBottomSheetState(activity, STATE_COLLAPSED);
-//            });
-
-            // Navigate to preview
-            longClickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
-
-            try (ViewPager2IdlingResource idlingResource =
-                    ViewPager2IdlingResource.register(mRule.getScenario(), PREVIEW_VIEW_PAGER_ID)) {
-                // No dragBar in preview
-                bottomSheetIdlingResource.setExpectedState(STATE_EXPANDED);
-                onView(withId(DRAG_BAR_ID)).check(matches(not(isDisplayed())));
-                // No privacy text in preview
-                onView(withId(PRIVACY_TEXT_ID)).check(matches(not(isDisplayed())));
-                mRule.getScenario().onActivity(activity -> {
-                    assertBottomSheetState(activity, STATE_EXPANDED);
-                });
-
-                // Verify image is previewed
-                assertSingleSelectCommonLayoutMatches();
-                onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
-                // Verify no special format icon is previewed
-                onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
-                onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
-                // Verify the overflow menu is not shown for PICK_IMAGES intent
-                assertOverflowMenuNotShown();
-            }
-            // Navigate back to Photo grid
-            onView(withContentDescription("Navigate up")).perform(click());
-
-            onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
-            onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
-            onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
-
-            // TODO(b/226318844): When accessibility is enabled, we always launch the photo picker
-            // in full screen mode. Accessibility is enabled in Espresso test, we can't check the
-            // COLLAPSED state.
-//            bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
-//            // Shows dragBar and privacy text after we are back to Photos tab
-//            mRule.getScenario().onActivity(activity -> {
-//                assertBottomSheetState(activity, STATE_COLLAPSED);
-//            });
-        } finally {
-            IdlingRegistry.getInstance().unregister(bottomSheetIdlingResource);
-        }
-    }
-
-    @Test
     public void testPreview_singleSelect_video() throws Exception {
         onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
 
         // Navigate to preview
         longClickItem(PICKER_TAB_RECYCLERVIEW_ID, VIDEO_POSITION, ICON_THUMBNAIL_ID);
 
+        UiEventLoggerTestUtils.verifyLogWithInstanceIdAndPosition(
+                mRule, PhotoPickerEvent.PHOTO_PICKER_PREVIEW_ITEM_MAIN_GRID,
+                _SPECIAL_FORMAT_NONE, MP4_VIDEO_MIME_TYPE, VIDEO_POSITION);
+
         try (ViewPager2IdlingResource idlingResource =
                 ViewPager2IdlingResource.register(mRule.getScenario(), PREVIEW_VIEW_PAGER_ID)) {
-            assertSingleSelectCommonLayoutMatches();
+            PreviewFragmentAssertionUtils.assertSingleSelectCommonLayoutMatches();
             // 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
@@ -169,7 +111,7 @@
         try (ViewPager2IdlingResource idlingResource =
                 ViewPager2IdlingResource.register(mRule.getScenario(), PREVIEW_VIEW_PAGER_ID)) {
             // Verify image is previewed
-            assertSingleSelectCommonLayoutMatches();
+            PreviewFragmentAssertionUtils.assertSingleSelectCommonLayoutMatches();
             onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
         }
 
@@ -250,14 +192,4 @@
         assertThat(bottomBarDrawable).isInstanceOf(ColorDrawable.class);
         assertThat(((ColorDrawable) bottomBarDrawable).getColor()).isEqualTo(expectedColor);
     }
-
-    private void assertSingleSelectCommonLayoutMatches() {
-        onView(withId(R.id.preview_viewPager)).check(matches(isDisplayed()));
-        onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(isDisplayed()));
-        // Verify that the text in Add button
-        onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(withText(R.string.add)));
-
-        onView(withId(R.id.preview_selected_check_button)).check(matches(not(isDisplayed())));
-        onView(withId(R.id.preview_add_button)).check(matches(not(isDisplayed())));
-    }
 }
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/ProgressBarTest.java b/tests/src/com/android/providers/media/photopicker/espresso/ProgressBarTest.java
new file mode 100644
index 0000000..8db8149
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/espresso/ProgressBarTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2023 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.espresso;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.hamcrest.Matchers.allOf;
+
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.UiSelector;
+import android.text.format.DateUtils;
+
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunOnlyOnPostsubmit
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class ProgressBarTest extends PhotoPickerBaseTest {
+    public ActivityScenario<PhotoPickerTestActivity> mScenario;
+    protected static final UiDevice sDevice = UiDevice.getInstance(getInstrumentation());
+
+    @Before
+    public void setup() {
+        startPhotoPickerActivityAndEnableCloudFlag();
+    }
+
+    @Test
+    public void test_progressBarAlbumsTab_isNotVisible() {
+
+        // Navigate to albums tab.
+        onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+                .perform(click());
+
+        // Verify that the progress bar and loading text is not visible.
+        assertProgressBarAndLoadingTextDoesNotAppears();
+    }
+
+    private void startPhotoPickerActivityAndEnableCloudFlag() {
+        sDevice.waitForIdle();
+        launchPhotosActivity();
+        mScenario.onActivity(
+                (activity -> {
+                    activity.getConfigStore()
+                            .enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(
+                                    getInstrumentation().getTargetContext().getPackageName());
+                }));
+    }
+
+    private void launchPhotosActivity() {
+        mScenario = ActivityScenario.launchActivityForResult(
+                PhotoPickerBaseTest.getSingleSelectionIntent());
+    }
+
+    private void assertProgressBarAndLoadingTextDoesNotAppears() {
+        final UiSelector progressBar = new UiSelector().resourceId(
+                getIsolatedContext().getPackageName()
+                        + ":id/progress_bar");
+        assertWithMessage("Waiting for progressBar to appear on photos grid").that(
+                new UiObject(progressBar).waitForExists(DateUtils.SECOND_IN_MILLIS / 2)).isFalse();
+
+        final UiSelector loadingText = new UiSelector().resourceId(
+                getIsolatedContext().getPackageName()
+                        + ":id/loading_text_view");
+        assertWithMessage("Waiting for progressBar to appear on photos grid").that(
+                new UiObject(loadingText).waitForExists(DateUtils.SECOND_IN_MILLIS / 2)).isFalse();
+    }
+
+    @After
+    public void tearDown() {
+        if (mScenario != null) {
+            mScenario.close();
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatMultiSelectTest.java b/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatMultiSelectTest.java
index 9a834d0..5897164 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatMultiSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatMultiSelectTest.java
@@ -32,11 +32,12 @@
 import androidx.test.ext.junit.rules.ActivityScenarioRule;
 
 import com.android.providers.media.R;
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
 
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 
+@RunOnlyOnPostsubmit
 public class SpecialFormatMultiSelectTest extends SpecialFormatBaseTest {
 
     @Rule
@@ -124,12 +125,10 @@
     }
 
     @Test
-    @Ignore("Enable after b/218806007 is fixed")
     public void testPreview_multiSelect_navigation() throws Exception {
         onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
 
         // Select items
-        clickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
         clickItem(PICKER_TAB_RECYCLERVIEW_ID, GIF_POSITION, ICON_THUMBNAIL_ID);
         clickItem(PICKER_TAB_RECYCLERVIEW_ID, ANIMATED_WEBP_POSITION, ICON_THUMBNAIL_ID);
         clickItem(PICKER_TAB_RECYCLERVIEW_ID, MOTION_PHOTO_POSITION, ICON_THUMBNAIL_ID);
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatSingleSelectTest.java b/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatSingleSelectTest.java
index 3e2edfc..2567296 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatSingleSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatSingleSelectTest.java
@@ -16,6 +16,10 @@
 
 package com.android.providers.media.photopicker.espresso;
 
+import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_ANIMATED_WEBP;
+import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_GIF;
+import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_MOTION_PHOTO;
+
 import static androidx.test.espresso.Espresso.onView;
 import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
 import static androidx.test.espresso.assertion.ViewAssertions.matches;
@@ -33,11 +37,13 @@
 import androidx.test.ext.junit.rules.ActivityScenarioRule;
 
 import com.android.providers.media.R;
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
+import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger.PhotoPickerEvent;
 
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 
+@RunOnlyOnPostsubmit
 public class SpecialFormatSingleSelectTest extends SpecialFormatBaseTest {
 
     @Rule
@@ -117,6 +123,10 @@
         // Navigate to preview
         longClickItem(PICKER_TAB_RECYCLERVIEW_ID, GIF_POSITION, ICON_THUMBNAIL_ID);
 
+        UiEventLoggerTestUtils.verifyLogWithInstanceIdAndPosition(
+                mRule, PhotoPickerEvent.PHOTO_PICKER_PREVIEW_ITEM_MAIN_GRID,
+                _SPECIAL_FORMAT_GIF, GIF_IMAGE_MIME_TYPE, GIF_POSITION);
+
         try (ViewPager2IdlingResource idlingResource =
                 ViewPager2IdlingResource.register(mRule.getScenario(), PREVIEW_VIEW_PAGER_ID)) {
             // Verify gif icon is displayed for gif preview
@@ -133,6 +143,10 @@
         // Navigate to preview
         longClickItem(PICKER_TAB_RECYCLERVIEW_ID, ANIMATED_WEBP_POSITION, ICON_THUMBNAIL_ID);
 
+        UiEventLoggerTestUtils.verifyLogWithInstanceIdAndPosition(
+                mRule, PhotoPickerEvent.PHOTO_PICKER_PREVIEW_ITEM_MAIN_GRID,
+                _SPECIAL_FORMAT_ANIMATED_WEBP, ANIMATED_WEBP_MIME_TYPE, ANIMATED_WEBP_POSITION);
+
         try (ViewPager2IdlingResource idlingResource =
                 ViewPager2IdlingResource.register(mRule.getScenario(), PREVIEW_VIEW_PAGER_ID)) {
             // Verify gif icon is displayed for animated preview
@@ -143,7 +157,6 @@
     }
 
     @Test
-    @Ignore("Enable after b/222013536 is fixed")
     public void testPreview_singleSelect_nonAnimatedWebp() throws Exception {
         onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
 
@@ -169,6 +182,10 @@
         // Navigate to preview
         longClickItem(PICKER_TAB_RECYCLERVIEW_ID, MOTION_PHOTO_POSITION, ICON_THUMBNAIL_ID);
 
+        UiEventLoggerTestUtils.verifyLogWithInstanceIdAndPosition(
+                mRule, PhotoPickerEvent.PHOTO_PICKER_PREVIEW_ITEM_MAIN_GRID,
+                _SPECIAL_FORMAT_MOTION_PHOTO, JPEG_IMAGE_MIME_TYPE, MOTION_PHOTO_POSITION);
+
         try (ViewPager2IdlingResource idlingResource =
                 ViewPager2IdlingResource.register(mRule.getScenario(), PREVIEW_VIEW_PAGER_ID)) {
             // Verify motion photo icon is displayed for motion photo preview
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/UiEventLoggerTestUtils.java b/tests/src/com/android/providers/media/photopicker/espresso/UiEventLoggerTestUtils.java
new file mode 100644
index 0000000..f088253
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/espresso/UiEventLoggerTestUtils.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2023 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.espresso;
+
+import static org.mockito.Mockito.verify;
+
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+
+import com.android.internal.logging.UiEventLogger;
+
+public class UiEventLoggerTestUtils {
+    static void verifyLogWithInstanceId(ActivityScenarioRule<PhotoPickerTestActivity> rule,
+            UiEventLogger.UiEventEnum event) {
+        verifyLogWithInstanceId(rule, event, /* uid */ 0, /* packageName */ null);
+    }
+
+    static void verifyLogWithInstanceId(ActivityScenarioRule<PhotoPickerTestActivity> rule,
+            UiEventLogger.UiEventEnum event, int uid, String packageName) {
+        rule.getScenario().onActivity(activity ->
+                verify(activity.getLogger()).logWithInstanceId(
+                        event, uid, packageName, activity.getInstanceId()));
+    }
+
+    static void verifyLogWithInstanceIdAndPosition(
+            ActivityScenarioRule<PhotoPickerTestActivity> rule,
+            UiEventLogger.UiEventEnum event, int position) {
+        verifyLogWithInstanceIdAndPosition(
+                rule, event, /* uid */ 0, /* packageName */ null, position);
+    }
+
+    static void verifyLogWithInstanceIdAndPosition(
+            ActivityScenarioRule<PhotoPickerTestActivity> rule, UiEventLogger.UiEventEnum event,
+            int uid, String packageName, int position) {
+        verifyLogWithInstanceIdAndPosition(rule.getScenario(), event, uid, packageName, position);
+    }
+
+    static <T extends PhotoPickerTestActivity> void verifyLogWithInstanceIdAndPosition(
+            ActivityScenario<T> scenario, UiEventLogger.UiEventEnum event,
+            int uid, String packageName, int position) {
+        scenario.onActivity(activity ->
+                verify(activity.getLogger())
+                        .logWithInstanceIdAndPosition(
+                                event, uid, packageName, activity.getInstanceId(), position));
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/ViewPager2IdlingResource.java b/tests/src/com/android/providers/media/photopicker/espresso/ViewPager2IdlingResource.java
index a7de9d6..27521ba 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/ViewPager2IdlingResource.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/ViewPager2IdlingResource.java
@@ -68,8 +68,8 @@
      * @return {@link ViewPager2IdlingResource} that is registered to the activity related to the
      *     given {@link ActivityScenarioRule} and the resource ID of the ViewPager2.
      */
-    public static ViewPager2IdlingResource register(
-            ActivityScenario<PhotoPickerTestActivity> scenario, int viewPager2Id) {
+    public static <T extends PhotoPickerTestActivity> ViewPager2IdlingResource register(
+            ActivityScenario<T> scenario, int viewPager2Id) {
         final ViewPager2IdlingResource[] idlingResources = new ViewPager2IdlingResource[1];
         scenario.onActivity(
                 (activity -> {
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/WorkAppsOffProfileButtonTest.java b/tests/src/com/android/providers/media/photopicker/espresso/WorkAppsOffProfileButtonTest.java
index 7eb2f7f..9d0c3f1 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/WorkAppsOffProfileButtonTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/WorkAppsOffProfileButtonTest.java
@@ -27,12 +27,14 @@
 import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
 
 import com.android.providers.media.R;
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
 
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+@RunOnlyOnPostsubmit
 @RunWith(AndroidJUnit4ClassRunner.class)
 public class WorkAppsOffProfileButtonTest extends PhotoPickerBaseTest {
     @BeforeClass
diff --git a/tests/src/com/android/providers/media/photopicker/sync/ImmediateAlbumSyncWorkerTest.java b/tests/src/com/android/providers/media/photopicker/sync/ImmediateAlbumSyncWorkerTest.java
new file mode 100644
index 0000000..5c5d65c
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/sync/ImmediateAlbumSyncWorkerTest.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+import static com.android.providers.media.photopicker.sync.PickerSyncNotificationHelper.NOTIFICATION_CHANNEL_ID;
+import static com.android.providers.media.photopicker.sync.PickerSyncNotificationHelper.NOTIFICATION_ID;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.buildTestWorker;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.getCloudAlbumSyncInputData;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.getLocalAlbumSyncInputData;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.getLocalAndCloudAlbumSyncInputData;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.getLocalAndCloudSyncInputData;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.initializeTestWorkManager;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.CancellationSignal;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.work.ForegroundInfo;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkInfo;
+import androidx.work.WorkManager;
+
+import com.android.providers.media.photopicker.PickerSyncController;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.concurrent.ExecutionException;
+
+// TODO enable tests in Android R after fixing b/293390235
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public class ImmediateAlbumSyncWorkerTest {
+    @Mock
+    private PickerSyncController mMockPickerSyncController;
+    @Mock
+    private SyncTracker mMockLocalAlbumSyncTracker;
+    @Mock
+    private SyncTracker mMockCloudAlbumSyncTracker;
+    private Context mContext;
+
+    @Before
+    public void setup() {
+        initMocks(this);
+
+        // Inject mock trackers
+        SyncTrackerRegistry.setLocalAlbumSyncTracker(mMockLocalAlbumSyncTracker);
+        SyncTrackerRegistry.setCloudAlbumSyncTracker(mMockCloudAlbumSyncTracker);
+
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        initializeTestWorkManager(mContext);
+    }
+
+    @After
+    public void teardown() {
+        // Reset mock trackers
+        SyncTrackerRegistry.setLocalAlbumSyncTracker(new SyncTracker());
+        SyncTrackerRegistry.setCloudAlbumSyncTracker(new SyncTracker());
+    }
+
+    @Test
+    public void testLocalAlbumImmediateSync() throws ExecutionException, InterruptedException {
+        // Setup
+        PickerSyncController.setInstance(mMockPickerSyncController);
+        final OneTimeWorkRequest request =
+                new OneTimeWorkRequest.Builder(ImmediateAlbumSyncWorker.class)
+                        .setInputData(getLocalAlbumSyncInputData(/* albumId */ "Not_null"))
+                        .build();
+
+        // Test run
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        workManager.enqueue(request).getResult().get();
+
+        // Verify
+        final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+        assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 1))
+                .syncAlbumMediaFromLocalProvider(anyString(), any(CancellationSignal.class));
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0))
+                .syncAlbumMediaFromCloudProvider(anyString(), any(CancellationSignal.class));
+
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testCloudAlbumImmediateSync() throws ExecutionException, InterruptedException {
+        // Setup
+        PickerSyncController.setInstance(mMockPickerSyncController);
+        final OneTimeWorkRequest request =
+                new OneTimeWorkRequest.Builder(ImmediateAlbumSyncWorker.class)
+                        .setInputData(getCloudAlbumSyncInputData(/* albumId */ "Not_null"))
+                        .build();
+
+        // Test run
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        workManager.enqueue(request).getResult().get();
+
+        // Verify
+        final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+        assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0))
+                .syncAlbumMediaFromLocalProvider(anyString(), any(CancellationSignal.class));
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 1))
+                .syncAlbumMediaFromCloudProvider(anyString(), any(CancellationSignal.class));
+
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .markSyncCompleted(any());
+
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testInvalidSyncSourceImmediateAlbumSync()
+            throws ExecutionException, InterruptedException {
+        // Setup
+        PickerSyncController.setInstance(mMockPickerSyncController);
+        final OneTimeWorkRequest request =
+                new OneTimeWorkRequest.Builder(ImmediateAlbumSyncWorker.class)
+                        .setInputData(getLocalAndCloudSyncInputData())
+                        .build();
+
+        // Test run
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        workManager.enqueue(request).getResult().get();
+
+        // Verify
+        final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+        assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0))
+                .syncAllMediaFromLocalProvider(any(CancellationSignal.class));
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0))
+                .syncAllMediaFromCloudProvider(any(CancellationSignal.class));
+
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testLocalAndCloudImmediateSyncFailure()
+            throws ExecutionException, InterruptedException {
+        // Setup
+        PickerSyncController.setInstance(null);
+        final OneTimeWorkRequest request =
+                new OneTimeWorkRequest.Builder(ImmediateAlbumSyncWorker.class)
+                        .setInputData(getLocalAndCloudAlbumSyncInputData(/* albumId */ "Not_null"))
+                        .build();
+
+        // Test run
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        workManager.enqueue(request).getResult().get();
+
+        // Verify
+        final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+        assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0))
+                .syncAllMediaFromLocalProvider(any(CancellationSignal.class));
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0))
+                .syncAllMediaFromCloudProvider(any(CancellationSignal.class));
+
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testInvalidAlbumIdImmediateSyncFailure()
+            throws ExecutionException, InterruptedException {
+        // Setup
+        PickerSyncController.setInstance(null);
+        final OneTimeWorkRequest request =
+                new OneTimeWorkRequest.Builder(ImmediateAlbumSyncWorker.class)
+                        .setInputData(getLocalAlbumSyncInputData(/* albumId */ ""))
+                        .build();
+
+        // Test run
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        workManager.enqueue(request).getResult().get();
+
+        // Verify
+        final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+        assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0))
+                .syncAllMediaFromLocalProvider(any(CancellationSignal.class));
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0))
+                .syncAllMediaFromCloudProvider(any(CancellationSignal.class));
+
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testImmediateAlbumSyncWorkerOnStopped() {
+        // Setup
+        final ImmediateAlbumSyncWorker immediateAlbumSyncWorker =
+                buildTestWorker(mContext, ImmediateAlbumSyncWorker.class);
+
+        // Test onStopped
+        immediateAlbumSyncWorker.onStopped();
+
+        // Verify
+        assertThat(immediateAlbumSyncWorker.getCancellationSignal().isCanceled()).isTrue();
+
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testGetForegroundInfo() {
+        final ForegroundInfo foregroundInfo =
+                buildTestWorker(mContext, ImmediateAlbumSyncWorker.class).getForegroundInfo();
+
+        assertThat(foregroundInfo.getNotificationId()).isEqualTo(NOTIFICATION_ID);
+        assertThat(foregroundInfo.getNotification().getChannelId())
+                .isEqualTo(NOTIFICATION_CHANNEL_ID);
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/sync/ImmediateSyncWorkerTest.java b/tests/src/com/android/providers/media/photopicker/sync/ImmediateSyncWorkerTest.java
new file mode 100644
index 0000000..a87caec
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/sync/ImmediateSyncWorkerTest.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+import static com.android.providers.media.photopicker.sync.PickerSyncNotificationHelper.NOTIFICATION_CHANNEL_ID;
+import static com.android.providers.media.photopicker.sync.PickerSyncNotificationHelper.NOTIFICATION_ID;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.buildTestWorker;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.getCloudSyncInputData;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.getLocalAndCloudSyncInputData;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.getLocalSyncInputData;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.initializeTestWorkManager;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.CancellationSignal;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.work.ForegroundInfo;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkInfo;
+import androidx.work.WorkManager;
+
+import com.android.providers.media.photopicker.PickerSyncController;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.concurrent.ExecutionException;
+
+// TODO enable tests in Android R after fixing b/293390235
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public class ImmediateSyncWorkerTest {
+    @Mock
+    private PickerSyncController mMockPickerSyncController;
+    @Mock
+    private SyncTracker mMockLocalSyncTracker;
+    @Mock
+    private SyncTracker mMockCloudSyncTracker;
+    private Context mContext;
+
+    @Before
+    public void setup() {
+        initMocks(this);
+
+        // Inject mock trackers
+        SyncTrackerRegistry.setLocalSyncTracker(mMockLocalSyncTracker);
+        SyncTrackerRegistry.setCloudSyncTracker(mMockCloudSyncTracker);
+
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        initializeTestWorkManager(mContext);
+    }
+
+    @After
+    public void teardown() {
+        // Reset mock trackers
+        SyncTrackerRegistry.setLocalSyncTracker(new SyncTracker());
+        SyncTrackerRegistry.setCloudSyncTracker(new SyncTracker());
+    }
+
+    @Test
+    public void testLocalImmediateSync() throws ExecutionException, InterruptedException {
+        // Setup
+        PickerSyncController.setInstance(mMockPickerSyncController);
+        final OneTimeWorkRequest request  =
+                new OneTimeWorkRequest.Builder(ImmediateSyncWorker.class)
+                        .setInputData(getLocalSyncInputData())
+                        .build();
+
+        // Test run
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        workManager.enqueue(request).getResult().get();
+
+        // Verify
+        final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+        assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 1))
+                .syncAllMediaFromLocalProvider(any(CancellationSignal.class));
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0))
+                .syncAllMediaFromCloudProvider(any(CancellationSignal.class));
+
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testCloudImmediateSync() throws ExecutionException, InterruptedException {
+        // Setup
+        PickerSyncController.setInstance(mMockPickerSyncController);
+        OneTimeWorkRequest request  = new OneTimeWorkRequest.Builder(ImmediateSyncWorker.class)
+                .setInputData(getCloudSyncInputData())
+                .build();
+
+        // Test run
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        workManager.enqueue(request).getResult().get();
+
+        // Verify
+        WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+        assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0))
+                .syncAllMediaFromLocalProvider(any(CancellationSignal.class));
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 1))
+                .syncAllMediaFromCloudProvider(any(CancellationSignal.class));
+
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .markSyncCompleted(any());
+
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testLocalAndCloudImmediateSync() throws ExecutionException, InterruptedException {
+        // Setup
+        PickerSyncController.setInstance(mMockPickerSyncController);
+        final OneTimeWorkRequest request  =
+                new OneTimeWorkRequest.Builder(ImmediateSyncWorker.class)
+                        .setInputData(getLocalAndCloudSyncInputData())
+                        .build();
+
+        // Test run
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        workManager.enqueue(request).getResult().get();
+
+        // Verify
+        final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+        assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 1))
+                .syncAllMediaFromLocalProvider(any(CancellationSignal.class));
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 1))
+                .syncAllMediaFromCloudProvider(any(CancellationSignal.class));
+
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testLocalAndCloudImmediateSyncFailure()
+            throws ExecutionException, InterruptedException {
+        // Setup
+        PickerSyncController.setInstance(null);
+        final OneTimeWorkRequest request  =
+                new OneTimeWorkRequest.Builder(ImmediateSyncWorker.class)
+                        .setInputData(getLocalAndCloudSyncInputData())
+                        .build();
+
+        // Test run
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        workManager.enqueue(request).getResult().get();
+
+        // Verify
+        final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+        assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0))
+                .syncAllMediaFromLocalProvider(any(CancellationSignal.class));
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0))
+                .syncAllMediaFromCloudProvider(any(CancellationSignal.class));
+
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testImmediateSyncWorkerOnStopped() {
+        // Setup
+        final ImmediateSyncWorker immediateSyncWorker =
+                buildTestWorker(mContext, ImmediateSyncWorker.class);
+
+        // Test onStopped
+        immediateSyncWorker.onStopped();
+
+        // Verify
+        assertThat(immediateSyncWorker.getCancellationSignal().isCanceled()).isTrue();
+
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testGetForegroundInfo() {
+        final ForegroundInfo foregroundInfo =
+                buildTestWorker(mContext, ImmediateSyncWorker.class).getForegroundInfo();
+
+        assertThat(foregroundInfo.getNotificationId()).isEqualTo(NOTIFICATION_ID);
+        assertThat(foregroundInfo.getNotification().getChannelId())
+                .isEqualTo(NOTIFICATION_CHANNEL_ID);
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/sync/MediaResetWorkerTest.java b/tests/src/com/android/providers/media/photopicker/sync/MediaResetWorkerTest.java
new file mode 100644
index 0000000..537de61
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/sync/MediaResetWorkerTest.java
@@ -0,0 +1,469 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_AND_CLOUD;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_RESET_ALBUM;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_AUTHORITY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_RESET_TYPE;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SYNC_SOURCE;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_TAG_IS_PERIODIC;
+import static com.android.providers.media.photopicker.sync.PickerSyncNotificationHelper.NOTIFICATION_CHANNEL_ID;
+import static com.android.providers.media.photopicker.sync.PickerSyncNotificationHelper.NOTIFICATION_ID;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.buildTestWorker;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.getAlbumResetInputData;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.initializeTestWorkManager;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.os.Build;
+import android.provider.CloudMediaProviderContract.MediaColumns;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.work.Data;
+import androidx.work.ForegroundInfo;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkInfo;
+import androidx.work.WorkManager;
+
+import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.data.PickerDatabaseHelper;
+import com.android.providers.media.photopicker.data.PickerDbFacade;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.io.File;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+// TODO enable tests in Android R after fixing b/293390235
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public class MediaResetWorkerTest {
+
+    private PickerSyncController mExistingPickerSyncController;
+
+    @Mock private PickerSyncController mMockPickerSyncController;
+    @Mock private SyncTracker mMockLocalAlbumSyncTracker;
+    @Mock private SyncTracker mMockCloudAlbumSyncTracker;
+
+    private PickerDbFacade mDbFacade;
+    private Context mContext;
+
+    private static final String TEST_ALBUM_ID_1 = "test-album-id-1";
+    private static final String TEST_ALBUM_ID_2 = "test-album-id-2";
+    private static final String TEST_ALBUM_ID_3 = "test-album-id-3";
+    private static final String TEST_ALBUM_ID_4 = "test-album-id-4";
+    private static final String TEST_LOCAL_AUTHORITY = "com.android.media.photopicker";
+    private static final String TEST_CLOUD_AUTHORITY = "com.hooli.super.awesome.cloud.provider";
+
+    @Before
+    public void setup() {
+        initMocks(this);
+
+        try {
+            mExistingPickerSyncController = PickerSyncController.getInstanceOrThrow();
+        } catch (IllegalStateException ignored) {
+        }
+
+        // Inject mock trackers
+        SyncTrackerRegistry.setLocalAlbumSyncTracker(mMockLocalAlbumSyncTracker);
+        SyncTrackerRegistry.setCloudAlbumSyncTracker(mMockCloudAlbumSyncTracker);
+
+        doReturn(new PickerSyncLockManager())
+                .when(mMockPickerSyncController).getPickerSyncLockManager();
+        doReturn(TEST_CLOUD_AUTHORITY).when(mMockPickerSyncController).getCloudProvider();
+        doReturn(TEST_LOCAL_AUTHORITY).when(mMockPickerSyncController).getLocalProvider();
+
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+        // Cleanup previous test run databases.
+        File dbPath = mContext.getDatabasePath(PickerDatabaseHelper.PICKER_DATABASE_NAME);
+        dbPath.delete();
+
+        mDbFacade = new PickerDbFacade(mContext, new PickerSyncLockManager(), TEST_LOCAL_AUTHORITY);
+        mDbFacade.setCloudProvider(TEST_CLOUD_AUTHORITY);
+
+        initializeTestWorkManager(mContext);
+        PickerSyncController.setInstance(mMockPickerSyncController);
+    }
+
+    @After
+    public void teardown() {
+        if (mExistingPickerSyncController != null) {
+            PickerSyncController.setInstance(mExistingPickerSyncController);
+        }
+
+        // Reset mock trackers
+        SyncTrackerRegistry.setLocalAlbumSyncTracker(new SyncTracker());
+        SyncTrackerRegistry.setCloudAlbumSyncTracker(new SyncTracker());
+    }
+
+    @Test
+    public void testResetCloudAlbumMediaForAlbumId()
+            throws ExecutionException, InterruptedException {
+
+        assertAddAlbumMediaWithAlbumId(TEST_ALBUM_ID_1, TEST_CLOUD_AUTHORITY);
+        assertAddAlbumMediaWithAlbumId(TEST_ALBUM_ID_2, TEST_CLOUD_AUTHORITY);
+        assertAddAlbumMediaWithAlbumId(TEST_ALBUM_ID_3, TEST_LOCAL_AUTHORITY);
+        assertAddAlbumMediaWithAlbumId(TEST_ALBUM_ID_4, TEST_LOCAL_AUTHORITY);
+
+        final OneTimeWorkRequest request =
+                new OneTimeWorkRequest.Builder(MediaResetWorker.class)
+                        .setInputData(
+                                getAlbumResetInputData(
+                                        TEST_ALBUM_ID_1, TEST_CLOUD_AUTHORITY, false))
+                        .build();
+
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        workManager.enqueue(request).getResult().get();
+
+        // Verify
+        final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+        assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+        // We should have deleted just the rows related to the TEST_ALBUM_ID_1 album.
+        Cursor cursor = queryAlbumMediaAll(TEST_CLOUD_AUTHORITY);
+        assertThat(cursor.getCount()).isEqualTo(60);
+        cursor.close();
+
+        cursor = queryAlbumMediaAll(TEST_ALBUM_ID_1, TEST_CLOUD_AUTHORITY);
+        assertThat(cursor.getCount()).isEqualTo(0);
+        cursor.close();
+
+        cursor = queryAlbumMediaAll(TEST_ALBUM_ID_2, TEST_CLOUD_AUTHORITY);
+        assertThat(cursor.getCount()).isEqualTo(20);
+        cursor.close();
+
+        cursor = queryAlbumMediaAll(TEST_ALBUM_ID_3, TEST_LOCAL_AUTHORITY);
+        assertThat(cursor.getCount()).isEqualTo(20);
+        cursor.close();
+
+        cursor = queryAlbumMediaAll(TEST_ALBUM_ID_4, TEST_LOCAL_AUTHORITY);
+        assertThat(cursor.getCount()).isEqualTo(20);
+        cursor.close();
+
+        // The sync future is created by the PickerSyncManager before the request is
+        // enqueued.
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+
+        // The worker should resolve its own sync future.
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testResetLocalAlbumMediaForAlbumId()
+            throws ExecutionException, InterruptedException {
+
+        assertAddAlbumMediaWithAlbumId(TEST_ALBUM_ID_1, TEST_LOCAL_AUTHORITY);
+        assertAddAlbumMediaWithAlbumId(TEST_ALBUM_ID_2, TEST_CLOUD_AUTHORITY);
+
+        final OneTimeWorkRequest request =
+                new OneTimeWorkRequest.Builder(MediaResetWorker.class)
+                        .setInputData(
+                                getAlbumResetInputData(TEST_ALBUM_ID_1, TEST_CLOUD_AUTHORITY, true))
+                        .build();
+
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        workManager.enqueue(request).getResult().get();
+
+        // Verify
+        final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+        assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+
+        // We should have deleted just the rows related to the TEST_ALBUM_ID_1 album.
+        Cursor cursor = queryAlbumMediaAll(TEST_CLOUD_AUTHORITY);
+        assertThat(cursor.getCount()).isEqualTo(20);
+        cursor.close();
+
+        cursor = queryAlbumMediaAll(TEST_ALBUM_ID_1, TEST_LOCAL_AUTHORITY);
+        assertThat(cursor.getCount()).isEqualTo(0);
+        cursor.close();
+
+        cursor = queryAlbumMediaAll(TEST_ALBUM_ID_2, TEST_CLOUD_AUTHORITY);
+        assertThat(cursor.getCount()).isEqualTo(20);
+        cursor.close();
+
+        // The sync future is created by the PickerSyncManager before the request is
+        // enqueued.
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+
+        // The worker should resolve its own sync future.
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testResetAllAlbumMedia() throws ExecutionException, InterruptedException {
+
+        assertAddAlbumMediaWithAlbumId(TEST_ALBUM_ID_1, TEST_CLOUD_AUTHORITY);
+        assertAddAlbumMediaWithAlbumId(TEST_ALBUM_ID_2, TEST_CLOUD_AUTHORITY);
+
+        final Data requestData =
+                new Data(
+                        Map.of(
+                                SYNC_WORKER_INPUT_AUTHORITY,
+                                TEST_CLOUD_AUTHORITY,
+                                SYNC_WORKER_INPUT_RESET_TYPE,
+                                SYNC_RESET_ALBUM,
+                                SYNC_WORKER_INPUT_SYNC_SOURCE,
+                                SYNC_LOCAL_AND_CLOUD));
+
+        final OneTimeWorkRequest request =
+                new OneTimeWorkRequest.Builder(MediaResetWorker.class)
+                        .setInputData(requestData)
+                        .build();
+
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        workManager.enqueue(request).getResult().get();
+
+        // Verify
+        final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+        assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+        Cursor cursor = queryAlbumMediaAll(TEST_CLOUD_AUTHORITY);
+        assertThat(cursor.getCount()).isEqualTo(0);
+        cursor.close();
+
+        cursor = queryAlbumMediaAll(TEST_LOCAL_AUTHORITY);
+        assertThat(cursor.getCount()).isEqualTo(0);
+        cursor.close();
+
+        // The sync future is created by the PickerSyncManager before the request is
+        // enqueued.
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+
+        // The worker should resolve its own sync future.
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testPeriodicWorkerAlbumReset_WithCloudProvider()
+            throws ExecutionException, InterruptedException {
+
+        assertAddAlbumMediaWithAlbumId(TEST_ALBUM_ID_1, TEST_LOCAL_AUTHORITY);
+        assertAddAlbumMediaWithAlbumId(TEST_ALBUM_ID_2, TEST_CLOUD_AUTHORITY);
+
+        final Data requestData =
+                new Data(
+                        Map.of(
+                                SYNC_WORKER_INPUT_RESET_TYPE,
+                                SYNC_RESET_ALBUM,
+                                SYNC_WORKER_INPUT_SYNC_SOURCE,
+                                SYNC_LOCAL_AND_CLOUD));
+
+        final OneTimeWorkRequest request =
+                new OneTimeWorkRequest.Builder(MediaResetWorker.class)
+                        .setInputData(requestData)
+                        .addTag(SYNC_WORKER_TAG_IS_PERIODIC)
+                        .build();
+
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        workManager.enqueue(request).getResult().get();
+
+        // Verify
+        final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+        assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+        // The sync future is created by the PickerSyncManager before the request is
+        // enqueued.
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .createSyncFuture(any());
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .createSyncFuture(any());
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+
+        Cursor cursor = queryAlbumMediaAll(TEST_CLOUD_AUTHORITY);
+        assertThat(cursor.getCount()).isEqualTo(0);
+        cursor.close();
+
+        cursor = queryAlbumMediaAll(TEST_LOCAL_AUTHORITY);
+        assertThat(cursor.getCount()).isEqualTo(0);
+        cursor.close();
+    }
+
+    @Test
+    public void testPeriodicWorkerAlbumReset_WithLocalProvider()
+            throws ExecutionException, InterruptedException {
+
+        doReturn(null).when(mMockPickerSyncController).getCloudProvider();
+
+        assertAddAlbumMediaWithAlbumId(TEST_ALBUM_ID_1, TEST_LOCAL_AUTHORITY);
+        assertAddAlbumMediaWithAlbumId(TEST_ALBUM_ID_2, TEST_LOCAL_AUTHORITY);
+
+        final Data requestData =
+                new Data(
+                        Map.of(
+                                SYNC_WORKER_INPUT_RESET_TYPE,
+                                SYNC_RESET_ALBUM,
+                                SYNC_WORKER_INPUT_SYNC_SOURCE,
+                                SYNC_LOCAL_AND_CLOUD));
+
+        final OneTimeWorkRequest request =
+                new OneTimeWorkRequest.Builder(MediaResetWorker.class)
+                        .setInputData(requestData)
+                        .addTag(SYNC_WORKER_TAG_IS_PERIODIC)
+                        .build();
+
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        workManager.enqueue(request).getResult().get();
+
+        // Verify
+        final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+        assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+        // The sync future is created by the PickerSyncManager before the request is
+        // enqueued.
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .createSyncFuture(any());
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .createSyncFuture(any());
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+
+        Cursor cursor = queryAlbumMediaAll(TEST_LOCAL_AUTHORITY);
+        assertThat(cursor.getCount()).isEqualTo(0);
+        cursor.close();
+    }
+
+    @Test
+    public void testMediaResetWorkerOnStopped() {
+        buildTestWorker(mContext, MediaResetWorker.class).onStopped();
+
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockLocalAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockCloudAlbumSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testGetForegroundInfo() {
+        final ForegroundInfo foregroundInfo =
+                buildTestWorker(mContext, MediaResetWorker.class).getForegroundInfo();
+
+        assertThat(foregroundInfo.getNotificationId()).isEqualTo(NOTIFICATION_ID);
+        assertThat(foregroundInfo.getNotification().getChannelId())
+                .isEqualTo(NOTIFICATION_CHANNEL_ID);
+    }
+
+    /**
+     * Builds a suitible mock Album media cursor that could be returned from a provider.
+     *
+     * @param id a base id for each file. will be appended with the current loop count.
+     */
+    private static Cursor getAlbumMediaCursor(String id) {
+        String[] projectionKey =
+                new String[] {
+                    MediaColumns.ID,
+                    MediaColumns.MEDIA_STORE_URI,
+                    MediaColumns.DATE_TAKEN_MILLIS,
+                    MediaColumns.SYNC_GENERATION,
+                    MediaColumns.SIZE_BYTES,
+                    MediaColumns.MIME_TYPE,
+                    MediaColumns.STANDARD_MIME_TYPE_EXTENSION,
+                    MediaColumns.DURATION_MILLIS,
+                };
+
+        MatrixCursor c = new MatrixCursor(projectionKey);
+        int counter = 0;
+
+        while (++counter <= 20) {
+
+            String[] projectionValue =
+                    new String[] {
+                        id + counter,
+                        "content://media/external/file/1234" + counter,
+                        String.valueOf(System.nanoTime()),
+                        String.valueOf(1),
+                        String.valueOf(1234),
+                        "image/png",
+                        String.valueOf(MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE),
+                        String.valueOf(1234),
+                    };
+
+            c.addRow(projectionValue);
+        }
+        return c;
+    }
+
+    /**
+     * Query all records in the Album media table.
+     *
+     * @param authority provider's authority
+     */
+    private Cursor queryAlbumMediaAll(String authority) {
+        return mDbFacade.queryAlbumMediaForUi(
+                new PickerDbFacade.QueryFilterBuilder(1000).build(), authority);
+    }
+
+    /**
+     * @param albumId limit the results to just files present in this album
+     * @param authority provider's authority
+     */
+    private Cursor queryAlbumMediaAll(String albumId, String authority) {
+        return mDbFacade.queryAlbumMediaForUi(
+                new PickerDbFacade.QueryFilterBuilder(1000).setAlbumId(albumId).build(), authority);
+    }
+
+    /**
+     * Creates a fake Album with the given Album ID and adds 20 fake files to it.
+     *
+     * @param albumId the id to use in creating the fake album
+     * @param authority the provider that owns the fake album.
+     */
+    private void assertAddAlbumMediaWithAlbumId(String albumId, String authority) {
+
+        try (PickerDbFacade.DbWriteOperation operation =
+                mDbFacade.beginAddAlbumMediaOperation(authority, albumId)) {
+            operation.execute(getAlbumMediaCursor("1234-" + albumId));
+            operation.setSuccess();
+        }
+
+        Cursor cr = queryAlbumMediaAll(albumId, authority);
+        assertThat(cr.getCount()).isEqualTo(20);
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/sync/PickerSyncLockManagerTest.java b/tests/src/com/android/providers/media/photopicker/sync/PickerSyncLockManagerTest.java
new file mode 100644
index 0000000..de01adb
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/sync/PickerSyncLockManagerTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+import static com.android.providers.media.photopicker.sync.PickerSyncLockManager.CLOUD_ALBUM_SYNC_LOCK;
+import static com.android.providers.media.photopicker.sync.PickerSyncLockManager.CLOUD_SYNC_LOCK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class PickerSyncLockManagerTest {
+    private PickerSyncLockManager mSyncLockManager;
+
+    @Before
+    public void setup() {
+        mSyncLockManager = new PickerSyncLockManager();
+    }
+
+    @Test
+    public void testLockIsCloseable() {
+        try (CloseableReentrantLock lock = mSyncLockManager.lock(CLOUD_SYNC_LOCK)) {
+            // Assert that the lock is help by the current thread.
+            assertThat(lock.isHeldByCurrentThread()).isTrue();
+            assertThat(lock.getHoldCount()).isEqualTo(1);
+
+            try (CloseableReentrantLock lockInLock = mSyncLockManager.lock(CLOUD_SYNC_LOCK)) {
+                // Assert that this is a reentrant lock and the thread was able to increment hold
+                // count.
+                assertThat(lock.isHeldByCurrentThread()).isTrue();
+                assertThat(lock).isEqualTo(lockInLock);
+                assertThat(lock.getHoldCount()).isEqualTo(2);
+            }
+
+            // Assert that the hold count has been decremented.
+            assertThat(lock.isHeldByCurrentThread()).isTrue();
+            assertThat(lock.getHoldCount()).isEqualTo(1);
+            assertThat(lock).isEqualTo(mSyncLockManager.getLock(CLOUD_SYNC_LOCK));
+        }
+
+        assertThat(mSyncLockManager.getLock(CLOUD_SYNC_LOCK).isHeldByCurrentThread()).isFalse();
+    }
+
+    @Test
+    public void testLockWithTimeoutIsCloseable() throws UnableToAcquireLockException {
+        try (CloseableReentrantLock lock = mSyncLockManager.tryLock(CLOUD_ALBUM_SYNC_LOCK)) {
+            // Assert that the lock is help by the current thread.
+            assertThat(lock.isHeldByCurrentThread()).isTrue();
+            assertThat(lock.getHoldCount()).isEqualTo(1);
+
+            try (CloseableReentrantLock lockInLock =
+                         mSyncLockManager.tryLock(CLOUD_ALBUM_SYNC_LOCK)) {
+                // Assert that this is a reentrant lock and the thread was able to increment hold
+                // count.
+                assertThat(lock.isHeldByCurrentThread()).isTrue();
+                assertThat(lock).isEqualTo(lockInLock);
+                assertThat(lock.getHoldCount()).isEqualTo(2);
+            }
+
+            // Assert that the hold count has been decremented.
+            assertThat(lock.isHeldByCurrentThread()).isTrue();
+            assertThat(lock.getHoldCount()).isEqualTo(1);
+            assertThat(lock).isEqualTo(mSyncLockManager.getLock(CLOUD_ALBUM_SYNC_LOCK));
+        }
+
+        assertThat(mSyncLockManager.getLock(CLOUD_ALBUM_SYNC_LOCK).isHeldByCurrentThread())
+                .isFalse();
+    }
+
+    @Test
+    public void testLockTimeout() throws InterruptedException, TimeoutException {
+        CloseableReentrantLock lock = new CloseableReentrantLock("testLock");
+        try (CloseableReentrantLock ignored =
+                     mSyncLockManager.tryLock(lock, 5, TimeUnit.MILLISECONDS)) {
+            // it is expected that the lock is held by the current thread within timeout.
+        } catch (UnableToAcquireLockException e) {
+            throw new AssertionError(
+                    "Should be able to acquire the lock since no other thread holds it.", e);
+        }
+
+        HandlerThread thread = new HandlerThread("PickerSyncLockTestThread",
+                android.os.Process.THREAD_PRIORITY_BACKGROUND);
+        thread.start();
+        Handler handler = new Handler(thread.getLooper());
+        acquireLock(handler, lock);
+
+        try (CloseableReentrantLock ignored =
+                     mSyncLockManager.tryLock(lock, 5, TimeUnit.MILLISECONDS)) {
+            throw new AssertionError("The lock should not be acquired by this thread because "
+                    + "it is already held by a different thread");
+        } catch (UnableToAcquireLockException e) {
+            // The expectation is that lock is not acquired within the timeout and
+            // UnableToAcquireLockException is thrown.
+        }
+
+        releaseLock(handler, lock);
+        thread.quitSafely();
+    }
+
+    private void acquireLock(Handler handler, CloseableReentrantLock lock)
+            throws InterruptedException, TimeoutException {
+        handler.post(() -> lock.lock());
+        waitForHandler(handler);
+    }
+
+    private void releaseLock(Handler handler, CloseableReentrantLock lock)
+            throws InterruptedException, TimeoutException {
+        handler.post(() -> lock.unlock());
+        waitForHandler(handler);
+    }
+
+    private void waitForHandler(Handler handler) throws InterruptedException, TimeoutException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        handler.post(() -> latch.countDown());
+        final boolean success = latch.await(30, TimeUnit.SECONDS);
+        if (!success) {
+            throw new TimeoutException("Could not wait for handler task to finish");
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java b/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java
new file mode 100644
index 0000000..ae7221c
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java
@@ -0,0 +1,439 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_AND_CLOUD;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SYNC_SOURCE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import androidx.work.ExistingPeriodicWorkPolicy;
+import androidx.work.ExistingWorkPolicy;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.Operation;
+import androidx.work.PeriodicWorkRequest;
+import androidx.work.WorkContinuation;
+import androidx.work.WorkInfo;
+import androidx.work.WorkManager;
+import androidx.work.WorkRequest;
+
+import com.android.modules.utils.BackgroundThread;
+import com.android.providers.media.TestConfigStore;
+import com.android.providers.media.photopicker.PickerSyncController;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class PickerSyncManagerTest {
+    private PickerSyncManager mPickerSyncManager;
+    private TestConfigStore mConfigStore;
+    @Mock
+    private WorkManager mMockWorkManager;
+    @Mock
+    private Operation mMockOperation;
+    @Mock
+    private WorkContinuation mMockWorkContinuation;
+    @Mock
+    private ListenableFuture<Operation.State.SUCCESS> mMockFuture;
+    @Mock
+    private Context mMockContext;
+    @Mock
+    private Resources mResources;
+    @Captor
+    ArgumentCaptor<PeriodicWorkRequest> mPeriodicWorkRequestArgumentCaptor;
+    @Captor
+    ArgumentCaptor<OneTimeWorkRequest> mOneTimeWorkRequestArgumentCaptor;
+    @Captor
+    ArgumentCaptor<List<OneTimeWorkRequest>> mOneTimeWorkRequestListArgumentCaptor;
+
+    @Before
+    public void setUp() {
+        initMocks(this);
+        doReturn(mResources).when(mMockContext).getResources();
+        mConfigStore = new TestConfigStore();
+        mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(
+                "com.hooli.super.awesome.cloudpicker");
+    }
+
+    @Test
+    public void testSchedulePeriodicSyncs() {
+        setupPickerSyncManager(/* schedulePeriodicSyncs */ true);
+
+        verify(mMockWorkManager, times(2))
+                .enqueueUniquePeriodicWork(anyString(),
+                        any(),
+                        mPeriodicWorkRequestArgumentCaptor.capture());
+
+        final PeriodicWorkRequest periodicWorkRequest =
+                mPeriodicWorkRequestArgumentCaptor.getAllValues().get(0);
+        assertThat(periodicWorkRequest.getWorkSpec().workerClassName)
+                .isEqualTo(ProactiveSyncWorker.class.getName());
+        assertThat(periodicWorkRequest.getWorkSpec().expedited).isFalse();
+        assertThat(periodicWorkRequest.getWorkSpec().isPeriodic()).isTrue();
+        assertThat(periodicWorkRequest.getWorkSpec().id).isNotNull();
+        assertThat(periodicWorkRequest.getWorkSpec().constraints.requiresCharging()).isTrue();
+        assertThat(periodicWorkRequest.getWorkSpec().constraints.requiresDeviceIdle()).isTrue();
+        assertThat(periodicWorkRequest.getWorkSpec().input
+                .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+                .isEqualTo(SYNC_LOCAL_AND_CLOUD);
+
+        final PeriodicWorkRequest periodicResetRequest =
+                mPeriodicWorkRequestArgumentCaptor.getAllValues().get(1);
+        assertThat(periodicResetRequest.getWorkSpec().workerClassName)
+                .isEqualTo(MediaResetWorker.class.getName());
+        assertThat(periodicResetRequest.getWorkSpec().expedited).isFalse();
+        assertThat(periodicResetRequest.getWorkSpec().isPeriodic()).isTrue();
+        assertThat(periodicResetRequest.getWorkSpec().id).isNotNull();
+        assertThat(periodicResetRequest.getWorkSpec().constraints.requiresCharging()).isTrue();
+        assertThat(periodicResetRequest.getWorkSpec().constraints.requiresDeviceIdle()).isTrue();
+        assertThat(periodicResetRequest.getWorkSpec().input
+                .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+                .isEqualTo(SYNC_LOCAL_AND_CLOUD);
+    }
+
+    @Test
+    public void testPeriodicWorkIsScheduledOnDeviceConfigChanges() {
+
+        mConfigStore.disableCloudMediaFeature();
+
+
+        setupPickerSyncManager(true);
+
+        // Ensure no syncs have been scheduled yet.
+        verify(mMockWorkManager, times(0))
+                .enqueueUniquePeriodicWork(anyString(),
+                        any(),
+                        mPeriodicWorkRequestArgumentCaptor.capture());
+
+        mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(
+                "com.hooli.some.cloud.provider");
+
+        waitForIdle();
+
+        // Ensure the syncs are now scheduled.
+        verify(mMockWorkManager, times(2))
+                .enqueueUniquePeriodicWork(anyString(),
+                        any(),
+                        mPeriodicWorkRequestArgumentCaptor.capture());
+
+        final PeriodicWorkRequest periodicWorkRequest =
+                mPeriodicWorkRequestArgumentCaptor.getAllValues().get(0);
+        assertThat(periodicWorkRequest.getWorkSpec().workerClassName)
+                .isEqualTo(ProactiveSyncWorker.class.getName());
+        assertThat(periodicWorkRequest.getWorkSpec().expedited).isFalse();
+        assertThat(periodicWorkRequest.getWorkSpec().isPeriodic()).isTrue();
+        assertThat(periodicWorkRequest.getWorkSpec().id).isNotNull();
+        assertThat(periodicWorkRequest.getWorkSpec().constraints.requiresCharging()).isTrue();
+        assertThat(periodicWorkRequest.getWorkSpec().constraints.requiresDeviceIdle()).isTrue();
+        assertThat(periodicWorkRequest.getWorkSpec().input
+                .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+                .isEqualTo(SYNC_LOCAL_AND_CLOUD);
+
+        final PeriodicWorkRequest periodicResetRequest =
+                mPeriodicWorkRequestArgumentCaptor.getAllValues().get(1);
+        assertThat(periodicResetRequest.getWorkSpec().workerClassName)
+                .isEqualTo(MediaResetWorker.class.getName());
+        assertThat(periodicResetRequest.getWorkSpec().expedited).isFalse();
+        assertThat(periodicResetRequest.getWorkSpec().isPeriodic()).isTrue();
+        assertThat(periodicResetRequest.getWorkSpec().id).isNotNull();
+        assertThat(periodicResetRequest.getWorkSpec().constraints.requiresCharging()).isTrue();
+        assertThat(periodicResetRequest.getWorkSpec().constraints.requiresDeviceIdle()).isTrue();
+        assertThat(periodicResetRequest.getWorkSpec().input
+                .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+                .isEqualTo(SYNC_LOCAL_AND_CLOUD);
+
+        clearInvocations(mMockWorkManager);
+
+        mConfigStore.disableCloudMediaFeature();
+        waitForIdle();
+
+        verify(mMockWorkManager, times(2)).cancelUniqueWork(anyString());
+    }
+
+    @Test
+    public void testAdhocProactiveSyncLocalOnly() {
+        setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+
+        mPickerSyncManager.syncMediaProactively(/* localOnly */ true);
+        verify(mMockWorkManager, times(1))
+                .enqueueUniqueWork(anyString(),
+                        any(),
+                        mOneTimeWorkRequestArgumentCaptor.capture());
+
+        final OneTimeWorkRequest workRequest = mOneTimeWorkRequestArgumentCaptor.getValue();
+        assertThat(workRequest.getWorkSpec().workerClassName)
+                .isEqualTo(ProactiveSyncWorker.class.getName());
+        assertThat(workRequest.getWorkSpec().expedited).isFalse();
+        assertThat(workRequest.getWorkSpec().isPeriodic()).isFalse();
+        assertThat(workRequest.getWorkSpec().id).isNotNull();
+        assertThat(workRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isTrue();
+        assertThat(workRequest.getWorkSpec().input
+                .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+                .isEqualTo(SYNC_LOCAL_ONLY);
+    }
+
+    @Test
+    public void testAdhocProactiveSync() {
+        setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+
+        mPickerSyncManager.syncMediaProactively(/* localOnly */ false);
+        verify(mMockWorkManager, times(1))
+                .enqueueUniqueWork(anyString(),
+                        any(),
+                        mOneTimeWorkRequestArgumentCaptor.capture());
+
+        final OneTimeWorkRequest workRequest = mOneTimeWorkRequestArgumentCaptor.getValue();
+        assertThat(workRequest.getWorkSpec().workerClassName)
+                .isEqualTo(ProactiveSyncWorker.class.getName());
+        assertThat(workRequest.getWorkSpec().expedited).isFalse();
+        assertThat(workRequest.getWorkSpec().isPeriodic()).isFalse();
+        assertThat(workRequest.getWorkSpec().id).isNotNull();
+        assertThat(workRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isTrue();
+        assertThat(workRequest.getWorkSpec().input
+                .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+                .isEqualTo(SYNC_LOCAL_AND_CLOUD);
+    }
+
+    @Test
+    public void testImmediateLocalSync() {
+        setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+
+        mPickerSyncManager.syncMediaImmediately(true);
+        verify(mMockWorkManager, times(1))
+                .enqueueUniqueWork(anyString(), any(), mOneTimeWorkRequestArgumentCaptor.capture());
+
+        final OneTimeWorkRequest workRequest = mOneTimeWorkRequestArgumentCaptor.getValue();
+        assertThat(workRequest.getWorkSpec().workerClassName)
+                .isEqualTo(ImmediateSyncWorker.class.getName());
+        assertThat(workRequest.getWorkSpec().expedited).isTrue();
+        assertThat(workRequest.getWorkSpec().isPeriodic()).isFalse();
+        assertThat(workRequest.getWorkSpec().id).isNotNull();
+        assertThat(workRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse();
+        assertThat(workRequest.getWorkSpec().input
+                .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+                .isEqualTo(SYNC_LOCAL_ONLY);
+    }
+
+    @Test
+    public void testImmediateCloudSync() {
+        setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+
+        mPickerSyncManager.syncMediaImmediately(false);
+        verify(mMockWorkManager, times(2))
+                .enqueueUniqueWork(anyString(), any(), mOneTimeWorkRequestArgumentCaptor.capture());
+
+        final List<OneTimeWorkRequest> workRequestList =
+                mOneTimeWorkRequestArgumentCaptor.getAllValues();
+        assertThat(workRequestList.size()).isEqualTo(2);
+
+        WorkRequest localWorkRequest = workRequestList.get(0);
+        assertThat(localWorkRequest.getWorkSpec().workerClassName)
+                .isEqualTo(ImmediateSyncWorker.class.getName());
+        assertThat(localWorkRequest.getWorkSpec().expedited).isTrue();
+        assertThat(localWorkRequest.getWorkSpec().isPeriodic()).isFalse();
+        assertThat(localWorkRequest.getWorkSpec().id).isNotNull();
+        assertThat(localWorkRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse();
+        assertThat(localWorkRequest.getWorkSpec().input
+                .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+                .isEqualTo(SYNC_LOCAL_ONLY);
+
+        WorkRequest cloudWorkRequest = workRequestList.get(1);
+        assertThat(cloudWorkRequest.getWorkSpec().workerClassName)
+                .isEqualTo(ImmediateSyncWorker.class.getName());
+        assertThat(cloudWorkRequest.getWorkSpec().expedited).isTrue();
+        assertThat(cloudWorkRequest.getWorkSpec().isPeriodic()).isFalse();
+        assertThat(cloudWorkRequest.getWorkSpec().id).isNotNull();
+        assertThat(cloudWorkRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse();
+        assertThat(cloudWorkRequest.getWorkSpec().input
+                .getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+                .isEqualTo(SYNC_CLOUD_ONLY);
+    }
+
+    @Test
+    public void testImmediateLocalAlbumSync() {
+        setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+
+        mPickerSyncManager.syncAlbumMediaForProviderImmediately(
+                "Not_null", PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY);
+        verify(mMockWorkManager, times(1))
+                .beginUniqueWork(
+                        anyString(),
+                        any(ExistingWorkPolicy.class),
+                        mOneTimeWorkRequestListArgumentCaptor.capture());
+        verify(mMockWorkContinuation, times(1))
+                .then(mOneTimeWorkRequestListArgumentCaptor.capture());
+        verify(mMockWorkContinuation).enqueue();
+
+        final OneTimeWorkRequest resetRequest =
+                mOneTimeWorkRequestListArgumentCaptor.getAllValues().get(0).get(0);
+        assertThat(resetRequest.getWorkSpec().workerClassName)
+                .isEqualTo(MediaResetWorker.class.getName());
+        assertThat(resetRequest.getWorkSpec().expedited).isTrue();
+        assertThat(resetRequest.getWorkSpec().isPeriodic()).isFalse();
+        assertThat(resetRequest.getWorkSpec().id).isNotNull();
+        assertThat(resetRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse();
+        assertThat(resetRequest.getWorkSpec().input.getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+                .isEqualTo(SYNC_LOCAL_ONLY);
+
+        final OneTimeWorkRequest workRequest =
+                mOneTimeWorkRequestListArgumentCaptor.getAllValues().get(1).get(0);
+        assertThat(workRequest.getWorkSpec().workerClassName)
+                .isEqualTo(ImmediateAlbumSyncWorker.class.getName());
+        assertThat(workRequest.getWorkSpec().expedited).isTrue();
+        assertThat(workRequest.getWorkSpec().isPeriodic()).isFalse();
+        assertThat(workRequest.getWorkSpec().id).isNotNull();
+        assertThat(workRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse();
+        assertThat(workRequest.getWorkSpec().input.getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+                .isEqualTo(SYNC_LOCAL_ONLY);
+    }
+
+    @Test
+    public void testImmediateCloudAlbumSync() {
+        setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+
+        mPickerSyncManager.syncAlbumMediaForProviderImmediately(
+                "Not_null", "com.hooli.cloudpicker");
+        verify(mMockWorkManager, times(1))
+                .beginUniqueWork(
+                        anyString(),
+                        any(ExistingWorkPolicy.class),
+                        mOneTimeWorkRequestListArgumentCaptor.capture());
+        verify(mMockWorkContinuation, times(1))
+                .then(mOneTimeWorkRequestListArgumentCaptor.capture());
+        verify(mMockWorkContinuation).enqueue();
+
+        final OneTimeWorkRequest resetRequest =
+                mOneTimeWorkRequestListArgumentCaptor.getAllValues().get(0).get(0);
+        assertThat(resetRequest.getWorkSpec().workerClassName)
+                .isEqualTo(MediaResetWorker.class.getName());
+        assertThat(resetRequest.getWorkSpec().expedited).isTrue();
+        assertThat(resetRequest.getWorkSpec().isPeriodic()).isFalse();
+        assertThat(resetRequest.getWorkSpec().id).isNotNull();
+        assertThat(resetRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse();
+        assertThat(resetRequest.getWorkSpec().input.getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+                .isEqualTo(SYNC_CLOUD_ONLY);
+
+        final OneTimeWorkRequest workRequest =
+                mOneTimeWorkRequestListArgumentCaptor.getAllValues().get(1).get(0);
+        assertThat(workRequest.getWorkSpec().workerClassName)
+                .isEqualTo(ImmediateAlbumSyncWorker.class.getName());
+        assertThat(workRequest.getWorkSpec().expedited).isTrue();
+        assertThat(workRequest.getWorkSpec().isPeriodic()).isFalse();
+        assertThat(workRequest.getWorkSpec().id).isNotNull();
+        assertThat(workRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse();
+        assertThat(workRequest.getWorkSpec().input.getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
+                .isEqualTo(SYNC_CLOUD_ONLY);
+    }
+
+    @Test
+    public void testUniqueWorkStatusForPendingWork() {
+        setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+        final String workName = "testWorkName";
+        final SettableFuture<List<WorkInfo>> future = SettableFuture.create();
+        final List<WorkInfo> futureResult = new ArrayList<>();
+        futureResult.add(getWorkInfo(WorkInfo.State.SUCCEEDED));
+        futureResult.add(getWorkInfo(WorkInfo.State.ENQUEUED));
+        future.set(futureResult);
+        doReturn(future).when(mMockWorkManager)
+                .getWorkInfosForUniqueWork(workName);
+
+        assertThat(mPickerSyncManager.isUniqueWorkPending(workName)).isTrue();
+    }
+
+    @Test
+    public void testUniqueWorkStatusForCompletedWork() {
+        setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+        final String workName = "testWorkName";
+        final SettableFuture<List<WorkInfo>> future = SettableFuture.create();
+        final List<WorkInfo> futureResult = new ArrayList<>();
+        futureResult.add(getWorkInfo(WorkInfo.State.SUCCEEDED));
+        futureResult.add(getWorkInfo(WorkInfo.State.FAILED));
+        futureResult.add(getWorkInfo(WorkInfo.State.CANCELLED));
+        future.set(futureResult);
+        doReturn(future).when(mMockWorkManager)
+                .getWorkInfosForUniqueWork(workName);
+
+        assertThat(mPickerSyncManager.isUniqueWorkPending(workName)).isFalse();
+    }
+
+    private WorkInfo getWorkInfo(WorkInfo.State state) {
+        return new WorkInfo(UUID.randomUUID(), state, new HashSet<>());
+    }
+
+    private void setupPickerSyncManager(boolean schedulePeriodicSyncs) {
+        doReturn(mMockOperation).when(mMockWorkManager)
+                .enqueueUniquePeriodicWork(anyString(),
+                        any(ExistingPeriodicWorkPolicy.class),
+                        any(PeriodicWorkRequest.class));
+        doReturn(mMockOperation).when(mMockWorkManager)
+                .enqueueUniqueWork(anyString(),
+                        any(ExistingWorkPolicy.class),
+                        any(OneTimeWorkRequest.class));
+        doReturn(mMockWorkContinuation)
+                .when(mMockWorkManager)
+                .beginUniqueWork(
+                        anyString(), any(ExistingWorkPolicy.class), any(List.class));
+        // Handle .then chaining
+        doReturn(mMockWorkContinuation)
+                .when(mMockWorkContinuation)
+                .then(any(List.class));
+        doReturn(mMockOperation).when(mMockWorkContinuation).enqueue();
+        doReturn(mMockFuture).when(mMockOperation).getResult();
+
+        mPickerSyncManager =
+                new PickerSyncManager(mMockWorkManager, mMockContext,
+                        mConfigStore, schedulePeriodicSyncs);
+    }
+
+    private static void waitForIdle() {
+        final CountDownLatch latch = new CountDownLatch(1);
+        BackgroundThread.getExecutor().execute(latch::countDown);
+        try {
+            latch.await(30, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            throw new IllegalStateException(e);
+        }
+
+    }
+
+}
diff --git a/tests/src/com/android/providers/media/photopicker/sync/ProactiveSyncWorkerTest.java b/tests/src/com/android/providers/media/photopicker/sync/ProactiveSyncWorkerTest.java
new file mode 100644
index 0000000..0039635
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/sync/ProactiveSyncWorkerTest.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+import static com.android.providers.media.photopicker.sync.PickerSyncNotificationHelper.NOTIFICATION_CHANNEL_ID;
+import static com.android.providers.media.photopicker.sync.PickerSyncNotificationHelper.NOTIFICATION_ID;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.buildTestWorker;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.getCloudSyncInputData;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.getLocalAndCloudSyncInputData;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.getLocalSyncInputData;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.initializeTestWorkManager;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.CancellationSignal;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.work.ForegroundInfo;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkInfo;
+import androidx.work.WorkManager;
+
+import com.android.providers.media.photopicker.PickerSyncController;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.concurrent.ExecutionException;
+
+// TODO enable tests in Android R after fixing b/293390235
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public class ProactiveSyncWorkerTest {
+    @Mock
+    private PickerSyncController mMockPickerSyncController;
+    @Mock
+    private SyncTracker mMockLocalSyncTracker;
+    @Mock
+    private SyncTracker mMockCloudSyncTracker;
+    private Context mContext;
+
+    @Before
+    public void setup() {
+        initMocks(this);
+
+        // Inject mock trackers
+        SyncTrackerRegistry.setLocalSyncTracker(mMockLocalSyncTracker);
+        SyncTrackerRegistry.setCloudSyncTracker(mMockCloudSyncTracker);
+
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        initializeTestWorkManager(mContext);
+    }
+
+    @After
+    public void teardown() {
+        // Reset mock trackers
+        SyncTrackerRegistry.setLocalSyncTracker(new SyncTracker());
+        SyncTrackerRegistry.setCloudSyncTracker(new SyncTracker());
+    }
+
+    @Test
+    public void testLocalProactiveSync() throws ExecutionException, InterruptedException {
+        // Setup
+        PickerSyncController.setInstance(mMockPickerSyncController);
+        final OneTimeWorkRequest request  =
+                new OneTimeWorkRequest.Builder(ProactiveSyncWorker.class)
+                        .setInputData(getLocalSyncInputData())
+                        .build();
+
+        // Test run
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        workManager.enqueue(request).getResult().get();
+
+        // Verify
+        final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+        assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 1))
+                .syncAllMediaFromLocalProvider(any(CancellationSignal.class));
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0))
+                .syncAllMediaFromCloudProvider(any(CancellationSignal.class));
+
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .createSyncFuture(any());
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testCloudProactiveSync() throws ExecutionException, InterruptedException {
+        // Setup
+        PickerSyncController.setInstance(mMockPickerSyncController);
+        final OneTimeWorkRequest request  =
+                new OneTimeWorkRequest.Builder(ProactiveSyncWorker.class)
+                        .setInputData(getCloudSyncInputData())
+                        .build();
+
+        // Test run
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        workManager.enqueue(request).getResult().get();
+
+        // Verify
+        final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+        assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0))
+                .syncAllMediaFromLocalProvider(any(CancellationSignal.class));
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 1))
+                .syncAllMediaFromCloudProvider(any(CancellationSignal.class));
+
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .markSyncCompleted(any());
+
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .createSyncFuture(any());
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testLocalAndCloudProactiveSync() throws ExecutionException, InterruptedException {
+        // Setup
+        PickerSyncController.setInstance(mMockPickerSyncController);
+        final OneTimeWorkRequest request  =
+                new OneTimeWorkRequest.Builder(ProactiveSyncWorker.class)
+                        .setInputData(getLocalAndCloudSyncInputData())
+                        .build();
+
+        // Test run
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        workManager.enqueue(request).getResult().get();
+
+        // Verify
+        final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+        assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 1))
+                .syncAllMediaFromLocalProvider(any(CancellationSignal.class));
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 1))
+                .syncAllMediaFromCloudProvider(any(CancellationSignal.class));
+
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .createSyncFuture(any());
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .createSyncFuture(any());
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testProactiveSyncFailure() throws ExecutionException, InterruptedException {
+        // Setup
+        PickerSyncController.setInstance(null);
+        final OneTimeWorkRequest request  =
+                new OneTimeWorkRequest.Builder(ProactiveSyncWorker.class)
+                        .setInputData(getLocalAndCloudSyncInputData())
+                        .build();
+
+        // Test run
+        final WorkManager workManager = WorkManager.getInstance(mContext);
+        workManager.enqueue(request).getResult().get();
+
+        // Verify
+        final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+        assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0))
+                .syncAllMediaFromLocalProvider(any(CancellationSignal.class));
+        verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0))
+                .syncAllMediaFromCloudProvider(any(CancellationSignal.class));
+
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .createSyncFuture(any());
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testProactiveSyncWorkerOnStopped() {
+        // Setup
+        final ProactiveSyncWorker proactiveSyncWorker =
+                buildTestWorker(mContext, ProactiveSyncWorker.class);
+
+        // Test onStopped
+        proactiveSyncWorker.onStopped();
+
+        // Verify
+        assertThat(proactiveSyncWorker.getCancellationSignal().isCanceled()).isTrue();
+
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockLocalSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+                .createSyncFuture(any());
+        verify(mMockCloudSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+                .markSyncCompleted(any());
+    }
+
+    @Test
+    public void testGetForegroundInfo() {
+        final ForegroundInfo foregroundInfo =
+                buildTestWorker(mContext, ProactiveSyncWorker.class).getForegroundInfo();
+
+        assertThat(foregroundInfo.getNotificationId()).isEqualTo(NOTIFICATION_ID);
+        assertThat(foregroundInfo.getNotification().getChannelId())
+                .isEqualTo(NOTIFICATION_CHANNEL_ID);
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/sync/SyncTrackerTests.java b/tests/src/com/android/providers/media/photopicker/sync/SyncTrackerTests.java
new file mode 100644
index 0000000..ed1d117
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/sync/SyncTrackerTests.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+import java.util.Collection;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class SyncTrackerTests {
+    private static final UUID PLACEHOLDER_UUID = UUID.randomUUID();
+
+    @Test
+    public void testCreateSyncFuture() {
+        SyncTracker syncTracker = new SyncTracker();
+        syncTracker.createSyncFuture(PLACEHOLDER_UUID);
+
+        Collection<CompletableFuture<Object>> futures = syncTracker.pendingSyncFutures();
+        assertThat(futures.size()).isEqualTo(1);
+    }
+
+    @Test
+    public void testMarkSyncAsComplete() {
+        SyncTracker syncTracker = new SyncTracker();
+        syncTracker.createSyncFuture(PLACEHOLDER_UUID);
+        syncTracker.markSyncCompleted(PLACEHOLDER_UUID);
+
+        Collection<CompletableFuture<Object>> futures = syncTracker.pendingSyncFutures();
+        assertThat(futures.size()).isEqualTo(0);
+    }
+
+    @Test
+    public void testCompleteOnTimeoutSyncFuture()
+            throws InterruptedException, ExecutionException, TimeoutException {
+        SyncTracker syncTracker = new SyncTracker();
+        syncTracker.createSyncFuture(PLACEHOLDER_UUID, 100L, TimeUnit.MILLISECONDS);
+
+        Collection<CompletableFuture<Object>> pendingSyncFutures = syncTracker.pendingSyncFutures();
+        for (CompletableFuture<Object> future : pendingSyncFutures) {
+            future.get(200, TimeUnit.MILLISECONDS);
+        }
+        assertThat(syncTracker.pendingSyncFutures().size()).isEqualTo(0);
+    }
+
+    @Test
+    public void getSyncTrackerFromRegistry() {
+        assertThat(SyncTrackerRegistry.getSyncTracker(/* isLocal */ true))
+                .isEqualTo(SyncTrackerRegistry.getLocalSyncTracker());
+        assertThat(SyncTrackerRegistry.getSyncTracker(/* isLocal */ false))
+                .isEqualTo(SyncTrackerRegistry.getCloudSyncTracker());
+        assertThat(SyncTrackerRegistry.getAlbumSyncTracker(/* isLocal */ true))
+                .isEqualTo(SyncTrackerRegistry.getLocalAlbumSyncTracker());
+        assertThat(SyncTrackerRegistry.getAlbumSyncTracker(/* isLocal */ false))
+                .isEqualTo(SyncTrackerRegistry.getCloudAlbumSyncTracker());
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/sync/SyncWorkerTestUtils.java b/tests/src/com/android/providers/media/photopicker/sync/SyncWorkerTestUtils.java
new file mode 100644
index 0000000..7eb9326
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/sync/SyncWorkerTestUtils.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_AND_CLOUD;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_RESET_ALBUM;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_ALBUM_ID;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_AUTHORITY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_RESET_TYPE;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SYNC_SOURCE;
+
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.work.Configuration;
+import androidx.work.Data;
+import androidx.work.Worker;
+import androidx.work.testing.SynchronousExecutor;
+import androidx.work.testing.TestWorkerBuilder;
+import androidx.work.testing.WorkManagerTestInitHelper;
+
+import java.util.Map;
+import java.util.Objects;
+
+public class SyncWorkerTestUtils {
+    public static void initializeTestWorkManager(@NonNull Context context) {
+        Configuration workManagerConfig = new Configuration.Builder()
+                .setMinimumLoggingLevel(Log.DEBUG)
+                .setExecutor(new SynchronousExecutor()) // This runs WM synchronously.
+                .build();
+
+        WorkManagerTestInitHelper.initializeTestWorkManager(
+                context, workManagerConfig);
+    }
+
+    @NonNull
+    public static Data getLocalSyncInputData() {
+        return new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_ONLY));
+    }
+
+    @NonNull
+    public static Data getLocalAlbumSyncInputData(@NonNull String albumId) {
+        Objects.requireNonNull(albumId);
+        return new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_ONLY,
+                SYNC_WORKER_INPUT_ALBUM_ID, albumId));
+    }
+
+    @NonNull
+    public static Data getCloudSyncInputData() {
+        return new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_CLOUD_ONLY));
+    }
+
+    @NonNull
+    public static Data getAlbumResetInputData(
+            @NonNull String albumId, String authority, boolean isLocal) {
+        Objects.requireNonNull(albumId);
+        Objects.requireNonNull(authority);
+        return new Data(
+                Map.of(
+                        SYNC_WORKER_INPUT_AUTHORITY, authority,
+                        SYNC_WORKER_INPUT_SYNC_SOURCE, isLocal ? SYNC_LOCAL_ONLY : SYNC_CLOUD_ONLY,
+                        SYNC_WORKER_INPUT_RESET_TYPE, SYNC_RESET_ALBUM,
+                        SYNC_WORKER_INPUT_ALBUM_ID, albumId));
+    }
+
+    @NonNull
+    public static Data getCloudAlbumSyncInputData(@NonNull String albumId) {
+        Objects.requireNonNull(albumId);
+        return new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_CLOUD_ONLY,
+                SYNC_WORKER_INPUT_ALBUM_ID, albumId));
+    }
+
+    @NonNull
+    public static Data getLocalAndCloudSyncInputData() {
+        return new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_AND_CLOUD));
+    }
+
+    public static Data getLocalAndCloudAlbumSyncInputData(@NonNull String albumId) {
+        Objects.requireNonNull(albumId);
+        return new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_LOCAL_AND_CLOUD,
+                SYNC_WORKER_INPUT_ALBUM_ID, albumId));
+    }
+
+    static <W extends Worker> W buildTestWorker(@NonNull Context context,
+            @NonNull Class<W> workerClass) {
+        return TestWorkerBuilder.from(context, workerClass)
+                .setInputData(getLocalAndCloudSyncInputData())
+                .build();
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/ui/PhotosTabAdapterTest.java b/tests/src/com/android/providers/media/photopicker/ui/PhotosTabAdapterTest.java
index ef1537b..169e86a 100644
--- a/tests/src/com/android/providers/media/photopicker/ui/PhotosTabAdapterTest.java
+++ b/tests/src/com/android/providers/media/photopicker/ui/PhotosTabAdapterTest.java
@@ -33,6 +33,8 @@
 import com.android.providers.media.photopicker.data.model.Item;
 import com.android.providers.media.photopicker.ui.PhotosTabAdapter.DateHeader;
 
+import com.bumptech.glide.util.ViewPreloadSizeProvider;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -176,8 +178,8 @@
 
     private static PhotosTabAdapter createAdapter(boolean shouldShowRecentSection) {
         return new PhotosTabAdapter(/* showRecentSection */ shouldShowRecentSection,
-                mock(Selection.class), mock(ImageLoader.class), mock(View.OnClickListener.class),
-                mock(View.OnLongClickListener.class), mock(LifecycleOwner.class),
+                mock(Selection.class), mock(ImageLoader.class),
+                mock(PhotosTabAdapter.OnMediaItemClickListener.class), mock(LifecycleOwner.class),
                 /* cloudMediaProviderAppTitle */ mock(LiveData.class),
                 /* cloudMediaAccountName */ mock(LiveData.class),
                 /* shouldShowChooseAppBanner */ mock(LiveData.class),
@@ -190,7 +192,9 @@
                 /* onAccountUpdatedBannerEventListener */
                 mock(TabAdapter.OnBannerEventListener.class),
                 /* onChooseAccountBannerEventListener */
-                mock(TabAdapter.OnBannerEventListener.class));
+                mock(TabAdapter.OnBannerEventListener.class),
+                /* onHoverListener */ mock(View.OnHoverListener.class),
+                /* mPreloadSizeProvider */ mock(ViewPreloadSizeProvider.class));
     }
 
     private static Item generateFakeImageItem(String id) {
diff --git a/tests/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaViewModelTest.java b/tests/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaViewModelTest.java
index fec1195..a178e76 100644
--- a/tests/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaViewModelTest.java
+++ b/tests/src/com/android/providers/media/photopicker/ui/settings/SettingsCloudMediaViewModelTest.java
@@ -60,9 +60,12 @@
 
 @RunWith(AndroidJUnit4.class)
 public class SettingsCloudMediaViewModelTest {
+    private static final String PKG1 = "com.providers.test1";
+    private static final String PKG2 = "com.providers.test2";
     private static final List<String> sProviderAuthorities =
-            List.of("cloud_provider_1", "cloud_provider_2");
+            List.of(PKG1 + ".cloud_provider_1", PKG2 + ".cloud_provider_2");
     private static final List<ResolveInfo> sAvailableProviders = getAvailableProviders();
+    private static final List<String> sAllowlistedPackages = List.of(PKG1);
 
     @Mock
     private ConfigStore mConfigStore;
@@ -163,11 +166,44 @@
                 .call(eq(SET_CLOUD_PROVIDER_CALL), any(), any());
     }
 
+    @Test
+    public void testLoadDataWithAllowListedProviders() throws RemoteException {
+        final String expectedCloudProvider = sProviderAuthorities.get(0);
+        setUpCurrentCloudProvider(expectedCloudProvider);
+        setUpAvailableCloudProviders(sAvailableProviders);
+        setUpAllowedCloudPackages(sAllowlistedPackages);
+
+        mCloudMediaViewModel.loadData(mConfigStore);
+
+        // Verify cloud provider options
+        final List<CloudMediaProviderOption> providerOptions =
+                mCloudMediaViewModel.getProviderOptions();
+        assertThat(providerOptions.size()).isEqualTo(sAllowlistedPackages.size() + 1);
+        for (int i = 0; i < sAllowlistedPackages.size(); i++) {
+            final int lastDotIndex = providerOptions.get(i).getKey().lastIndexOf('.');
+            final String providerOptionsPackage = providerOptions.get(i).getKey().substring(0,
+                    lastDotIndex);
+            assertThat(sAllowlistedPackages).contains(providerOptionsPackage);
+        }
+        assertThat(providerOptions.get(providerOptions.size() - 1).getKey())
+                .isEqualTo(SettingsCloudMediaViewModel.NONE_PREF_KEY);
+
+        // Verify selected cloud provider
+        final String resultCloudProvider =
+                mCloudMediaViewModel.getSelectedProviderAuthority();
+        assertThat(resultCloudProvider).isEqualTo(expectedCloudProvider);
+    }
+
     private void setUpAvailableCloudProviders(@NonNull List<ResolveInfo> availableProviders) {
         doReturn(availableProviders).when(mPackageManager)
                 .queryIntentContentProvidersAsUser(any(), eq(0), any());
     }
 
+    private void setUpAllowedCloudPackages(@NonNull List<String> allowlistedPackages) {
+        doReturn(true).when(mConfigStore).shouldEnforceCloudProviderAllowlist();
+        doReturn(allowlistedPackages).when(mConfigStore).getAllowedCloudProviderPackages();
+    }
+
     private void setUpCurrentCloudProvider(@Nullable String providerAuthority)
             throws RemoteException {
         final Bundle result = new Bundle();
@@ -195,15 +231,17 @@
     @NonNull
     private static ProviderInfo createProviderInfo(@NonNull String authority) {
         final ProviderInfo providerInfo = new ProviderInfo();
+        final int lastDotIndex = authority.lastIndexOf('.');
         providerInfo.authority = authority;
         providerInfo.readPermission = MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION;
+        providerInfo.packageName = authority.substring(0, lastDotIndex);
         providerInfo.applicationInfo = createApplicationInfo(authority);
         return providerInfo;
     }
 
     @NonNull
     private static ApplicationInfo createApplicationInfo(@NonNull String authority) {
-        final ApplicationInfo applicationInfo =  new ApplicationInfo();
+        final ApplicationInfo applicationInfo = new ApplicationInfo();
         applicationInfo.packageName = authority;
         applicationInfo.uid = 0;
         return applicationInfo;
diff --git a/tests/src/com/android/providers/media/photopicker/util/CloudProviderUtilsTest.java b/tests/src/com/android/providers/media/photopicker/util/CloudProviderUtilsTest.java
new file mode 100644
index 0000000..8649d4d
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/util/CloudProviderUtilsTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 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.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.providers.media.IsolatedContext;
+import com.android.providers.media.TestConfigStore;
+import com.android.providers.media.cloudproviders.CloudProviderPrimary;
+import com.android.providers.media.cloudproviders.CloudProviderSecondary;
+import com.android.providers.media.cloudproviders.FlakyCloudProvider;
+import com.android.providers.media.photopicker.data.CloudProviderInfo;
+
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Set;
+
+
+public class CloudProviderUtilsTest {
+
+    @Test
+    public void getAllAvailableCloudProvidersTest() {
+        final Context context = InstrumentationRegistry.getTargetContext();
+        final Context isolatedContext =
+                new IsolatedContext(context, "CloudProviderUtilsTest", /*asFuseThread*/ false);
+        final Set<String> testCloudProviders = Set.of(
+                FlakyCloudProvider.AUTHORITY,
+                CloudProviderPrimary.AUTHORITY,
+                CloudProviderSecondary.AUTHORITY);
+        final TestConfigStore configStore = new TestConfigStore();
+        configStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(
+                testCloudProviders.toArray(new String[0]));
+
+        List<CloudProviderInfo> availableProviders =
+                CloudProviderUtils.getAllAvailableCloudProviders(isolatedContext, configStore);
+
+        assertThat(availableProviders.size()).isEqualTo(testCloudProviders.size());
+        for (CloudProviderInfo info : availableProviders) {
+            assertThat(testCloudProviders.contains(info.authority)).isTrue();
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/util/ThreadUtilsTest.java b/tests/src/com/android/providers/media/photopicker/util/ThreadUtilsTest.java
new file mode 100644
index 0000000..676d8e5
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/util/ThreadUtilsTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 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.util;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertThrows;
+
+import com.android.modules.utils.BackgroundThread;
+
+import org.junit.Test;
+
+public class ThreadUtilsTest {
+    @Test
+    public void testAssertMainThread_runOnMainThread() {
+        getInstrumentation().runOnMainSync(ThreadUtils::assertMainThread);
+    }
+
+    @Test
+    public void testAssertMainThread_runOnNonMainThread() {
+        BackgroundThread.getExecutor().execute(() ->
+                assertThrows(IllegalStateException.class, ThreadUtils::assertMainThread));
+    }
+
+    @Test
+    public void testAssertNonMainThread_runOnMainThread() {
+        getInstrumentation().runOnMainSync(() ->
+                assertThrows(IllegalStateException.class, ThreadUtils::assertNonMainThread));
+    }
+
+    @Test
+    public void testAssertNonMainThread_runOnNonMainThread() {
+        BackgroundThread.getExecutor().execute(ThreadUtils::assertNonMainThread);
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/viewmodel/BannerControllerTest.java b/tests/src/com/android/providers/media/photopicker/viewmodel/BannerControllerTest.java
index a351553..92894c2 100644
--- a/tests/src/com/android/providers/media/photopicker/viewmodel/BannerControllerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/viewmodel/BannerControllerTest.java
@@ -16,53 +16,63 @@
 
 package com.android.providers.media.photopicker.viewmodel;
 
-import static android.os.Process.myUserHandle;
+import static android.provider.MediaStore.AUTHORITY;
+import static android.provider.MediaStore.getCurrentCloudProvider;
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.providers.media.photopicker.util.CloudProviderUtils.persistSelectedProvider;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.os.RemoteException;
 
+import androidx.annotation.Nullable;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
+import com.android.providers.media.IsolatedContext;
+import com.android.providers.media.TestConfigStore;
+import com.android.providers.media.cloudproviders.FlakyCloudProvider;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 @RunWith(AndroidJUnit4.class)
 public class BannerControllerTest {
-    private BannerController mBannerController;
+    private static final Context sTargetContext = getInstrumentation().getTargetContext();
+    private static final String TEST_PACKAGE_NAME = "com.android.providers.media.tests";
     private static final String CMP_AUTHORITY = "authority";
     private static final String CMP_ACCOUNT_NAME = "account_name";
 
+    private IsolatedContext mIsolatedContext;
+    private ContentResolver mContentResolver;
+    private BannerController mBannerController;
+
     @Before
-    public void setUp() {
-        final Context context = getInstrumentation().getTargetContext();
+    public void setUp() throws RemoteException {
+        final TestConfigStore configStore = new TestConfigStore();
+        configStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(TEST_PACKAGE_NAME);
 
-        mBannerController = new BannerController(context, myUserHandle()) {
-            @Override
-            void updateCloudProviderDataFile() {
-                // No-op
-            }
+        mIsolatedContext = new IsolatedContext(sTargetContext, /* tag= */ "databases",
+                /* asFuseThread= */ false, sTargetContext.getUser(), configStore);
+        mContentResolver = mIsolatedContext.getContentResolver();
 
-            @Override
-            boolean areCloudProviderOptionsAvailable() {
-                return true;
-            }
-        };
+        setCloudProvider(/* authority= */ null);
+
+        mBannerController = BannerTestUtils.getTestBannerController(
+                mIsolatedContext, mIsolatedContext.getUser(), configStore);
 
         assertNull(mBannerController.getCloudMediaProviderAuthority());
         assertNull(mBannerController.getCloudMediaProviderLabel());
         assertNull(mBannerController.getCloudMediaProviderAccountName());
-
-        assertFalse(mBannerController.shouldShowCloudMediaAvailableBanner());
-        assertFalse(mBannerController.shouldShowAccountUpdatedBanner());
-        assertFalse(mBannerController.shouldShowChooseAccountBanner());
-        assertFalse(mBannerController.shouldShowChooseAppBanner());
     }
 
     @Test
@@ -146,4 +156,51 @@
         assertFalse(mBannerController.shouldShowChooseAccountBanner());
         assertFalse(mBannerController.shouldShowChooseAppBanner());
     }
+
+    @Test
+    public void testCloudProviderSlowQueryFallback() throws RemoteException {
+        setCloudProvider(FlakyCloudProvider.AUTHORITY);
+
+        // Test for fast query
+        mIsolatedContext.resetFlakyCloudProviderToNotFlakeInTheNextRequest();
+        mBannerController.onChangeCloudMediaInfo(
+                /* cmpAuthority */ null, /* cmpAccountName */ null);
+        mBannerController.reset();
+
+        assertEquals(FlakyCloudProvider.AUTHORITY,
+                mBannerController.getCloudMediaProviderAuthority());
+        assertEquals(FlakyCloudProvider.ACCOUNT_NAME,
+                mBannerController.getCloudMediaProviderAccountName());
+
+        assertTrue(mBannerController.shouldShowCloudMediaAvailableBanner());
+        assertFalse(mBannerController.shouldShowAccountUpdatedBanner());
+        assertFalse(mBannerController.shouldShowChooseAccountBanner());
+        assertFalse(mBannerController.shouldShowChooseAppBanner());
+
+        // Test for slow query
+        mIsolatedContext.setFlakyCloudProviderToFlakeInTheNextRequest();
+        mBannerController.onChangeCloudMediaInfo(
+                /* cmpAuthority */ null, /* cmpAccountName */ null);
+        mBannerController.reset();
+
+        assertEquals(FlakyCloudProvider.AUTHORITY,
+                mBannerController.getCloudMediaProviderAuthority());
+        assertNull(mBannerController.getCloudMediaProviderAccountName());
+
+        assertFalse(mBannerController.shouldShowCloudMediaAvailableBanner());
+        assertFalse(mBannerController.shouldShowAccountUpdatedBanner());
+        assertFalse(mBannerController.shouldShowChooseAccountBanner());
+        assertFalse(mBannerController.shouldShowChooseAppBanner());
+    }
+
+    private void setCloudProvider(@Nullable String authority) throws RemoteException {
+        final ContentProviderClient client =
+                mContentResolver.acquireContentProviderClient(AUTHORITY);
+        assertNotNull(client);
+
+        persistSelectedProvider(client, authority);
+
+        final String actualAuthority = getCurrentCloudProvider(mContentResolver);
+        assertEquals(authority, actualAuthority);
+    }
 }
diff --git a/tests/src/com/android/providers/media/photopicker/viewmodel/BannerTestUtils.java b/tests/src/com/android/providers/media/photopicker/viewmodel/BannerTestUtils.java
new file mode 100644
index 0000000..4a8badf
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/viewmodel/BannerTestUtils.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2023 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.viewmodel;
+
+import android.content.Context;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+
+import com.android.providers.media.ConfigStore;
+import com.android.providers.media.photopicker.data.UserIdManager;
+
+class BannerTestUtils {
+    static BannerController getTestBannerController(@NonNull Context context,
+            @NonNull UserHandle userHandle, @NonNull ConfigStore configStore) {
+        return new BannerController(context, userHandle, configStore) {
+            @Override
+            void updateCloudProviderDataFile() {
+                // No-op
+            }
+        };
+    }
+
+    static BannerManager getTestCloudBannerManager(@NonNull Context context,
+            @NonNull UserIdManager userIdManager, @NonNull ConfigStore configStore) {
+        return new BannerManager.CloudBannerManager(context, userIdManager, configStore) {
+            @Override
+            void maybeInitialiseAndSetBannersForCurrentUser() {
+                // Get (iff exists) or create the banner controller for the current user
+                final BannerController bannerController =
+                        getBannerControllersPerUser().forUser(getCurrentUserProfileId());
+                // Post the banner related live data values from this current user banner controller
+                getCloudMediaProviderAuthorityLiveData()
+                        .postValue(bannerController.getCloudMediaProviderAuthority());
+                getCloudMediaProviderAppTitleLiveData()
+                        .postValue(bannerController.getCloudMediaProviderLabel());
+                getCloudMediaAccountNameLiveData()
+                        .postValue(bannerController.getCloudMediaProviderAccountName());
+                setChooseCloudMediaAccountActivityIntent(
+                        bannerController.getChooseCloudMediaAccountActivityIntent());
+                shouldShowChooseAppBannerLiveData()
+                        .postValue(bannerController.shouldShowChooseAppBanner());
+                shouldShowCloudMediaAvailableBannerLiveData()
+                        .postValue(bannerController.shouldShowCloudMediaAvailableBanner());
+                shouldShowAccountUpdatedBannerLiveData()
+                        .postValue(bannerController.shouldShowAccountUpdatedBanner());
+                shouldShowChooseAccountBannerLiveData()
+                        .postValue(bannerController.shouldShowChooseAccountBanner());
+            }
+
+            @NonNull
+            @Override
+            BannerController createBannerController(@NonNull Context context,
+                    @NonNull UserHandle userHandle, @NonNull ConfigStore configStore) {
+                return getTestBannerController(context, userHandle, configStore);
+            }
+        };
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/viewmodel/CategoryOrganiserTest.java b/tests/src/com/android/providers/media/photopicker/viewmodel/CategoryOrganiserTest.java
new file mode 100644
index 0000000..3d6eccc
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/viewmodel/CategoryOrganiserTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2023 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.viewmodel;
+
+import static com.android.providers.media.photopicker.PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.provider.CloudMediaProviderContract;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.providers.media.photopicker.data.model.Category;
+import com.android.providers.media.photopicker.util.CategoryOrganiserUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Unit test to ensure that the CategoryOrganiser reorders categories in the required way.
+ */
+@RunWith(AndroidJUnit4.class)
+public class CategoryOrganiserTest {
+
+    @Test
+    public void test_categoryOrder_meetsRequirements() {
+        List<Category> inputCategoryList = new ArrayList() {
+            {
+                add(new Category(
+                        CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS,
+                        LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true));
+                add(new Category(
+                        CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA,
+                        LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true));
+                add(new Category(
+                        "TestCategory1",
+                        LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true));
+                add(new Category(
+                        CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES,
+                        LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true));
+                add(new Category(
+                        "TestCategory2",
+                        LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true));
+                add(new Category(
+                        CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS,
+                        LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true));
+                add(new Category(
+                        "TestCategory3",
+                        LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true));
+                add(new Category(
+                        CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS,
+                        LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true));
+            }
+        };
+
+        // Expected list contains all the categories in the input list but in the required order,
+        // the order of custom categories is maintained.
+        List<Category> expectedCategoryList = new ArrayList() {
+            {
+                add(new Category(
+                        CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES,
+                        LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true));
+                add(new Category(
+                        CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA,
+                        LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true));
+                add(new Category(
+                        CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS,
+                        LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true));
+                add(new Category(
+                        CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS,
+                        LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true));
+                add(new Category(
+                        CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS,
+                        LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true));
+                add(new Category(
+                        "TestCategory1",
+                        LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true));
+                add(new Category(
+                        "TestCategory2",
+                        LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true));
+                add(new Category(
+                        "TestCategory3",
+                        LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true));
+            }
+        };
+
+        // perform reorder.
+        CategoryOrganiserUtils.getReorganisedCategoryList(inputCategoryList);
+
+        assertThat(inputCategoryList).isNotNull();
+        assertThat(inputCategoryList.size()).isEqualTo(expectedCategoryList.size());
+        for (int itr = 0; itr < inputCategoryList.size(); itr++) {
+            assertThat(inputCategoryList.get(itr).getId()).isEqualTo(
+                    expectedCategoryList.get(itr).getId());
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelPaginationTest.java b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelPaginationTest.java
new file mode 100644
index 0000000..9e1ec53
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelPaginationTest.java
@@ -0,0 +1,524 @@
+/*
+ * Copyright (C) 2023 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.viewmodel;
+
+import static android.provider.MediaStore.VOLUME_EXTERNAL;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.providers.media.photopicker.PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY;
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_CLEAR_AND_UPDATE_LIST;
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_LOAD_NEXT_PAGE;
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_REFRESH_ITEMS;
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_VIEW_CREATED;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.app.Application;
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.CloudMediaProviderContract;
+import android.provider.MediaStore;
+
+import androidx.lifecycle.LiveData;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.providers.media.IsolatedContext;
+import com.android.providers.media.TestConfigStore;
+import com.android.providers.media.photopicker.DataLoaderThread;
+import com.android.providers.media.photopicker.data.ItemsProvider;
+import com.android.providers.media.photopicker.data.PaginationParameters;
+import com.android.providers.media.photopicker.data.UserIdManager;
+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 org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class PickerViewModelPaginationTest {
+
+    @Rule
+    public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
+
+    @Mock
+    private Application mApplication;
+
+    private PickerViewModel mPickerViewModel;
+
+    private static final Instrumentation sInstrumentation = getInstrumentation();
+    private static final Context sTargetContext = sInstrumentation.getTargetContext();
+
+    private static final String TAG = "PickerViewModelTest";
+    private ContentResolver mIsolatedResolver;
+
+    public PickerViewModelPaginationTest() {
+
+    }
+
+    @Before
+    public void setUp() {
+        final UiAutomation uiAutomation = sInstrumentation.getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+                Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
+                Manifest.permission.READ_DEVICE_CONFIG,
+                Manifest.permission.INTERACT_ACROSS_USERS);
+        MockitoAnnotations.initMocks(this);
+
+        final TestConfigStore testConfigStore = new TestConfigStore();
+        testConfigStore.enableCloudMediaFeature();
+
+        final Context isolatedContext = new IsolatedContext(sTargetContext, /* tag */ "databases",
+                /* asFuseThread */ false, sTargetContext.getUser(), testConfigStore);
+        when(mApplication.getApplicationContext()).thenReturn(isolatedContext);
+        sInstrumentation.runOnMainSync(() -> {
+            mPickerViewModel = new PickerViewModel(mApplication) {
+                @Override
+                protected void initConfigStore() {
+                    setConfigStore(testConfigStore);
+                }
+            };
+        });
+        final UserIdManager userIdManager = mock(UserIdManager.class);
+        when(userIdManager.getCurrentUserProfileId()).thenReturn(UserId.CURRENT_USER);
+        mPickerViewModel.setUserIdManager(userIdManager);
+        mIsolatedResolver = isolatedContext.getContentResolver();
+        final ItemsProvider itemsProvider = new ItemsProvider(isolatedContext);
+        mPickerViewModel.setItemsProvider(itemsProvider);
+        mPickerViewModel.clearItemsAndCategoryItemsList();
+    }
+
+    @Test
+    public void test_getItems_noItemsPresent() throws Exception {
+        int pageSize = 4;
+        final int numberOfTestItems = 0;
+        try {
+            // Generate test items.
+            assertCreateNewImagesWithCategoryDownloads(numberOfTestItems);
+
+            // Get live data for items, this also loads the first page.
+            LiveData<PickerViewModel.PaginatedItemsResult> testItems =
+                    mPickerViewModel.getPaginatedItemsForAction(
+                            ACTION_VIEW_CREATED, new PaginationParameters(
+                                    pageSize, /*dateBeforeMs*/ Long.MIN_VALUE, /* rowId*/ -1));
+            DataLoaderThread.waitForIdle();
+
+            // Empty list should be returned.
+            assertThat(testItems.getValue().getItems().size()).isEqualTo(numberOfTestItems);
+
+            // Load next page size number of images.
+            mPickerViewModel.getPaginatedItemsForAction(ACTION_LOAD_NEXT_PAGE, null);
+            DataLoaderThread.waitForIdle();
+
+            // Empty list should be returned.
+            assertThat(testItems.getValue().getItems().size()).isEqualTo(numberOfTestItems);
+        } finally {
+            mPickerViewModel.clearItemsAndCategoryItemsList();
+            deleteAllFilesNoThrow();
+        }
+    }
+
+    @Test
+    public void test_getCategoryItems_noItemsPresent() throws Exception {
+        int pageSize = 4;
+        final int numberOfTestItems = 0;
+        Category downloadsAlbum = new Category(
+                CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS,
+                LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true);
+        try {
+            // Generate test items.
+            assertCreateNewImagesWithCategoryDownloads(
+                    numberOfTestItems);
+
+            // Get live data for items, this also loads the first page.
+            LiveData<PickerViewModel.PaginatedItemsResult> testItems =
+                    mPickerViewModel.getPaginatedCategoryItemsForAction(
+                            downloadsAlbum, ACTION_VIEW_CREATED,
+                            new PaginationParameters(
+                                    pageSize, /*dateBeforeMs*/ Long.MIN_VALUE, /* rowId*/ -1));
+            DataLoaderThread.waitForIdle();
+
+            // Empty list should be returned.
+            assertThat(testItems.getValue().getItems().size()).isEqualTo(numberOfTestItems);
+
+            // Load next page size number of images.
+            mPickerViewModel.getPaginatedCategoryItemsForAction(
+                    downloadsAlbum, ACTION_LOAD_NEXT_PAGE, null);
+            DataLoaderThread.waitForIdle();
+
+            // Empty list should be returned.
+            assertThat(testItems.getValue().getItems().size()).isEqualTo(numberOfTestItems);
+        } finally {
+            mPickerViewModel.clearItemsAndCategoryItemsList();
+            deleteAllFilesNoThrow();
+        }
+    }
+
+    @Test
+    public void test_getItems_correctItemsReturned() throws Exception {
+        int pageSize = 4;
+        final int numberOfTestItems = 10;
+
+        try {
+            // Generate test items.
+            assertCreateNewImagesWithCategoryDownloads(
+                    numberOfTestItems);
+
+            // Get live data for items, this also loads the first page.
+            LiveData<PickerViewModel.PaginatedItemsResult> testItems =
+                    mPickerViewModel.getPaginatedItemsForAction(
+                            ACTION_VIEW_CREATED, new PaginationParameters(
+                                    pageSize, /*dateBeforeMs*/ Long.MIN_VALUE, /* rowId*/ -1));
+            DataLoaderThread.waitForIdle();
+
+            // Page 1: Since the page size is set to 4, only 4 images should be returned.
+            assertThat(testItems.getValue().getItems().size()).isEqualTo(pageSize);
+
+            // Load next page size number of images.
+            mPickerViewModel.getPaginatedItemsForAction(ACTION_LOAD_NEXT_PAGE, null);
+            DataLoaderThread.waitForIdle();
+
+            // Page 2: 8 images should be returned.
+            assertThat(testItems.getValue().getItems().size()).isEqualTo(2 * pageSize);
+
+            // Load next page size number of images.
+            mPickerViewModel.getPaginatedItemsForAction(ACTION_LOAD_NEXT_PAGE, null);
+            DataLoaderThread.waitForIdle();
+
+            // Page 3: all 10 images should be returned. All items loaded.
+            assertThat(testItems.getValue().getItems().size()).isEqualTo(numberOfTestItems);
+
+            // Try loading once more, but the number of images should not change since we have
+            // exhausted the list.
+            mPickerViewModel.getPaginatedItemsForAction(ACTION_LOAD_NEXT_PAGE, null);
+            DataLoaderThread.waitForIdle();
+
+            // All items loaded.
+            assertThat(testItems.getValue().getItems().size()).isEqualTo(numberOfTestItems);
+
+
+        } finally {
+            mPickerViewModel.clearItemsAndCategoryItemsList();
+            deleteAllFilesNoThrow();
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+    @Test
+    public void test_differentCategories_getCategoryItems() throws Exception {
+        int pageSize = 4;
+        final int numberOfTestItemsInDownloads = 10;
+        final int numberOfTestItemsInCamera = 7;
+        try {
+            // generate items in category downloads.
+            assertCreateNewImagesWithCategoryDownloads(numberOfTestItemsInDownloads);
+
+            // generate items in category camera.
+            assertCreateNewImagesWithCategoryCamera(numberOfTestItemsInCamera);
+
+            ////////////////// Verify Category Camera //////////////////
+
+            Category cameraAlbum = new Category(
+                    CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA,
+                    LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true);
+
+            mPickerViewModel.initPhotoPickerData(cameraAlbum);
+            DataLoaderThread.waitForIdle();
+            LiveData<PickerViewModel.PaginatedItemsResult> testItems =
+                    mPickerViewModel.getPaginatedCategoryItemsForAction(
+                            cameraAlbum, ACTION_VIEW_CREATED,
+                            new PaginationParameters(
+                                    pageSize, /*dateBeforeMs*/ Long.MIN_VALUE, /* rowId*/ -1));
+            DataLoaderThread.waitForIdle();
+
+            // Page 1: Since the page size is set to 4, only 4 images should be returned.
+            assertThat(testItems.getValue().getItems().size()).isEqualTo(pageSize);
+
+            // Load next page size number of images.
+            mPickerViewModel.getPaginatedCategoryItemsForAction(cameraAlbum,
+                    ACTION_LOAD_NEXT_PAGE,
+                    null);
+            DataLoaderThread.waitForIdle();
+
+            // Page 2: 7 images should be returned.
+            assertThat(testItems.getValue().getItems().size()).isEqualTo(numberOfTestItemsInCamera);
+
+            // Try loading once more, but the number of images should not change since we have
+            // exhausted the list.
+            mPickerViewModel.getPaginatedCategoryItemsForAction(cameraAlbum,
+                    ACTION_LOAD_NEXT_PAGE,
+                    null);
+            DataLoaderThread.waitForIdle();
+
+            // All items loaded.
+            assertThat(testItems.getValue().getItems().size()).isEqualTo(numberOfTestItemsInCamera);
+
+            ////////////////// Verify Category Downloads //////////////////
+
+            Category downloadsAlbum = new Category(
+                    CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS,
+                    LOCAL_PICKER_PROVIDER_AUTHORITY, "", null, 100, true);
+
+            mPickerViewModel.initPhotoPickerData(downloadsAlbum);
+            DataLoaderThread.waitForIdle();
+            LiveData<PickerViewModel.PaginatedItemsResult> testItemsDownloads =
+                    mPickerViewModel.getPaginatedCategoryItemsForAction(
+                            downloadsAlbum, ACTION_VIEW_CREATED,
+                            new PaginationParameters(
+                                    pageSize, /*dateBeforeMs*/ Long.MIN_VALUE, /* rowId*/ -1));
+            DataLoaderThread.waitForIdle();
+
+            // Page 1: Since the page size is set to 4, only 4 images should be returned.
+            assertThat(testItemsDownloads.getValue().getItems().size()).isEqualTo(pageSize);
+
+            // Load next page size number of images.
+            mPickerViewModel.getPaginatedCategoryItemsForAction(
+                    downloadsAlbum, ACTION_LOAD_NEXT_PAGE, null);
+            DataLoaderThread.waitForIdle();
+
+            // Page 2: 8 images should be returned.
+            assertThat(testItemsDownloads.getValue().getItems().size()).isEqualTo(2 * pageSize);
+
+            // Load next page size number of images.
+            mPickerViewModel.getPaginatedCategoryItemsForAction(
+                    downloadsAlbum, ACTION_LOAD_NEXT_PAGE, null);
+            DataLoaderThread.waitForIdle();
+
+            // Page 3: all 10 images should be returned.
+            assertThat(testItemsDownloads.getValue().getItems().size()).isEqualTo(
+                    numberOfTestItemsInDownloads);
+
+
+            // Try loading once more, but the number of images should not change since we have
+            // exhausted the list.
+            mPickerViewModel.getPaginatedCategoryItemsForAction(
+                    downloadsAlbum, ACTION_LOAD_NEXT_PAGE, null);
+            DataLoaderThread.waitForIdle();
+
+            // All items loaded.
+            assertThat(testItemsDownloads.getValue().getItems().size()).isEqualTo(
+                    numberOfTestItemsInDownloads);
+
+        } finally {
+            mPickerViewModel.clearItemsAndCategoryItemsList();
+            deleteAllFilesNoThrow();
+        }
+    }
+
+    @Test
+    public void test_updateItems_itemsResetAndFirstPageLoaded() throws Exception {
+        int pageSize = 4;
+        final int numberOfTestItems = 10;
+
+        try {
+            // Generate test items.
+            assertCreateNewImagesWithCategoryDownloads(
+                    numberOfTestItems);
+
+            // Get live data for items, this also loads the first page.
+            LiveData<PickerViewModel.PaginatedItemsResult> testItems =
+                    mPickerViewModel.getPaginatedItemsForAction(
+                            ACTION_VIEW_CREATED, new PaginationParameters(pageSize,
+                                    /*dateBeforeMs*/ Long.MIN_VALUE, /* rowId*/ -1));
+            DataLoaderThread.waitForIdle();
+
+            // Page 1: Since the page size is set to 4, only 4 images should be returned.
+            assertThat(testItems.getValue().getItems().size()).isEqualTo(pageSize);
+
+            // Load next page size number of images.
+            mPickerViewModel.getPaginatedItemsForAction(ACTION_LOAD_NEXT_PAGE, null);
+            DataLoaderThread.waitForIdle();
+
+            // Page 2: 8 images should be returned.
+            assertThat(testItems.getValue().getItems().size()).isEqualTo(2 * pageSize);
+
+            // Now 8 items have been loaded in the item list.
+            // Call updateItems which is usually called on profile switch or reset.
+            // This should clear out the list and load the first page.
+            mPickerViewModel.getPaginatedItemsForAction(ACTION_CLEAR_AND_UPDATE_LIST, null);
+            DataLoaderThread.waitForIdle();
+
+            // Assert that only one page of items are present now.
+            assertThat(testItems.getValue().getItems().size()).isEqualTo(pageSize);
+
+
+        } finally {
+            mPickerViewModel.clearItemsAndCategoryItemsList();
+            deleteAllFilesNoThrow();
+        }
+    }
+
+    @Test
+    public void test_onReceivingNotification_itemsRefreshed() throws Exception {
+        int pageSize = 10;
+        final int numberOfTestItems = 10;
+
+        try {
+            // Generate test items.
+            assertCreateNewImagesWithCategoryDownloads(
+                    numberOfTestItems);
+
+            // Get live data for items, this also loads the first page. Here all 10 items will be
+            // loaded.
+            LiveData<PickerViewModel.PaginatedItemsResult> testItems =
+                    mPickerViewModel.getPaginatedItemsForAction(
+                            ACTION_VIEW_CREATED, new PaginationParameters(pageSize,
+                                    /*dateBeforeMs*/ Long.MIN_VALUE, /* rowId*/ -1));
+            DataLoaderThread.waitForIdle();
+
+            assertThat(testItems.getValue().getItems().size()).isEqualTo(pageSize);
+
+            // Store this values.
+            List<Item> previousList = testItems.getValue().getItems();
+
+            // add 2 new images.
+            assertCreateNewImagesWithCategoryDownloads(/* count of new items */ 2);
+
+            mPickerViewModel.setNotificationForUpdateReceived(true);
+
+            // Now 8 items have been loaded in the item list.
+            // Call updateItems which is usually called on profile switch or reset.
+            // This should clear out the list and load the first page.
+            mPickerViewModel.getPaginatedItemsForAction(ACTION_REFRESH_ITEMS,
+                    new PaginationParameters(
+                            pageSize, /*dateBeforeMs*/ Long.MIN_VALUE, /* rowId*/ -1));
+            DataLoaderThread.waitForIdle();
+
+            // Assert that only one page of items are present now.
+            assertThat(testItems.getValue().getItems().size()).isEqualTo(pageSize);
+            List<Item> currentList = testItems.getValue().getItems();
+            for (int itr = 0; itr < currentList.size(); itr++) {
+                assertThat(currentList.get(itr).compareTo(previousList.get(itr))).isNotEqualTo(0);
+                if (itr >= 2) {
+                    // assert items have shifted by 2.
+                    assertThat(currentList.get(itr).compareTo(previousList.get(itr - 2))).isEqualTo(
+                            0);
+                }
+            }
+
+
+        } finally {
+            mPickerViewModel.clearItemsAndCategoryItemsList();
+            deleteAllFilesNoThrow();
+        }
+    }
+
+    private List<File> assertCreateNewImagesWithCategoryDownloads(int numberOfImages)
+            throws Exception {
+        List<File> imageFiles = new ArrayList<>();
+        for (int itr = 0; itr < numberOfImages; itr++) {
+            String fileName = TAG + "_file_" + String.valueOf(System.nanoTime()) + ".jpg";
+            imageFiles.add(assertCreateNewFileWithLastModifiedTime(getDownloadsDir(), fileName,
+                    System.nanoTime() / 1000));
+        }
+        return imageFiles;
+    }
+
+    private List<File> assertCreateNewImagesWithCategoryCamera(int numberOfImages)
+            throws Exception {
+        List<File> imageFiles = new ArrayList<>();
+        for (int itr = 0; itr < numberOfImages; itr++) {
+            String fileName = TAG + "_file_" + String.valueOf(System.nanoTime()) + ".jpg";
+            imageFiles.add(assertCreateNewFileWithLastModifiedTime(getCameraDir(), fileName,
+                    System.nanoTime() / 1000));
+        }
+        return imageFiles;
+    }
+
+    private File getDownloadsDir() {
+        return new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS);
+    }
+
+    private File getCameraDir() {
+        return new File(getDcimDir(), "Camera");
+    }
+
+    private File getDcimDir() {
+        return new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DCIM);
+    }
+
+    private File assertCreateNewFileWithLastModifiedTime(File parentDir, String fileName,
+            long lastModifiedTime) throws Exception {
+        final File file = new File(parentDir, fileName);
+        prepareFileAndGetUri(file, lastModifiedTime);
+        return file;
+    }
+
+    private Uri prepareFileAndGetUri(File file, long lastModifiedTime) throws IOException {
+        ensureParentExists(file.getParentFile());
+
+        assertThat(file.createNewFile()).isTrue();
+
+        // Write 1 byte because 0byte files are not valid in the picker db
+        try (FileOutputStream fos = new FileOutputStream(file)) {
+            fos.write(1);
+        }
+
+        if (lastModifiedTime != -1) {
+            file.setLastModified(lastModifiedTime);
+        }
+
+        final Uri uri = MediaStore.scanFile(mIsolatedResolver, file);
+        assertWithMessage("Uri obtained by scanning file " + file)
+                .that(uri).isNotNull();
+        // Wait for picker db sync
+        MediaStore.waitForIdle(mIsolatedResolver);
+
+        return uri;
+    }
+
+    private void ensureParentExists(File parent) {
+        if (!parent.exists()) {
+            parent.mkdirs();
+        }
+        assertThat(parent.exists()).isTrue();
+    }
+
+    private void deleteAllFilesNoThrow() {
+        try (Cursor c = mIsolatedResolver.query(
+                MediaStore.Files.getContentUri(VOLUME_EXTERNAL),
+                new String[]{MediaStore.MediaColumns.DATA}, null, null)) {
+            while (c.moveToNext()) {
+                (new File(c.getString(
+                        c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)))).delete();
+            }
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
index 2eb0aa1..4ca8dd9 100644
--- a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
+++ b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
@@ -17,37 +17,66 @@
 package com.android.providers.media.photopicker.viewmodel;
 
 import static android.provider.CloudMediaProviderContract.AlbumColumns;
-import static android.provider.CloudMediaProviderContract.MediaColumns;
+import static android.provider.CloudMediaProviderContract.MediaColumns.AUTHORITY;
+import static android.provider.CloudMediaProviderContract.MediaColumns.DATA;
+import static android.provider.CloudMediaProviderContract.MediaColumns.DATE_TAKEN_MILLIS;
+import static android.provider.CloudMediaProviderContract.MediaColumns.DURATION_MILLIS;
+import static android.provider.CloudMediaProviderContract.MediaColumns.HEIGHT;
+import static android.provider.CloudMediaProviderContract.MediaColumns.ID;
+import static android.provider.CloudMediaProviderContract.MediaColumns.IS_FAVORITE;
+import static android.provider.CloudMediaProviderContract.MediaColumns.MEDIA_STORE_URI;
+import static android.provider.CloudMediaProviderContract.MediaColumns.MIME_TYPE;
+import static android.provider.CloudMediaProviderContract.MediaColumns.ORIENTATION;
+import static android.provider.CloudMediaProviderContract.MediaColumns.SIZE_BYTES;
+import static android.provider.CloudMediaProviderContract.MediaColumns.STANDARD_MIME_TYPE_EXTENSION;
+import static android.provider.CloudMediaProviderContract.MediaColumns.SYNC_GENERATION;
+import static android.provider.CloudMediaProviderContract.MediaColumns.WIDTH;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.providers.media.PickerUriResolver.REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI;
+import static com.android.providers.media.photopicker.data.model.Item.ROW_ID;
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_CLEAR_AND_UPDATE_LIST;
+import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_VIEW_CREATED;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import android.app.Application;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.database.Cursor;
 import android.database.MatrixCursor;
+import android.os.CancellationSignal;
 import android.provider.MediaStore;
 import android.text.format.DateUtils;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.test.InstrumentationRegistry;
+import androidx.lifecycle.LiveData;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.providers.media.ConfigStore;
 import com.android.providers.media.TestConfigStore;
+import com.android.providers.media.photopicker.DataLoaderThread;
 import com.android.providers.media.photopicker.PickerSyncController;
 import com.android.providers.media.photopicker.data.ItemsProvider;
+import com.android.providers.media.photopicker.data.PaginationParameters;
+import com.android.providers.media.photopicker.data.Selection;
 import com.android.providers.media.photopicker.data.UserIdManager;
 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.ModelTestUtils;
 import com.android.providers.media.photopicker.data.model.UserId;
-import com.android.providers.media.util.ForegroundThread;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -57,12 +86,19 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
 
 @RunWith(AndroidJUnit4.class)
 public class PickerViewModelTest {
     private static final String FAKE_CATEGORY_NAME = "testCategoryName";
     private static final String FAKE_ID = "5";
+    private static final Context sTargetContext = getInstrumentation().getTargetContext();
+    private static final String TEST_PACKAGE_NAME = "com.android.providers.media.tests";
+    private static final String CMP_AUTHORITY = "authority";
+    private static final String CMP_ACCOUNT_NAME = "account_name";
 
     @Rule
     public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
@@ -73,40 +109,62 @@
     private PickerViewModel mPickerViewModel;
     private TestItemsProvider mItemsProvider;
     private TestConfigStore mConfigStore;
+    private BannerManager mBannerManager;
+    private BannerController mBannerController;
+
+    public PickerViewModelTest() {
+    }
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
 
-        final Context context = InstrumentationRegistry.getTargetContext();
-        when(mApplication.getApplicationContext()).thenReturn(context);
+        when(mApplication.getApplicationContext()).thenReturn(sTargetContext);
         mConfigStore = new TestConfigStore();
-        mConfigStore.enableCloudMediaFeature();
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+        mConfigStore.enableCloudMediaFeatureAndSetAllowedCloudProviderPackages(TEST_PACKAGE_NAME);
+        mConfigStore.enablePickerChoiceManagedSelectionEnabled();
+
+        getInstrumentation().runOnMainSync(() -> {
             mPickerViewModel = new PickerViewModel(mApplication) {
                 @Override
-                protected ConfigStore getConfigStore() {
-                    return mConfigStore;
+                protected void initConfigStore() {
+                    setConfigStore(mConfigStore);
                 }
             };
         });
-        mItemsProvider = new TestItemsProvider(context);
+        mItemsProvider = new TestItemsProvider(sTargetContext);
         mPickerViewModel.setItemsProvider(mItemsProvider);
-        UserIdManager userIdManager = mock(UserIdManager.class);
+        final UserIdManager userIdManager = mock(UserIdManager.class);
         when(userIdManager.getCurrentUserProfileId()).thenReturn(UserId.CURRENT_USER);
         mPickerViewModel.setUserIdManager(userIdManager);
+
+        mBannerManager = BannerTestUtils.getTestCloudBannerManager(sTargetContext, userIdManager,
+                mConfigStore);
+        mPickerViewModel.setBannerManager(mBannerManager);
+
+        // Set default banner manager values
+        mBannerController = mBannerManager.getBannerControllersPerUser().get(
+                UserId.CURRENT_USER.getIdentifier());
+        assertNotNull(mBannerController);
+        mBannerController.onChangeCloudMediaInfo(
+                /* cmpAuthority= */ null, /* cmpAccountName= */ null);
+        mBannerManager.maybeInitialiseAndSetBannersForCurrentUser();
     }
 
     @Test
     public void testGetItems_noItems() {
         final int itemCount = 0;
         mItemsProvider.setItems(generateFakeImageItemList(itemCount));
-        mPickerViewModel.updateItems();
-        // We use ForegroundThread to execute the loadItems in updateItems(), wait for the thread
+        mPickerViewModel.getPaginatedItemsForAction(
+                ACTION_CLEAR_AND_UPDATE_LIST, null);
+        // We use DataLoader thread to execute the loadItems in updateItems(), wait for the thread
         // idle
-        ForegroundThread.waitForIdle();
+        DataLoaderThread.waitForIdle();
 
-        final List<Item> itemList = mPickerViewModel.getItems().getValue();
+        final List<Item> itemList = Objects.requireNonNull(
+                mPickerViewModel.getPaginatedItemsForAction(
+                        ACTION_VIEW_CREATED,
+                        new PaginationParameters()).getValue()).getItems();
 
         // No date headers, the size should be 0
         assertThat(itemList.size()).isEqualTo(itemCount);
@@ -114,7 +172,6 @@
 
     @Test
     public void testGetCategories() throws Exception {
-        final Context context = InstrumentationRegistry.getTargetContext();
         final int categoryCount = 2;
         try (final Cursor fakeCursor = generateCursorForFakeCategories(categoryCount)) {
             fakeCursor.moveToFirst();
@@ -126,28 +183,90 @@
             // move the cursor to original position
             fakeCursor.moveToPosition(-1);
             mPickerViewModel.updateCategories();
-            // We use ForegroundThread to execute the loadCategories in updateCategories(), wait for
+            // We use DataLoaderThread to execute the loadCategories in updateCategories(), wait for
             // the thread idle
-            ForegroundThread.waitForIdle();
+            DataLoaderThread.waitForIdle();
 
             final List<Category> categoryList = mPickerViewModel.getCategories().getValue();
 
             assertThat(categoryList.size()).isEqualTo(categoryCount);
             // Verify the first category
             final Category firstCategory = categoryList.get(0);
-            assertThat(firstCategory.getDisplayName(context)).isEqualTo(
-                    fakeFirstCategory.getDisplayName(context));
+            assertThat(firstCategory.getDisplayName(sTargetContext)).isEqualTo(
+                    fakeFirstCategory.getDisplayName(sTargetContext));
             assertThat(firstCategory.getItemCount()).isEqualTo(fakeFirstCategory.getItemCount());
             assertThat(firstCategory.getCoverUri()).isEqualTo(fakeFirstCategory.getCoverUri());
             // Verify the second category
             final Category secondCategory = categoryList.get(1);
-            assertThat(secondCategory.getDisplayName(context)).isEqualTo(
-                    fakeSecondCategory.getDisplayName(context));
+            assertThat(secondCategory.getDisplayName(sTargetContext)).isEqualTo(
+                    fakeSecondCategory.getDisplayName(sTargetContext));
             assertThat(secondCategory.getItemCount()).isEqualTo(fakeSecondCategory.getItemCount());
             assertThat(secondCategory.getCoverUri()).isEqualTo(fakeSecondCategory.getCoverUri());
         }
     }
 
+    @Test
+    public void test_getItems_correctItemsReturned() {
+        final int numberOfTestItems = 4;
+        final List<Item> expectedItems = generateFakeImageItemList(numberOfTestItems);
+        mItemsProvider.setItems(expectedItems);
+
+        LiveData<PickerViewModel.PaginatedItemsResult> testItems =
+                mPickerViewModel.getPaginatedItemsForAction(
+                        ACTION_VIEW_CREATED,
+                        new PaginationParameters());
+        DataLoaderThread.waitForIdle();
+
+        assertThat(testItems).isNotNull();
+        assertThat(testItems.getValue()).isNotNull();
+        assertThat(testItems.getValue().getItems().size()).isEqualTo(numberOfTestItems);
+
+        for (int itr = 0; itr < numberOfTestItems; itr++) {
+            // Assert that all test and expected items are equal.
+            assertThat(testItems.getValue().getItems().get(itr).compareTo(
+                    expectedItems.get(itr))).isEqualTo(0);
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 34, codeName = "UpsideDownCake")
+    @Test
+    public void test_getRemainingPreGrantedItems_correctItemsLoaded() {
+        // Enable managed selection for this test.
+        Intent intent = new Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP);
+        intent.putExtra(Intent.EXTRA_UID, 0);
+        mPickerViewModel.parseValuesFromIntent(intent);
+
+        final int numberOfTestItems = 4;
+        final List<Item> expectedItems = generateFakeImageItemList(numberOfTestItems);
+        for (Item item : expectedItems) {
+            item.setPreGranted();
+        }
+        mItemsProvider.setItems(expectedItems);
+        List<String> preGrantedItems = List.of(expectedItems.get(0).getId(),
+                expectedItems.get(1).getId(),
+                expectedItems.get(2).getId());
+        Selection selection = mPickerViewModel.getSelection();
+        // Add 3 item ids is preGranted set.
+        selection.setPreGrantedItemSet(new HashSet<>(preGrantedItems));
+
+        // adding 1 item in selection item set.
+        selection.addSelectedItem(expectedItems.get(1));
+
+        // revoking grant for 1 id.
+        selection.removeSelectedItem(expectedItems.get(0));
+
+        // since only one item is added in selection set, the size should be one.
+        assertThat(selection.getSelectedItems().size()).isEqualTo(1);
+
+        // Since out of 3 one grant was removed, so there would be one item loaded when remaining
+        // grants are loaded.
+        mPickerViewModel.getRemainingPreGrantedItems();
+        DataLoaderThread.waitForIdle();
+
+        // Now the selection set should have 2 items.
+        assertThat(selection.getSelectedItems().size()).isEqualTo(2);
+    }
+
     private static Item generateFakeImageItem(String id) {
         final long dateTakenMs = System.currentTimeMillis()
                 + Long.parseLong(id) * DateUtils.DAY_IN_MILLIS;
@@ -173,7 +292,7 @@
                     FAKE_ID + String.valueOf(i),
                     itemCount + i,
                     PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY
-                    });
+            });
         }
         return cursor;
     }
@@ -188,14 +307,37 @@
         }
 
         @Override
-        public Cursor getAllItems(Category category, int limit, @Nullable String[] mimeType,
-                @Nullable UserId userId) throws
+        public Cursor getAllItems(Category category,
+                PaginationParameters paginationParameters, @Nullable String[] mimeType,
+                @Nullable UserId userId,
+                @Nullable CancellationSignal cancellationSignal) throws
                 IllegalArgumentException, IllegalStateException {
-            final MatrixCursor c = new MatrixCursor(MediaColumns.ALL_PROJECTION);
+            final String[] all_projection = new String[]{
+                    ID,
+                    // This field is unique to the cursor received by the pickerVIewModel.
+                    // It is not a part of cloud provider contract.
+                    ROW_ID,
+                    DATE_TAKEN_MILLIS,
+                    SYNC_GENERATION,
+                    MIME_TYPE,
+                    STANDARD_MIME_TYPE_EXTENSION,
+                    SIZE_BYTES,
+                    MEDIA_STORE_URI,
+                    DURATION_MILLIS,
+                    IS_FAVORITE,
+                    WIDTH,
+                    HEIGHT,
+                    ORIENTATION,
+                    DATA,
+                    AUTHORITY,
+            };
+            final MatrixCursor c = new MatrixCursor(all_projection);
 
+            int itr = 1;
             for (Item item : mItemList) {
-                c.addRow(new String[] {
+                c.addRow(new String[]{
                         item.getId(),
+                        String.valueOf(itr),
                         String.valueOf(item.getDateTaken()),
                         String.valueOf(item.getGenerationModified()),
                         item.getMimeType(),
@@ -204,16 +346,127 @@
                         null, // media_store_uri
                         String.valueOf(item.getDuration()),
                         "0", // is_favorite
+                        String.valueOf(800), // width
+                        String.valueOf(500), // height
+                        String.valueOf(0), // orientation
                         "/storage/emulated/0/foo",
                         PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY
                 });
+                itr++;
             }
 
             return c;
         }
 
+        @Override
+        public Cursor getLocalItems(Category category,
+                PaginationParameters paginationParameters, @Nullable String[] mimeType,
+                @Nullable UserId userId,
+                @Nullable CancellationSignal cancellationSignal) throws
+                IllegalArgumentException, IllegalStateException {
+            final String[] all_projection = new String[]{
+                    ID,
+                    // This field is unique to the cursor received by the pickerVIewModel.
+                    // It is not a part of cloud provider contract.
+                    ROW_ID,
+                    DATE_TAKEN_MILLIS,
+                    SYNC_GENERATION,
+                    MIME_TYPE,
+                    STANDARD_MIME_TYPE_EXTENSION,
+                    SIZE_BYTES,
+                    MEDIA_STORE_URI,
+                    DURATION_MILLIS,
+                    IS_FAVORITE,
+                    WIDTH,
+                    HEIGHT,
+                    ORIENTATION,
+                    DATA,
+                    AUTHORITY,
+            };
+            final MatrixCursor c = new MatrixCursor(all_projection);
+
+            int itr = 1;
+            for (Item item : mItemList) {
+                c.addRow(new String[]{
+                        item.getId(),
+                        String.valueOf(itr),
+                        String.valueOf(item.getDateTaken()),
+                        String.valueOf(item.getGenerationModified()),
+                        item.getMimeType(),
+                        String.valueOf(item.getSpecialFormat()),
+                        "1", // size_bytes
+                        null, // media_store_uri
+                        String.valueOf(item.getDuration()),
+                        "0", // is_favorite
+                        String.valueOf(800), // width
+                        String.valueOf(500), // height
+                        String.valueOf(0), // orientation
+                        "/storage/emulated/0/foo",
+                        PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY
+                });
+                itr++;
+            }
+
+            return c;
+        }
+
+        @Override
+        public Cursor getLocalItemsForSelection(Category category,
+                @NonNull List<Integer> localIdSelection,
+                @Nullable String[] mimeTypes,
+                @Nullable UserId userId,
+                @Nullable CancellationSignal cancellationSignal) throws IllegalArgumentException {
+            final String[] all_projection = new String[]{
+                    ID,
+                    // This field is unique to the cursor received by the pickerVIewModel.
+                    // It is not a part of cloud provider contract.
+                    ROW_ID,
+                    DATE_TAKEN_MILLIS,
+                    SYNC_GENERATION,
+                    MIME_TYPE,
+                    STANDARD_MIME_TYPE_EXTENSION,
+                    SIZE_BYTES,
+                    MEDIA_STORE_URI,
+                    DURATION_MILLIS,
+                    IS_FAVORITE,
+                    WIDTH,
+                    HEIGHT,
+                    ORIENTATION,
+                    DATA,
+                    AUTHORITY,
+            };
+            final MatrixCursor c = new MatrixCursor(all_projection);
+
+            int itr = 1;
+            for (Item item : mItemList) {
+                if (localIdSelection.contains(Integer.parseInt(item.getId()))) {
+                    c.addRow(new String[]{
+                            item.getId(),
+                            String.valueOf(itr),
+                            String.valueOf(item.getDateTaken()),
+                            String.valueOf(item.getGenerationModified()),
+                            item.getMimeType(),
+                            String.valueOf(item.getSpecialFormat()),
+                            "1", // size_bytes
+                            null, // media_store_uri
+                            String.valueOf(item.getDuration()),
+                            "0", // is_favorite
+                            String.valueOf(800), // width
+                            String.valueOf(500), // height
+                            String.valueOf(0), // orientation
+                            "/storage/emulated/0/foo",
+                            PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY
+                    });
+                    itr++;
+                }
+            }
+            return c;
+
+        }
+
         @Nullable
-        public Cursor getAllCategories(@Nullable String[] mimeType, @Nullable UserId userId) {
+        public Cursor getAllCategories(@Nullable String[] mimeType, @Nullable UserId userId,
+                @Nullable CancellationSignal cancellationSignal) {
             if (mCategoriesCursor != null) {
                 return mCategoriesCursor;
             }
@@ -263,7 +516,7 @@
     @Test
     public void testParseValuesFromPickImagesIntent_validExtraMimeType() {
         final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
-        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"image/gif", "video/*"});
+        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/gif", "video/*"});
 
         mPickerViewModel.parseValuesFromIntent(intent);
 
@@ -273,7 +526,7 @@
     @Test
     public void testParseValuesFromPickImagesIntent_invalidExtraMimeType() {
         final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
-        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"audio/*", "video/*"});
+        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"audio/*", "video/*"});
 
         try {
             mPickerViewModel.parseValuesFromIntent(intent);
@@ -305,7 +558,7 @@
     @Test
     public void testParseValuesFromGetContentIntent_validExtraMimeType() {
         final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
-        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"image/gif", "video/*"});
+        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/gif", "video/*"});
 
         mPickerViewModel.parseValuesFromIntent(intent);
 
@@ -315,7 +568,7 @@
     @Test
     public void testParseValuesFromGetContentIntent_invalidExtraMimeType() {
         final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
-        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"audio/*", "video/*"});
+        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"audio/*", "video/*"});
 
         mPickerViewModel.parseValuesFromIntent(intent);
 
@@ -326,7 +579,7 @@
     @Test
     public void testParseValuesFromGetContentIntent_localOnlyTrue() {
         final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
-        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"video/*"});
+        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"video/*"});
         intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
 
         mPickerViewModel.parseValuesFromIntent(intent);
@@ -337,7 +590,7 @@
     @Test
     public void testParseValuesFromGetContentIntent_localOnlyFalse() {
         final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
-        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"video/*"});
+        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"video/*"});
 
         mPickerViewModel.parseValuesFromIntent(intent);
 
@@ -362,4 +615,123 @@
         mConfigStore.disableCloudMediaFeature();
         assertThat(mPickerViewModel.shouldShowOnlyLocalFeatures()).isTrue();
     }
+
+    @Test
+    public void testRefreshUiNotifications() throws InterruptedException {
+        final LiveData<Boolean> shouldRefreshUi = mPickerViewModel.shouldRefreshUiLiveData();
+        assertFalse(shouldRefreshUi.getValue());
+
+        final ContentResolver contentResolver = sTargetContext.getContentResolver();
+        contentResolver.notifyChange(REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI, null);
+
+        TimeUnit.MILLISECONDS.sleep(100);
+        assertTrue(shouldRefreshUi.getValue());
+
+        mPickerViewModel.resetAllContentInCurrentProfile();
+        assertFalse(shouldRefreshUi.getValue());
+    }
+
+    @Test
+    public void testDismissChooseAppBanner() {
+        mBannerController.onChangeCloudMediaInfo(CMP_AUTHORITY, CMP_ACCOUNT_NAME);
+        mBannerManager.maybeInitialiseAndSetBannersForCurrentUser();
+
+        mBannerController.onChangeCloudMediaInfo(
+                /* cmpAuthority= */ null, /* cmpAccountName= */ null);
+        mBannerManager.maybeInitialiseAndSetBannersForCurrentUser();
+        assertTrue(mBannerController.shouldShowChooseAppBanner());
+        assertTrue(mPickerViewModel.shouldShowChooseAppBannerLiveData().getValue());
+
+        getInstrumentation().runOnMainSync(() -> mPickerViewModel.onUserDismissedChooseAppBanner());
+        assertFalse(mBannerController.shouldShowChooseAppBanner());
+        assertFalse(mPickerViewModel.shouldShowChooseAppBannerLiveData().getValue());
+
+        // Assert no change on dismiss when the banner is already hidden
+        getInstrumentation().runOnMainSync(() -> mPickerViewModel.onUserDismissedChooseAppBanner());
+        assertFalse(mBannerController.shouldShowChooseAppBanner());
+        assertFalse(mPickerViewModel.shouldShowChooseAppBannerLiveData().getValue());
+    }
+
+    @Test
+    public void testDismissCloudMediaAvailableBanner() {
+        mBannerController.onChangeCloudMediaInfo(CMP_AUTHORITY, CMP_ACCOUNT_NAME);
+        mBannerManager.maybeInitialiseAndSetBannersForCurrentUser();
+        assertTrue(mBannerController.shouldShowCloudMediaAvailableBanner());
+        assertTrue(mPickerViewModel.shouldShowCloudMediaAvailableBannerLiveData().getValue());
+
+        getInstrumentation().runOnMainSync(() ->
+                mPickerViewModel.onUserDismissedCloudMediaAvailableBanner());
+        assertFalse(mBannerController.shouldShowCloudMediaAvailableBanner());
+        assertFalse(mPickerViewModel.shouldShowCloudMediaAvailableBannerLiveData().getValue());
+
+        // Assert no change on dismiss when the banner is already hidden
+        getInstrumentation().runOnMainSync(() ->
+                mPickerViewModel.onUserDismissedCloudMediaAvailableBanner());
+        assertFalse(mBannerController.shouldShowCloudMediaAvailableBanner());
+        assertFalse(mPickerViewModel.shouldShowCloudMediaAvailableBannerLiveData().getValue());
+    }
+
+    @Test
+    public void testDismissAccountUpdatedBanner() {
+        mBannerController.onChangeCloudMediaInfo(CMP_AUTHORITY, /* cmpAccountName= */ null);
+        mBannerManager.maybeInitialiseAndSetBannersForCurrentUser();
+
+        mBannerController.onChangeCloudMediaInfo(CMP_AUTHORITY, CMP_ACCOUNT_NAME);
+        mBannerManager.maybeInitialiseAndSetBannersForCurrentUser();
+        assertTrue(mBannerController.shouldShowAccountUpdatedBanner());
+        assertTrue(mPickerViewModel.shouldShowAccountUpdatedBannerLiveData().getValue());
+
+        getInstrumentation().runOnMainSync(() ->
+                mPickerViewModel.onUserDismissedAccountUpdatedBanner());
+        assertFalse(mBannerController.shouldShowAccountUpdatedBanner());
+        assertFalse(mPickerViewModel.shouldShowAccountUpdatedBannerLiveData().getValue());
+
+        // Assert no change on dismiss when the banner is already hidden
+        getInstrumentation().runOnMainSync(() ->
+                mPickerViewModel.onUserDismissedAccountUpdatedBanner());
+        assertFalse(mBannerController.shouldShowAccountUpdatedBanner());
+        assertFalse(mPickerViewModel.shouldShowAccountUpdatedBannerLiveData().getValue());
+    }
+
+    @Test
+    public void testDismissChooseAccountBanner() {
+        mBannerController.onChangeCloudMediaInfo(CMP_AUTHORITY, /* cmpAccountName= */ null);
+        mBannerManager.maybeInitialiseAndSetBannersForCurrentUser();
+        assertTrue(mBannerController.shouldShowChooseAccountBanner());
+        assertTrue(mPickerViewModel.shouldShowChooseAccountBannerLiveData().getValue());
+
+        getInstrumentation().runOnMainSync(() ->
+                mPickerViewModel.onUserDismissedChooseAccountBanner());
+        assertFalse(mBannerController.shouldShowChooseAccountBanner());
+        assertFalse(mPickerViewModel.shouldShowChooseAccountBannerLiveData().getValue());
+
+        // Assert no change on dismiss when the banner is already hidden
+        getInstrumentation().runOnMainSync(() ->
+                mPickerViewModel.onUserDismissedChooseAccountBanner());
+        assertFalse(mBannerController.shouldShowChooseAccountBanner());
+        assertFalse(mPickerViewModel.shouldShowChooseAccountBannerLiveData().getValue());
+    }
+
+    @Test
+    public void testGetCloudMediaProviderAuthorityLiveData() {
+        assertNull(mPickerViewModel.getCloudMediaProviderAuthorityLiveData().getValue());
+
+        mBannerController.onChangeCloudMediaInfo(CMP_AUTHORITY, /* cmpAccountName= */ null);
+        mBannerManager.maybeInitialiseAndSetBannersForCurrentUser();
+
+        assertEquals(CMP_AUTHORITY,
+                mPickerViewModel.getCloudMediaProviderAuthorityLiveData().getValue());
+    }
+
+    @Test
+    public void testGetChooseCloudMediaAccountActivityIntent() {
+        assertNull(mPickerViewModel.getChooseCloudMediaAccountActivityIntent());
+
+        final Intent testIntent = new Intent();
+        mBannerController.setChooseCloudMediaAccountActivityIntent(testIntent);
+        mBannerManager.maybeInitialiseAndSetBannersForCurrentUser();
+
+        assertEquals(testIntent,
+                mPickerViewModel.getChooseCloudMediaAccountActivityIntent());
+    }
 }
diff --git a/tests/src/com/android/providers/media/scan/LegacyMediaScannerTest.java b/tests/src/com/android/providers/media/scan/LegacyMediaScannerTest.java
index 2831e96..0902ea5 100644
--- a/tests/src/com/android/providers/media/scan/LegacyMediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/LegacyMediaScannerTest.java
@@ -19,16 +19,17 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
-import android.provider.MediaStore;
-
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.providers.media.library.RunOnlyOnPostsubmit;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.io.File;
 
+@RunOnlyOnPostsubmit
 @RunWith(AndroidJUnit4.class)
 public class LegacyMediaScannerTest {
     @Test
diff --git a/tests/src/com/android/providers/media/scan/NullMediaScannerTest.java b/tests/src/com/android/providers/media/scan/NullMediaScannerTest.java
deleted file mode 100644
index 265d1a9..0000000
--- a/tests/src/com/android/providers/media/scan/NullMediaScannerTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.providers.media.scan;
-
-import static org.junit.Assert.assertNotNull;
-
-import android.provider.MediaStore;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.File;
-
-@RunWith(AndroidJUnit4.class)
-public class NullMediaScannerTest {
-    @Test
-    public void testSimple() throws Exception {
-        final NullMediaScanner scanner = new NullMediaScanner(
-                InstrumentationRegistry.getTargetContext());
-        assertNotNull(scanner.getContext());
-
-        scanner.scanDirectory(new File("/dev/null"), MediaScanner.REASON_UNKNOWN);
-        scanner.scanFile(new File("/dev/null"), MediaScanner.REASON_UNKNOWN);
-
-        scanner.onDetachVolume(null);
-    }
-}
diff --git a/tests/src/com/android/providers/media/stableuris/dao/BackupIdRowTest.java b/tests/src/com/android/providers/media/stableuris/dao/BackupIdRowTest.java
index bfadf87..f008735 100644
--- a/tests/src/com/android/providers/media/stableuris/dao/BackupIdRowTest.java
+++ b/tests/src/com/android/providers/media/stableuris/dao/BackupIdRowTest.java
@@ -44,8 +44,9 @@
                 .setIsTrashed(0)
                 .setOwnerPackagedId(1)
                 .setUserId(1)
-                .setDateExpires("10")
+                .setDateExpires(null)
                 .setIsDirty(true)
+                .setMediaType(1)
                 .build();
         String s = BackupIdRow.serialize(row);
 
@@ -59,6 +60,7 @@
                 .setUserId(1)
                 .setDateExpires("10")
                 .setIsDirty(false)
+                .setMediaType(0)
                 .build();
 
         assertThat(BackupIdRow.deserialize(s)).isNotEqualTo(row2);
diff --git a/tests/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceServiceTest.java b/tests/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceServiceTest.java
index 9d29f11..7352309 100644
--- a/tests/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceServiceTest.java
+++ b/tests/src/com/android/providers/media/stableuris/job/StableUriIdleMaintenanceServiceTest.java
@@ -16,14 +16,26 @@
 
 package com.android.providers.media.stableuris.job;
 
+import static com.android.providers.media.tests.utils.PublicVolumeSetupHelper.createNewPublicVolume;
+import static com.android.providers.media.tests.utils.PublicVolumeSetupHelper.deletePublicVolumes;
+import static com.android.providers.media.util.FileUtils.getVolumePath;
+
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import android.Manifest;
+import android.app.job.JobScheduler;
 import android.content.ContentResolver;
+import android.content.ContentUris;
 import android.content.Context;
 import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.SystemClock;
+import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.provider.MediaStore;
 import android.util.Log;
@@ -33,85 +45,282 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.providers.media.ConfigStore;
+import com.android.providers.media.stableuris.dao.BackupIdRow;
 
-import org.junit.After;
-import org.junit.Before;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.io.IOException;
+import java.io.File;
+import java.io.FileOutputStream;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 @RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 31, codeName = "S")
 public class StableUriIdleMaintenanceServiceTest {
     private static final String TAG = "StableUriIdleMaintenanceServiceTest";
 
     private static final String INTERNAL_BACKUP_NAME = "leveldb-internal";
 
-    private boolean mInitialDeviceConfigValue = false;
+    private static final String EXTERNAL_BACKUP_NAME = "leveldb-external_primary";
 
-    @Before
-    public void setUp() throws IOException {
-        InstrumentationRegistry.getInstrumentation().getUiAutomation()
-                .adoptShellPermissionIdentity(android.Manifest.permission.LOG_COMPAT_CHANGE,
-                        android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
-                        android.Manifest.permission.READ_DEVICE_CONFIG,
-                        android.Manifest.permission.WRITE_DEVICE_CONFIG,
-                        Manifest.permission.WRITE_MEDIA_STORAGE);
+    private static final String OWNERSHIP_BACKUP_NAME = "leveldb-ownership";
+
+    private static final String PUBLIC_VOLUME_BACKUP_NAME = "leveldb-";
+
+    private static boolean sInitialDeviceConfigValueForInternal = false;
+
+    private static boolean sInitialDeviceConfigValueForExternal = false;
+
+    private static boolean sInitialDeviceConfigValueForPublic = false;
+
+    private static final int IDLE_JOB_ID = -500;
+
+    @BeforeClass
+    public static void setUpClass() throws Exception {
+        adoptShellPermission();
+
         // Read existing value of the flag
-        mInitialDeviceConfigValue = Boolean.parseBoolean(
-                DeviceConfig.getProperty(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
-                        ConfigStore.ConfigStoreImpl.KEY_STABILISE_VOLUME_INTERNAL));
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
-                ConfigStore.ConfigStoreImpl.KEY_STABILISE_VOLUME_INTERNAL, Boolean.TRUE.toString(),
+        sInitialDeviceConfigValueForInternal = Boolean.parseBoolean(
+                DeviceConfig.getProperty(ConfigStore.NAMESPACE_MEDIAPROVIDER,
+                        ConfigStore.ConfigStoreImpl.KEY_STABILIZE_VOLUME_INTERNAL));
+        DeviceConfig.setProperty(ConfigStore.NAMESPACE_MEDIAPROVIDER,
+                ConfigStore.ConfigStoreImpl.KEY_STABILIZE_VOLUME_INTERNAL, Boolean.TRUE.toString(),
+                false);
+        sInitialDeviceConfigValueForExternal = Boolean.parseBoolean(
+                DeviceConfig.getProperty(ConfigStore.NAMESPACE_MEDIAPROVIDER,
+                        ConfigStore.ConfigStoreImpl.KEY_STABILIZE_VOLUME_EXTERNAL));
+        DeviceConfig.setProperty(ConfigStore.NAMESPACE_MEDIAPROVIDER,
+                ConfigStore.ConfigStoreImpl.KEY_STABILIZE_VOLUME_EXTERNAL, Boolean.TRUE.toString(),
+                false);
+        sInitialDeviceConfigValueForPublic = Boolean.parseBoolean(
+                DeviceConfig.getProperty(ConfigStore.NAMESPACE_MEDIAPROVIDER,
+                        ConfigStore.ConfigStoreImpl.KEY_STABILIZE_VOLUME_PUBLIC));
+        DeviceConfig.setProperty(ConfigStore.NAMESPACE_MEDIAPROVIDER,
+                ConfigStore.ConfigStoreImpl.KEY_STABILIZE_VOLUME_PUBLIC, Boolean.TRUE.toString(),
                 false);
     }
 
-    @After
-    public void tearDown() throws IOException {
+    @AfterClass
+    public static void tearDownClass() throws Exception {
+
         // Restore previous value of the flag
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
-                ConfigStore.ConfigStoreImpl.KEY_STABILISE_VOLUME_INTERNAL,
-                String.valueOf(mInitialDeviceConfigValue), false);
-        InstrumentationRegistry.getInstrumentation()
-                .getUiAutomation().dropShellPermissionIdentity();
+        DeviceConfig.setProperty(ConfigStore.NAMESPACE_MEDIAPROVIDER,
+                ConfigStore.ConfigStoreImpl.KEY_STABILIZE_VOLUME_INTERNAL,
+                String.valueOf(sInitialDeviceConfigValueForInternal), false);
+        DeviceConfig.setProperty(ConfigStore.NAMESPACE_MEDIAPROVIDER,
+                ConfigStore.ConfigStoreImpl.KEY_STABILIZE_VOLUME_EXTERNAL,
+                String.valueOf(sInitialDeviceConfigValueForExternal), false);
+        DeviceConfig.setProperty(ConfigStore.NAMESPACE_MEDIAPROVIDER,
+                ConfigStore.ConfigStoreImpl.KEY_STABILIZE_VOLUME_PUBLIC,
+                String.valueOf(sInitialDeviceConfigValueForPublic), false);
+        SystemClock.sleep(3000);
+        dropShellPermission();
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 31, codeName = "S")
-    public void testDataMigrationForInternalVolume() {
+    public void testDataMigrationForInternalVolume() throws Exception {
         final Context context = InstrumentationRegistry.getTargetContext();
         final ContentResolver resolver = context.getContentResolver();
-        Set<String> internalFiles = new HashSet<>();
+        Set<String> internalFilePaths = new HashSet<>();
+        Map<String, Long> pathToIdMap = new HashMap<>();
         MediaStore.waitForIdle(resolver);
         try (Cursor c = resolver.query(MediaStore.Files.getContentUri(MediaStore.VOLUME_INTERNAL),
-                new String[]{MediaStore.Files.FileColumns.DATA}, null, null)) {
+                new String[]{MediaStore.Files.FileColumns.DATA, MediaStore.Files.FileColumns._ID},
+                null, null)) {
             assertNotNull(c);
             while (c.moveToNext()) {
                 String path = c.getString(0);
-                internalFiles.add(path);
+                internalFilePaths.add(path);
+                pathToIdMap.put(path, c.getLong(1));
             }
         }
-        assertFalse(internalFiles.isEmpty());
-        // Delete any existing backup to confirm that backup created is by idle maintenance job
-        MediaStore.deleteBackedUpFilePaths(resolver, MediaStore.VOLUME_INTERNAL);
+        assertFalse(internalFilePaths.isEmpty());
 
         MediaStore.waitForIdle(resolver);
         // Creates backup
         MediaStore.runIdleMaintenanceForStableUris(resolver);
 
-        List<String> backedUpFiles = Arrays.asList(MediaStore.getBackupFiles(resolver));
-        assertTrue(backedUpFiles.contains(INTERNAL_BACKUP_NAME));
-        // Read all backed up paths
-        List<String> backedUpPaths = Arrays.asList(
-                MediaStore.readBackedUpFilePaths(resolver, MediaStore.VOLUME_INTERNAL));
-        Log.i(TAG, "BackedUpPaths count:" + backedUpPaths.size());
+        verifyLevelDbPresence(resolver, INTERNAL_BACKUP_NAME);
         // Verify that all internal files are backed up
-        for (String path : internalFiles) {
-            assertTrue(backedUpPaths.contains(path));
+        for (String path : internalFilePaths) {
+            BackupIdRow backupIdRow = BackupIdRow.deserialize(MediaStore.readBackup(resolver,
+                    MediaStore.VOLUME_EXTERNAL_PRIMARY, path));
+            assertNotNull(backupIdRow);
+            assertEquals(pathToIdMap.get(path).longValue(), backupIdRow.getId());
+            assertEquals(UserHandle.myUserId(), backupIdRow.getUserId());
+        }
+    }
+
+    @Test
+    public void testDataMigrationForExternalVolume() throws Exception {
+        final Context context = InstrumentationRegistry.getTargetContext();
+        final ContentResolver resolver = context.getContentResolver();
+        Set<String> newFilePaths = new HashSet<String>();
+        Map<String, Long> pathToIdMap = new HashMap<>();
+        MediaStore.waitForIdle(resolver);
+
+        try {
+            for (int i = 0; i < 10; i++) {
+                final File dir =
+                        Environment.getExternalStoragePublicDirectory(
+                                Environment.DIRECTORY_DOWNLOADS);
+                final File file = new File(dir, System.nanoTime() + ".png");
+
+                // Write 1 byte because 0 byte files are not valid in the db
+                try (FileOutputStream fos = new FileOutputStream(file)) {
+                    fos.write(1);
+                }
+
+                Uri uri = MediaStore.scanFile(resolver, file);
+                long id = ContentUris.parseId(uri);
+                newFilePaths.add(file.getAbsolutePath());
+                pathToIdMap.put(file.getAbsolutePath(), id);
+            }
+
+            assertFalse(newFilePaths.isEmpty());
+            MediaStore.waitForIdle(resolver);
+            // Creates backup
+            MediaStore.runIdleMaintenanceForStableUris(resolver);
+
+            verifyLevelDbPresence(resolver, EXTERNAL_BACKUP_NAME);
+            verifyLevelDbPresence(resolver, OWNERSHIP_BACKUP_NAME);
+            // Verify that all internal files are backed up
+            for (String filePath : newFilePaths) {
+                BackupIdRow backupIdRow = BackupIdRow.deserialize(
+                        MediaStore.readBackup(resolver, MediaStore.VOLUME_EXTERNAL_PRIMARY,
+                                filePath));
+                Log.i(TAG, "BackupIdRow is " + backupIdRow);
+                assertNotNull(backupIdRow);
+                assertEquals(pathToIdMap.get(filePath).longValue(), backupIdRow.getId());
+                assertEquals(UserHandle.myUserId(), backupIdRow.getUserId());
+                assertEquals(context.getPackageName(),
+                        MediaStore.getOwnerPackageName(resolver, backupIdRow.getOwnerPackageId()));
+            }
+        } finally {
+            for (String path : newFilePaths) {
+                new File(path).delete();
+            }
+        }
+    }
+
+    @Test
+    @Ignore
+    public void testDataMigrationForPublicVolume() throws Exception {
+        createNewPublicVolume();
+        try {
+            final Context context = InstrumentationRegistry.getTargetContext();
+            final ContentResolver resolver = context.getContentResolver();
+            final Set<String> volNames = MediaStore.getExternalVolumeNames(context);
+
+            for (String volName : volNames) {
+                if (!MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volName)
+                        && !MediaStore.VOLUME_INTERNAL.equalsIgnoreCase(volName)) {
+                    // public volume
+                    Set<String> newFilePaths = new HashSet<String>();
+                    Map<String, Long> pathToIdMap = new HashMap<>();
+                    MediaStore.waitForIdle(resolver);
+
+                    try {
+                        for (int i = 0; i < 10; i++) {
+                            File volPath = getVolumePath(context, volName);
+                            final File dir = new File(volPath.getAbsoluteFile() + "/Download");
+                            final File file = new File(dir, System.nanoTime() + ".png");
+
+                            // Write 1 byte because 0 byte files are not valid in the db
+                            try (FileOutputStream fos = new FileOutputStream(file)) {
+                                fos.write(1);
+                            }
+
+                            Uri uri = MediaStore.scanFile(resolver, file);
+                            long id = ContentUris.parseId(uri);
+                            newFilePaths.add(file.getAbsolutePath());
+                            pathToIdMap.put(file.getAbsolutePath(), id);
+                        }
+
+                        assertFalse(newFilePaths.isEmpty());
+                        MediaStore.waitForIdle(resolver);
+                        // Creates backup
+                        MediaStore.runIdleMaintenanceForStableUris(resolver);
+
+                        verifyLevelDbPresence(resolver, PUBLIC_VOLUME_BACKUP_NAME + volName);
+                        verifyLevelDbPresence(resolver, OWNERSHIP_BACKUP_NAME);
+                        // Verify that all internal files are backed up
+                        for (String filePath : newFilePaths) {
+                            BackupIdRow backupIdRow = BackupIdRow.deserialize(
+                                    MediaStore.readBackup(resolver, volName, filePath));
+                            assertNotNull(backupIdRow);
+                            assertEquals(pathToIdMap.get(filePath).longValue(),
+                                    backupIdRow.getId());
+                            assertEquals(UserHandle.myUserId(), backupIdRow.getUserId());
+                            assertEquals(context.getPackageName(),
+                                    MediaStore.getOwnerPackageName(resolver,
+                                            backupIdRow.getOwnerPackageId()));
+                        }
+                    } finally {
+                        for (String path : newFilePaths) {
+                            new File(path).delete();
+                        }
+                    }
+                }
+            }
+        } finally {
+            deletePublicVolumes();
+        }
+    }
+
+    @Test
+    public void testJobScheduling() {
+        try {
+            final Context context = InstrumentationRegistry.getTargetContext();
+            final JobScheduler scheduler = InstrumentationRegistry.getTargetContext()
+                    .getSystemService(JobScheduler.class);
+            cancelJob();
+            assertNull(scheduler.getPendingJob(IDLE_JOB_ID));
+
+            StableUriIdleMaintenanceService.scheduleIdlePass(context);
+            assertNotNull(scheduler.getPendingJob(IDLE_JOB_ID));
+        } finally {
+            cancelJob();
+        }
+    }
+
+    private void verifyLevelDbPresence(ContentResolver resolver, String backupName) {
+        List<String> backedUpFiles = Arrays.asList(MediaStore.getBackupFiles(resolver));
+        assertTrue(backedUpFiles.contains(backupName));
+    }
+
+    private static void adoptShellPermission() {
+        androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation()
+                .adoptShellPermissionIdentity(
+                        Manifest.permission.READ_DEVICE_CONFIG,
+                        Manifest.permission.WRITE_DEVICE_CONFIG,
+                        Manifest.permission.WRITE_MEDIA_STORAGE,
+                        android.Manifest.permission.LOG_COMPAT_CHANGE,
+                        android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
+                        Manifest.permission.INTERACT_ACROSS_USERS,
+                        android.Manifest.permission.DUMP);
+        SystemClock.sleep(3000);
+    }
+
+    private static void dropShellPermission() {
+        InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation().dropShellPermissionIdentity();
+    }
+
+    private void cancelJob() {
+        final JobScheduler scheduler = InstrumentationRegistry.getTargetContext()
+                .getSystemService(JobScheduler.class);
+        if (scheduler.getPendingJob(IDLE_JOB_ID) != null) {
+            scheduler.cancel(IDLE_JOB_ID);
         }
     }
 }
diff --git a/tests/src/com/android/providers/media/util/FileCreationUtils.java b/tests/src/com/android/providers/media/util/FileCreationUtils.java
index 4c4b281..02ead9c 100644
--- a/tests/src/com/android/providers/media/util/FileCreationUtils.java
+++ b/tests/src/com/android/providers/media/util/FileCreationUtils.java
@@ -33,18 +33,33 @@
  * A utility class to assist creating files for tests
  */
 public class FileCreationUtils {
+
     /**
      * Helper method to insert a test image/png into given {@code contentResolver}
      *
-     * @param  contentResolver ContentResolver to which file is inserted
-     * @param name file name
+     * @param contentResolver ContentResolver to which file is inserted
+     * @param name            file name
      * @return {@link Long} the files table {@link MediaStore.MediaColumns.ID}
      */
     public static Long insertFileInResolver(ContentResolver contentResolver, String name)
             throws IOException {
+        return insertFileInResolver(contentResolver, name, "png");
+    }
+
+    /**
+     * Helper method to insert a test item into given {@code contentResolver} with the provided
+     * mimeType.
+     *
+     * @param contentResolver ContentResolver to which file is inserted
+     * @param name            file name
+     * @return {@link Long} the files table {@link MediaStore.MediaColumns.ID}
+     */
+    public static Long insertFileInResolver(ContentResolver contentResolver, String name,
+            String mimeType)
+            throws IOException {
         final File dir =
                 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
-        final File file = new File(dir, name + System.nanoTime() + ".png");
+        final File file = new File(dir, name + System.nanoTime() + "." + mimeType);
 
         // Write 1 byte because 0 byte files are not valid in the db
         try (FileOutputStream fos = new FileOutputStream(file)) {
diff --git a/tests/src/com/android/providers/media/util/FileUtilsTest.java b/tests/src/com/android/providers/media/util/FileUtilsTest.java
index eeb7054..7c63807 100644
--- a/tests/src/com/android/providers/media/util/FileUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/FileUtilsTest.java
@@ -1006,6 +1006,10 @@
         // Marking as dirty with a .nomedia file works
         FileUtils.setDirectoryDirty(dirInDownload, true);
         assertTrue(FileUtils.isDirectoryDirty(dirInDownload));
+
+        // Test case-insensitivity
+        File dirInDownloadDifferentCase = new File(mTestDownloadDir, "TeStDirEctoRYdirTy");
+        assertTrue(FileUtils.isDirectoryDirty(dirInDownloadDifferentCase));
     }
 
     @Test
diff --git a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
index be66c09..661fd62 100644
--- a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
@@ -66,6 +66,7 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.Build;
+import android.os.SystemClock;
 
 import androidx.test.filters.SdkSuppress;
 import androidx.test.runner.AndroidJUnit4;
@@ -460,6 +461,8 @@
             assertThat(checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid,
                     packageName, null, isAtLeastT)).isFalse();
             modifyAppOp(testAppUid, OPSTR_READ_MEDIA_VIDEO, AppOpsManager.MODE_ALLOWED);
+            // Adding sleep before appops check to allow appops change to propagate
+            SystemClock.sleep(200);
             assertThat(checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid,
                 packageName, null, isAtLeastT)).isTrue();
         } finally {
@@ -518,6 +521,8 @@
                         packageName, null, isAtLeastT)).isFalse();
 
             modifyAppOp(testAppUid, OPSTR_READ_MEDIA_AUDIO, AppOpsManager.MODE_ALLOWED);
+            // Adding sleep before appops check to allow appops change to propagate
+            SystemClock.sleep(200);
             assertThat(checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid,
                         packageName, null, isAtLeastT)).isTrue();
         } finally {
@@ -550,6 +555,8 @@
                             packageName, null, isAtLeastT)).isFalse();
 
             modifyAppOp(testAppUid, OPSTR_READ_MEDIA_IMAGES, AppOpsManager.MODE_ALLOWED);
+            // Adding sleep before appops check to allow appops change to propagate
+            SystemClock.sleep(200);
             assertThat(checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid,
                             packageName, null, isAtLeastT)).isTrue();
         } finally {
diff --git a/tools/photopicker/res/layout/activity_main.xml b/tools/photopicker/res/layout/activity_main.xml
index 441cd0f..6348a4e 100644
--- a/tools/photopicker/res/layout/activity_main.xml
+++ b/tools/photopicker/res/layout/activity_main.xml
@@ -100,6 +100,13 @@
             android:textSize="16sp" />
     </LinearLayout>
 
+    <CheckBox
+        android:id="@+id/cbx_ordered_selection"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="ORDERED SELECTION"
+        android:textSize="16sp" />
+
     <Button
         android:id="@+id/launch_button"
         android:layout_width="match_parent"
diff --git a/tools/photopicker/src/com/android/providers/media/tools/photopicker/PhotoPickerToolActivity.java b/tools/photopicker/src/com/android/providers/media/tools/photopicker/PhotoPickerToolActivity.java
index fc91077..1d549f3 100644
--- a/tools/photopicker/src/com/android/providers/media/tools/photopicker/PhotoPickerToolActivity.java
+++ b/tools/photopicker/src/com/android/providers/media/tools/photopicker/PhotoPickerToolActivity.java
@@ -63,6 +63,7 @@
     private CheckBox mSetSelectionCountCheckBox;
     private CheckBox mAllowMultipleCheckBox;
     private CheckBox mGetContentCheckBox;
+    private CheckBox mOrderedSelectionCheckBox;
     private EditText mMaxCountText;
     private EditText mMimeTypeText;
 
@@ -77,6 +78,7 @@
         mSetMimeTypeCheckBox = findViewById(R.id.cbx_set_mime_type);
         mSetSelectionCountCheckBox = findViewById(R.id.cbx_set_selection_count);
         mSetVideoOnlyCheckBox = findViewById(R.id.cbx_set_video_only);
+        mOrderedSelectionCheckBox = findViewById(R.id.cbx_ordered_selection);
         mMaxCountText = findViewById(R.id.edittext_max_count);
         mMimeTypeText = findViewById(R.id.edittext_mime_type);
         mScrollView = findViewById(R.id.scrollview);
@@ -169,6 +171,10 @@
                 intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
             } else {
                 intent.putExtra(EXTRA_PICK_IMAGES_MAX, PICK_IMAGES_MAX_LIMIT);
+                // ordered selection is not allowed in get content.
+                if (mOrderedSelectionCheckBox.isChecked()) {
+                    intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, true);
+                }
             }
         }