Merge "Add mute button in video preview" into sc-mainline-prod
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 46e36fc..b4a098b 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -14,6 +14,9 @@
         },
         {
             "name": "CtsMediaProviderTranscodeTests[com.google.android.mediaprovider.apex]"
+        },
+        {
+            "name": "CtsPhotoPickerTest[com.google.android.mediaprovider.apex]"
         }
     ],
     "presubmit": [
diff --git a/apex/framework/java/android/provider/CloudMediaProvider.java b/apex/framework/java/android/provider/CloudMediaProvider.java
index a3e52c8..bf2e55c 100644
--- a/apex/framework/java/android/provider/CloudMediaProvider.java
+++ b/apex/framework/java/android/provider/CloudMediaProvider.java
@@ -16,7 +16,10 @@
 
 package android.provider;
 
+import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED;
 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER;
+import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED;
+import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_EVENT_CALLBACK;
 import static android.provider.CloudMediaProviderContract.METHOD_CREATE_SURFACE_CONTROLLER;
 import static android.provider.CloudMediaProviderContract.METHOD_GET_ACCOUNT_INFO;
 import static android.provider.CloudMediaProviderContract.METHOD_GET_MEDIA_INFO;
@@ -28,6 +31,7 @@
 import static android.provider.CloudMediaProviderContract.URI_PATH_SURFACE_CONTROLLER;
 
 import android.annotation.DurationMillisLong;
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
@@ -44,12 +48,16 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.CancellationSignal;
+import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.util.Log;
 import android.view.Surface;
 import android.view.SurfaceHolder;
 
 import java.io.FileNotFoundException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
 
 /**
  * Base class for a cloud media provider. A cloud media provider offers read-only access to durable
@@ -105,6 +113,9 @@
     private static final int MATCH_MEDIA_INFO = 5;
     private static final int MATCH_SURFACE_CONTROLLER = 6;
 
+    private static final boolean DEFAULT_LOOPING_PLAYBACK_ENABLED = true;
+    private static final boolean DEFAULT_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED = false;
+
     private final UriMatcher mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
     private volatile int mMediaStoreAuthorityAppId;
 
@@ -306,13 +317,17 @@
      * <p>This is meant to be called on the main thread, hence the implementation should not block
      * by performing any heavy operation.
      *
-     * @param extras containing configuration parameters for {@link SurfaceController}
+     * @param config containing configuration parameters for {@link SurfaceController}
      * <ul>
      * <li> {@link CloudMediaProviderContract#EXTRA_LOOPING_PLAYBACK_ENABLED}
+     * <li> {@link CloudMediaProviderContract#EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED}
      * </ul>
+     * @param callback {@link SurfaceEventCallback} to send event updates for {@link Surface} to
+     *                 picker launched via {@link MediaStore#ACTION_PICK_IMAGES}
      */
     @Nullable
-    public SurfaceController onCreateSurfaceController(@Nullable Bundle extras) {
+    public SurfaceController onCreateSurfaceController(@NonNull Bundle config,
+            @NonNull SurfaceEventCallback callback) {
         return null;
     }
 
@@ -342,19 +357,38 @@
         } else if (METHOD_GET_ACCOUNT_INFO.equals(method)) {
             return onGetAccountInfo(extras);
         } else if (METHOD_CREATE_SURFACE_CONTROLLER.equals(method)) {
-            SurfaceController controller = onCreateSurfaceController(extras);
-            Bundle bundle = new Bundle();
-            if (controller == null) {
-                return bundle;
-            }
-            bundle.putBinder(EXTRA_SURFACE_CONTROLLER,
-                    new SurfaceControllerWrapper(controller).asBinder());
-            return bundle;
+            return onCreateSurfaceController(extras);
         }  else {
             throw new UnsupportedOperationException("Method not supported " + method);
         }
     }
 
+    private Bundle onCreateSurfaceController(@NonNull Bundle extras) {
+        Objects.requireNonNull(extras);
+
+        final IBinder binder = extras.getBinder(EXTRA_SURFACE_EVENT_CALLBACK);
+        if (binder == null) {
+            throw new IllegalArgumentException("Missing surface event callback");
+        }
+
+        final SurfaceEventCallback callback =
+                new SurfaceEventCallback(ICloudSurfaceEventCallback.Stub.asInterface(binder));
+        final Bundle config = new Bundle();
+        config.putBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, DEFAULT_LOOPING_PLAYBACK_ENABLED);
+        config.putBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED,
+                DEFAULT_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED);
+        final SurfaceController controller = onCreateSurfaceController(config, callback);
+        if (controller == null) {
+            Log.d(TAG, "onCreateSurfaceController returned null");
+            return Bundle.EMPTY;
+        }
+
+        Bundle result = new Bundle();
+        result.putBinder(EXTRA_SURFACE_CONTROLLER,
+                new SurfaceControllerWrapper(controller).asBinder());
+        return result;
+    }
+
     /**
      * Implementation is provided by the parent class. Cannot be overridden.
      *
@@ -608,8 +642,19 @@
          * @param surfaceId id which uniquely identifies the {@link Surface} for rendering
          * @param timestampMillis the timestamp in milliseconds from the start to seek to
          */
-        public abstract void onMediaSeekTo(int surfaceId,
-                @DurationMillisLong long timestampMillis);
+        public abstract void onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis);
+
+        /**
+         * Changes the configuration parameters for the SurfaceController.
+         *
+         * @param config the updated config to change to. This can include config changes for the
+         * following:
+         * <ul>
+         * <li> {@link CloudMediaProviderContract#EXTRA_LOOPING_PLAYBACK_ENABLED}
+         * <li> {@link CloudMediaProviderContract#EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED}
+         * </ul>
+         */
+        public abstract void onConfigChange(@NonNull Bundle config);
 
         /**
          * Indicates destruction of this SurfaceController object.
@@ -620,7 +665,93 @@
         public abstract void onDestroy();
     }
 
-    /** @hide */
+    /**
+     * This class is used by {@link CloudMediaProvider} to send {@link Surface} event updates to
+     * picker launched via {@link MediaStore#ACTION_PICK_IMAGES}.
+     *
+     * @see MediaStore#ACTION_PICK_IMAGES
+     */
+    public static final class SurfaceEventCallback {
+
+        /** {@hide} */
+        @IntDef(flag = true, prefix = { "PLAYBACK_EVENT_" }, value = {
+                PLAYBACK_EVENT_BUFFERING,
+                PLAYBACK_EVENT_READY,
+                PLAYBACK_EVENT_STARTED,
+                PLAYBACK_EVENT_PAUSED,
+                PLAYBACK_EVENT_COMPLETED,
+                PLAYBACK_EVENT_ERROR_RETRIABLE_FAILURE,
+                PLAYBACK_EVENT_ERROR_PERMANENT_FAILURE
+        })
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface PlaybackEvent {}
+
+        /**
+         * Constant to notify that the playback is buffering
+         */
+        public static final int PLAYBACK_EVENT_BUFFERING = 1;
+
+        /**
+         * Constant to notify that the playback is ready to be played
+         */
+        public static final int PLAYBACK_EVENT_READY = 2;
+
+        /**
+         * Constant to notify that the playback has started
+         */
+        public static final int PLAYBACK_EVENT_STARTED = 3;
+
+        /**
+         * Constant to notify that the playback is paused.
+         */
+        public static final int PLAYBACK_EVENT_PAUSED = 4;
+
+        /**
+         * Constant to notify that the playback event has completed
+         */
+        public static final int PLAYBACK_EVENT_COMPLETED = 5;
+
+        /**
+         * Constant to notify that the playback has failed with a retriable error.
+         */
+        public static final int PLAYBACK_EVENT_ERROR_RETRIABLE_FAILURE = 6;
+
+        /**
+         * Constant to notify that the playback has failed with a permanent error.
+         */
+        public static final int PLAYBACK_EVENT_ERROR_PERMANENT_FAILURE = 7;
+
+        private final ICloudSurfaceEventCallback mCallback;
+
+        SurfaceEventCallback (ICloudSurfaceEventCallback callback) {
+            mCallback = callback;
+        }
+
+        /**
+         * This is called to notify playback event update for a {@link Surface}
+         * on the picker launched via {@link MediaStore#ACTION_PICK_IMAGES}.
+         *
+         * @param surfaceId id which uniquely identifies a {@link Surface}
+         * @param playbackEventType playback event type to notify picker about
+         * @param playbackEventInfo {@link Bundle} which may contain extra information about the
+         *                          playback event. There is no particular event info that
+         *                          we are currently expecting. This may change if we want to
+         *                          support more features for Video Preview like progress/seek
+         *                          bar or show video playback error messages to the user.
+         */
+        public void onPlaybackEvent(int surfaceId, @PlaybackEvent int playbackEventType,
+                @Nullable Bundle playbackEventInfo) {
+            try {
+                mCallback.onPlaybackEvent(surfaceId, playbackEventType, playbackEventInfo);
+            } catch (Exception e) {
+                Log.d(TAG, "Failed to notify playback event (" + playbackEventType + ") for "
+                        + "surfaceId: " + surfaceId + " ; playbackEventInfo: " + playbackEventInfo,
+                        e);
+            }
+        }
+    }
+
+    /** {@hide} */
     private static class SurfaceControllerWrapper extends ICloudMediaSurfaceController.Stub {
 
         final private SurfaceController mSurfaceController;
@@ -681,6 +812,12 @@
         }
 
         @Override
