Merge "[DO NOT MERGE] Bouncer - Update selected item text color" into tm-dev
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index acdab1b..ae16e01 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -3925,15 +3925,32 @@
      *         task will be moved to the back of the activity stack instead of being finished.
      *         Other activities will simply be finished.
      *
-     *         <p>If you target version {@link android.os.Build.VERSION_CODES#S} or later and
-     *         override this method, it is strongly recommended to call through to the superclass
+     *      <li><p>If you target version {@link android.os.Build.VERSION_CODES#S} and
+     *         override this method, we strongly recommend to call through to the superclass
      *         implementation after you finish handling navigation within the app.
+     *
+     *      <li><p>If you target version {@link android.os.Build.VERSION_CODES#TIRAMISU} or later,
+     *          you should not use this method but register an {@link OnBackInvokedCallback} on an
+     *          {@link OnBackInvokedDispatcher} that you can retrieve using
+     *          {@link #getOnBackInvokedDispatcher()}. You should also set
+     *          {@code android:enableOnBackInvokedCallback="true"} in the application manifest.
+     *          <p>Alternatively, you can use
+     *          {@code  androidx.activity.ComponentActivity#getOnBackPressedDispatcher()}
+     *          for backward compatibility.
      * </ul>
      *
      * @see #moveTaskToBack(boolean)
      *
      * @deprecated Use {@link OnBackInvokedCallback} or
      * {@code androidx.activity.OnBackPressedCallback} to handle back navigation instead.
+     * <p>
+     * Starting from Android 13 (API level 33), back event handling is
+     * moving to an ahead-of-time model and {@link Activity#onBackPressed()} and
+     * {@link KeyEvent#KEYCODE_BACK} should not be used to handle back events (back gesture or
+     * back button click). Instead, an {@link OnBackInvokedCallback} should be registered using
+     * {@link Activity#getOnBackInvokedDispatcher()}
+     * {@link OnBackInvokedDispatcher#registerOnBackInvokedCallback(int, OnBackInvokedCallback)
+     * .registerOnBackInvokedCallback(priority, callback)}.
      */
     @Deprecated
     public void onBackPressed() {
diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java
index f0af9ba..33cf712 100644
--- a/core/java/android/app/Dialog.java
+++ b/core/java/android/app/Dialog.java
@@ -720,8 +720,27 @@
      * key.  The default implementation simply cancels the dialog (only if
      * it is cancelable), but you can override this to do whatever you want.
      *
+     * <p>
+     * If you target version {@link android.os.Build.VERSION_CODES#TIRAMISU} or later, you
+     * should not use this method but register an {@link OnBackInvokedCallback} on an
+     * {@link OnBackInvokedDispatcher} that you can retrieve using
+     * {@link #getOnBackInvokedDispatcher()}. You should also set
+     * {@code android:enableOnBackInvokedCallback="true"} in the application manifest.
+     *
+     * <p>Alternatively, you
+     * can use {@code androidx.activity.ComponentDialog#getOnBackPressedDispatcher()}
+     * for backward compatibility.
+     *
      * @deprecated Use {@link OnBackInvokedCallback} or
      * {@code androidx.activity.OnBackPressedCallback} to handle back navigation instead.
+     * <p>
+     * Starting from Android 13 (API level 33), back event handling is
+     * moving to an ahead-of-time model and {@link #onBackPressed()} and
+     * {@link KeyEvent#KEYCODE_BACK} should not be used to handle back events (back gesture or
+     * back button click). Instead, an {@link OnBackInvokedCallback} should be registered using
+     * {@link Dialog#getOnBackInvokedDispatcher()}
+     * {@link OnBackInvokedDispatcher#registerOnBackInvokedCallback(int, OnBackInvokedCallback)
+     * .registerOnBackInvokedCallback(priority, callback)}.
      */
     @Deprecated
     public void onBackPressed() {
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 20a2bdf..1ef1ac5 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -15040,6 +15040,14 @@
          */
         public static final String DEVICE_CONFIG_SYNC_DISABLED = "device_config_sync_disabled";
 
+
+        /**
+         * Whether back preview animations are played when user does a back gesture or presses
+         * the back button.
+         * @hide
+         */
+        public static final String ENABLE_BACK_ANIMATION = "enable_back_animation";
+
         /** @hide */ public static String zenModeToString(int mode) {
             if (mode == ZEN_MODE_IMPORTANT_INTERRUPTIONS) return "ZEN_MODE_IMPORTANT_INTERRUPTIONS";
             if (mode == ZEN_MODE_ALARMS) return "ZEN_MODE_ALARMS";
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 8115eaf..82f8a13 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -81,9 +81,12 @@
     @NonNull
     private Consumer<List<SplitInfo>> mEmbeddingCallback;
     private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>();
+    private final Handler mHandler;
 
     public SplitController() {
-        mPresenter = new SplitPresenter(new MainThreadExecutor(), this);
+        final MainThreadExecutor executor = new MainThreadExecutor();
+        mHandler = executor.mHandler;
+        mPresenter = new SplitPresenter(executor, this);
         ActivityThread activityThread = ActivityThread.currentActivityThread();
         // Register a callback to be notified about activities being created.
         activityThread.getApplication().registerActivityLifecycleCallbacks(
@@ -167,11 +170,13 @@
                 // to fullscreen.
                 cleanupForEnterPip(wct, container);
                 mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct);
-            } else {
+            } else if (taskFragmentInfo.isTaskClearedForReuse()) {
                 // Do not finish the dependents if this TaskFragment was cleared due to launching
                 // activity in the Task.
-                final boolean shouldFinishDependent = !taskFragmentInfo.isTaskClearedForReuse();
-                mPresenter.cleanupContainer(container, shouldFinishDependent, wct);
+                mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct);
+            } else if (!container.isWaitingActivityAppear()) {
+                // Do not finish the container before the expected activity appear until timeout.
+                mPresenter.cleanupContainer(container, true /* shouldFinishDependent */, wct);
             }
         } else if (wasInPip && isInPip) {
             // No update until exit PIP.
@@ -418,6 +423,14 @@
     }
 
     /**
+     * Called when we have been waiting too long for the TaskFragment to become non-empty after
+     * creation.
+     */
+    void onTaskFragmentAppearEmptyTimeout(@NonNull TaskFragmentContainer container) {
+        mPresenter.cleanupContainer(container, false /* shouldFinishDependent */);
+    }
+
+    /**
      * Returns a container that this activity is registered with. An activity can only belong to one
      * container, or no container at all.
      */
@@ -452,7 +465,7 @@
         if (activityInTask == null) {
             throw new IllegalArgumentException("activityInTask must not be null,");
         }
-        final TaskFragmentContainer container = new TaskFragmentContainer(activity, taskId);
+        final TaskFragmentContainer container = new TaskFragmentContainer(activity, taskId, this);
         if (!mTaskContainers.contains(taskId)) {
             mTaskContainers.put(taskId, new TaskContainer(taskId));
         }
@@ -590,7 +603,12 @@
         }
         for (int i = taskContainer.mContainers.size() - 1; i >= 0; i--) {
             final TaskFragmentContainer container = taskContainer.mContainers.get(i);
-            if (!container.isFinished() && container.getRunningActivityCount() > 0) {
+            if (!container.isFinished() && (container.getRunningActivityCount() > 0
+                    // We may be waiting for the top TaskFragment to become non-empty after
+                    // creation. In that case, we don't want to treat the TaskFragment below it as
+                    // top active, otherwise it may incorrectly launch placeholder on top of the
+                    // pending TaskFragment.
+                    || container.isWaitingActivityAppear())) {
                 return container;
             }
         }
@@ -920,6 +938,10 @@
         return mTaskContainers.get(taskId);
     }
 
+    Handler getHandler() {
+        return mHandler;
+    }
+
     /**
      * Returns {@code true} if an Activity with the provided component name should always be
      * expanded to occupy full task bounds. Such activity must not be put in a split.
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 03f38ed..35981d3 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -30,6 +30,8 @@
 import android.window.TaskFragmentInfo;
 import android.window.WindowContainerTransaction;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
@@ -39,6 +41,11 @@
  * on the server side.
  */
 class TaskFragmentContainer {
+    private static final int APPEAR_EMPTY_TIMEOUT_MS = 3000;
+
+    @NonNull
+    private final SplitController mController;
+
     /**
      * Client-created token that uniquely identifies the task fragment container instance.
      */
@@ -51,7 +58,8 @@
     /**
      * Server-provided task fragment information.
      */
-    private TaskFragmentInfo mInfo;
+    @VisibleForTesting
+    TaskFragmentInfo mInfo;
 
     /**
      * Activities that are being reparented or being started to this container, but haven't been
@@ -81,10 +89,20 @@
     private int mLastRequestedWindowingMode = WINDOWING_MODE_UNDEFINED;
 
     /**
+     * When the TaskFragment has appeared in server, but is empty, we should remove the TaskFragment
+     * if it is still empty after the timeout.
+     */
+    @VisibleForTesting
+    @Nullable
+    Runnable mAppearEmptyTimeout;
+
+    /**
      * Creates a container with an existing activity that will be re-parented to it in a window
      * container transaction.
      */
-    TaskFragmentContainer(@Nullable Activity activity, int taskId) {
+    TaskFragmentContainer(@Nullable Activity activity, int taskId,
+            @NonNull SplitController controller) {
+        mController = controller;
         mToken = new Binder("TaskFragmentContainer");
         if (taskId == INVALID_TASK_ID) {
             throw new IllegalArgumentException("Invalid Task id");
@@ -155,12 +173,30 @@
         return count;
     }
 
+    /** Whether we are waiting for the TaskFragment to appear and become non-empty. */
+    boolean isWaitingActivityAppear() {
+        return !mIsFinished && (mInfo == null || mAppearEmptyTimeout != null);
+    }
+
     @Nullable
     TaskFragmentInfo getInfo() {
         return mInfo;
     }
 
     void setInfo(@NonNull TaskFragmentInfo info) {
+        if (!mIsFinished && mInfo == null && info.isEmpty()) {
+            // onTaskFragmentAppeared with empty info. We will remove the TaskFragment if it is
+            // still empty after timeout.
+            mAppearEmptyTimeout = () -> {
+                mAppearEmptyTimeout = null;
+                mController.onTaskFragmentAppearEmptyTimeout(this);
+            };
+            mController.getHandler().postDelayed(mAppearEmptyTimeout, APPEAR_EMPTY_TIMEOUT_MS);
+        } else if (mAppearEmptyTimeout != null && !info.isEmpty()) {
+            mController.getHandler().removeCallbacks(mAppearEmptyTimeout);
+            mAppearEmptyTimeout = null;
+        }
+
         mInfo = info;
         if (mInfo == null || mPendingAppearedActivities.isEmpty()) {
             return;
@@ -234,6 +270,10 @@
             @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) {
         if (!mIsFinished) {
             mIsFinished = true;
+            if (mAppearEmptyTimeout != null) {
+                mController.getHandler().removeCallbacks(mAppearEmptyTimeout);
+                mAppearEmptyTimeout = null;
+            }
             finishActivities(shouldFinishDependent, presenter, wct, controller);
         }
 
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
index 1f12c448..7aa47ef 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
@@ -18,6 +18,7 @@
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
@@ -29,6 +30,7 @@
 
 import android.content.res.Configuration;
 import android.graphics.Point;
+import android.os.Handler;
 import android.platform.test.annotations.Presubmit;
 import android.window.TaskFragmentInfo;
 import android.window.WindowContainerToken;
@@ -61,6 +63,10 @@
     private WindowContainerTransaction mTransaction;
     @Mock
     private JetpackTaskFragmentOrganizer.TaskFragmentCallback mCallback;
+    @Mock
+    private SplitController mSplitController;
+    @Mock
+    private Handler mHandler;
     private JetpackTaskFragmentOrganizer mOrganizer;
 
     @Before
@@ -69,6 +75,7 @@
         mOrganizer = new JetpackTaskFragmentOrganizer(Runnable::run, mCallback);
         mOrganizer.registerOrganizer();
         spyOn(mOrganizer);
+        doReturn(mHandler).when(mSplitController).getHandler();
     }
 
     @Test
@@ -106,7 +113,8 @@
 
     @Test
     public void testExpandTaskFragment() {
-        final TaskFragmentContainer container = new TaskFragmentContainer(null, TASK_ID);
+        final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
+                TASK_ID, mSplitController);
         final TaskFragmentInfo info = createMockInfo(container);
         mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info);
         container.setInfo(info);
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index 32c61ea..983208c 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -35,6 +35,7 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
+import android.os.Handler;
 import android.platform.test.annotations.Presubmit;
 import android.window.TaskFragmentInfo;
 import android.window.WindowContainerTransaction;
@@ -48,6 +49,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -71,6 +73,9 @@
     private TaskFragmentInfo mInfo;
     @Mock
     private WindowContainerTransaction mTransaction;
+    @Mock
+    private Handler mHandler;
+
     private SplitController mSplitController;
     private SplitPresenter mSplitPresenter;
 
@@ -86,6 +91,7 @@
         activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS);
         doReturn(mActivityResources).when(mActivity).getResources();
         doReturn(activityConfig).when(mActivityResources).getConfiguration();
+        doReturn(mHandler).when(mSplitController).getHandler();
     }
 
     @Test
@@ -94,28 +100,45 @@
         // tf3 is finished so is not active.
         TaskFragmentContainer tf3 = mock(TaskFragmentContainer.class);
         doReturn(true).when(tf3).isFinished();
+        doReturn(false).when(tf3).isWaitingActivityAppear();
         // tf2 has running activity so is active.
         TaskFragmentContainer tf2 = mock(TaskFragmentContainer.class);
         doReturn(1).when(tf2).getRunningActivityCount();
         // tf1 has no running activity so is not active.
-        TaskFragmentContainer tf1 = new TaskFragmentContainer(null, TASK_ID);
+        TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, TASK_ID,
+                mSplitController);
 
-        taskContainer.mContainers.add(tf3);
-        taskContainer.mContainers.add(tf2);
         taskContainer.mContainers.add(tf1);
+        taskContainer.mContainers.add(tf2);
+        taskContainer.mContainers.add(tf3);
         mSplitController.mTaskContainers.put(TASK_ID, taskContainer);
 
         assertWithMessage("Must return tf2 because tf3 is not active.")
                 .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf2);
 
-        taskContainer.mContainers.remove(tf1);
+        taskContainer.mContainers.remove(tf3);
 
         assertWithMessage("Must return tf2 because tf2 has running activity.")
                 .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf2);
 
         taskContainer.mContainers.remove(tf2);
 
-        assertWithMessage("Must return null because tf1 has no running activity.")
+        assertWithMessage("Must return tf because we are waiting for tf1 to appear.")
+                .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf1);
+
+        final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
+        doReturn(new ArrayList<>()).when(info).getActivities();
+        doReturn(true).when(info).isEmpty();
+        tf1.setInfo(info);
+
+        assertWithMessage("Must return tf because we are waiting for tf1 to become non-empty after"
+                + " creation.")
+                .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf1);
+
+        doReturn(false).when(info).isEmpty();
+        tf1.setInfo(info);
+
+        assertWithMessage("Must return null because tf1 becomes empty.")
                 .that(mSplitController.getTopActiveContainer(TASK_ID)).isNull();
     }
 
@@ -133,6 +156,14 @@
     }
 
     @Test
+    public void testOnTaskFragmentAppearEmptyTimeout() {
+        final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID);
+        mSplitController.onTaskFragmentAppearEmptyTimeout(tf);
+
+        verify(mSplitPresenter).cleanupContainer(tf, false /* shouldFinishDependent */);
+    }
+
+    @Test
     public void testNewContainer() {
         // Must pass in a valid activity.
         assertThrows(IllegalArgumentException.class, () ->
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
index c7feb7e..c40bab8 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
@@ -32,8 +32,11 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 /**
  * Test class for {@link TaskContainer}.
@@ -48,6 +51,14 @@
     private static final int TASK_ID = 10;
     private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200);
 
+    @Mock
+    private SplitController mController;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+    }
+
     @Test
     public void testIsTaskBoundsInitialized() {
         final TaskContainer taskContainer = new TaskContainer(TASK_ID);
@@ -126,7 +137,8 @@
 
         assertTrue(taskContainer.isEmpty());
 
-        final TaskFragmentContainer tf = new TaskFragmentContainer(null, TASK_ID);
+        final TaskFragmentContainer tf = new TaskFragmentContainer(null /* activity */, TASK_ID,
+                mController);
         taskContainer.mContainers.add(tf);
 
         assertFalse(taskContainer.isEmpty());
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
index 97896c2..d80f2b9 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
@@ -18,12 +18,18 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
+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.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 
 import android.app.Activity;
+import android.os.Handler;
 import android.platform.test.annotations.Presubmit;
 import android.window.TaskFragmentInfo;
 import android.window.WindowContainerTransaction;
@@ -59,15 +65,19 @@
     private Activity mActivity;
     @Mock
     private TaskFragmentInfo mInfo;
+    @Mock
+    private Handler mHandler;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+        doReturn(mHandler).when(mController).getHandler();
     }
 
     @Test
     public void testFinish() {
-        final TaskFragmentContainer container = new TaskFragmentContainer(mActivity, TASK_ID);
+        final TaskFragmentContainer container = new TaskFragmentContainer(mActivity, TASK_ID,
+                mController);
         final WindowContainerTransaction wct = new WindowContainerTransaction();
 
         // Only remove the activity, but not clear the reference until appeared.
@@ -94,4 +104,62 @@
         verify(mPresenter).deleteTaskFragment(wct, container.getTaskFragmentToken());
         verify(mController).removeContainer(container);
     }
+
+    @Test
+    public void testIsWaitingActivityAppear() {
+        final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
+                TASK_ID, mController);
+
+        assertTrue(container.isWaitingActivityAppear());
+
+        final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
+        doReturn(new ArrayList<>()).when(info).getActivities();
+        doReturn(true).when(info).isEmpty();
+        container.setInfo(info);
+
+        assertTrue(container.isWaitingActivityAppear());
+
+        doReturn(false).when(info).isEmpty();
+        container.setInfo(info);
+
+        assertFalse(container.isWaitingActivityAppear());
+    }
+
+    @Test
+    public void testAppearEmptyTimeout() {
+        final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
+                TASK_ID, mController);
+
+        assertNull(container.mAppearEmptyTimeout);
+
+        // Not set if it is not appeared empty.
+        final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
+        doReturn(new ArrayList<>()).when(info).getActivities();
+        doReturn(false).when(info).isEmpty();
+        container.setInfo(info);
+
+        assertNull(container.mAppearEmptyTimeout);
+
+        // Set timeout if the first info set is empty.
+        container.mInfo = null;
+        doReturn(true).when(info).isEmpty();
+        container.setInfo(info);
+
+        assertNotNull(container.mAppearEmptyTimeout);
+
+        // Remove timeout after the container becomes non-empty.
+        doReturn(false).when(info).isEmpty();
+        container.setInfo(info);
+
+        assertNull(container.mAppearEmptyTimeout);
+
+        // Running the timeout will call into SplitController.onTaskFragmentAppearEmptyTimeout.
+        container.mInfo = null;
+        doReturn(true).when(info).isEmpty();
+        container.setInfo(info);
+        container.mAppearEmptyTimeout.run();
+
+        assertNull(container.mAppearEmptyTimeout);
+        verify(mController).onTaskFragmentAppearEmptyTimeout(container);
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index ced36a7..c3fbe55 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -24,12 +24,18 @@
 import android.app.ActivityTaskManager;
 import android.app.IActivityTaskManager;
 import android.app.WindowConfiguration;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.database.ContentObserver;
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.hardware.HardwareBuffer;
+import android.net.Uri;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.Settings.Global;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.RemoteAnimationTarget;
@@ -42,22 +48,27 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.annotations.ShellBackgroundThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 
+import java.util.concurrent.atomic.AtomicBoolean;
+
 /**
  * Controls the window animation run when a user initiates a back gesture.
  */
 public class BackAnimationController implements RemoteCallable<BackAnimationController> {
     private static final String TAG = "BackAnimationController";
+    private static final int SETTING_VALUE_OFF = 0;
+    private static final int SETTING_VALUE_ON = 1;
     private static final String PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP =
             "persist.wm.debug.predictive_back_progress_threshold";
     public static final boolean IS_ENABLED =
-            SystemProperties.getInt("persist.wm.debug.predictive_back", 1) != 0;
+            SystemProperties.getInt("persist.wm.debug.predictive_back",
+                    SETTING_VALUE_ON) != SETTING_VALUE_OFF;
     private static final int PROGRESS_THRESHOLD = SystemProperties
             .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1);
-    @VisibleForTesting
-    boolean mEnableAnimations = SystemProperties.getInt(
-            "persist.wm.debug.predictive_back_anim", 0) != 0;
+
+    private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
 
     /**
      * Location of the initial touch event of the back gesture.
@@ -87,21 +98,50 @@
     private float mProgressThreshold;
 
     public BackAnimationController(
-            @ShellMainThread ShellExecutor shellExecutor,
+            @NonNull @ShellMainThread ShellExecutor shellExecutor,
+            @NonNull @ShellBackgroundThread Handler backgroundHandler,
             Context context) {
-        this(shellExecutor, new SurfaceControl.Transaction(), ActivityTaskManager.getService(),
-                context);
+        this(shellExecutor, backgroundHandler, new SurfaceControl.Transaction(),
+                ActivityTaskManager.getService(), context, context.getContentResolver());
     }
 
     @VisibleForTesting
-    BackAnimationController(@NonNull ShellExecutor shellExecutor,
+    BackAnimationController(@NonNull @ShellMainThread ShellExecutor shellExecutor,
+            @NonNull @ShellBackgroundThread Handler handler,
             @NonNull SurfaceControl.Transaction transaction,
             @NonNull IActivityTaskManager activityTaskManager,
-            Context context) {
+            Context context, ContentResolver contentResolver) {
         mShellExecutor = shellExecutor;
         mTransaction = transaction;
         mActivityTaskManager = activityTaskManager;
         mContext = context;
+        setupAnimationDeveloperSettingsObserver(contentResolver, handler);
+    }
+
+    private void setupAnimationDeveloperSettingsObserver(
+            @NonNull ContentResolver contentResolver,
+            @NonNull @ShellBackgroundThread final Handler backgroundHandler) {
+        ContentObserver settingsObserver = new ContentObserver(backgroundHandler) {
+            @Override
+            public void onChange(boolean selfChange, Uri uri) {
+                updateEnableAnimationFromSetting();
+            }
+        };
+        contentResolver.registerContentObserver(
+                Global.getUriFor(Global.ENABLE_BACK_ANIMATION),
+                false, settingsObserver, UserHandle.USER_SYSTEM
+        );
+        updateEnableAnimationFromSetting();
+    }
+
+    @ShellBackgroundThread
+    private void updateEnableAnimationFromSetting() {
+        int settingValue = Global.getInt(mContext.getContentResolver(),
+                Global.ENABLE_BACK_ANIMATION, SETTING_VALUE_OFF);
+        boolean isEnabled = settingValue == SETTING_VALUE_ON;
+        mEnableAnimations.set(isEnabled);
+        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s",
+                isEnabled);
     }
 
     public BackAnimation getBackAnimationImpl() {
@@ -340,12 +380,7 @@
     private boolean shouldDispatchToLauncher(int backType) {
         return backType == BackNavigationInfo.TYPE_RETURN_TO_HOME
                 && mBackToLauncherCallback != null
-                && mEnableAnimations;
-    }
-
-    @VisibleForTesting
-    void setEnableAnimations(boolean shouldEnable) {
-        mEnableAnimations = shouldEnable;
+                && mEnableAnimations.get();
     }
 
     private static void dispatchOnBackStarted(IOnBackInvokedCallback callback) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 4ad0868..3335673 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -55,6 +55,7 @@
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.annotations.ShellAnimationThread;
+import com.android.wm.shell.common.annotations.ShellBackgroundThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.common.annotations.ShellSplashscreenThread;
 import com.android.wm.shell.compatui.CompatUI;
@@ -734,11 +735,12 @@
     @Provides
     static Optional<BackAnimationController> provideBackAnimationController(
             Context context,
-            @ShellMainThread ShellExecutor shellExecutor
+            @ShellMainThread ShellExecutor shellExecutor,
+            @ShellBackgroundThread Handler backgroundHandler
     ) {
         if (BackAnimationController.IS_ENABLED) {
             return Optional.of(
-                    new BackAnimationController(shellExecutor, context));
+                    new BackAnimationController(shellExecutor, backgroundHandler, context));
         }
         return Optional.empty();
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp
index fb53e535..a899709 100644
--- a/libs/WindowManager/Shell/tests/unittest/Android.bp
+++ b/libs/WindowManager/Shell/tests/unittest/Android.bp
@@ -43,6 +43,7 @@
         "truth-prebuilt",
         "testables",
         "platform-test-annotations",
+        "frameworks-base-testutils",
     ],
 
     libs: [
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index a905dca..6cf8829 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -26,17 +26,23 @@
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import android.app.IActivityTaskManager;
 import android.app.WindowConfiguration;
-import android.content.Context;
+import android.content.pm.ApplicationInfo;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
+import android.os.Handler;
 import android.os.RemoteCallback;
 import android.os.RemoteException;
+import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
+import android.testing.TestableContentResolver;
+import android.testing.TestableContext;
+import android.testing.TestableLooper;
 import android.view.MotionEvent;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
@@ -45,12 +51,14 @@
 import android.window.IOnBackInvokedCallback;
 
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.wm.shell.TestShellExecutor;
-import com.android.wm.shell.common.ShellExecutor;
 
 import org.junit.Before;
 import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -60,14 +68,17 @@
 /**
  * atest WMShellUnitTests:BackAnimationControllerTest
  */
+@TestableLooper.RunWithLooper
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 public class BackAnimationControllerTest {
 
-    private final ShellExecutor mShellExecutor = new TestShellExecutor();
+    private static final String ANIMATION_ENABLED = "1";
+    private final TestShellExecutor mShellExecutor = new TestShellExecutor();
 
-    @Mock
-    private Context mContext;
+    @Rule
+    public TestableContext mContext =
+            new TestableContext(InstrumentationRegistry.getInstrumentation().getContext());
 
     @Mock
     private SurfaceControl.Transaction mTransaction;
@@ -80,18 +91,32 @@
 
     private BackAnimationController mController;
 
+    private int mEventTime = 0;
+    private TestableContentResolver mContentResolver;
+    private TestableLooper mTestableLooper;
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
+        mContext.getApplicationInfo().privateFlags |= ApplicationInfo.PRIVATE_FLAG_PRIVILEGED;
+        mContentResolver = new TestableContentResolver(mContext);
+        mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+        Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION,
+                ANIMATION_ENABLED);
+        mTestableLooper = TestableLooper.get(this);
         mController = new BackAnimationController(
-                mShellExecutor, mTransaction, mActivityTaskManager, mContext);
-        mController.setEnableAnimations(true);
+                mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction,
+                mActivityTaskManager, mContext,
+                mContentResolver);
+        mEventTime = 0;
+        mShellExecutor.flushAll();
     }
 
     private void createNavigationInfo(RemoteAnimationTarget topAnimationTarget,
             SurfaceControl screenshotSurface,
             HardwareBuffer hardwareBuffer,
-            int backType) {
+            int backType,
+            IOnBackInvokedCallback onBackInvokedCallback) {
         BackNavigationInfo navigationInfo = new BackNavigationInfo(
                 backType,
                 topAnimationTarget,
@@ -99,7 +124,7 @@
                 hardwareBuffer,
                 new WindowConfiguration(),
                 new RemoteCallback((bundle) -> {}),
-                null);
+                onBackInvokedCallback);
         try {
             doReturn(navigationInfo).when(mActivityTaskManager).startBackNavigation();
         } catch (RemoteException ex) {
@@ -124,15 +149,10 @@
     }
 
     private void triggerBackGesture() {
-        MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0);
-        mController.onMotionEvent(event, event.getAction(), BackEvent.EDGE_LEFT);
-
-        event = MotionEvent.obtain(10, 0, MotionEvent.ACTION_MOVE, 100, 100, 0);
-        mController.onMotionEvent(event, event.getAction(), BackEvent.EDGE_LEFT);
-
+        doMotionEvent(MotionEvent.ACTION_DOWN, 0);
+        doMotionEvent(MotionEvent.ACTION_MOVE, 0);
         mController.setTriggerBack(true);
-        event = MotionEvent.obtain(10, 0, MotionEvent.ACTION_UP, 100, 100, 0);
-        mController.onMotionEvent(event, event.getAction(), BackEvent.EDGE_LEFT);
+        doMotionEvent(MotionEvent.ACTION_UP, 0);
     }
 
     @Test
@@ -141,11 +161,8 @@
         SurfaceControl screenshotSurface = new SurfaceControl();
         HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class);
         createNavigationInfo(createAnimationTarget(), screenshotSurface, hardwareBuffer,
-                BackNavigationInfo.TYPE_CROSS_ACTIVITY);
-        mController.onMotionEvent(
-                MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0),
-                MotionEvent.ACTION_DOWN,
-                BackEvent.EDGE_LEFT);
+                BackNavigationInfo.TYPE_CROSS_ACTIVITY, null);
+        doMotionEvent(MotionEvent.ACTION_DOWN, 0);
         verify(mTransaction).setBuffer(screenshotSurface, hardwareBuffer);
         verify(mTransaction).setVisibility(screenshotSurface, true);
         verify(mTransaction).apply();
@@ -157,15 +174,9 @@
         HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class);
         RemoteAnimationTarget animationTarget = createAnimationTarget();
         createNavigationInfo(animationTarget, screenshotSurface, hardwareBuffer,
-                BackNavigationInfo.TYPE_CROSS_ACTIVITY);
-        mController.onMotionEvent(
-                MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0),
-                MotionEvent.ACTION_DOWN,
-                BackEvent.EDGE_LEFT);
-        mController.onMotionEvent(
-                MotionEvent.obtain(10, 0, MotionEvent.ACTION_MOVE, 100, 100, 0),
-                MotionEvent.ACTION_MOVE,
-                BackEvent.EDGE_LEFT);
+                BackNavigationInfo.TYPE_CROSS_ACTIVITY, null);
+        doMotionEvent(MotionEvent.ACTION_DOWN, 0);
+        doMotionEvent(MotionEvent.ACTION_MOVE, 100);
         // b/207481538, we check that the surface is not moved for now, we can re-enable this once
         // we implement the animation
         verify(mTransaction, never()).setScale(eq(screenshotSurface), anyInt(), anyInt());
@@ -196,30 +207,56 @@
         mController.setBackToLauncherCallback(mIOnBackInvokedCallback);
         RemoteAnimationTarget animationTarget = createAnimationTarget();
         createNavigationInfo(animationTarget, null, null,
-                BackNavigationInfo.TYPE_RETURN_TO_HOME);
+                BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
 
         // Check that back start is dispatched.