+        public void onConfigChange(@NonNull Bundle config) {
+            Log.i(TAG, "Config changed. Updated config params: " + config);
+            mSurfaceController.onConfigChange(config);
+        }
+
+        @Override
         public void onDestroy() {
             Log.i(TAG, "Controller destroyed");
             mSurfaceController.onDestroy();
diff --git a/apex/framework/java/android/provider/CloudMediaProviderContract.java b/apex/framework/java/android/provider/CloudMediaProviderContract.java
index 95d6845..3e0c3c9 100644
--- a/apex/framework/java/android/provider/CloudMediaProviderContract.java
+++ b/apex/framework/java/android/provider/CloudMediaProviderContract.java
@@ -535,14 +535,39 @@
      * <p>
      * In case this is not present, the default value should be false.
      *
-     * @see CloudMediaProvider#onCreateSurfaceController(Bundle)
+     * @see CloudMediaProvider#onCreateSurfaceController
+     * @see CloudMediaProvider.SurfaceController#onConfigChange
      * <p>
      * Type: BOOLEAN
+     * By default, the value is true
      */
     public static final String EXTRA_LOOPING_PLAYBACK_ENABLED =
             "android.provider.extra.LOOPING_PLAYBACK_ENABLED";
 
     /**
+     * Indicates whether to mute audio during preview of media items.
+     *
+     * @see CloudMediaProvider#onCreateSurfaceController
+     * @see CloudMediaProvider.SurfaceController#onConfigChange
+     * <p>
+     * Type: BOOLEAN
+     * By default, the value is false
+     */
+    public static final String EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED =
+            "android.provider.extra.SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED";
+
+    /**
+     * Gets surface event callback from picker launched via
+     * {@link MediaStore#ACTION_PICK_IMAGES}).
+     *
+     * @see MediaStore#ACTION_PICK_IMAGES
+     *
+     * {@hide}
+     */
+    public static final String EXTRA_SURFACE_EVENT_CALLBACK =
+            "android.provider.extra.SURFACE_EVENT_CALLBACK";
+
+    /**
      * URI path for {@link CloudMediaProvider#onQueryMedia}
      *
      * {@hide}
diff --git a/apex/framework/java/android/provider/ICloudMediaSurfaceController.aidl b/apex/framework/java/android/provider/ICloudMediaSurfaceController.aidl
index 410d42a..a9f48de 100644
--- a/apex/framework/java/android/provider/ICloudMediaSurfaceController.aidl
+++ b/apex/framework/java/android/provider/ICloudMediaSurfaceController.aidl
@@ -34,5 +34,7 @@
     void onMediaPause(int surfaceId);
     void onMediaSeekTo(int surfaceId, long timestampMillis);
 
+    void onConfigChange(in Bundle config);
+
     void onDestroy();
 }
diff --git a/apex/framework/java/android/provider/ICloudSurfaceEventCallback.aidl b/apex/framework/java/android/provider/ICloudSurfaceEventCallback.aidl
new file mode 100644
index 0000000..369da77
--- /dev/null
+++ b/apex/framework/java/android/provider/ICloudSurfaceEventCallback.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider;
+
+import android.os.Bundle;
+
+/**
+ * Interface through which Photo Picker receives playback events from the CloudMediaProviders
+ * @hide
+ */
+interface ICloudSurfaceEventCallback {
+    void onPlaybackEvent(int surfaceId, int eventType, in Bundle eventInfo);
+}
diff --git a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
index ffbd44c..4386388 100644
--- a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
+++ b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
@@ -21,7 +21,10 @@
 
 import android.annotation.IntDef;
 import android.app.Activity;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.res.Configuration;
 import android.content.res.TypedArray;
 import android.graphics.Color;
@@ -32,6 +35,7 @@
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.SystemProperties;
+import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -47,12 +51,15 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.appcompat.widget.Toolbar;
+import androidx.fragment.app.FragmentManager;
 import androidx.lifecycle.ViewModelProvider;
 
 import com.android.modules.utils.build.SdkLevel;
 import com.android.providers.media.R;
 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.UserId;
 import com.android.providers.media.photopicker.ui.AlbumsTabFragment;
 import com.android.providers.media.photopicker.ui.PhotosTabFragment;
 import com.android.providers.media.photopicker.ui.PreviewFragment;
@@ -62,9 +69,11 @@
 import com.google.android.material.bottomsheet.BottomSheetBehavior;
 import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback;
 import com.google.android.material.chip.Chip;
+import com.google.common.collect.Lists;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.List;
 
 /**
  * Photo Picker allows users to choose one or more photos and/or videos to share with an app. The
@@ -97,6 +106,7 @@
     private View mDragBar;
     private View mPrivacyText;
     private Toolbar mToolbar;
+    private CrossProfileListeners mCrossProfileListeners;
 
     @TabChipType
     private int mSelectedTabChipType;
@@ -157,6 +167,15 @@
         // Save the fragment container layout so that we can adjust the padding based on preview or
         // non-preview mode.
         mFragmentContainerView = findViewById(R.id.fragment_container);
+
+        mCrossProfileListeners = new CrossProfileListeners();
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        // This is required to unregister any broadcast receivers.
+        mCrossProfileListeners.onDestroy();
     }
 
     /**
@@ -269,14 +288,20 @@
             }
             restoreBottomSheetState();
         } else {
-            // This is the first launch, set the default behavior. Hide the title, show the chips
-            // and show the PhotosTabFragment
-            updateCommonLayouts(MODE_PHOTOS_TAB, /* title */ "");
-            onTabChipClick(mPhotosTabChip);
-            saveBottomSheetState();
+            setupInitialLaunchState();
         }
     }
 