-        mController.onMotionEvent(
-                MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0),
-                MotionEvent.ACTION_DOWN,
-                BackEvent.EDGE_LEFT);
+        doMotionEvent(MotionEvent.ACTION_DOWN, 0);
         verify(mIOnBackInvokedCallback).onBackStarted();
 
         // Check that back progress is dispatched.
-        mController.onMotionEvent(
-                MotionEvent.obtain(10, 0, MotionEvent.ACTION_MOVE, 100, 100, 0),
-                MotionEvent.ACTION_MOVE,
-                BackEvent.EDGE_LEFT);
+        doMotionEvent(MotionEvent.ACTION_MOVE, 100);
         ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
         verify(mIOnBackInvokedCallback).onBackProgressed(backEventCaptor.capture());
         assertEquals(animationTarget, backEventCaptor.getValue().getDepartingAnimationTarget());
 
         // Check that back invocation is dispatched.
         mController.setTriggerBack(true);   // Fake trigger back
-        mController.onMotionEvent(
-                MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0),
-                MotionEvent.ACTION_UP,
-                BackEvent.EDGE_LEFT);
+        doMotionEvent(MotionEvent.ACTION_UP, 0);
         verify(mIOnBackInvokedCallback).onBackInvoked();
     }
+
+    @Test
+    public void animationDisabledFromSettings() throws RemoteException {
+        // Toggle the setting off
+        Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION, "0");
+        mController = new BackAnimationController(
+                mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction,
+                mActivityTaskManager, mContext,
+                mContentResolver);
+        mController.setBackToLauncherCallback(mIOnBackInvokedCallback);
+
+        RemoteAnimationTarget animationTarget = createAnimationTarget();
+        IOnBackInvokedCallback appCallback = mock(IOnBackInvokedCallback.class);
+        ArgumentCaptor<BackEvent> backEventCaptor = ArgumentCaptor.forClass(BackEvent.class);
+        createNavigationInfo(animationTarget, null, null,
+                BackNavigationInfo.TYPE_RETURN_TO_HOME, appCallback);
+
+        triggerBackGesture();
+
+        verify(appCallback, never()).onBackStarted();
+        verify(appCallback, never()).onBackProgressed(backEventCaptor.capture());
+        verify(appCallback, times(1)).onBackInvoked();
+
+        verify(mIOnBackInvokedCallback, never()).onBackStarted();
+        verify(mIOnBackInvokedCallback, never()).onBackProgressed(backEventCaptor.capture());
+        verify(mIOnBackInvokedCallback, never()).onBackInvoked();
+    }
+
+    private void doMotionEvent(int actionDown, int coordinate) {
+        mController.onMotionEvent(
+                MotionEvent.obtain(0, mEventTime, actionDown, coordinate, coordinate, 0),
+                actionDown,
+                BackEvent.EDGE_LEFT);
+        mEventTime += 10;
+    }
 }
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index df2685d..a171f86 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1604,4 +1604,11 @@
     <string name="bt_le_audio_broadcast_dialog_switch_app">Broadcast <xliff:g id="switchApp" example="App Name 2">%1$s</xliff:g></string>
     <!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, different output. -->
     <string name="bt_le_audio_broadcast_dialog_different_output">Change output</string>
+
+    <!-- Developer setting: enable animations when a back gesture is executed [CHAR LIMIT=50] -->
+    <string name="back_navigation_animation">Predictive back animations</string>
+    <!-- Developer setting: enable animations when a back gesture is executed [CHAR LIMIT=150] -->
+    <string name="back_navigation_animation_summary">Enable system animations for predictive back.</string>
+    <!-- Developer setting: enable animations when a back gesture is executed, full explanation[CHAR LIMIT=NONE] -->
+    <string name="back_navigation_animation_dialog">This setting enables system animations for predictive gesture animation. It requires setting per-app "enableOnBackInvokedCallback" to true in the manifest file.</string>
 </resources>
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index e1a2e8d..d6d7304 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -46,8 +46,6 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 
@@ -56,7 +54,6 @@
  */
 @RequiresApi(Build.VERSION_CODES.R)
 public class LocalMediaManager implements BluetoothCallback {
-    private static final Comparator<MediaDevice> COMPARATOR = Comparator.naturalOrder();
     private static final String TAG = "LocalMediaManager";
     private static final int MAX_DISCONNECTED_DEVICE_NUM = 5;
 
@@ -65,13 +62,15 @@
             MediaDeviceState.STATE_CONNECTING,
             MediaDeviceState.STATE_DISCONNECTED,
             MediaDeviceState.STATE_CONNECTING_FAILED,
-            MediaDeviceState.STATE_SELECTED})
+            MediaDeviceState.STATE_SELECTED,
+            MediaDeviceState.STATE_GROUPING})
     public @interface MediaDeviceState {
         int STATE_CONNECTED = 0;
         int STATE_CONNECTING = 1;
         int STATE_DISCONNECTED = 2;
         int STATE_CONNECTING_FAILED = 3;
         int STATE_SELECTED = 4;
+        int STATE_GROUPING = 5;
     }
 
     private final Collection<DeviceCallback> mCallbacks = new CopyOnWriteArrayList<>();
@@ -322,6 +321,7 @@
      * @return If add device successful return {@code true}, otherwise return {@code false}
      */
     public boolean addDeviceToPlayMedia(MediaDevice device) {
+        device.setState(MediaDeviceState.STATE_GROUPING);
         return mInfoMediaManager.addDeviceToPlayMedia(device);
     }
 
@@ -332,6 +332,7 @@
      * @return If device stop successful return {@code true}, otherwise return {@code false}
      */
     public boolean removeDeviceFromPlayMedia(MediaDevice device) {
+        device.setState(MediaDeviceState.STATE_GROUPING);
         return mInfoMediaManager.removeDeviceFromPlayMedia(device);
     }
 
@@ -524,7 +525,6 @@
         @Override
         public void onDeviceListAdded(List<MediaDevice> devices) {
             synchronized (mMediaDevicesLock) {
-                Collections.sort(devices, COMPARATOR);
                 mMediaDevices.clear();
                 mMediaDevices.addAll(devices);
                 // Add disconnected bluetooth devices only when phone output device is available.
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 4b7d0d2..cce5154 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -598,6 +598,7 @@
                     Settings.Global.WATCHDOG_TIMEOUT_MILLIS,
                     Settings.Global.MANAGED_PROVISIONING_DEFER_PROVISIONING_TO_ROLE_HOLDER,
                     Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
+                    Settings.Global.ENABLE_BACK_ANIMATION, // Temporary for T, dev option only
                     Settings.Global.Wearable.BATTERY_SAVER_MODE,
                     Settings.Global.Wearable.COMBINED_LOCATION_ENABLED,
                     Settings.Global.Wearable.HAS_PAY_TOKENS,
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_seekbar_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_seekbar_background.xml
index e1b99ce..55dce8f 100644
--- a/packages/SystemUI/res/drawable/media_output_dialog_seekbar_background.xml
+++ b/packages/SystemUI/res/drawable/media_output_dialog_seekbar_background.xml
@@ -27,7 +27,7 @@
         <clip>
             <shape>
                 <corners
-                    android:radius="28dp"/>
+                    android:radius="16dp"/>
                 <size
                     android:height="64dp"/>
                 <solid android:color="@color/material_dynamic_primary80" />
diff --git a/packages/SystemUI/res/layout/media_output_list_item.xml b/packages/SystemUI/res/layout/media_output_list_item.xml
index dc86afa..f79e534 100644
--- a/packages/SystemUI/res/layout/media_output_list_item.xml
+++ b/packages/SystemUI/res/layout/media_output_list_item.xml
@@ -33,12 +33,13 @@
             android:layout_height="match_parent"
             android:background="@drawable/media_output_item_background"
             android:layout_gravity="center_vertical|start">
-            <SeekBar
+            <com.android.systemui.media.dialog.MediaOutputSeekbar
                 android:id="@+id/volume_seekbar"
                 android:splitTrack="false"
                 android:visibility="gone"
                 android:paddingStart="0dp"
                 android:paddingEnd="0dp"
+                android:background="@null"
                 android:progressDrawable="@drawable/media_output_dialog_seekbar_background"
                 android:thumb="@null"
                 android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 4370432..f77430b 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1125,6 +1125,8 @@
     <dimen name="media_output_dialog_icon_corner_radius">16dp</dimen>
     <dimen name="media_output_dialog_title_anim_y_delta">12.5dp</dimen>
     <dimen name="media_output_dialog_app_tier_icon_size">20dp</dimen>
+    <dimen name="media_output_dialog_background_radius">16dp</dimen>
+    <dimen name="media_output_dialog_active_background_radius">28dp</dimen>
 
     <!-- Distance that the full shade transition takes in order to complete by tapping on a button
          like "expand". -->
diff --git a/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt b/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt
new file mode 100644
index 0000000..f195d20
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard
+
+import android.content.ContentResolver
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL
+import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT
+import android.provider.Settings.Secure.ACTIVE_UNLOCK_ON_WAKE
+import com.android.keyguard.KeyguardUpdateMonitor.getCurrentUser
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.settings.SecureSettings
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * Handles active unlock settings changes.
+ */
+@SysUISingleton
+class ActiveUnlockConfig @Inject constructor(
+    @Main private val handler: Handler,
+    private val secureSettings: SecureSettings,
+    private val contentResolver: ContentResolver,
+    dumpManager: DumpManager
+) : Dumpable {
+
+    /**
+     * Indicates the origin for an active unlock request.
+     */
+    enum class ACTIVE_UNLOCK_REQUEST_ORIGIN {
+        WAKE, UNLOCK_INTENT, BIOMETRIC_FAIL, ASSISTANT
+    }
+
+    private var requestActiveUnlockOnWakeup = false
+    private var requestActiveUnlockOnUnlockIntent = false
+    private var requestActiveUnlockOnBioFail = false
+
+    private val settingsObserver = object : ContentObserver(handler) {
+        private val wakeUri: Uri = secureSettings.getUriFor(ACTIVE_UNLOCK_ON_WAKE)
+        private val unlockIntentUri: Uri = secureSettings.getUriFor(ACTIVE_UNLOCK_ON_UNLOCK_INTENT)
+        private val bioFailUri: Uri = secureSettings.getUriFor(ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL)
+
+        fun register() {
+            contentResolver.registerContentObserver(
+                    wakeUri,
+                    false,
+                    this,
+                    UserHandle.USER_ALL)
+            contentResolver.registerContentObserver(
+                    unlockIntentUri,
+                    false,
+                    this,
+                    UserHandle.USER_ALL)
+            contentResolver.registerContentObserver(
+                    bioFailUri,
+                    false,
+                    this,
+                    UserHandle.USER_ALL)
+
+            onChange(true, ArrayList(), 0, getCurrentUser())
+        }
+
+        override fun onChange(
+            selfChange: Boolean,
+            uris: Collection<Uri>,
+            flags: Int,
+            userId: Int
+        ) {
+            if (getCurrentUser() != userId) {
+                return
+            }
+
+            if (selfChange || uris.contains(wakeUri)) {
+                requestActiveUnlockOnWakeup = secureSettings.getIntForUser(
+                        ACTIVE_UNLOCK_ON_WAKE, 0, getCurrentUser()) == 1
+            }
+
+            if (selfChange || uris.contains(unlockIntentUri)) {
+                requestActiveUnlockOnUnlockIntent = secureSettings.getIntForUser(
+                        ACTIVE_UNLOCK_ON_UNLOCK_INTENT, 0, getCurrentUser()) == 1
+            }
+
+            if (selfChange || uris.contains(bioFailUri)) {
+                requestActiveUnlockOnBioFail = secureSettings.getIntForUser(
+                        ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL, 0, getCurrentUser()) == 1
+            }
+        }
+    }
+
+    init {
+        settingsObserver.register()
+        dumpManager.registerDumpable(this)
+    }
+
+    /**
+     * Whether to trigger active unlock based on where the request is coming from and
+     * the current settings.
+     */
+    fun shouldAllowActiveUnlockFromOrigin(requestOrigin: ACTIVE_UNLOCK_REQUEST_ORIGIN): Boolean {
+        return when (requestOrigin) {
+            ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE -> requestActiveUnlockOnWakeup
+
+            ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT ->
+                requestActiveUnlockOnUnlockIntent || requestActiveUnlockOnWakeup
+
+            ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL ->
+                requestActiveUnlockOnBioFail || requestActiveUnlockOnUnlockIntent ||
+                        requestActiveUnlockOnWakeup
+
+            ACTIVE_UNLOCK_REQUEST_ORIGIN.ASSISTANT -> isActiveUnlockEnabled()
+        }
+    }
+
+    /**
+     * If any active unlock triggers are enabled.
+     */
+    fun isActiveUnlockEnabled(): Boolean {
+        return requestActiveUnlockOnWakeup || requestActiveUnlockOnUnlockIntent ||
+                requestActiveUnlockOnBioFail
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("   requestActiveUnlockOnWakeup=$requestActiveUnlockOnWakeup")
+        pw.println("   requestActiveUnlockOnUnlockIntent=$requestActiveUnlockOnUnlockIntent")
+        pw.println("   requestActiveUnlockOnBioFail=$requestActiveUnlockOnBioFail")
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 90f53a1..19a2d9e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -221,10 +221,10 @@
                 mKeyguardSecurityCallback.userActivity();
                 showMessage(null, null);
             }
-            if (mUpdateMonitor.isFaceEnrolled()
-                    && mUpdateMonitor.mRequestActiveUnlockOnUnlockIntent) {
-                mUpdateMonitor.requestActiveUnlock("unlock-intent, reason=swipeUpOnBouncer",
-                        true);
+            if (mUpdateMonitor.isFaceEnrolled()) {
+                mUpdateMonitor.requestActiveUnlock(
+                        ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+                        "swipeUpOnBouncer");
             }
         }
     };
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index a6feedb5..bbe9a362 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -248,12 +248,6 @@
         }
     }
 
-    public final boolean mRequestActiveUnlockOnAssistant;
-    public final boolean mRequestActiveUnlockOnWakeup;
-    public final boolean mInitiateActiveUnlockOnWakeup;
-    public final boolean mRequestActiveUnlockOnUnlockIntent;
-    public final boolean mRequestActiveUnlockOnBioFail;
-
     private final Context mContext;
     private final boolean mIsPrimaryUser;
     private final boolean mIsAutomotive;
@@ -340,6 +334,7 @@
     private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
     private final Executor mBackgroundExecutor;
     private SensorPrivacyManager mSensorPrivacyManager;
+    private final ActiveUnlockConfig mActiveUnlockConfig;
 
     /**
      * Short delay before restarting fingerprint authentication after a successful try. This should
@@ -441,12 +436,12 @@
         Assert.isMainThread();
         boolean wasTrusted = mUserHasTrust.get(userId, false);
         mUserHasTrust.put(userId, enabled);
-        // If there was no change in trusted state, make sure we are not authenticating.
-        // TrustManager sends an onTrustChanged whenever a user unlocks keyguard, for
-        // this reason we need to make sure to not authenticate.
-        if (wasTrusted == enabled) {
+        // If there was no change in trusted state or trust granted, make sure we are not
+        // authenticating.  TrustManager sends an onTrustChanged whenever a user unlocks keyguard,
+        // for this reason we need to make sure to not authenticate.
+        if (wasTrusted == enabled || enabled) {
             updateBiometricListeningState(BIOMETRIC_ACTION_STOP);
-        } else if (!enabled) {
+        } else {
             updateBiometricListeningState(BIOMETRIC_ACTION_START);
         }
 
@@ -1364,14 +1359,18 @@
                 cb.onTrustAgentErrorMessage(message);
             }
         }
+
     }
 
     @VisibleForTesting
     void setAssistantVisible(boolean assistantVisible) {
         mAssistantVisible = assistantVisible;
         updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE);
-        if (mAssistantVisible && mRequestActiveUnlockOnAssistant) {
-            requestActiveUnlock("assistant", false);
+        if (mAssistantVisible) {
+            requestActiveUnlock(
+                    ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.ASSISTANT,
+                    "assistant",
+                    false);
         }
     }
 
@@ -1518,10 +1517,9 @@
 
                 @Override
                 public void onAuthenticationFailed() {
-                    if (mRequestActiveUnlockOnBioFail) {
-                        requestActiveUnlock("biometric-failure, extra=fingerprintFailure",
-                                true);
-                    }
+                    requestActiveUnlock(
+                            ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
+                            "fingerprintFailure");
                     handleFingerprintAuthFailed();
                 }
 
@@ -1580,14 +1578,13 @@
 
                 @Override
                 public void onAuthenticationFailed() {
-                    if (shouldRequestActiveUnlockOnFaceError()) {
                         String reason =
                                 mKeyguardBypassController.canBypass() ? "bypass"
                                         : mUdfpsBouncerShowing ? "udfpsBouncer" :
                                                 mBouncerFullyShown ? "bouncer" : "udfpsFpDown";
-                        requestActiveUnlock("biometric-failure"
-                                + ", extra=faceFailure-" + reason, true);
-                    }
+                        requestActiveUnlock(
+                                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
+                                "faceFailure-" + reason);
 
                     handleFaceAuthFailed();
                     if (mKeyguardBypassController != null) {
@@ -1617,10 +1614,11 @@
                     if (mKeyguardBypassController != null) {
                         mKeyguardBypassController.setUserHasDeviceEntryIntent(false);
                     }
-                    if (errMsgId == BiometricFaceConstants.FACE_ERROR_TIMEOUT
-                            && shouldRequestActiveUnlockOnFaceError()) {
-                        requestActiveUnlock("biometric-failure"
-                                + ", extra=faceError-" + errMsgId, true);
+
+                    if (errMsgId == BiometricFaceConstants.FACE_ERROR_TIMEOUT) {
+                        requestActiveUnlock(
+                                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
+                                "faceError-" + errMsgId);
                     }
                 }
 
@@ -1628,12 +1626,6 @@
                 public void onAuthenticationAcquired(int acquireInfo) {
                     handleFaceAcquired(acquireInfo);
                 }
-
-                private boolean shouldRequestActiveUnlockOnFaceError() {
-                    return mRequestActiveUnlockOnBioFail
-                            && (mKeyguardBypassController.canBypass() || mBouncerFullyShown
-                            || mUdfpsBouncerShowing || mAuthController.isUdfpsFingerDown());
-                }
     };
 
     @VisibleForTesting
@@ -1749,11 +1741,7 @@
         Trace.beginSection("KeyguardUpdateMonitor#handleStartedWakingUp");
         Assert.isMainThread();
         updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE);
-        if (mRequestActiveUnlockOnWakeup) {
-            requestActiveUnlock("wake-unlock");
-        } else if (mInitiateActiveUnlockOnWakeup) {
-            initiateActiveUnlock("wake-initiate");
-        }
+        requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "wakingUp");
         for (int i = 0; i < mCallbacks.size(); i++) {
             KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
             if (cb != null) {
@@ -1882,7 +1870,8 @@
             AuthController authController,
             TelephonyListenerManager telephonyListenerManager,
             InteractionJankMonitor interactionJankMonitor,
-            LatencyTracker latencyTracker) {
+            LatencyTracker latencyTracker,
+            ActiveUnlockConfig activeUnlockConfiguration) {
         mContext = context;
         mSubscriptionManager = SubscriptionManager.from(context);
         mTelephonyListenerManager = telephonyListenerManager;
@@ -1899,18 +1888,7 @@
         mAuthController = authController;
         dumpManager.registerDumpable(getClass().getName(), this);
         mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
-
-        // TODO, b/222459888: add official configurable names to Settings.java
-        mRequestActiveUnlockOnWakeup = Settings.Global.getInt(
-                mContext.getContentResolver(), "wake-unlock", 0) == 1;
-        mInitiateActiveUnlockOnWakeup = Settings.Global.getInt(
-                mContext.getContentResolver(), "wake-initiate", 1) == 1;
-        mRequestActiveUnlockOnUnlockIntent = Settings.Global.getInt(
-                mContext.getContentResolver(), "unlock-intent", 0) == 1;
-        mRequestActiveUnlockOnBioFail = Settings.Global.getInt(
-                mContext.getContentResolver(), "bio-fail", 0) == 1;
-        mRequestActiveUnlockOnAssistant = Settings.Global.getInt(
-                mContext.getContentResolver(), "assistant", 0) == 1;
+        mActiveUnlockConfig = activeUnlockConfiguration;
 
         mHandler = new Handler(mainLooper) {
             @Override
@@ -2292,11 +2270,7 @@
         }
         mAuthInterruptActive = active;
         updateFaceListeningState(BIOMETRIC_ACTION_UPDATE);
-        if (mRequestActiveUnlockOnWakeup) {
-            requestActiveUnlock("wake-unlock, extra=onReach");
-        } else if (mInitiateActiveUnlockOnWakeup) {
-            initiateActiveUnlock("wake-initiate, extra=onReach");
-        }
+        requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "onReach");
     }
 
     /**
@@ -2348,7 +2322,7 @@
     /**
      * Initiates active unlock to get the unlock token ready.
      */