+    /**
+     * Sets up states for the initial launch. This includes updating common layouts, selecting
+     * Photos tab chip and saving the current bottom sheet state for later.
+     */
+    private void setupInitialLaunchState() {
+        updateCommonLayouts(MODE_PHOTOS_TAB, /* title */ "");
+        onTabChipClick(mPhotosTabChip);
+        saveBottomSheetState();
+    }
+
     private static Chip generateTabChip(LayoutInflater inflater, ViewGroup parent, String title) {
         final Chip chip = (Chip) inflater.inflate(R.layout.picker_chip_tab_header, parent, false);
         chip.setText(title);
@@ -570,4 +595,114 @@
         final boolean shouldShowPrivacyMessage = mode.isPhotosTabOrAlbumsTab;
         mPrivacyText.setVisibility(shouldShowPrivacyMessage ? View.VISIBLE : View.GONE);
     }
+
+    private class CrossProfileListeners {
+
+        private final List<String> MANAGED_PROFILE_FILTER_ACTIONS = Lists.newArrayList(
+                Intent.ACTION_MANAGED_PROFILE_ADDED, // add profile button switch
+                Intent.ACTION_MANAGED_PROFILE_REMOVED, // remove profile button switch
+                Intent.ACTION_MANAGED_PROFILE_UNLOCKED, // activate profile button switch
+                Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE // disable profile button switch
+        );
+
+        private final UserIdManager mUserIdManager;
+
+        public CrossProfileListeners() {
+            mUserIdManager = mPickerViewModel.getUserIdManager();
+
+            registerBroadcastReceivers();
+        }
+
+        public void onDestroy() {
+            unregisterReceiver(mReceiver);
+        }
+
+        private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                final String action = intent.getAction();
+
+                final UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER);
+                final UserId userId = UserId.of(userHandle);
+
+                // We only need to refresh the layout when the received profile user is the
+                // managed user corresponding to the current profile or a new work profile is added
+                // for the current user.
+                if (!userId.equals(mUserIdManager.getManagedUserId()) &&
+                        !action.equals(Intent.ACTION_MANAGED_PROFILE_ADDED)) {
+                    return;
+                }
+
+                switch (action) {
+                    case Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE:
+                        handleWorkProfileOff();
+                        break;
+                    case Intent.ACTION_MANAGED_PROFILE_REMOVED:
+                        handleWorkProfileRemoved();
+                        break;
+                    case Intent.ACTION_MANAGED_PROFILE_UNLOCKED:
+                        handleWorkProfileOn();
+                        break;
+                    case Intent.ACTION_MANAGED_PROFILE_ADDED:
+                        handleWorkProfileAdded();
+                        break;
+                    default:
+                        // do nothing
+                }
+            }
+        };
+
+        private void registerBroadcastReceivers() {
+            final IntentFilter managedProfileFilter = new IntentFilter();
+            for (String managedProfileAction : MANAGED_PROFILE_FILTER_ACTIONS) {
+                managedProfileFilter.addAction(managedProfileAction);
+            }
+            registerReceiver(mReceiver, managedProfileFilter);
+        }
+
+        private void handleWorkProfileOff() {
+            if (mUserIdManager.isManagedUserSelected()) {
+                switchToPersonalProfileInitialLaunchState();
+            }
+            mUserIdManager.updateWorkProfileOffValue();
+        }
+
+        private void handleWorkProfileRemoved() {
+            if (mUserIdManager.isManagedUserSelected()) {
+                switchToPersonalProfileInitialLaunchState();
+            }
+            mUserIdManager.resetUserIds();
+        }
+
+        private void handleWorkProfileAdded() {
+            mUserIdManager.resetUserIds();
+        }
+
+        private void handleWorkProfileOn() {
+            // Update UI for switch to profile button
+            // When the managed profile becomes available, the provider may not be available
+            // immediately, we need to check if it is ready before we reload the content.
+            mUserIdManager.waitForMediaProviderToBeAvailable();
+        }
+
+        private void switchToPersonalProfileInitialLaunchState() {
+            // 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.
+            resetToPersonalProfile();
+
+            final FragmentManager fragmentManager = getSupportFragmentManager();
+            // This is important so that doing a back does not take back to work profile fragment
+            // state.
+            fragmentManager.popBackStack();
+            PhotosTabFragment.show(fragmentManager, Category.getDefaultCategory());
+        }
+
+        /**
+         * Reset to Photo Picker initial launch state (Photos grid tab) in personal profile mode.
+         */
+        private void resetToPersonalProfile() {
+            mPickerViewModel.resetToPersonalProfile();
+            setupInitialLaunchState();
+        }
+    }
 }
diff --git a/src/com/android/providers/media/photopicker/PhotoPickerProvider.java b/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
index 576ce99..820161b 100644
--- a/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
+++ b/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
@@ -17,6 +17,7 @@
 package com.android.providers.media.photopicker;
 
 import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED;
+import static android.provider.CloudMediaProvider.SurfaceEventCallback.PLAYBACK_EVENT_READY;
 import static android.provider.CloudMediaProviderContract.MediaInfo;
 
 import android.annotation.DurationMillisLong;
@@ -161,11 +162,12 @@
 
     @Override
     @Nullable
-    public SurfaceController onCreateSurfaceController(@Nullable Bundle extras) {
+    public SurfaceController onCreateSurfaceController(@Nullable Bundle config,
+            SurfaceEventCallback callback) {
         if (RemotePreviewHandler.isRemotePreviewEnabled()) {
-            boolean enableLoop = extras != null && extras.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED,
+            boolean enableLoop = config != null && config.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED,
                     false);
-            return new SurfaceControllerImpl(getContext(), enableLoop);
+            return new SurfaceControllerImpl(getContext(), enableLoop, callback);
         }
         return null;
     }
@@ -205,12 +207,14 @@
                         BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS).build();
 
         private final Context mContext;
+        private final SurfaceEventCallback mCallback;
         private final Handler mHandler = new Handler(Looper.getMainLooper());
         private final boolean mEnableLoop;
         private ExoPlayer mPlayer;
         private int mCurrentSurfaceId = -1;
 
-        SurfaceControllerImpl(Context context, boolean enableLoop) {
+        SurfaceControllerImpl(Context context, boolean enableLoop, SurfaceEventCallback callback) {
+            mCallback = callback;
             mContext = context;
             mEnableLoop = enableLoop;
             Log.d(TAG, "Surface controller created.");
@@ -249,10 +253,8 @@
                     mPlayer.setVideoSurface(surface);
                     mCurrentSurfaceId = surfaceId;
                     mPlayer.prepare();
-                    // TODO(b/215175249): Don't directly start playback here, just send an event
-                    // instead. It should be up to the photo picker to decide when to start
-                    // playback.
-                    mPlayer.setPlayWhenReady(true);
+
+                    mCallback.onPlaybackEvent(surfaceId, PLAYBACK_EVENT_READY, null);
 
                     Log.d(TAG, "Surface prepared: " + surfaceId + ". Surface: " + surface
                             + ". MediaId: " + mediaId);
@@ -313,6 +315,13 @@
         }
 
         @Override
+        public void onConfigChange(@NonNull Bundle config) {
+            // TODO(b/195009562): Implement mute/unmute audio and loop enabled/disabled
+            // for video preview
+            Log.d(TAG, "Config changed. Updated config params: " + config);
+        }
+
+        @Override
         public void onDestroy() {
             Log.d(TAG, "Surface controller destroyed.");
         }
diff --git a/src/com/android/providers/media/photopicker/data/ItemsProvider.java b/src/com/android/providers/media/photopicker/data/ItemsProvider.java
index 38f594a..9c41579 100644
--- a/src/com/android/providers/media/photopicker/data/ItemsProvider.java
+++ b/src/com/android/providers/media/photopicker/data/ItemsProvider.java
@@ -220,6 +220,11 @@
         final Uri contentUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL);
         try (ContentProviderClient client = userId.getContentResolver(mContext)
                 .acquireUnstableContentProviderClient(MediaStore.AUTHORITY)) {
+            if (client == null) {
+                Log.e(TAG, "Unable to acquire unstable content provider for "
+                        + MediaStore.AUTHORITY);
+                return null;
+            }
             Bundle extras = new Bundle();
             extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection);
             extras.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs);