-    public void initiateActiveUnlock(String reason) {
+    private void initiateActiveUnlock(String reason) {
         // If this message exists, FP has already authenticated, so wait until that is handled
         if (mHandler.hasMessages(MSG_BIOMETRIC_AUTHENTICATION_CONTINUE)) {
             return;
@@ -2365,15 +2339,30 @@
     /**
      * Attempts to trigger active unlock from trust agent.
      */
-    public void requestActiveUnlock(String reason, boolean dismissKeyguard) {
+    private void requestActiveUnlock(
+            ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
+            String reason,
+            boolean dismissKeyguard
+    ) {
         // If this message exists, FP has already authenticated, so wait until that is handled
         if (mHandler.hasMessages(MSG_BIOMETRIC_AUTHENTICATION_CONTINUE)) {
             return;
         }
 
-        if (shouldTriggerActiveUnlock()) {
+        final boolean allowRequest =
+                mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin(requestOrigin);
+        if (requestOrigin == ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE
+                && !allowRequest && mActiveUnlockConfig.isActiveUnlockEnabled()) {
+            // instead of requesting the active unlock, initiate the unlock
+            initiateActiveUnlock(reason);
+            return;
+        }
+
+        if (allowRequest && shouldTriggerActiveUnlock()) {
             if (DEBUG) {
-                Log.d("ActiveUnlock", "reportUserRequestedUnlock triggerReason=" + reason
+                Log.d("ActiveUnlock", "reportUserRequestedUnlock"
+                        + " origin=" + requestOrigin.name()
+                        + " reason=" + reason
                         + " dismissKeyguard=" + dismissKeyguard);
             }
             mTrustManager.reportUserRequestedUnlock(KeyguardUpdateMonitor.getCurrentUser(),
@@ -2383,11 +2372,20 @@
 
     /**
      * Attempts to trigger active unlock from trust agent.
-     * Only dismisses the keyguard if only face is enrolled (no FP) and bypass is enabled.
+     * Only dismisses the keyguard under certain conditions.
      */
-    public void requestActiveUnlock(String reason) {
-        requestActiveUnlock(reason, isFaceEnrolled() && !isUdfpsEnrolled()
-                && mKeyguardBypassController.getBypassEnabled());
+    public void requestActiveUnlock(
+            ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin,
+            String extraReason
+    ) {
+        final boolean canFaceBypass = isFaceEnrolled() && mKeyguardBypassController != null
+                && mKeyguardBypassController.canBypass();
+        requestActiveUnlock(
+                requestOrigin,
+                extraReason, canFaceBypass
+                        || mUdfpsBouncerShowing
+                        || mBouncerFullyShown
+                        || mAuthController.isUdfpsFingerDown());
     }
 
     /**
@@ -2397,9 +2395,9 @@
         mUdfpsBouncerShowing = showing;
         if (mUdfpsBouncerShowing) {
             updateFaceListeningState(BIOMETRIC_ACTION_START);
-            if (mRequestActiveUnlockOnUnlockIntent) {
-                requestActiveUnlock("unlock-intent, extra=udfpsBouncer", true);
-            }
+            requestActiveUnlock(
+                    ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+                    "udfpsBouncer");
         }
     }
 
@@ -3233,8 +3231,10 @@
         }
 
         if (wasBouncerFullyShown != mBouncerFullyShown) {
-            if (mBouncerFullyShown && mRequestActiveUnlockOnUnlockIntent) {
-                requestActiveUnlock("unlock-intent, reason=bouncerFullyShown", true);
+            if (mBouncerFullyShown) {
+                requestActiveUnlock(
+                        ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+                        "bouncerFullyShown");
             }
             for (int i = 0; i < mCallbacks.size(); i++) {
                 KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
@@ -3771,13 +3771,6 @@
         }
         mListenModels.print(pw);
 
-        pw.println("Enabled active unlock triggers:");
-        pw.println("   mRequestActiveUnlockOnWakeup=" + mRequestActiveUnlockOnWakeup);
-        pw.println("   mInitiateActiveUnlockOnWakeup=" + mInitiateActiveUnlockOnWakeup);
-        pw.println("   mRequestActiveUnlockOnUnlockIntent=" + mRequestActiveUnlockOnUnlockIntent);
-        pw.println("   mRequestActiveUnlockOnBiometricFail=" + mRequestActiveUnlockOnBioFail);
-        pw.println("   mRequestActiveUnlockOnAssistant=" + mRequestActiveUnlockOnAssistant);
-
         if (mIsAutomotive) {
             pw.println("  Running on Automotive build");
         }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 20efafc..463db5c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -51,6 +51,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.LatencyTracker;
+import com.android.keyguard.ActiveUnlockConfig;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.dagger.SysUISingleton;
@@ -658,9 +659,15 @@
         mExecution.assertIsMainThread();
 
         mOverlay = overlay;
+        final int requestReason = overlay.getRequestReason();
+        if (requestReason == REASON_AUTH_KEYGUARD
+                && !mKeyguardUpdateMonitor.isFingerprintDetectionRunning()) {
+            Log.d(TAG, "Attempting to showUdfpsOverlay when fingerprint detection"
+                    + " isn't running on keyguard. Skip show.");
+            return;
+        }
         if (overlay.show(this, mOverlayParams)) {
-            Log.v(TAG, "showUdfpsOverlay | adding window reason="
-                    + overlay.getRequestReason());
+            Log.v(TAG, "showUdfpsOverlay | adding window reason=" + requestReason);
             mOnFingerDown = false;
             mAttemptedToDismissKeyguard = false;
             mOrientationListener.enable();
@@ -791,10 +798,9 @@
                 mKeyguardUpdateMonitor.requestFaceAuth(/* userInitiatedRequest */ false);
             }
 
-            if (mKeyguardUpdateMonitor.mRequestActiveUnlockOnUnlockIntent) {
-                mKeyguardUpdateMonitor.requestActiveUnlock("unlock-intent extra=udfpsFingerDown",
-                        true);
-            }
+            mKeyguardUpdateMonitor.requestActiveUnlock(
+                    ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+                    "udfpsFingerDown");
         }
         mOnFingerDown = true;
         if (mAlternateTouchProvider != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index 07001ee..a397f32 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -112,6 +112,7 @@
             super.onBind(device, topMargin, bottomMargin, position);
             final boolean currentlyConnected = !mIncludeDynamicGroup
                     && isCurrentlyConnected(device);
+            boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE;
             if (currentlyConnected) {
                 mConnectedItem = mContainerLayout;
             }
@@ -165,6 +166,14 @@
                             true /* showSubtitle */, true /* showStatus */);
                     mSubTitleText.setText(R.string.media_output_dialog_connect_failed);
                     mContainerLayout.setOnClickListener(v -> onItemClick(v, device));
+                } else if (device.getState() == MediaDeviceState.STATE_GROUPING) {
+                    mProgressBar.getIndeterminateDrawable().setColorFilter(
+                            new PorterDuffColorFilter(
+                                    mController.getColorItemContent(),
+                                    PorterDuff.Mode.SRC_IN));
+                    setSingleLineLayout(getItemTitle(device), true /* bFocused */,
+                            false /* showSeekBar*/,
+                            true /* showProgressBar */, false /* showStatus */);
                 } else if (mController.getSelectedMediaDevice().size() > 1
                         && isDeviceIncluded(mController.getSelectedMediaDevice(), device)) {
                     mTitleText.setTextColor(mController.getColorItemContent());
@@ -178,7 +187,7 @@
                     mCheckBox.setOnCheckedChangeListener(
                             (buttonView, isChecked) -> onGroupActionTriggered(false, device));
                     setCheckBoxColor(mCheckBox, mController.getColorItemContent());
-                    initSeekbar(device);
+                    initSeekbar(device, isCurrentSeekbarInvisible);
                     mEndTouchArea.setVisibility(View.VISIBLE);
                     mEndTouchArea.setOnClickListener(null);
                     mEndTouchArea.setOnClickListener((v) -> mCheckBox.performClick());
@@ -193,7 +202,7 @@
                     setSingleLineLayout(getItemTitle(device), true /* bFocused */,
                             true /* showSeekBar */,
                             false /* showProgressBar */, true /* showStatus */);
-                    initSeekbar(device);
+                    initSeekbar(device, isCurrentSeekbarInvisible);
                     setUpContentDescriptionForView(mContainerLayout, false, device);
                     mCurrentActivePosition = position;
                 } else if (isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
@@ -257,9 +266,7 @@
             mCurrentActivePosition = -1;
             mController.connectDevice(device);
             device.setState(MediaDeviceState.STATE_CONNECTING);
-            if (!isAnimating()) {
-                notifyDataSetChanged();
-            }
+            notifyDataSetChanged();
         }
 
         private void setUpContentDescriptionForView(View view, boolean clickable,
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index 9dc29bd..5c2cc0b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -17,18 +17,22 @@
 package com.android.systemui.media.dialog;
 
 import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
 import android.app.WallpaperColors;
 import android.content.Context;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
 import android.graphics.Typeface;
+import android.graphics.drawable.ClipDrawable;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
 import android.graphics.drawable.Icon;
+import android.graphics.drawable.LayerDrawable;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.animation.LinearInterpolator;
 import android.widget.CheckBox;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
@@ -44,7 +48,6 @@
 import com.android.settingslib.media.MediaDevice;
 import com.android.settingslib.utils.ThreadUtils;
 import com.android.systemui.R;
-import com.android.systemui.animation.Interpolators;
 
 import java.util.List;
 
@@ -61,7 +64,6 @@
     protected final MediaOutputController mController;
 
     private int mMargin;
-    private boolean mIsAnimating;
 
     Context mContext;
     View mHolderView;
@@ -114,10 +116,6 @@
         return mIsDragging;
     }
 
-    boolean isAnimating() {
-        return mIsAnimating;
-    }
-
     int getCurrentActivePosition() {
         return mCurrentActivePosition;
     }
@@ -131,7 +129,7 @@
      */
     abstract class MediaDeviceBaseViewHolder extends RecyclerView.ViewHolder {
 
-        private static final int ANIM_DURATION = 200;
+        private static final int ANIM_DURATION = 500;
 
         final LinearLayout mContainerLayout;
         final FrameLayout mItemLayout;
@@ -140,12 +138,14 @@
         final TextView mSubTitleText;
         final ImageView mTitleIcon;
         final ProgressBar mProgressBar;
-        final SeekBar mSeekBar;
+        final MediaOutputSeekbar mSeekBar;
         final LinearLayout mTwoLineLayout;
         final ImageView mStatusIcon;
         final CheckBox mCheckBox;
         final LinearLayout mEndTouchArea;
         private String mDeviceId;
+        private ValueAnimator mCornerAnimator;
+        private ValueAnimator mVolumeAnimator;
 
         MediaDeviceBaseViewHolder(View view) {
             super(view);
@@ -161,6 +161,7 @@
             mStatusIcon = view.requireViewById(R.id.media_output_item_status);
             mCheckBox = view.requireViewById(R.id.check_box);
             mEndTouchArea = view.requireViewById(R.id.end_action_area);
+            initAnimator();
         }
 
         void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin, int position) {
@@ -186,20 +187,39 @@
                 boolean showProgressBar, boolean showStatus) {
             mTwoLineLayout.setVisibility(View.GONE);
             boolean isActive = showSeekBar || showProgressBar;
-            final Drawable backgroundDrawable =
-                    isActive
-                            ? mContext.getDrawable(R.drawable.media_output_item_background_active)
-                                    .mutate() : mContext.getDrawable(
-                            R.drawable.media_output_item_background)
-                            .mutate();
-            backgroundDrawable.setColorFilter(new PorterDuffColorFilter(
-                    isActive ? mController.getColorConnectedItemBackground()
-                            : mController.getColorItemBackground(),
-                    PorterDuff.Mode.SRC_IN));
-            mItemLayout.setBackground(backgroundDrawable);
+            if (!mCornerAnimator.isRunning()) {
+                final Drawable backgroundDrawable =
+                        showSeekBar
+                                ? mContext.getDrawable(
+                                        R.drawable.media_output_item_background_active)
+                                .mutate() : mContext.getDrawable(
+                                        R.drawable.media_output_item_background)
+                                .mutate();
+                backgroundDrawable.setColorFilter(new PorterDuffColorFilter(
+                        isActive ? mController.getColorConnectedItemBackground()
+                                : mController.getColorItemBackground(),
+                        PorterDuff.Mode.SRC_IN));
+                mItemLayout.setBackground(backgroundDrawable);
+                if (showSeekBar) {
+                    final ClipDrawable clipDrawable =
+                            (ClipDrawable) ((LayerDrawable) mSeekBar.getProgressDrawable())
+                                    .findDrawableByLayerId(android.R.id.progress);
+                    final GradientDrawable progressDrawable =
+                            (GradientDrawable) clipDrawable.getDrawable();
+                    progressDrawable.setCornerRadius(mController.getActiveRadius());
+                }
+            } else {
+                mItemLayout.getBackground().setColorFilter(new PorterDuffColorFilter(
+                        isActive ? mController.getColorConnectedItemBackground()
+                                : mController.getColorItemBackground(),
+                        PorterDuff.Mode.SRC_IN));
+            }
             mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE);
             mSeekBar.setAlpha(1);
             mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE);
+            if (!showSeekBar) {
+                mSeekBar.resetVolume();
+            }
             mStatusIcon.setVisibility(showStatus ? View.VISIBLE : View.GONE);
             mTitleText.setText(title);
             mTitleText.setVisibility(View.VISIBLE);
@@ -257,15 +277,21 @@
             }
         }
 
-        void initSeekbar(MediaDevice device) {
+        void initSeekbar(MediaDevice device, boolean isCurrentSeekbarInvisible) {
             if (!mController.isVolumeControlEnabled(device)) {
                 disableSeekBar();
             }
-            mSeekBar.setMax(device.getMaxVolume());
-            mSeekBar.setMin(0);
+            mSeekBar.setMaxVolume(device.getMaxVolume());
             final int currentVolume = device.getCurrentVolume();
-            if (mSeekBar.getProgress() != currentVolume) {
-                mSeekBar.setProgress(currentVolume, true);
+            if (mSeekBar.getVolume() != currentVolume) {
+                if (isCurrentSeekbarInvisible) {
+                    animateCornerAndVolume(mSeekBar.getProgress(),
+                            MediaOutputSeekbar.scaleVolumeToProgress(currentVolume));
+                } else {
+                    if (!mVolumeAnimator.isStarted()) {
+                        mSeekBar.setVolume(currentVolume);
+                    }
+                }
             }
             mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                 @Override
@@ -273,7 +299,11 @@
                     if (device == null || !fromUser) {
                         return;
                     }
-                    mController.adjustVolume(device, progress);
+                    int currentVolume = MediaOutputSeekbar.scaleProgressToVolume(progress);
+                    int deviceVolume = device.getCurrentVolume();
+                    if (currentVolume != deviceVolume) {
+                        mController.adjustVolume(device, currentVolume);
+                    }
                 }
 
                 @Override
@@ -317,65 +347,57 @@
             });
         }
 
-        void playSwitchingAnim(@NonNull View from, @NonNull View to) {
-            final float delta = (float) (mContext.getResources().getDimensionPixelSize(
-                    R.dimen.media_output_dialog_title_anim_y_delta));
-            final SeekBar fromSeekBar = from.requireViewById(R.id.volume_seekbar);
-            final TextView toTitleText = to.requireViewById(R.id.title);
-            if (fromSeekBar.getVisibility() != View.VISIBLE || toTitleText.getVisibility()
-                    != View.VISIBLE) {
-                return;
-            }
-            mIsAnimating = true;
-            // Animation for title text
-            toTitleText.setTypeface(Typeface.create(mContext.getString(
-                    com.android.internal.R.string.config_headlineFontFamilyMedium),
-                    Typeface.NORMAL));
-            toTitleText.animate()
-                    .setDuration(ANIM_DURATION)
-                    .translationY(-delta)
-                    .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
-                    .setListener(new AnimatorListenerAdapter() {
-                        @Override
-                        public void onAnimationEnd(Animator animation) {
-                            to.requireViewById(R.id.volume_indeterminate_progress).setVisibility(
-                                    View.VISIBLE);
-                            // Unset the listener, otherwise this may persist for another view
-                            // property animation
-                            toTitleText.animate().setListener(null);
-                        }
-                    });
-            // Animation for seek bar
-            fromSeekBar.animate()
-                    .alpha(0)
-                    .setDuration(ANIM_DURATION)
-                    .setListener(new AnimatorListenerAdapter() {
-                        @Override
-                        public void onAnimationEnd(Animator animation) {
-                            final TextView fromTitleText = from.requireViewById(
-                                    R.id.two_line_title);
-                            fromTitleText.setTypeface(Typeface.create(mContext.getString(
-                                    com.android.internal.R.string.config_headlineFontFamily),
-                                    Typeface.NORMAL));
-                            fromTitleText.animate()
-                                    .setDuration(ANIM_DURATION)
-                                    .translationY(delta)
-                                    .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
-                                    .setListener(new AnimatorListenerAdapter() {
-                                        @Override
-                                        public void onAnimationEnd(Animator animation) {
-                                            mIsAnimating = false;
-                                            notifyDataSetChanged();
-                                            // Unset the listener, otherwise this may persist for
-                                            // another view property animation
-                                            fromTitleText.animate().setListener(null);
-                                        }
-                                    });
-                            // Unset the listener, otherwise this may persist for another view
-                            // property animation
-                            fromSeekBar.animate().setListener(null);
-                        }
-                    });
+        private void animateCornerAndVolume(int fromProgress, int toProgress) {
+            final GradientDrawable layoutBackgroundDrawable =
+                    (GradientDrawable) mItemLayout.getBackground();
+            final ClipDrawable clipDrawable =
+                    (ClipDrawable) ((LayerDrawable) mSeekBar.getProgressDrawable())
+                            .findDrawableByLayerId(android.R.id.progress);
+            final GradientDrawable progressDrawable = (GradientDrawable) clipDrawable.getDrawable();
+            mCornerAnimator.addUpdateListener(animation -> {
+                float value = (float) animation.getAnimatedValue();
+                layoutBackgroundDrawable.setCornerRadius(value);
+                progressDrawable.setCornerRadius(value);
+            });
+            mVolumeAnimator.setIntValues(fromProgress, toProgress);
+            mVolumeAnimator.start();
+            mCornerAnimator.start();
+        }
+
+        private void initAnimator() {
+            mCornerAnimator = ValueAnimator.ofFloat(mController.getInactiveRadius(),
+                    mController.getActiveRadius());
+            mCornerAnimator.setDuration(ANIM_DURATION);
+            mCornerAnimator.setInterpolator(new LinearInterpolator());
+
+            mVolumeAnimator = ValueAnimator.ofInt();
+            mVolumeAnimator.addUpdateListener(animation -> {
+                int value = (int) animation.getAnimatedValue();
+                mSeekBar.setProgress(value);
+            });
+            mVolumeAnimator.setDuration(ANIM_DURATION);
+            mVolumeAnimator.setInterpolator(new LinearInterpolator());
+            mVolumeAnimator.addListener(new Animator.AnimatorListener() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+                    mSeekBar.setEnabled(false);
+                }
+
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mSeekBar.setEnabled(true);
+                }
+
+                @Override
+                public void onAnimationCancel(Animator animation) {
+                    mSeekBar.setEnabled(true);
+                }
+
+                @Override
+                public void onAnimationRepeat(Animator animation) {
+
+                }
+            });
         }
 
         Drawable getSpeakerDrawable() {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index e5e7eb6..5bb6557 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -184,6 +184,19 @@
                 }
             };
 