@@ -254,6 +259,11 @@
         final Bundle extras = new Bundle();
         try (ContentProviderClient client = userId.getContentResolver(mContext)
                 .acquireUnstableContentProviderClient(MediaStore.AUTHORITY)) {
+            if (client == null) {
+                Log.e(TAG, "Unable to acquire unstable content provider for "
+                        + MediaStore.AUTHORITY);
+                return null;
+            }
             extras.putInt(MediaStore.QUERY_ARG_LIMIT, limit);
             extras.putString(MediaStore.QUERY_ARG_MIME_TYPE, mimeType);
             if (category != null) {
@@ -280,6 +290,11 @@
         final Bundle extras = new Bundle();
         try (ContentProviderClient client = userId.getContentResolver(mContext)
                 .acquireUnstableContentProviderClient(MediaStore.AUTHORITY)) {
+            if (client == null) {
+                Log.e(TAG, "Unable to acquire unstable content provider for "
+                        + MediaStore.AUTHORITY);
+                return null;
+            }
             extras.putString(MediaStore.QUERY_ARG_MIME_TYPE, mimeType);
 
             final Uri uri = PickerUriResolver.PICKER_INTERNAL_URI.buildUpon()
diff --git a/src/com/android/providers/media/photopicker/data/Selection.java b/src/com/android/providers/media/photopicker/data/Selection.java
index 7d7883c..79692ba 100644
--- a/src/com/android/providers/media/photopicker/data/Selection.java
+++ b/src/com/android/providers/media/photopicker/data/Selection.java
@@ -95,6 +95,15 @@
     }
 
     /**
+     * Clear all selected items
+     */
+    public void clearSelectedItems() {
+        mSelectedItems.clear();
+        mSelectedItemSize.postValue(mSelectedItems.size());
+        updateSelectionAllowed();
+    }
+
+    /**
      * @return {@code true} if give {@code item} is present in selected items
      *         {@link #mSelectedItems}, {@code false} otherwise
      */
diff --git a/src/com/android/providers/media/photopicker/data/UserIdManager.java b/src/com/android/providers/media/photopicker/data/UserIdManager.java
index 1f265de..07d8c5c 100644
--- a/src/com/android/providers/media/photopicker/data/UserIdManager.java
+++ b/src/com/android/providers/media/photopicker/data/UserIdManager.java
@@ -18,18 +18,19 @@
 
 import static androidx.core.util.Preconditions.checkNotNull;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.WorkerThread;
-import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.Looper;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.Log;
 
-import com.android.internal.annotations.GuardedBy;
+import androidx.lifecycle.MutableLiveData;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.providers.media.photopicker.data.model.UserId;
 import com.android.providers.media.photopicker.util.CrossProfileUtils;
@@ -122,10 +123,32 @@
     void setIntentAndCheckRestrictions(Intent intent);
 
     /**
-     * Updates cross profile restrictions values
+     * Waits for Media Provider of the work profile to be available.
      */
-    @WorkerThread
-    void updateCrossProfileValues();
+    void waitForMediaProviderToBeAvailable();
+
+    /**
+     * Checks if work profile is switched off and updates the data.
+     */
+    void updateWorkProfileOffValue();
+
+    /**
+     * Resets the user ids. This is usually called as a result of receiving broadcast that
+     * managed profile has been added or removed.
+     */
+    void resetUserIds();
+
+    /**
+     * @return {@link MutableLiveData} to check if cross profile interaction allowed or not
+     */
+    @NonNull
+    MutableLiveData<Boolean> getCrossProfileAllowed();
+
+    /**
+     * @return {@link MutableLiveData} to check if there are multiple user profiles or not
+     */
+    @NonNull
+    MutableLiveData<Boolean> getIsMultiUserProfiles();
 
     /**
      * Creates an implementation of {@link UserIdManager}.
@@ -135,41 +158,34 @@
     }
 
     /**
-     * Implementation of {@link UserIdManager}.
+     * Implementation of {@link UserIdManager}. The class assumes that all its public methods are
+     * called from main thread only.
      */
     final class RuntimeUserIdManager implements UserIdManager {
 
         private static final String TAG = "UserIdManager";
 
+        // These values are copied from DocumentsUI
+        private static final int PROVIDER_AVAILABILITY_MAX_RETRIES = 10;
+        private static final long PROVIDER_AVAILABILITY_CHECK_DELAY = 4000;
+
         private final Context mContext;
         private final UserId mCurrentUser;
+        private final Handler mHandler;
 
-        @GuardedBy("mLock")
-        private final Object mLock = new Object();
-        @GuardedBy("mLock")
+        private Runnable mIsProviderAvailableRunnable;
+
         private UserId mPersonalUser = null;
-        @GuardedBy("mLock")
         private UserId mManagedUser = null;
 
-        @GuardedBy("mLock")
         private UserId mCurrentUserProfile = null;
 
-        private Intent mIntent = null;
         // Set default values to negative case, only set as false if checks pass.
         private boolean mIsBlockedByAdmin = true;
         private boolean mIsWorkProfileOff = true;
 
-        private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
-
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                synchronized (mLock) {
-                    mPersonalUser = null;
-                    mManagedUser = null;
-                    setUserIds();
-                }
-            }
-        };
+        private final MutableLiveData<Boolean> mIsMultiUserProfiles = new MutableLiveData<>();
+        private final MutableLiveData<Boolean> mIsCrossProfileAllowed = new MutableLiveData<>();
 
         private RuntimeUserIdManager(Context context) {
             this(context, UserId.CURRENT_USER);
@@ -180,94 +196,104 @@
             mContext = context.getApplicationContext();
             mCurrentUser = checkNotNull(currentUser);
             mCurrentUserProfile = mCurrentUser;
+            mHandler = new Handler(Looper.getMainLooper());
             setUserIds();
+        }
 
-            IntentFilter filter = new IntentFilter();
-            filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED);
-            filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
-            mContext.registerReceiver(mIntentReceiver, filter);
+        @Override
+        public MutableLiveData<Boolean> getCrossProfileAllowed() {
+            return mIsCrossProfileAllowed;
+        }
+
+        @Override
+        public MutableLiveData<Boolean> getIsMultiUserProfiles() {
+            return mIsMultiUserProfiles;
+        }
+
+        @Override
+        public void resetUserIds() {
+            assertMainThread();
+            setUserIds();
         }
 
         @Override
         public boolean isMultiUserProfiles() {
-            synchronized (mLock) {
-                return mPersonalUser != null;
-            }
+            assertMainThread();
+            return mPersonalUser != null;
         }
 
         @Override
         public UserId getPersonalUserId() {
-            synchronized (mLock) {
-                return mPersonalUser;
-            }
+            assertMainThread();
+            return mPersonalUser;
         }
 
         @Override
         public UserId getManagedUserId() {
-            synchronized (mLock) {
-                return mManagedUser;
-            }
+            assertMainThread();
+            return mManagedUser;
         }
 
         @Override
         public UserId getCurrentUserProfileId() {
-            synchronized (mLock) {
-                return mCurrentUserProfile;
-            }
+            assertMainThread();
+            return mCurrentUserProfile;
         }
 
         @Override
         public void setManagedAsCurrentUserProfile() {
+            assertMainThread();
             setCurrentUserProfileId(getManagedUserId());
         }
 
         @Override
         public void setPersonalAsCurrentUserProfile() {
+            assertMainThread();
             setCurrentUserProfileId(getPersonalUserId());
         }
 
         @Override
         public void setIntentAndCheckRestrictions(Intent intent) {
-            mIntent = intent;
-            updateCrossProfileValues();
+            assertMainThread();
+            if (isMultiUserProfiles()) {
+                updateCrossProfileValues(intent);
+            }
         }
 
         public boolean isCurrentUserSelected() {
-            synchronized (mLock) {
-                return mCurrentUserProfile.equals(UserId.CURRENT_USER);
-            }
+            assertMainThread();
+            return mCurrentUserProfile.equals(UserId.CURRENT_USER);
         }
 
         public boolean isManagedUserSelected() {
-            synchronized (mLock) {
-                return mCurrentUserProfile.equals(getManagedUserId());
-            }
+            assertMainThread();
+            return mCurrentUserProfile.equals(getManagedUserId());
         }
 
         @Override
         public boolean isPersonalUserId() {
+            assertMainThread();
             return mCurrentUser.equals(getPersonalUserId());
         }
 
         @Override
         public boolean isManagedUserId() {
+            assertMainThread();
             return mCurrentUser.equals(getManagedUserId());
         }
 
-        private void setUserIds() {
-            synchronized (mLock) {
-                setUserIdsInternalLocked();
-            }
-        }
-
         private void setCurrentUserProfileId(UserId userId) {
-            synchronized (mLock) {
-                mCurrentUserProfile = userId;
-            }
+            mCurrentUserProfile = userId;
         }
 
-        @GuardedBy("mLock")
-        private void setUserIdsInternalLocked() {
+        private void setUserIds() {
+            setUserIdsInternal();
+            mIsMultiUserProfiles.postValue(isMultiUserProfiles());
+        }
+
+        private void setUserIdsInternal() {
+            mPersonalUser = null;
+            mManagedUser = null;
             UserManager userManager =  mContext.getSystemService(UserManager.class);
             if (userManager == null) {
                 Log.e(TAG, "Cannot obtain user manager");
@@ -308,49 +334,126 @@
 
         @Override
         public boolean isCrossProfileAllowed() {
+            assertMainThread();
             return (!isWorkProfileOff() && !isBlockedByAdmin());
         }
 
         @Override
         public boolean isWorkProfileOff() {
+            assertMainThread();
             return mIsWorkProfileOff;
         }
 
         @Override
         public boolean isBlockedByAdmin() {
+            assertMainThread();
             return mIsBlockedByAdmin;
         }
 
         @Override
-        @WorkerThread
-        public void updateCrossProfileValues() {
-            setCrossProfileValues();
+        public void updateWorkProfileOffValue() {
+            assertMainThread();
+            mIsWorkProfileOff = isWorkProfileOffInternal(getManagedUserId());
+            mIsCrossProfileAllowed.postValue(isCrossProfileAllowed());
         }
 
-        @WorkerThread
-        private void setCrossProfileValues() {
-            final PackageManager packageManager = mContext.getPackageManager();
+        @Override
+        public void waitForMediaProviderToBeAvailable() {
+            assertMainThread();
+            final UserId managedUserProfileId = getManagedUserId();
+            if (CrossProfileUtils.isMediaProviderAvailable(managedUserProfileId, mContext)) {
+                mIsWorkProfileOff = false;
+                mIsCrossProfileAllowed.postValue(isCrossProfileAllowed());
+                stopWaitingForProviderToBeAvailable();
+                return;
+            }
+            waitForProviderToBeAvailable(managedUserProfileId, /* numOfTries */ 1);
+        }
+
+        private void updateCrossProfileValues(Intent intent) {
+            setCrossProfileValues(intent);
+            mIsCrossProfileAllowed.postValue(isCrossProfileAllowed());
+        }
+
+        private void setCrossProfileValues(Intent intent) {
             // 1. Check if PICK_IMAGES intent is allowed by admin to show cross user content
-            if (mIntent == null) {
+            setBlockedByAdminValue(intent);
+
+            // 2. Check if work profile is off
+            updateWorkProfileOffValue();
+
+            // 3. For first initial setup, wait for MediaProvider to be on.
+            // (This is not blocking)
+            if (mIsWorkProfileOff) {
+                waitForMediaProviderToBeAvailable();
+            }
+        }
+
+        private void setBlockedByAdminValue(Intent intent) {
+            if (intent == null) {
                 Log.e(TAG, "No intent specified to check if cross profile forwarding is"
                         + " allowed.");
                 return;
             }
-            if (!CrossProfileUtils.isIntentAllowedCrossProfileAccess(mIntent, packageManager)) {
+            final PackageManager packageManager = mContext.getPackageManager();
+            if (!CrossProfileUtils.isIntentAllowedCrossProfileAccess(intent, packageManager)) {
                 mIsBlockedByAdmin = true;
                 return;
             }
             mIsBlockedByAdmin = false;
+        }
 
-            // 2. Check if work profile is off
-            if (!isManagedUserSelected()) {
-                final UserId managedUserProfileId = getManagedUserId();
-                if (!CrossProfileUtils.isMediaProviderAvailable(managedUserProfileId, mContext)) {
-                    mIsWorkProfileOff = true;
+        private boolean isWorkProfileOffInternal(UserId managedUserProfileId) {
+            return CrossProfileUtils.isQuietModeEnabled(managedUserProfileId, mContext) ||
+                    !CrossProfileUtils.isMediaProviderAvailable(managedUserProfileId, mContext);
+        }
+
+        private void waitForProviderToBeAvailable(UserId userId, int numOfTries) {
+            // The runnable should make sure to post update on the live data if it is changed.
+            mIsProviderAvailableRunnable = () -> {
+                // We stop the recursive check when
+                // 1. the provider is available
+                // 2. the profile is in quiet mode, i.e. provider will not be available
+                // 3. after maximum retries
+                if (CrossProfileUtils.isMediaProviderAvailable(userId, mContext)) {
+                    mIsWorkProfileOff = false;
+                    mIsCrossProfileAllowed.postValue(isCrossProfileAllowed());
                     return;
                 }
+
+                if (CrossProfileUtils.isQuietModeEnabled(userId, mContext)) {
+                    return;
+                }
+
+                if (numOfTries <= PROVIDER_AVAILABILITY_MAX_RETRIES) {
+                    Log.d(TAG, "MediaProvider is not available. Retry after " +
+                            PROVIDER_AVAILABILITY_CHECK_DELAY);
+                    waitForProviderToBeAvailable(userId, numOfTries + 1);
+                    return;
+                }
+
+                Log.w(TAG, "Failed waiting for MediaProvider for user:" + userId +
+                        " to be available");
+            };
+
+            mHandler.postDelayed(mIsProviderAvailableRunnable, PROVIDER_AVAILABILITY_CHECK_DELAY);
+        }
+
+        private void stopWaitingForProviderToBeAvailable() {
+            if (mIsProviderAvailableRunnable == null) {
+                return;
             }
-            mIsWorkProfileOff = false;
+            mHandler.removeCallbacks(mIsProviderAvailableRunnable);
+            mIsProviderAvailableRunnable = null;
+        }
+
+        private void assertMainThread() {
+            if (Looper.getMainLooper().isCurrentThread()) return;
+
+            throw new IllegalStateException("UserIdManager methods are expected to be called from "
+                    + "main thread. " + (Looper.myLooper() == null ? "" : "Current thread "
+                    + Looper.myLooper().getThread() + ", Main thread "
+                    + Looper.getMainLooper().getThread()));
         }
     }
 }
diff --git a/src/com/android/providers/media/photopicker/ui/TabFragment.java b/src/com/android/providers/media/photopicker/ui/TabFragment.java
index c5626ab..ddc36c5 100644
--- a/src/com/android/providers/media/photopicker/ui/TabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/TabFragment.java
@@ -30,6 +30,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.fragment.app.Fragment;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
 import androidx.lifecycle.ViewModelProvider;
 import androidx.recyclerview.widget.RecyclerView;
 
@@ -38,7 +40,6 @@
 import com.android.providers.media.photopicker.data.Selection;
 import com.android.providers.media.photopicker.data.UserIdManager;
 import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
-import com.android.providers.media.util.ForegroundThread;
 
 import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
 
@@ -49,6 +50,7 @@
  * The base abstract Tab fragment
  */
 public abstract class TabFragment extends Fragment {
+
     protected PickerViewModel mPickerViewModel;
     protected Selection mSelection;
     protected ImageLoader mImageLoader;
@@ -141,6 +143,25 @@
             });
         }
 
+        // 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();
+            });
+        }
+
+        // Observe for multi-user changes.
+        final LiveData<Boolean> isMultiUserProfiles = mUserIdManager.getIsMultiUserProfiles();
+        if (isMultiUserProfiles != null) {
+            isMultiUserProfiles.observe(this, this::setUpProfileButtonWithListeners);
+        }
+    }
+
+    private void setUpListenersForProfileButton() {
         mProfileButton.setOnClickListener(v -> onClickProfileButton());
         mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
             @Override
@@ -156,12 +177,6 @@
     }
 
     @Override
-    public void onResume() {
-        super.onResume();
-        updateProfileButtonAsync();
-    }
-
-    @Override
     public void onDestroy() {
         super.onDestroy();
         if (mRecyclerView != null) {
@@ -169,12 +184,13 @@
         }
     }
 
-    private void updateProfileButtonAsync() {
-        ForegroundThread.getExecutor().execute(() -> {
-            mUserIdManager.updateCrossProfileValues();
-
-            getActivity().runOnUiThread(() -> setUpProfileButton());
-        });
+    private void setUpProfileButtonWithListeners(boolean isMultiUserProfile) {
+        if (isMultiUserProfile) {
+            setUpListenersForProfileButton();
+        } else {
+            mRecyclerView.clearOnScrollListeners();
+        }
+        setUpProfileButton();
     }
 
     private void setUpProfileButton() {
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 b03d9d4..1a4010f 100644
--- a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java
+++ b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java
@@ -18,7 +18,9 @@
 
 import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED;
 import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER;
+import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_EVENT_CALLBACK;
 import static android.provider.CloudMediaProviderContract.METHOD_CREATE_SURFACE_CONTROLLER;
+import static android.provider.CloudMediaProvider.SurfaceEventCallback.PLAYBACK_EVENT_READY;
 
 import static com.android.providers.media.PickerUriResolver.createSurfaceControllerUri;
 
@@ -29,6 +31,8 @@
 import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.provider.ICloudMediaSurfaceController;
+
+import android.provider.ICloudSurfaceEventCallback;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.view.Surface;
@@ -56,6 +60,7 @@
     private final Map<String, SurfaceControllerProxy> mControllers =
             new ArrayMap<>();
     private final SurfaceHolder.Callback mSurfaceHolderCallback = new PreviewSurfaceCallback();
+    private final SurfaceEventCallbackWrapper mSurfaceEventCallbackWrapper;
 
     private Item mCurrentPreviewItem;
     private boolean mIsInBackground = false;
@@ -67,6 +72,7 @@
 
     public RemotePreviewHandler(Context context) {
         mContext = context;
+        mSurfaceEventCallbackWrapper = new SurfaceEventCallbackWrapper();
     }
 
     /**
@@ -164,6 +170,15 @@
         return null;
     }
 
+    private RemotePreviewSession getSessionForSurfaceId(int surfaceId) {
+        for (RemotePreviewSession session : mSessionMap.values()) {
+            if (session.getSurfaceId() == surfaceId) {
+                return session;
+            }
+        }
+        return null;
+    }
+
     @Nullable
     private SurfaceControllerProxy getSurfaceController(String authority) {
         if (mControllers.containsKey(authority)) {
@@ -197,6 +212,7 @@
         Log.i(TAG, "Creating new SurfaceController for authority: " + authority);
         Bundle extras = new Bundle();
         extras.putBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, true);
+        extras.putBinder(EXTRA_SURFACE_EVENT_CALLBACK, mSurfaceEventCallbackWrapper);
         final Bundle surfaceControllerBundle = mContext.getContentResolver().call(
                 createSurfaceControllerUri(authority),
                 METHOD_CREATE_SURFACE_CONTROLLER, /* arg */ null, extras);
@@ -206,6 +222,32 @@
                 : null;
     }
 
+    /**
+     * Wrapper class for {@link ICloudSurfaceEventCallback} interface implementation.
+     */
+    private final class SurfaceEventCallbackWrapper extends ICloudSurfaceEventCallback.Stub {
+
+        @Override
+        public void onPlaybackEvent(int surfaceId, int eventType, Bundle eventInfo) {
+            final RemotePreviewSession session = getSessionForSurfaceId(surfaceId);
+
+            if (session == null) {
+                Log.w(TAG, "No RemotePreviewSession found.");
+                return;
+            }
+            switch (eventType) {
+                case PLAYBACK_EVENT_READY:
+                    session.playMedia();
+                    return;
+                default:
+                    Log.d(TAG, "RemotePreviewHandler onPlaybackEvent for surfaceId: " +
+                            surfaceId + " ; media id: " + session.getMediaId() +
+                            " ; eventType: " + eventType + " ; eventInfo: " + eventInfo);
+
+            }
+        }
+    }
+
     private final class PreviewSurfaceCallback implements SurfaceHolder.Callback {
 
         @Override
diff --git a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java
index 3f8fc4d..35b421b 100644
--- a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java
+++ b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java
@@ -44,6 +44,10 @@
         this.mSurfaceController = surfaceController;
     }
 
+    int getSurfaceId() {
+        return mSurfaceId;
+    }
+
     @NonNull
     String getMediaId() {
         return mMediaId;
diff --git a/src/com/android/providers/media/photopicker/util/CrossProfileUtils.java b/src/com/android/providers/media/photopicker/util/CrossProfileUtils.java
index a9b345c..301b623 100644
--- a/src/com/android/providers/media/photopicker/util/CrossProfileUtils.java
+++ b/src/com/android/providers/media/photopicker/util/CrossProfileUtils.java
@@ -21,6 +21,7 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.os.UserManager;
 import android.provider.MediaStore;
 import android.util.Log;
 
@@ -73,4 +74,16 @@
         }
         return false;
     }