+    private class LayoutManagerWrapper extends LinearLayoutManager {
+        LayoutManagerWrapper(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onLayoutCompleted(RecyclerView.State state) {
+            super.onLayoutCompleted(state);
+            mMediaOutputController.setRefreshing(false);
+            mMediaOutputController.refreshDataSetIfNeeded();
+        }
+    }
+
     public MediaOutputBaseDialog(Context context, BroadcastSender broadcastSender,
             MediaOutputController mediaOutputController) {
         super(context, R.style.Theme_SystemUI_Dialog_Media);
@@ -192,7 +205,7 @@
         mContext = getContext();
         mBroadcastSender = broadcastSender;
         mMediaOutputController = mediaOutputController;
-        mLayoutManager = new LinearLayoutManager(mContext);
+        mLayoutManager = new LayoutManagerWrapper(mContext);
         mListMaxHeight = context.getResources().getDimensionPixelSize(
                 R.dimen.media_output_dialog_list_max_height);
         mExecutor = Executors.newSingleThreadExecutor();
@@ -274,6 +287,10 @@
     }
 
     void refresh(boolean deviceSetChanged) {
+        if (mMediaOutputController.isRefreshing()) {
+            return;
+        }
+        mMediaOutputController.setRefreshing(true);
         // Update header icon
         final int iconRes = getHeaderIconRes();
         final IconCompat iconCompat = getHeaderIcon();
@@ -334,7 +351,7 @@
             mHeaderSubtitle.setText(subTitle);
             mHeaderTitle.setGravity(Gravity.NO_GRAVITY);
         }
-        if (!mAdapter.isDragging() && !mAdapter.isAnimating()) {
+        if (!mAdapter.isDragging()) {
             int currentActivePosition = mAdapter.getCurrentActivePosition();
             if (!colorSetUpdated && !deviceSetChanged && currentActivePosition >= 0
                     && currentActivePosition < mAdapter.getItemCount()) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index 8723f4f..e7f97d2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -81,6 +81,8 @@
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -108,11 +110,15 @@
     private final DialogLaunchAnimator mDialogLaunchAnimator;
     private final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>();
     private final CommonNotifCollection mNotifCollection;
+    private final Object mMediaDevicesLock = new Object();
     @VisibleForTesting
     final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>();
+    final List<MediaDevice> mCachedMediaDevices = new CopyOnWriteArrayList<>();
     private final NearbyMediaDevicesManager mNearbyMediaDevicesManager;
     private final Map<String, Integer> mNearbyDeviceInfoMap = new ConcurrentHashMap<>();
 
+    private boolean mIsRefreshing = false;
+    private boolean mNeedRefresh = false;
     private MediaController mMediaController;
     @VisibleForTesting
     Callback mCallback;
@@ -127,6 +133,8 @@
     private int mColorItemBackground;
     private int mColorConnectedItemBackground;
     private int mColorPositiveButtonText;
+    private float mInactiveRadius;
+    private float mActiveRadius;
 
     public enum BroadcastNotifyDialog {
         ACTION_FIRST_LAUNCH,
@@ -163,10 +171,17 @@
                 R.color.media_dialog_connected_item_background);
         mColorPositiveButtonText = Utils.getColorStateListDefaultColor(mContext,
                 R.color.media_dialog_solid_button_text);
+        mInactiveRadius = mContext.getResources().getDimension(
+                R.dimen.media_output_dialog_background_radius);
+        mActiveRadius = mContext.getResources().getDimension(
+                R.dimen.media_output_dialog_active_background_radius);
     }
 
     void start(@NonNull Callback cb) {
-        mMediaDevices.clear();
+        synchronized (mMediaDevicesLock) {
+            mCachedMediaDevices.clear();
+            mMediaDevices.clear();
+        }
         mNearbyDeviceInfoMap.clear();
         if (mNearbyMediaDevicesManager != null) {
             mNearbyMediaDevicesManager.registerNearbyDevicesCallback(this);
@@ -205,6 +220,14 @@
         return routerParams != null && !routerParams.isMediaTransferReceiverEnabled();
     }
 
+    void setRefreshing(boolean refreshing) {
+        mIsRefreshing = refreshing;
+    }
+
+    boolean isRefreshing() {
+        return mIsRefreshing;
+    }
+
     void stop() {
         if (mMediaController != null) {
             mMediaController.unregisterCallback(mCb);
@@ -213,7 +236,10 @@
             mLocalMediaManager.unregisterCallback(this);
             mLocalMediaManager.stopScan();
         }
-        mMediaDevices.clear();
+        synchronized (mMediaDevicesLock) {
+            mCachedMediaDevices.clear();
+            mMediaDevices.clear();
+        }
         if (mNearbyMediaDevicesManager != null) {
             mNearbyMediaDevicesManager.unregisterNearbyDevicesCallback(this);
         }
@@ -222,15 +248,23 @@
 
     @Override
     public void onDeviceListUpdate(List<MediaDevice> devices) {
-        buildMediaDevices(devices);
-        mCallback.onDeviceListChanged();
+        if (mMediaDevices.isEmpty() || !mIsRefreshing) {
+            buildMediaDevices(devices);
+            mCallback.onDeviceListChanged();
+        } else {
+            synchronized (mMediaDevicesLock) {
+                mNeedRefresh = true;
+                mCachedMediaDevices.clear();
+                mCachedMediaDevices.addAll(devices);
+            }
+        }
     }
 
     @Override
     public void onSelectedDeviceStateChanged(MediaDevice device,
             @LocalMediaManager.MediaDeviceState int state) {
         mCallback.onRouteChanged();
-        mMetricLogger.logOutputSuccess(device.toString(), mMediaDevices);
+        mMetricLogger.logOutputSuccess(device.toString(), new ArrayList<>(mMediaDevices));
     }
 
     @Override
@@ -241,7 +275,7 @@
     @Override
     public void onRequestFailed(int reason) {
         mCallback.onRouteChanged();
-        mMetricLogger.logOutputFailure(mMediaDevices, reason);
+        mMetricLogger.logOutputFailure(new ArrayList<>(mMediaDevices), reason);
     }
 
     Drawable getAppSourceIcon() {
@@ -393,6 +427,14 @@
         }
     }
 
+    void refreshDataSetIfNeeded() {
+        if (mNeedRefresh) {
+            buildMediaDevices(mCachedMediaDevices);
+            mCallback.onDeviceListChanged();
+            mNeedRefresh = false;
+        }
+    }
+
     public int getColorConnectedItemBackground() {
         return mColorConnectedItemBackground;
     }
@@ -417,51 +459,64 @@
         return mColorItemBackground;
     }
 
-    private void buildMediaDevices(List<MediaDevice> devices) {
-        // For the first time building list, to make sure the top device is the connected device.
-        if (mMediaDevices.isEmpty()) {
-            final MediaDevice connectedMediaDevice = getCurrentConnectedMediaDevice();
-            if (connectedMediaDevice == null) {
-                if (DEBUG) {
-                    Log.d(TAG, "No connected media device.");
-                }
-                mMediaDevices.addAll(devices);
-                return;
-            }
-            for (MediaDevice device : devices) {
-                if (TextUtils.equals(device.getId(), connectedMediaDevice.getId())) {
-                    mMediaDevices.add(0, device);
-                } else {
-                    mMediaDevices.add(device);
-                }
-            }
-            return;
-        }
-        // To keep the same list order
-        final Collection<MediaDevice> targetMediaDevices = new ArrayList<>();
-        for (MediaDevice originalDevice : mMediaDevices) {
-            for (MediaDevice newDevice : devices) {
-                if (TextUtils.equals(originalDevice.getId(), newDevice.getId())) {
-                    targetMediaDevices.add(newDevice);
-                    break;
-                }
-            }
-        }
-        if (targetMediaDevices.size() != devices.size()) {
-            devices.removeAll(targetMediaDevices);
-            targetMediaDevices.addAll(devices);
-        }
-        mMediaDevices.clear();
-        mMediaDevices.addAll(targetMediaDevices);
-        attachRangeInfo();
+    public float getInactiveRadius() {
+        return mInactiveRadius;
     }
 
-    private void attachRangeInfo() {
-        for (MediaDevice mediaDevice : mMediaDevices) {
+    public float getActiveRadius() {
+        return mActiveRadius;
+    }
+
+    private void buildMediaDevices(List<MediaDevice> devices) {
+        synchronized (mMediaDevicesLock) {
+            attachRangeInfo(devices);
+            Collections.sort(devices, Comparator.naturalOrder());
+            // For the first time building list, to make sure the top device is the connected
+            // device.
+            if (mMediaDevices.isEmpty()) {
+                final MediaDevice connectedMediaDevice = getCurrentConnectedMediaDevice();
+                if (connectedMediaDevice == null) {
+                    if (DEBUG) {
+                        Log.d(TAG, "No connected media device.");
+                    }
+                    mMediaDevices.addAll(devices);
+                    return;
+                }
+                for (MediaDevice device : devices) {
+                    if (TextUtils.equals(device.getId(), connectedMediaDevice.getId())) {
+                        mMediaDevices.add(0, device);
+                    } else {
+                        mMediaDevices.add(device);
+                    }
+                }
+                return;
+            }
+            // To keep the same list order
+            final List<MediaDevice> targetMediaDevices = new ArrayList<>();
+            for (MediaDevice originalDevice : mMediaDevices) {
+                for (MediaDevice newDevice : devices) {
+                    if (TextUtils.equals(originalDevice.getId(), newDevice.getId())) {
+                        targetMediaDevices.add(newDevice);
+                        break;
+                    }
+                }
+            }
+            if (targetMediaDevices.size() != devices.size()) {
+                devices.removeAll(targetMediaDevices);
+                targetMediaDevices.addAll(devices);
+            }
+            mMediaDevices.clear();
+            mMediaDevices.addAll(targetMediaDevices);
+        }
+    }
+
+    private void attachRangeInfo(List<MediaDevice> devices) {
+        for (MediaDevice mediaDevice : devices) {
             if (mNearbyDeviceInfoMap.containsKey(mediaDevice.getId())) {
                 mediaDevice.setRangeZone(mNearbyDeviceInfoMap.get(mediaDevice.getId()));
             }
         }
+
     }
 
     List<MediaDevice> getGroupMediaDevices() {
@@ -595,26 +650,30 @@
     }
 
     boolean isTransferring() {
-        for (MediaDevice device : mMediaDevices) {
-            if (device.getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
-                return true;
+        synchronized (mMediaDevicesLock) {
+            for (MediaDevice device : mMediaDevices) {
+                if (device.getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
+                    return true;
+                }
             }
         }
         return false;
     }
 
     boolean isZeroMode() {
-        if (mMediaDevices.size() == 1) {
-            final MediaDevice device = mMediaDevices.iterator().next();
-            // Add "pair new" only when local output device exists
-            final int type = device.getDeviceType();
-            if (type == MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE
-                    || type == MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE
-                    || type == MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE) {
-                return true;
+        synchronized (mMediaDevicesLock) {
+            if (mMediaDevices.size() == 1) {
+                final MediaDevice device = mMediaDevices.iterator().next();
+                // Add "pair new" only when local output device exists
+                final int type = device.getDeviceType();
+                if (type == MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE
+                        || type == MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE
+                        || type == MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE) {
+                    return true;
+                }
             }
+            return false;
         }
-        return false;
     }
 
     void launchBluetoothPairing(View view) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java
index 9b42b1d..ba2f006 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java
@@ -101,9 +101,10 @@
             mCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
                 onCheckBoxClicked(isChecked, device);
             });
+            boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE;
             setTwoLineLayout(device, false /* bFocused */, true /* showSeekBar */,
                     false /* showProgressBar */, false /* showSubtitle*/);
-            initSeekbar(device);
+            initSeekbar(device, isCurrentSeekbarInvisible);
             final List<MediaDevice> selectedDevices = mController.getSelectedMediaDevice();
             if (isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
                 mCheckBox.setButtonDrawable(R.drawable.ic_check_box);
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java
new file mode 100644
index 0000000..4ff79d6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSeekbar.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.dialog;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.SeekBar;
+
+/**
+ * Customized SeekBar for MediaOutputDialog, apply scale between device volume and progress, to make
+ * adjustment smoother.
+ */
+public class MediaOutputSeekbar extends SeekBar {
+    private static final int SCALE_SIZE = 1000;
+
+    public MediaOutputSeekbar(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setMin(0);
+    }
+
+    static int scaleProgressToVolume(int progress) {
+        return progress / SCALE_SIZE;
+    }
+
+    static int scaleVolumeToProgress(int volume) {
+        return volume * SCALE_SIZE;
+    }
+
+    int getVolume() {
+        return getProgress() / SCALE_SIZE;
+    }
+
+    void setVolume(int volume) {
+        setProgress(volume * SCALE_SIZE, true);
+    }
+
+    void setMaxVolume(int maxVolume) {
+        setMax(maxVolume * SCALE_SIZE);
+    }
+
+    void resetVolume() {
+        setProgress(getMin());
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index ce6f3e4..6602f99 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -510,7 +510,8 @@
                             .setMessage(trustGrantedIndication)
                             .setTextColor(mInitialTextColorState)
                             .build(),
-                    false);
+                    true);
+            hideBiometricMessage();
         } else if (!TextUtils.isEmpty(trustManagedIndication)
                 && mKeyguardUpdateMonitor.getUserTrustIsManaged(userId)
                 && !userHasTrust) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
index 3e32b64..3f8e97f1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt
@@ -21,6 +21,7 @@
 import android.hardware.Sensor
 import android.hardware.TriggerEvent
 import android.hardware.TriggerEventListener
+import com.android.keyguard.ActiveUnlockConfig
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.CoreStartable
@@ -71,13 +72,9 @@
             isListening = false
             updateListeningState()
             keyguardUpdateMonitor.requestFaceAuth(true)
-            if (keyguardUpdateMonitor.mRequestActiveUnlockOnWakeup) {
-                keyguardUpdateMonitor.requestActiveUnlock("wake-unlock," +
-                        " extra=KeyguardLiftController")
-            } else if (keyguardUpdateMonitor.mInitiateActiveUnlockOnWakeup) {
-                keyguardUpdateMonitor.initiateActiveUnlock("wake-initiate," +
-                        " extra=KeyguardLiftController")
-            }
+            keyguardUpdateMonitor.requestActiveUnlock(
+                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE,
+                "KeyguardLiftController")
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index 98a711d..b5789fb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -98,6 +98,7 @@
 import com.android.internal.policy.ScreenDecorationsUtils;
 import com.android.internal.policy.SystemBarUtils;
 import com.android.internal.util.LatencyTracker;
+import com.android.keyguard.ActiveUnlockConfig;
 import com.android.keyguard.KeyguardStatusView;
 import com.android.keyguard.KeyguardStatusViewController;
 import com.android.keyguard.KeyguardUnfoldTransition;
@@ -3383,11 +3384,10 @@
                             .log(LockscreenUiEvent.LOCKSCREEN_LOCK_SHOW_HINT);
                         startUnlockHintAnimation();
                     }
-                    if (mUpdateMonitor.isFaceEnrolled()
-                            && mUpdateMonitor.mRequestActiveUnlockOnUnlockIntent
-                            && mKeyguardBypassController.canBypass()) {
-                        mUpdateMonitor.requestActiveUnlock("unlock-intent,"
-                                + " extra=lockScreenEmptySpaceTap", true);
+                    if (mUpdateMonitor.isFaceEnrolled()) {
+                        mUpdateMonitor.requestActiveUnlock(
+                                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
+                                "lockScreenEmptySpaceTap");
                     }
                 }
                 return true;
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ActiveUnlockConfigTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ActiveUnlockConfigTest.kt
new file mode 100644
index 0000000..7476490
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ActiveUnlockConfigTest.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard
+
+import android.content.ContentResolver
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.settings.SecureSettings
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+class ActiveUnlockConfigTest : SysuiTestCase() {
+    private val fakeWakeUri = Uri.Builder().appendPath("wake").build()
+    private val fakeUnlockIntentUri = Uri.Builder().appendPath("unlock-intent").build()
+    private val fakeBioFailUri = Uri.Builder().appendPath("bio-fail").build()
+
+    @Mock
+    private lateinit var secureSettings: SecureSettings
+
+    @Mock
+    private lateinit var contentResolver: ContentResolver
+
+    @Mock
+    private lateinit var handler: Handler
+
+    @Mock
+    private lateinit var dumpManager: DumpManager
+
+    @Captor
+    private lateinit var settingsObserverCaptor: ArgumentCaptor<ContentObserver>
+
+    private lateinit var activeUnlockConfig: ActiveUnlockConfig
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        `when`(secureSettings.getUriFor(Settings.Secure.ACTIVE_UNLOCK_ON_WAKE))
+                .thenReturn(fakeWakeUri)
+        `when`(secureSettings.getUriFor(Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT))
+                .thenReturn(fakeUnlockIntentUri)
+        `when`(secureSettings.getUriFor(Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL))
+                .thenReturn(fakeBioFailUri)
+
+        activeUnlockConfig = ActiveUnlockConfig(
+                handler,
+                secureSettings,
+                contentResolver,
+                dumpManager
+        )
+    }
+
+    @Test
+    fun testRegsitersForSettingsChanges() {
+        verifyRegisterSettingObserver()
+    }
+
+    @Test
+    fun testOnWakeupSettingChanged() {
+        verifyRegisterSettingObserver()
+
+        // GIVEN no active unlock settings enabled
+        assertFalse(
+                activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+                        ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE)
+        )
+
+        // WHEN unlock on wake is allowed
+        `when`(secureSettings.getIntForUser(Settings.Secure.ACTIVE_UNLOCK_ON_WAKE,
+                0, 0)).thenReturn(1)
+        settingsObserverCaptor.value.onChange(
+                false,
+                listOf(fakeWakeUri),
+                0,
+                0
+        )
+
+        // THEN active unlock triggers allowed on: wake, unlock-intent, and biometric failure
+        assertTrue(
+                activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+                        ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE)
+        )
+        assertTrue(
+                activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+                        ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT)
+        )
+        assertTrue(
+                activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+                        ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL)
+        )
+    }
+
+    @Test
+    fun testOnUnlockIntentSettingChanged() {
+        verifyRegisterSettingObserver()
+
+        // GIVEN no active unlock settings enabled
+        assertFalse(
+                activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+                        ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT)
+        )
+
+        // WHEN unlock on biometric failed is allowed
+        `when`(secureSettings.getIntForUser(Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT,
+                0, 0)).thenReturn(1)
+        settingsObserverCaptor.value.onChange(
+                false,
+                listOf(fakeUnlockIntentUri),
+                0,
+                0
+        )
+
+        // THEN active unlock triggers allowed on: biometric failure ONLY
+        assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE))
+        assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+        assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+    }
+
+    @Test
+    fun testOnBioFailSettingChanged() {
+        verifyRegisterSettingObserver()
+
+        // GIVEN no active unlock settings enabled
+        assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+
+        // WHEN unlock on biometric failed is allowed
+        `when`(secureSettings.getIntForUser(Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL,
+                0, 0)).thenReturn(1)
+        settingsObserverCaptor.value.onChange(
+                false,
+                listOf(fakeBioFailUri),
+                0,
+                0
+        )
+
+        // THEN active unlock triggers allowed on: biometric failure ONLY
+        assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE))
+        assertFalse(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT))
+        assertTrue(activeUnlockConfig.shouldAllowActiveUnlockFromOrigin(
+                ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL))
+    }
+
+    private fun verifyRegisterSettingObserver() {
+        verify(contentResolver).registerContentObserver(
+                eq(fakeWakeUri),
+                eq(false),
+                capture(settingsObserverCaptor),
+                eq(UserHandle.USER_ALL))
+
+        verify(contentResolver).registerContentObserver(
+                eq(fakeUnlockIntentUri),
+                eq(false),
+                capture(settingsObserverCaptor),
+                eq(UserHandle.USER_ALL))
+
+        verify(contentResolver).registerContentObserver(
+                eq(fakeBioFailUri),
+                eq(false),
+                capture(settingsObserverCaptor),
+                eq(UserHandle.USER_ALL))
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 86a4f5a..2dc066c 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -177,6 +177,8 @@
     private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateListenerCaptor;
     @Mock
     private KeyguardUpdateMonitorCallback mTestCallback;
+    @Mock
+    private ActiveUnlockConfig mActiveUnlockConfig;
     // Direct executor
     private Executor mBackgroundExecutor = Runnable::run;
     private Executor mMainExecutor = Runnable::run;
@@ -1188,7 +1190,7 @@
                     mBackgroundExecutor, mMainExecutor,
                     mStatusBarStateController, mLockPatternUtils,
                     mAuthController, mTelephonyListenerManager,
-                    mInteractionJankMonitor, mLatencyTracker);
+                    mInteractionJankMonitor, mLatencyTracker, mActiveUnlockConfig);
             setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index 7779f42..da5939a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -212,6 +212,7 @@
                 .thenReturn(mFpmOtherView);
         when(mEnrollView.getContext()).thenReturn(mContext);
         when(mKeyguardStateController.isOccluded()).thenReturn(false);
+        when(mKeyguardUpdateMonitor.isFingerprintDetectionRunning()).thenReturn(true);
         final List<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
 
         final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java
index cf6fd24..9256cd3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java
@@ -185,25 +185,14 @@
     }
 
     @Test
-    public void onBindViewHolder_verifySessionVolume() {
-        when(mMediaOutputController.getSessionVolume()).thenReturn(TEST_VOLUME);
-        when(mMediaOutputController.getSessionVolumeMax()).thenReturn(TEST_MAX_VOLUME);
-
-        mGroupAdapter.onBindViewHolder(mGroupViewHolder, 0);
-
-        assertThat(mGroupViewHolder.mSeekBar.getProgress()).isEqualTo(TEST_VOLUME);
-        assertThat(mGroupViewHolder.mSeekBar.getMax()).isEqualTo(TEST_MAX_VOLUME);
-    }
-
-    @Test
     public void onBindViewHolder_verifyDeviceVolume() {
         when(mMediaDevice1.getCurrentVolume()).thenReturn(TEST_VOLUME);
         when(mMediaDevice1.getMaxVolume()).thenReturn(TEST_MAX_VOLUME);
+        mGroupViewHolder.mSeekBar.setVisibility(View.VISIBLE);
 
         mGroupAdapter.onBindViewHolder(mGroupViewHolder, 1);
 
-        assertThat(mGroupViewHolder.mSeekBar.getProgress()).isEqualTo(TEST_VOLUME);
-        assertThat(mGroupViewHolder.mSeekBar.getMax()).isEqualTo(TEST_MAX_VOLUME);
+        assertThat(mGroupViewHolder.mSeekBar.getVolume()).isEqualTo(TEST_VOLUME);
     }
 
     @Test
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 0b3c18b..3b715a2 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -82,6 +82,11 @@
 
     /** ID for Communication strategy retrieved form audio policy manager */
     private int mCommunicationStrategyId = -1;