+
+    /**
+     * Whether the given profile is in quiet mode or not.
+     * Notes: Quiet mode is only supported for managed profiles.
+     *
+     * @param userId The user id of the profile to be queried.
+     * @return true if the profile is in quiet mode, false otherwise.
+     */
+    public static boolean isQuietModeEnabled(UserId userId, Context context) {
+        final UserManager userManager = context.getSystemService(UserManager.class);
+        return userManager.isQuietModeEnabled(userId.getUserHandle());
+    }
 }
diff --git a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
index 3b80a1d..b5f708a 100644
--- a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
+++ b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
@@ -115,6 +115,19 @@
     }
 
     /**
+     * Reset to personal profile mode.
+     */
+    public void resetToPersonalProfile() {
+        // 1. Clear Selected items
+        mSelection.clearSelectedItems();
+        // 2. Change profile to personal user
+        mUserIdManager.setPersonalAsCurrentUserProfile();
+        // 3. Update Item and Category lists
+        updateItems();
+        updateCategories();
+    }
+
+    /**
      * @return the list of Items with all photos and videos {@link #mItemList} on the device.
      */
     public LiveData<List<Item>> getItems() {
@@ -124,9 +137,8 @@
         return mItemList;
     }
 
-    private List<Item> loadItems(@Nullable @CategoryType String category) {
+    private List<Item> loadItems(@Nullable @CategoryType String category, UserId userId) {
         final List<Item> items = new ArrayList<>();
-        final UserId userId = mUserIdManager.getCurrentUserProfileId();
 
         try (Cursor cursor = mItemsProvider.getItems(category, /* offset */ 0,
                 /* limit */ -1, mMimeTypeFilter, userId)) {
@@ -179,8 +191,9 @@
     }
 
     private void loadItemsAsync() {
+        final UserId userId = mUserIdManager.getCurrentUserProfileId();
         ForegroundThread.getExecutor().execute(() -> {
-            mItemList.postValue(loadItems(/* category= */ null));
+            mItemList.postValue(loadItems(/* category= */ null, userId));
         });
     }
 
@@ -207,8 +220,9 @@
     }
 
     private void loadCategoryItemsAsync(@NonNull @CategoryType String category) {
+        final UserId userId = mUserIdManager.getCurrentUserProfileId();
         ForegroundThread.getExecutor().execute(() -> {
-            mCategoryItemList.postValue(loadItems(category));
+            mCategoryItemList.postValue(loadItems(category, userId));
         });
     }
 
@@ -232,9 +246,8 @@
         return mCategoryList;
     }
 