+
+    /** ID for Accessibility strategy retrieved form audio policy manager */
+    private int mAccessibilityStrategyId = -1;
+
+
     /** Active communication device reported by audio policy manager */
     private AudioDeviceInfo mActiveCommunicationDevice;
     /** Last preferred device set for communication strategy */
@@ -141,22 +146,28 @@
         init();
     }
 
-    private void initCommunicationStrategyId() {
+    private void initRoutingStrategyIds() {
         List<AudioProductStrategy> strategies = AudioProductStrategy.getAudioProductStrategies();
+        mCommunicationStrategyId = -1;
+        mAccessibilityStrategyId = -1;
         for (AudioProductStrategy strategy : strategies) {
-            if (strategy.getAudioAttributesForLegacyStreamType(AudioSystem.STREAM_VOICE_CALL)
-                    != null) {
+            if (mCommunicationStrategyId == -1
+                    && strategy.getAudioAttributesForLegacyStreamType(
+                            AudioSystem.STREAM_VOICE_CALL) != null) {
                 mCommunicationStrategyId = strategy.getId();
-                return;
+            }
+            if (mAccessibilityStrategyId == -1
+                    && strategy.getAudioAttributesForLegacyStreamType(
+                            AudioSystem.STREAM_ACCESSIBILITY) != null) {
+                mAccessibilityStrategyId = strategy.getId();
             }
         }
-        mCommunicationStrategyId = -1;
     }
 
     private void init() {
         setupMessaging(mContext);
 
-        initCommunicationStrategyId();
+        initRoutingStrategyIds();
         mPreferredCommunicationDevice = null;
         updateActiveCommunicationDevice();
 
@@ -813,19 +824,10 @@
         return mDeviceInventory.setPreferredDevicesForStrategySync(strategy, devices);
     }
 
-    /*package*/ void postSetPreferredDevicesForStrategy(int strategy,
-            @NonNull List<AudioDeviceAttributes> devices) {
-        sendILMsgNoDelay(MSG_IL_SET_PREF_DEVICES_FOR_STRATEGY, SENDMSG_REPLACE, strategy, devices);
-    }
-
     /*package*/ int removePreferredDevicesForStrategySync(int strategy) {
         return mDeviceInventory.removePreferredDevicesForStrategySync(strategy);
     }
 
-    /*package*/ void postRemovePreferredDevicesForStrategy(int strategy) {
-        sendIMsgNoDelay(MSG_I_REMOVE_PREF_DEVICES_FOR_STRATEGY, SENDMSG_REPLACE, strategy);
-    }
-
     /*package*/ void registerStrategyPreferredDevicesDispatcher(
             @NonNull IStrategyPreferredDevicesDispatcher dispatcher) {
         mDeviceInventory.registerStrategyPreferredDevicesDispatcher(dispatcher);
@@ -1157,6 +1159,9 @@
         pw.println(prefix + "mCommunicationStrategyId: "
                 +  mCommunicationStrategyId);
 
+        pw.println(prefix + "mAccessibilityStrategyId: "
+                +  mAccessibilityStrategyId);
+
         pw.println("\n" + prefix + "mModeOwnerPid: " + mModeOwnerPid);
 
         mBtHelper.dump(pw, prefix);
@@ -1252,7 +1257,7 @@
                 case MSG_RESTORE_DEVICES:
                     synchronized (mSetModeLock) {
                         synchronized (mDeviceStateLock) {
-                            initCommunicationStrategyId();
+                            initRoutingStrategyIds();
                             updateActiveCommunicationDevice();
                             mDeviceInventory.onRestoreDevices();
                             mBtHelper.onAudioServerDiedRestoreA2dp();
@@ -1440,22 +1445,6 @@
                     final int strategy = msg.arg1;
                     mDeviceInventory.onSaveRemovePreferredDevices(strategy);
                 } break;
-                case MSG_IL_SET_PREF_DEVICES_FOR_STRATEGY: {
-                    final int strategy = msg.arg1;
-                    final List<AudioDeviceAttributes> devices =
-                            (List<AudioDeviceAttributes>) msg.obj;
-                    setPreferredDevicesForStrategySync(strategy, devices);
-                    if (strategy == mCommunicationStrategyId) {
-                        onUpdatePhoneStrategyDevice(devices.isEmpty() ? null : devices.get(0));
-                    }
-                } break;
-                case MSG_I_REMOVE_PREF_DEVICES_FOR_STRATEGY: {
-                    final int strategy = msg.arg1;
-                    removePreferredDevicesForStrategySync(strategy);
-                    if (strategy == mCommunicationStrategyId) {
-                        onUpdatePhoneStrategyDevice(null);
-                    }
-                } break;
                 case MSG_CHECK_MUTE_MUSIC:
                     checkMessagesMuteMusic(0);
                     break;
@@ -1533,8 +1522,6 @@
     private static final int MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET = 38;
 
     private static final int MSG_L_UPDATE_COMMUNICATION_ROUTE = 39;
-    private static final int MSG_IL_SET_PREF_DEVICES_FOR_STRATEGY = 40;
-    private static final int MSG_I_REMOVE_PREF_DEVICES_FOR_STRATEGY = 41;
     private static final int MSG_L_SET_COMMUNICATION_ROUTE_FOR_CLIENT = 42;
     private static final int MSG_L_UPDATE_COMMUNICATION_ROUTE_CLIENT = 43;
     private static final int MSG_I_SCO_AUDIO_STATE_CHANGED = 44;
@@ -1836,9 +1823,12 @@
         }
         if (preferredCommunicationDevice == null) {
             removePreferredDevicesForStrategySync(mCommunicationStrategyId);
+            removePreferredDevicesForStrategySync(mAccessibilityStrategyId);
         } else {
             setPreferredDevicesForStrategySync(
                     mCommunicationStrategyId, Arrays.asList(preferredCommunicationDevice));
+            setPreferredDevicesForStrategySync(
+                    mAccessibilityStrategyId, Arrays.asList(preferredCommunicationDevice));
         }
         onUpdatePhoneStrategyDevice(preferredCommunicationDevice);
     }
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 6a78739..47e606a 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -181,7 +181,7 @@
             ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "TaskFragment info changed name=%s",
                     tf.getName());
             try {
-                mOrganizer.onTaskFragmentInfoChanged(tf.getTaskFragmentInfo());
+                mOrganizer.onTaskFragmentInfoChanged(info);
                 mLastSentTaskFragmentInfos.put(tf, info);
             } catch (RemoteException e) {
                 Slog.d(TAG, "Exception sending onTaskFragmentInfoChanged callback", e);
@@ -424,6 +424,10 @@
             }
             // Remove and add for re-ordering.
             mPendingTaskFragmentEvents.remove(pendingEvent);
+            // Reset the defer time when TaskFragment is changed, so that it can check again if
+            // the event should be sent to the organizer, for example the TaskFragment may become
+            // empty.
+            pendingEvent.mDeferTime = 0;
         }
         mPendingTaskFragmentEvents.add(pendingEvent);
     }
@@ -654,26 +658,15 @@
         return null;
     }
 
-    private boolean shouldSendEventWhenTaskInvisible(@NonNull Task task,
-            @NonNull PendingTaskFragmentEvent event) {
+    private boolean shouldSendEventWhenTaskInvisible(@NonNull PendingTaskFragmentEvent event) {
         final TaskFragmentOrganizerState state =
                 mTaskFragmentOrganizerState.get(event.mTaskFragmentOrg.asBinder());
         final TaskFragmentInfo lastInfo = state.mLastSentTaskFragmentInfos.get(event.mTaskFragment);
         final TaskFragmentInfo info = event.mTaskFragment.getTaskFragmentInfo();
         // Send an info changed callback if this event is for the last activities to finish in a
-        // Task so that the {@link TaskFragmentOrganizer} can delete this TaskFragment. Otherwise,
-        // the Task may be removed before it becomes visible again to send this event because it no
-        // longer has activities. As a result, the organizer will never get this info changed event
-        // and will not delete the TaskFragment because the organizer thinks the TaskFragment still
-        // has running activities.
-        // Another case is when an organized TaskFragment became empty because the last running
-        // activity is reparented to a new Task due to enter PiP. We also want to notify the
-        // organizer, so it can remove the empty TaskFragment and update the paired TaskFragment
-        // without causing the extra delay.
+        // TaskFragment so that the {@link TaskFragmentOrganizer} can delete this TaskFragment.
         return event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED
-                && (task.topRunningActivity() == null || info.isTaskFragmentClearedForPip())
-                && lastInfo != null
-                && lastInfo.getRunningActivityCount() > 0 && info.getRunningActivityCount() == 0;
+                && lastInfo != null && lastInfo.hasRunningActivity() && info.isEmpty();
     }
 
     void dispatchPendingEvents() {
@@ -690,7 +683,7 @@
             final Task task = event.mTaskFragment != null ? event.mTaskFragment.getTask() : null;
             if (task != null && (task.lastActiveTime <= event.mDeferTime
                     || !(isTaskVisible(task, visibleTasks, invisibleTasks)
-                    || shouldSendEventWhenTaskInvisible(task, event)))) {
+                    || shouldSendEventWhenTaskInvisible(event)))) {
                 // Defer sending events to the TaskFragment until the host task is active again.
                 event.mDeferTime = task.lastActiveTime;
                 continue;
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 24d04da..75b5e73 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -779,6 +779,47 @@
     }
 
     /**
+     * Tests that a task fragment info changed event is sent if the TaskFragment becomes empty
+     * even if the Task is invisible.
+     */
+    @Test
+    public void testPendingTaskFragmentInfoChangedEvent_emptyTaskFragment() {
+        // Create a TaskFragment with an activity, all within a parent task
+        final Task task = createTask(mDisplayContent);
+        final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(mFragmentToken)
+                .createActivityCount(1)
+                .build();
+        final ActivityRecord embeddedActivity = taskFragment.getTopNonFinishingActivity();
+        // Add another activity in the Task so that it always contains a non-finishing activitiy.
+        final ActivityRecord nonEmbeddedActivity = createActivityRecord(task);
+        assertTrue(task.shouldBeVisible(null));
+
+        // Dispatch pending info changed event from creating the activity
+        mController.registerOrganizer(mIOrganizer);
+        taskFragment.mTaskFragmentAppearedSent = true;
+        mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
+        mController.dispatchPendingEvents();
+        verify(mOrganizer).onTaskFragmentInfoChanged(any());
+
+        // Verify the info changed callback is not called when the task is invisible
+        reset(mOrganizer);
+        doReturn(false).when(task).shouldBeVisible(any());
+        mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
+        mController.dispatchPendingEvents();
+        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
+
+        // Finish the embedded activity, and verify the info changed callback is called because the
+        // TaskFragment is becoming empty.
+        embeddedActivity.finishing = true;
+        mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
+        mController.dispatchPendingEvents();
+        verify(mOrganizer).onTaskFragmentInfoChanged(any());
+    }
+
+    /**
      * When an embedded {@link TaskFragment} is removed, we should clean up the reference in the
      * {@link WindowOrganizerController}.
      */