-    private List<Category> loadCategories() {
+    private List<Category> loadCategories(UserId userId) {
         final List<Category> categoryList = new ArrayList<>();
-        final UserId userId = mUserIdManager.getCurrentUserProfileId();
         try (final Cursor cursor = mItemsProvider.getCategories(mMimeTypeFilter, userId)) {
             if (cursor == null || cursor.getCount() == 0) {
                 Log.d(TAG, "Didn't receive any categories, either cursor is null or"
@@ -254,8 +267,9 @@
     }
 
     private void loadCategoriesAsync() {
+        final UserId userId = mUserIdManager.getCurrentUserProfileId();
         ForegroundThread.getExecutor().execute(() -> {
-            mCategoryList.postValue(loadCategories());
+            mCategoryList.postValue(loadCategories(userId));
         });
     }
 
diff --git a/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java b/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
index 0ccbafe..49dfaf6 100644
--- a/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
+++ b/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
@@ -391,12 +391,12 @@
     public void testGetItems_sortOrder() throws Exception {
         try {
             final long timeNow = System.nanoTime() / 1000;
+            final Uri imageFileDateNowPlus1Uri = prepareFileAndGetUri(
+                    new File(getDownloadsDir(),  "latest_" + IMAGE_FILE_NAME), timeNow + 1000);
             final Uri imageFileDateNowUri
-                    = createFileAndGet(getDcimDir(), IMAGE_FILE_NAME, timeNow);
+                    = prepareFileAndGetUri(new File(getDcimDir(), IMAGE_FILE_NAME), timeNow);
             final Uri videoFileDateNowUri
-                    = createFileAndGet(getCameraDir(), VIDEO_FILE_NAME, timeNow);
-            final Uri imageFileDateNowPlus1Uri = createFileAndGet(getDownloadsDir(),
-                    "latest_" + IMAGE_FILE_NAME, timeNow + 1000);
+                    = prepareFileAndGetUri(new File(getCameraDir(), VIDEO_FILE_NAME), timeNow);
 
             // This is the list of uris based on the expected sort order of items returned by
             // ItemsProvider#getItems
@@ -747,19 +747,16 @@
         return assertCreateNewFile(getDownloadsDir(), IMAGE_FILE_NAME);
     }
 
-    private File assertCreateNewFile(File dir, String fileName) throws Exception {
-        if (!dir.exists()) {
-            dir.mkdirs();
-        }
-        assertThat(dir.exists()).isTrue();
-
-        final File file = new File(dir, fileName);
-        createAndPrepareFile(file);
+    private File assertCreateNewFile(File parentDir, String fileName) throws Exception {
+        final File file = new File(parentDir, fileName);
+        prepareFileAndGetUri(file, /* lastModifiedTime */ -1);
 
         return file;
     }
 
-    private Uri createAndPrepareFile(File file) throws IOException {
+    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
@@ -767,12 +764,26 @@
             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 File getDownloadsDir() {
         return new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS);
     }
@@ -825,14 +836,4 @@
         }
 
     }
-
-    private Uri createFileAndGet(File parent, String fileName, long lastModifiedTime)
-            throws IOException {
-        final File file = new File(parent, fileName);
-        final Uri uri = createAndPrepareFile(file);
-
-        assertWithMessage("Uri obtained by scanning file " + file)
-                .that(uri).isNotNull();
-        return uri;
-    }
 }
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 4a7eaa1..ec68646 100644
--- a/tests/src/com/android/providers/media/photopicker/data/SelectionTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/SelectionTest.java
@@ -57,7 +57,9 @@
 
         final Context context = InstrumentationRegistry.getTargetContext();
         when(mApplication.getApplicationContext()).thenReturn(context);
-        mSelection = new PickerViewModel(mApplication).getSelection();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            mSelection = new PickerViewModel(mApplication).getSelection();
+        });
     }
 
     @Test
@@ -90,6 +92,23 @@
     }
 
     @Test
+    public void testClearSelectedItem() {
+        final String id = "1";
+        final Item item = generateFakeImageItem(id);
+        final String id2 = "2";
+        final Item item2 = generateFakeImageItem(id2);
+
+        assertThat(mSelection.getSelectedItemCount().getValue()).isEqualTo(0);
+
+        mSelection.addSelectedItem(item);
+        mSelection.addSelectedItem(item2);
+        assertThat(mSelection.getSelectedItemCount().getValue()).isEqualTo(2);
+
+        mSelection.clearSelectedItems();
+        assertThat(mSelection.getSelectedItemCount().getValue()).isEqualTo(0);
+    }
+
+    @Test
     public void testSetSelectedItem() {
         final String id1 = "1";
         final Item item1 = generateFakeImageItem(id1);
diff --git a/tests/src/com/android/providers/media/photopicker/data/UserIdManagerTest.java b/tests/src/com/android/providers/media/photopicker/data/UserIdManagerTest.java
index 93d3cc8..83f8fab 100644
--- a/tests/src/com/android/providers/media/photopicker/data/UserIdManagerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/UserIdManagerTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -26,6 +27,8 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 
+import androidx.test.platform.app.InstrumentationRegistry;
+
 import com.android.providers.media.photopicker.data.model.UserId;
 
 import org.junit.Before;
@@ -60,19 +63,34 @@
         when(mockContext.getSystemService(UserManager.class)).thenReturn(mockUserManager);
     }
 
+    @Test
+    public void testUserIdManagerThrowsErrorIfCalledFromNonMainThread() {
+        UserId currentUser = UserId.of(personalUser);
+        initializeUserIdManager(currentUser, Arrays.asList(personalUser));
+
+        assertThrows(IllegalStateException.class, () -> userIdManager.isMultiUserProfiles());
+        assertThrows(IllegalStateException.class, () -> userIdManager.getCurrentUserProfileId());
+        assertThrows(IllegalStateException.class, () -> userIdManager.isPersonalUserId());
+        assertThrows(IllegalStateException.class, () -> userIdManager.isManagedUserId());
+        assertThrows(IllegalStateException.class, () -> userIdManager.getPersonalUserId());
+        assertThrows(IllegalStateException.class, () -> userIdManager.getManagedUserId());
+    }
+
     // common cases for User Profiles
     @Test
     public void testUserIds_personaUser_currentUserIsPersonalUser() {
         // Returns the current user if there is only 1 user.
         UserId currentUser = UserId.of(personalUser);
         initializeUserIdManager(currentUser, Arrays.asList(personalUser));
-        assertThat(userIdManager.isMultiUserProfiles()).isFalse();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            assertThat(userIdManager.isMultiUserProfiles()).isFalse();
 
-        assertThat(userIdManager.isPersonalUserId()).isFalse();
-        assertThat(userIdManager.isManagedUserId()).isFalse();
+            assertThat(userIdManager.isPersonalUserId()).isFalse();
+            assertThat(userIdManager.isManagedUserId()).isFalse();
 
-        assertThat(userIdManager.getPersonalUserId()).isNull();
-        assertThat(userIdManager.getManagedUserId()).isNull();
+            assertThat(userIdManager.getPersonalUserId()).isNull();
+            assertThat(userIdManager.getManagedUserId()).isNull();
+        });
     }
 
     @Test
@@ -80,13 +98,15 @@
         // Returns both if there are personal and managed users.
         UserId currentUser = UserId.of(personalUser);
         initializeUserIdManager(currentUser, Arrays.asList(personalUser, managedUser1));
-        assertThat(userIdManager.isMultiUserProfiles()).isTrue();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            assertThat(userIdManager.isMultiUserProfiles()).isTrue();
 
-        assertThat(userIdManager.isPersonalUserId()).isTrue();
-        assertThat(userIdManager.isManagedUserId()).isFalse();
+            assertThat(userIdManager.isPersonalUserId()).isTrue();
+            assertThat(userIdManager.isManagedUserId()).isFalse();
 
-        assertThat(userIdManager.getPersonalUserId()).isEqualTo(currentUser);
-        assertThat(userIdManager.getManagedUserId()).isEqualTo(UserId.of(managedUser1));
+            assertThat(userIdManager.getPersonalUserId()).isEqualTo(currentUser);
+            assertThat(userIdManager.getManagedUserId()).isEqualTo(UserId.of(managedUser1));
+        });
     }
 
     @Test
@@ -94,13 +114,15 @@
         // Returns both if there are system and managed users.
         UserId currentUser = UserId.of(managedUser1);
         initializeUserIdManager(currentUser, Arrays.asList(personalUser, managedUser1));
-        assertThat(userIdManager.isMultiUserProfiles()).isTrue();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            assertThat(userIdManager.isMultiUserProfiles()).isTrue();
 
-        assertThat(userIdManager.isPersonalUserId()).isFalse();
-        assertThat(userIdManager.isManagedUserId()).isTrue();
+            assertThat(userIdManager.isPersonalUserId()).isFalse();
+            assertThat(userIdManager.isManagedUserId()).isTrue();
 
-        assertThat(userIdManager.getPersonalUserId()).isEqualTo(UserId.of(personalUser));
-        assertThat(userIdManager.getManagedUserId()).isEqualTo(currentUser);
+            assertThat(userIdManager.getPersonalUserId()).isEqualTo(UserId.of(personalUser));
+            assertThat(userIdManager.getManagedUserId()).isEqualTo(currentUser);
+        });
     }
 
     // other cases for User Profiles involving different users
@@ -109,28 +131,31 @@
         // When there is no managed user, returns the current user.
         UserId currentUser = UserId.of(otherUser2);
         initializeUserIdManager(currentUser, Arrays.asList(otherUser1, otherUser2));
-        assertThat(userIdManager.isMultiUserProfiles()).isFalse();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            assertThat(userIdManager.isMultiUserProfiles()).isFalse();
 
-        assertThat(userIdManager.isPersonalUserId()).isFalse();
-        assertThat(userIdManager.isManagedUserId()).isFalse();
+            assertThat(userIdManager.isPersonalUserId()).isFalse();
+            assertThat(userIdManager.isManagedUserId()).isFalse();
 
-        assertThat(userIdManager.getPersonalUserId()).isNull();
-        assertThat(userIdManager.getManagedUserId()).isNull();
+            assertThat(userIdManager.getPersonalUserId()).isNull();
+            assertThat(userIdManager.getManagedUserId()).isNull();
+        });
     }
 
     @Test
-    public void testUserIds_otherUserAndManagedUserAndPersonalUser_currentUserIsOtherUser(
-        ) {
+    public void testUserIds_otherUserAndManagedUserAndPersonalUser_currentUserIsOtherUser() {
         UserId currentUser = UserId.of(otherUser1);
         initializeUserIdManager(currentUser, Arrays.asList(otherUser1, managedUser1,
                 personalUser));
-        assertThat(userIdManager.isMultiUserProfiles()).isFalse();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            assertThat(userIdManager.isMultiUserProfiles()).isFalse();
 
-        assertThat(userIdManager.isPersonalUserId()).isFalse();
-        assertThat(userIdManager.isManagedUserId()).isFalse();
+            assertThat(userIdManager.isPersonalUserId()).isFalse();
+            assertThat(userIdManager.isManagedUserId()).isFalse();
 
-        assertThat(userIdManager.getPersonalUserId()).isNull();
-        assertThat(userIdManager.getManagedUserId()).isNull();
+            assertThat(userIdManager.getPersonalUserId()).isNull();
+            assertThat(userIdManager.getManagedUserId()).isNull();
+        });
     }
 
     @Test
@@ -141,27 +166,33 @@
         UserId currentUser = UserId.of(managedUser1);
         initializeUserIdManager(currentUser, Arrays.asList(otherUser1, managedUser1,
                 personalUser));
-        assertThat(userIdManager.isMultiUserProfiles()).isTrue();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            assertThat(userIdManager.isMultiUserProfiles()).isTrue();
 
-        assertThat(userIdManager.isPersonalUserId()).isFalse();
-        assertThat(userIdManager.isManagedUserId()).isTrue();
+            assertThat(userIdManager.isPersonalUserId()).isFalse();
+            assertThat(userIdManager.isManagedUserId()).isTrue();
 
-        assertThat(userIdManager.getPersonalUserId()).isEqualTo(UserId.of(personalUser));
-        assertThat(userIdManager.getManagedUserId()).isEqualTo(currentUser);
+            assertThat(userIdManager.getPersonalUserId()).isEqualTo(UserId.of(personalUser));
+            assertThat(userIdManager.getManagedUserId()).isEqualTo(currentUser);
+        });
     }
 
     @Test
     public void testUserIds_personalUserAndManagedUser_returnCachedList() {
         UserId currentUser = UserId.of(personalUser);
         initializeUserIdManager(currentUser, Arrays.asList(personalUser, managedUser1));
-        assertThat(userIdManager.getPersonalUserId()).isSameInstanceAs(
-                userIdManager.getPersonalUserId());
-        assertThat(userIdManager.getManagedUserId()).isSameInstanceAs(
-                userIdManager.getManagedUserId());
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            assertThat(userIdManager.getPersonalUserId()).isSameInstanceAs(
+                    userIdManager.getPersonalUserId());
+            assertThat(userIdManager.getManagedUserId()).isSameInstanceAs(
+                    userIdManager.getManagedUserId());
+        });
     }
 
     private void initializeUserIdManager(UserId current, List<UserHandle> usersOnDevice) {
         when(mockUserManager.getUserProfiles()).thenReturn(usersOnDevice);
-        userIdManager = new UserIdManager.RuntimeUserIdManager(mockContext, current);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            userIdManager = new UserIdManager.RuntimeUserIdManager(mockContext, current);
+        });
     }
 }
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 9b995f5..fc5c5a5 100644
--- a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
+++ b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
@@ -20,6 +20,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import android.app.Application;
@@ -35,6 +36,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.providers.media.photopicker.data.ItemsProvider;
+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.ItemTest;
@@ -73,9 +75,14 @@
 
         final Context context = InstrumentationRegistry.getTargetContext();
         when(mApplication.getApplicationContext()).thenReturn(context);
-        mPickerViewModel = new PickerViewModel(mApplication);
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            mPickerViewModel = new PickerViewModel(mApplication);
+        });
         mItemsProvider = new TestItemsProvider(context);
         mPickerViewModel.setItemsProvider(mItemsProvider);
+        UserIdManager userIdManager = mock(UserIdManager.class);
+        when(userIdManager.getCurrentUserProfileId()).thenReturn(UserId.CURRENT_USER);
+        mPickerViewModel.setUserIdManager(userIdManager);
     }
 
     @Test