Merge "Move all bouncer messages domain logic to the interactor" into main
diff --git a/apct-tests/perftests/core/src/android/accessibility/AccessibilityPerfTest.java b/apct-tests/perftests/core/src/android/accessibility/AccessibilityPerfTest.java
index 7927aa9..885000f 100644
--- a/apct-tests/perftests/core/src/android/accessibility/AccessibilityPerfTest.java
+++ b/apct-tests/perftests/core/src/android/accessibility/AccessibilityPerfTest.java
@@ -22,7 +22,6 @@
 import android.app.Instrumentation;
 import android.app.UiAutomation;
 import android.perftests.utils.PerfTestActivity;
-import android.platform.test.annotations.LargeTest;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
@@ -32,6 +31,7 @@
 
 import androidx.benchmark.BenchmarkState;
 import androidx.benchmark.junit4.BenchmarkRule;
+import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.rule.ActivityTestRule;
 
diff --git a/apct-tests/perftests/core/src/android/text/TextViewCursorAnchorInfoPerfTest.java b/apct-tests/perftests/core/src/android/text/TextViewCursorAnchorInfoPerfTest.java
index 898111f..436ee16 100644
--- a/apct-tests/perftests/core/src/android/text/TextViewCursorAnchorInfoPerfTest.java
+++ b/apct-tests/perftests/core/src/android/text/TextViewCursorAnchorInfoPerfTest.java
@@ -22,13 +22,13 @@
 import android.perftests.utils.BenchmarkState;
 import android.perftests.utils.PerfStatusReporter;
 import android.perftests.utils.PerfTestActivity;
-import android.platform.test.annotations.LargeTest;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.inputmethod.CursorAnchorInfo;
 import android.widget.TextView;
 
+import androidx.test.filters.LargeTest;
 import androidx.test.rule.ActivityTestRule;
 
 import org.junit.Before;
diff --git a/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING b/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING
index d9c4632..8504b1f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING
+++ b/apex/jobscheduler/service/java/com/android/server/job/TEST_MAPPING
@@ -4,7 +4,7 @@
             "name": "CtsJobSchedulerTestCases",
             "options": [
                 {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
-                {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
+                {"exclude-annotation": "androidx.test.filters.LargeTest"},
                 {"exclude-annotation": "androidx.test.filters.FlakyTest"},
                 {"exclude-annotation": "androidx.test.filters.LargeTest"}
             ]
@@ -14,7 +14,7 @@
             "options": [
                 {"include-filter": "com.android.server.job"},
                 {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
-                {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
+                {"exclude-annotation": "androidx.test.filters.LargeTest"},
                 {"exclude-annotation": "androidx.test.filters.FlakyTest"}
             ]
         },
@@ -23,7 +23,7 @@
             "options": [
                 {"include-filter": "com.android.server.job"},
                 {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
-                {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
+                {"exclude-annotation": "androidx.test.filters.LargeTest"},
                 {"exclude-annotation": "androidx.test.filters.FlakyTest"}
             ]
         }
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 660859d..ad65806 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3232,6 +3232,7 @@
     method public int getDefaultActivityPolicy();
     method public int getDefaultNavigationPolicy();
     method public int getDevicePolicy(int);
+    method @FlaggedApi(Flags.FLAG_VDM_CUSTOM_HOME) @Nullable public android.content.ComponentName getHomeComponent();
     method public int getLockState();
     method @Nullable public String getName();
     method @NonNull public java.util.Set<android.os.UserHandle> getUsersWithMatchingAccounts();
@@ -3263,6 +3264,7 @@
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setBlockedActivities(@NonNull java.util.Set<android.content.ComponentName>);
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setBlockedCrossTaskNavigations(@NonNull java.util.Set<android.content.ComponentName>);
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setDevicePolicy(int, int);
+    method @FlaggedApi(Flags.FLAG_VDM_CUSTOM_HOME) @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setHomeComponent(@Nullable android.content.ComponentName);
     method @NonNull @RequiresPermission(value=android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY, conditional=true) public android.companion.virtual.VirtualDeviceParams.Builder setLockState(int);
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setName(@NonNull String);
     method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setUsersWithMatchingAccounts(@NonNull java.util.Set<android.os.UserHandle>);
diff --git a/core/java/android/app/TEST_MAPPING b/core/java/android/app/TEST_MAPPING
index 93107ce..315a055 100644
--- a/core/java/android/app/TEST_MAPPING
+++ b/core/java/android/app/TEST_MAPPING
@@ -138,7 +138,7 @@
                     "include-annotation": "android.platform.test.annotations.Presubmit"
                 },
                 {
-                    "exclude-annotation": "android.platform.test.annotations.LargeTest"
+                    "exclude-annotation": "androidx.test.filters.LargeTest"
                 },
                 {
                     "exclude-annotation": "androidx.test.filters.FlakyTest"
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 213e5cb..7704486 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -10335,11 +10335,14 @@
      * @return the current credential manager policy if null then this policy has not been
      * configured.
      */
+    @UserHandleAware(
+            enabledSinceTargetSdkVersion = UPSIDE_DOWN_CAKE,
+            requiresPermissionIfNotCaller = INTERACT_ACROSS_USERS)
     public @Nullable PackagePolicy getCredentialManagerPolicy() {
         throwIfParentInstance("getCredentialManagerPolicy");
         if (mService != null) {
             try {
-                return mService.getCredentialManagerPolicy();
+                return mService.getCredentialManagerPolicy(myUserId());
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             }
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index c49b820..58f9d57 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -346,7 +346,7 @@
     boolean hasManagedProfileCallerIdAccess(int userId, String packageName);
 
     void setCredentialManagerPolicy(in PackagePolicy policy);
-    PackagePolicy getCredentialManagerPolicy();
+    PackagePolicy getCredentialManagerPolicy(int userId);
 
     void setManagedProfileContactsAccessPolicy(in PackagePolicy policy);
     PackagePolicy getManagedProfileContactsAccessPolicy();
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index b4c740ec..0fa78c8 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -229,6 +229,7 @@
     @Nullable private final String mName;
     // Mapping of @PolicyType to @DevicePolicy
     @NonNull private final SparseIntArray mDevicePolicies;
+    @Nullable private final ComponentName mHomeComponent;
     @NonNull private final List<VirtualSensorConfig> mVirtualSensorConfigs;
     @Nullable private final IVirtualSensorCallback mVirtualSensorCallback;
     private final int mAudioPlaybackSessionId;
@@ -243,6 +244,7 @@
             @NonNull Set<ComponentName> activityPolicyExemptions,
             @Nullable String name,
             @NonNull SparseIntArray devicePolicies,
+            @Nullable ComponentName homeComponent,
             @NonNull List<VirtualSensorConfig> virtualSensorConfigs,
             @Nullable IVirtualSensorCallback virtualSensorCallback,
             int audioPlaybackSessionId,
@@ -258,6 +260,7 @@
                 new ArraySet<>(Objects.requireNonNull(activityPolicyExemptions));
         mName = name;
         mDevicePolicies = Objects.requireNonNull(devicePolicies);
+        mHomeComponent = homeComponent;
         mVirtualSensorConfigs = Objects.requireNonNull(virtualSensorConfigs);
         mVirtualSensorCallback = virtualSensorCallback;
         mAudioPlaybackSessionId = audioPlaybackSessionId;
@@ -280,6 +283,7 @@
                 IVirtualSensorCallback.Stub.asInterface(parcel.readStrongBinder());
         mAudioPlaybackSessionId = parcel.readInt();
         mAudioRecordingSessionId = parcel.readInt();
+        mHomeComponent = parcel.readTypedObject(ComponentName.CREATOR);
     }
 
     /**
@@ -291,6 +295,19 @@
     }
 
     /**
+     * Returns the custom component used as home on all displays owned by this virtual device that
+     * support home activities.
+     *
+     * @see Builder#setHomeComponent
+     */
+    // TODO(b/297168328): Link to the relevant API for creating displays with home support.
+    @FlaggedApi(Flags.FLAG_VDM_CUSTOM_HOME)
+    @Nullable
+    public ComponentName getHomeComponent() {
+        return mHomeComponent;
+    }
+
+    /**
      * Returns the user handles with matching managed accounts on the remote device to which
      * this virtual device is streaming.
      *
@@ -468,6 +485,7 @@
                 mVirtualSensorCallback != null ? mVirtualSensorCallback.asBinder() : null);
         dest.writeInt(mAudioPlaybackSessionId);
         dest.writeInt(mAudioRecordingSessionId);
+        dest.writeTypedObject(mHomeComponent, flags);
     }
 
     @Override
@@ -508,7 +526,7 @@
         int hashCode = Objects.hash(
                 mLockState, mUsersWithMatchingAccounts, mCrossTaskNavigationExemptions,
                 mDefaultNavigationPolicy, mActivityPolicyExemptions, mDefaultActivityPolicy, mName,
-                mDevicePolicies, mAudioPlaybackSessionId, mAudioRecordingSessionId);
+                mDevicePolicies, mHomeComponent, mAudioPlaybackSessionId, mAudioRecordingSessionId);
         for (int i = 0; i < mDevicePolicies.size(); i++) {
             hashCode = 31 * hashCode + mDevicePolicies.keyAt(i);
             hashCode = 31 * hashCode + mDevicePolicies.valueAt(i);
@@ -528,6 +546,7 @@
                 + " mActivityPolicyExemptions=" + mActivityPolicyExemptions
                 + " mName=" + mName
                 + " mDevicePolicies=" + mDevicePolicies
+                + " mHomeComponent=" + mHomeComponent
                 + " mAudioPlaybackSessionId=" + mAudioPlaybackSessionId
                 + " mAudioRecordingSessionId=" + mAudioRecordingSessionId
                 + ")";
@@ -588,6 +607,7 @@
         @Nullable private VirtualSensorCallback mVirtualSensorCallback;
         @Nullable private Executor mVirtualSensorDirectChannelCallbackExecutor;
         @Nullable private VirtualSensorDirectChannelCallback mVirtualSensorDirectChannelCallback;
+        @Nullable private ComponentName mHomeComponent;
 
         private static class VirtualSensorCallbackDelegate extends IVirtualSensorCallback.Stub {
             @NonNull
@@ -665,6 +685,23 @@
         }
 
         /**
+         * Specifies a component to be used as home on all displays owned by this virtual device
+         * that support home activities.
+         * *
+         * <p>Note: Only relevant for virtual displays that support home activities.</p>
+         *
+         * @param homeComponent The component name to be used as home. If unset, then the system-
+         *   default secondary home activity will be used.
+         */
+        // TODO(b/297168328): Link to the relevant API for creating displays with home support.
+        @FlaggedApi(Flags.FLAG_VDM_CUSTOM_HOME)
+        @NonNull
+        public Builder setHomeComponent(@Nullable ComponentName homeComponent) {
+            mHomeComponent = homeComponent;
+            return this;
+        }
+
+        /**
          * Sets the user handles with matching managed accounts on the remote device to which
          * this virtual device is streaming. The caller is responsible for verifying the presence
          * and legitimacy of a matching managed account on the remote device.
@@ -1031,6 +1068,7 @@
                     mActivityPolicyExemptions,
                     mName,
                     mDevicePolicies,
+                    mHomeComponent,
                     mVirtualSensorConfigs,
                     virtualSensorCallbackDelegate,
                     mAudioPlaybackSessionId,
diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig
index ee36f18..3e96c96 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags.aconfig
@@ -15,6 +15,13 @@
 }
 
 flag {
+  name: "vdm_custom_home"
+  namespace: "virtual_devices"
+  description: "Enable custom home API"
+  bug: "297168328"
+}
+
+flag {
   name: "vdm_public_apis"
   namespace: "virtual_devices"
   description: "Enable public VDM API for device capabilities"
diff --git a/core/java/android/content/TEST_MAPPING b/core/java/android/content/TEST_MAPPING
index 01a9373..addede4 100644
--- a/core/java/android/content/TEST_MAPPING
+++ b/core/java/android/content/TEST_MAPPING
@@ -7,7 +7,7 @@
           "include-annotation": "android.platform.test.annotations.Presubmit"
         },
         {
-          "exclude-annotation": "android.platform.test.annotations.LargeTest"
+          "exclude-annotation": "androidx.test.filters.LargeTest"
         },
         {
           "exclude-annotation": "androidx.test.filters.FlakyTest"
diff --git a/core/java/android/os/TEST_MAPPING b/core/java/android/os/TEST_MAPPING
index 60622f18..ad3abd9 100644
--- a/core/java/android/os/TEST_MAPPING
+++ b/core/java/android/os/TEST_MAPPING
@@ -7,7 +7,7 @@
       ],
       "name": "FrameworksVibratorCoreTests",
       "options": [
-        {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
+        {"exclude-annotation": "androidx.test.filters.LargeTest"},
         {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
         {"exclude-annotation": "androidx.test.filters.FlakyTest"},
         {"exclude-annotation": "org.junit.Ignore"}
@@ -20,7 +20,7 @@
       ],
       "name": "FrameworksVibratorServicesTests",
       "options": [
-        {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
+        {"exclude-annotation": "androidx.test.filters.LargeTest"},
         {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
         {"exclude-annotation": "androidx.test.filters.FlakyTest"},
         {"exclude-annotation": "org.junit.Ignore"}
@@ -33,7 +33,7 @@
       ],
       "name": "CtsVibratorTestCases",
       "options": [
-        {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
+        {"exclude-annotation": "androidx.test.filters.LargeTest"},
         {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
         {"exclude-annotation": "androidx.test.filters.FlakyTest"},
         {"exclude-annotation": "org.junit.Ignore"}
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index febe6f7..a95e66d 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -16,7 +16,7 @@
 
 flag {
     name: "allow_private_profile"
-    namespace: "private_profile"
+    namespace: "profile_experiences"
     description: "Guards a new Private Profile type in UserManager - everything from its setup to config to deletion."
     bug: "299069460"
 }
diff --git a/core/java/android/service/notification/TEST_MAPPING b/core/java/android/service/notification/TEST_MAPPING
index 59b2bc1..7b8d52f 100644
--- a/core/java/android/service/notification/TEST_MAPPING
+++ b/core/java/android/service/notification/TEST_MAPPING
@@ -13,9 +13,6 @@
           "exclude-annotation": "org.junit.Ignore"
         },
         {
-          "exclude-annotation": "android.platform.test.annotations.LargeTest"
-        },
-        {
           "exclude-annotation": "androidx.test.filters.LargeTest"
         }
       ]
@@ -33,9 +30,6 @@
           "exclude-annotation": "org.junit.Ignore"
         },
         {
-          "exclude-annotation": "android.platform.test.annotations.LargeTest"
-        },
-        {
           "exclude-annotation": "androidx.test.filters.LargeTest"
         }
       ]
diff --git a/core/java/android/view/TEST_MAPPING b/core/java/android/view/TEST_MAPPING
index 1e39716..db35908 100644
--- a/core/java/android/view/TEST_MAPPING
+++ b/core/java/android/view/TEST_MAPPING
@@ -10,7 +10,7 @@
           "include-annotation": "android.platform.test.annotations.Presubmit"
         },
         {
-          "exclude-annotation": "android.platform.test.annotations.LargeTest"
+          "exclude-annotation": "androidx.test.filters.LargeTest"
         },
         {
           "exclude-annotation": "androidx.test.filters.FlakyTest"
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index ff6165b..0ba5d06 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -1151,14 +1151,10 @@
     }
 
     private boolean isInTouchMode() {
-        IWindowManager windowManager = WindowManagerGlobal.getWindowManagerService();
-        if (windowManager != null) {
-            try {
-                return windowManager.isInTouchMode(getDisplayId());
-            } catch (RemoteException e) {
-            }
+        if (mAttachInfo == null) {
+            return mContext.getResources().getBoolean(R.bool.config_defaultInTouchMode);
         }
-        return false;
+        return mAttachInfo.mInTouchMode;
     }
 
     /**
diff --git a/core/java/android/window/DisplayWindowPolicyController.java b/core/java/android/window/DisplayWindowPolicyController.java
index 2747233a..8d71a8e 100644
--- a/core/java/android/window/DisplayWindowPolicyController.java
+++ b/core/java/android/window/DisplayWindowPolicyController.java
@@ -109,6 +109,12 @@
     }
 
     /**
+     * @return the custom home component specified for the relevant display, if any.
+     */
+    @Nullable
+    public abstract ComponentName getCustomHomeComponent();
+
+    /**
      * Returns {@code true} if all of the given activities can be launched on this virtual display
      * in the configuration defined by the rest of the arguments.
      *
diff --git a/core/java/com/android/internal/policy/AttributeCache.java b/core/java/com/android/internal/policy/AttributeCache.java
index 1bdad25..970f511 100644
--- a/core/java/com/android/internal/policy/AttributeCache.java
+++ b/core/java/com/android/internal/policy/AttributeCache.java
@@ -16,12 +16,18 @@
 
 package com.android.internal.policy;
 
+import android.annotation.RequiresPermission;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
+import android.net.Uri;
+import android.os.Handler;
 import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.util.LruCache;
@@ -46,6 +52,8 @@
     @GuardedBy("this")
     private final Configuration mConfiguration = new Configuration();
 
+    private PackageMonitor mPackageMonitor;
+
     public final static class Package {
         public final Context context;
         private final SparseArray<ArrayMap<int[], Entry>> mMap = new SparseArray<>();
@@ -77,6 +85,34 @@
         }
     }
 
+    /**
+     * Start monitor package change, so the resources can be loaded correctly.
+     */
+    void monitorPackageRemove(Handler handler) {
+        if (mPackageMonitor == null) {
+            mPackageMonitor = new PackageMonitor(mContext, handler);
+        }
+    }
+
+    static class PackageMonitor extends BroadcastReceiver {
+        @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+        PackageMonitor(Context context, Handler handler) {
+            final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
+            filter.addDataScheme(IntentFilter.SCHEME_PACKAGE);
+            context.registerReceiverAsUser(this, UserHandle.ALL, filter,
+                    null /* broadcastPermission */, handler);
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final Uri packageUri = intent.getData();
+            if (packageUri != null) {
+                final String packageName = packageUri.getEncodedSchemeSpecificPart();
+                AttributeCache.instance().removePackage(packageName);
+            }
+        }
+    }
+
     public static AttributeCache instance() {
         return sInstance;
     }
diff --git a/core/java/com/android/internal/policy/TransitionAnimation.java b/core/java/com/android/internal/policy/TransitionAnimation.java
index 8f4df80..40a437f 100644
--- a/core/java/com/android/internal/policy/TransitionAnimation.java
+++ b/core/java/com/android/internal/policy/TransitionAnimation.java
@@ -48,6 +48,7 @@
 import android.hardware.HardwareBuffer;
 import android.media.Image;
 import android.media.ImageReader;
+import android.os.Handler;
 import android.os.SystemProperties;
 import android.util.Slog;
 import android.view.InflateException;
@@ -1399,4 +1400,14 @@
         // Approximation of WCAG 2.0 relative luminance.
         return ((r * 8) + (g * 22) + (b * 2)) >> 5;
     }
+
+    /**
+     * For non-system server process, it must call this method to initialize the AttributeCache and
+     * start monitor package change, so the resources can be loaded correctly.
+     */
+    public static void initAttributeCache(Context context, Handler handler) {
+        AttributeCache.init(context);
+        AttributeCache.instance().monitorPackageRemove(handler);
+    }
+
 }
diff --git a/core/tests/coretests/src/android/colormodel/CamTest.java b/core/tests/coretests/src/android/colormodel/CamTest.java
index 5bcc593..05fc0e0 100644
--- a/core/tests/coretests/src/android/colormodel/CamTest.java
+++ b/core/tests/coretests/src/android/colormodel/CamTest.java
@@ -18,7 +18,7 @@
 
 import static org.junit.Assert.assertEquals;
 
-import android.platform.test.annotations.LargeTest;
+import androidx.test.filters.LargeTest;
 
 import org.junit.Assert;
 import org.junit.Test;
diff --git a/core/tests/coretests/src/android/provider/NameValueCacheTest.java b/core/tests/coretests/src/android/provider/NameValueCacheTest.java
index 87e4a42..989c992 100644
--- a/core/tests/coretests/src/android/provider/NameValueCacheTest.java
+++ b/core/tests/coretests/src/android/provider/NameValueCacheTest.java
@@ -55,7 +55,6 @@
  * Due to how the classes are structured, we have to test it in a somewhat roundabout way. We're
  * mocking out the contentProvider and are handcrafting very specific Bundles to answer the queries.
  */
-@Ignore("b/297724333")
 @Presubmit
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -229,6 +228,8 @@
 
     @After
     public void cleanUp() throws IOException {
+        Settings.Config.clearProviderForTest();
+        Settings.Secure.clearProviderForTest();
         mConfigsStorage.clear();
         mSettingsStorage.clear();
         mSettingsCacheGenerationStore.close();
diff --git a/core/tests/vibrator/TEST_MAPPING b/core/tests/vibrator/TEST_MAPPING
index f3333d8..2f3afa6 100644
--- a/core/tests/vibrator/TEST_MAPPING
+++ b/core/tests/vibrator/TEST_MAPPING
@@ -3,7 +3,7 @@
     {
       "name": "FrameworksVibratorCoreTests",
       "options": [
-        {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
+        {"exclude-annotation": "androidx.test.filters.LargeTest"},
         {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
         {"exclude-annotation": "androidx.test.filters.FlakyTest"},
         {"exclude-annotation": "org.junit.Ignore"}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
index 13c0ac4..b71c48e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
@@ -76,6 +76,10 @@
     private IRemoteTransition mOccludeByDreamTransition = null;
     private IRemoteTransition mUnoccludeTransition = null;
 
+    // While set true, Keyguard has created a remote animation runner to handle the open app
+    // transition.
+    private boolean mIsLaunchingActivityOverLockscreen;
+
     private final class StartedTransition {
         final TransitionInfo mInfo;
         final SurfaceControl.Transaction mFinishT;
@@ -120,7 +124,7 @@
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull TransitionFinishCallback finishCallback) {
-        if (!handles(info)) {
+        if (!handles(info) || mIsLaunchingActivityOverLockscreen) {
             return false;
         }
 
@@ -313,5 +317,11 @@
                 mUnoccludeTransition = unoccludeTransition;
             });
         }
+
+        @Override
+        public void setLaunchingActivityOverLockscreen(boolean isLaunchingActivityOverLockscreen) {
+            mMainExecutor.execute(() ->
+                    mIsLaunchingActivityOverLockscreen = isLaunchingActivityOverLockscreen);
+        }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java
index b4b327f..33c299f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java
@@ -38,4 +38,9 @@
             @NonNull IRemoteTransition occludeTransition,
             @NonNull IRemoteTransition occludeByDreamTransition,
             @NonNull IRemoteTransition unoccludeTransition) {}
+
+    /**
+     * Notify whether keyguard has created a remote animation runner for next app launch.
+     */
+    default void setLaunchingActivityOverLockscreen(boolean isLaunchingActivityOverLockscreen) {}
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 00f6a1c..83dc7fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -111,6 +111,14 @@
         WindowContainerTransaction mFinishWCT = null;
 
         /**
+         * Whether the transition has request for remote transition while mLeftoversHandler
+         * isn't remote transition handler.
+         * If true and the mLeftoversHandler can handle the transition, need to notify remote
+         * transition handler to consume the transition.
+         */
+        boolean mHasRequestToRemote;
+
+        /**
          * Mixed transitions are made up of multiple "parts". This keeps track of how many
          * parts are currently animating.
          */
@@ -200,6 +208,10 @@
                     MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE, transition);
             mixed.mLeftoversHandler = handler.first;
             mActiveTransitions.add(mixed);
+            if (mixed.mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) {
+                mixed.mHasRequestToRemote = true;
+                mPlayer.getRemoteTransitionHandler().handleRequest(transition, request);
+            }
             return handler.second;
         } else if (mSplitHandler.isSplitScreenVisible()
                 && isOpeningType(request.getType())
@@ -316,12 +328,22 @@
         // the time of handleRequest, but we need more information than is available at that time.
         if (KeyguardTransitionHandler.handles(info)) {
             if (mixed != null && mixed.mType != MixedTransition.TYPE_KEYGUARD) {
-                ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
-                        "Converting mixed transition into a keyguard transition");
-                onTransitionConsumed(transition, false, null);
+                final MixedTransition keyguardMixed =
+                        new MixedTransition(MixedTransition.TYPE_KEYGUARD, transition);
+                mActiveTransitions.add(keyguardMixed);
+                final boolean hasAnimateKeyguard = animateKeyguard(keyguardMixed, info,
+                        startTransaction, finishTransaction, finishCallback);
+                if (hasAnimateKeyguard) {
+                    ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+                            "Converting mixed transition into a keyguard transition");
+                    // Consume the original mixed transition
+                    onTransitionConsumed(transition, false, null);
+                    return true;
+                } else {
+                    // Keyguard handler cannot handle it, process through original mixed
+                    mActiveTransitions.remove(keyguardMixed);
+                }
             }
-            mixed = new MixedTransition(MixedTransition.TYPE_KEYGUARD, transition);
-            mActiveTransitions.add(mixed);
         }
 
         if (mixed == null) return false;
@@ -332,8 +354,17 @@
         } else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
             return false;
         } else if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) {
-            return animateOpenIntentWithRemoteAndPip(mixed, info, startTransaction,
-                    finishTransaction, finishCallback);
+            final boolean handledToPip = animateOpenIntentWithRemoteAndPip(mixed, info,
+                    startTransaction, finishTransaction, finishCallback);
+            // Consume the transition on remote handler if the leftover handler already handle this
+            // transition. And if it cannot, the transition will be handled by remote handler, so
+            // don't consume here.
+            // Need to check leftOverHandler as it may change in #animateOpenIntentWithRemoteAndPip
+            if (handledToPip && mixed.mHasRequestToRemote
+                    && mixed.mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) {
+                mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, false, null);
+            }
+            return handledToPip;
         } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) {
             for (int i = info.getChanges().size() - 1; i >= 0; --i) {
                 final TransitionInfo.Change change = info.getChanges().get(i);
@@ -799,5 +830,8 @@
         } else if (mixed.mType == MixedTransition.TYPE_UNFOLD) {
             mUnfoldHandler.onTransitionConsumed(transition, aborted, finishT);
         }
+        if (mixed.mHasRequestToRemote) {
+            mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, aborted, finishT);
+        }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 7df658e..de03f58 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -99,7 +99,6 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.policy.AttributeCache;
 import com.android.internal.policy.ScreenDecorationsUtils;
 import com.android.internal.policy.TransitionAnimation;
 import com.android.internal.protolog.common.ProtoLog;
@@ -182,7 +181,7 @@
                 /* broadcastPermission = */ null,
                 mMainHandler);
 
-        AttributeCache.init(mContext);
+        TransitionAnimation.initAttributeCache(mContext, mMainHandler);
     }
 
     private void updateEnterpriseThumbnailDrawable() {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
index 77f14f1..adf92d8 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
@@ -50,7 +50,7 @@
         }
 
     @Before
-    fun before() {
+    fun setUp() {
         Assume.assumeTrue(tapl.isTablet && letterboxRule.isIgnoreOrientationRequest)
     }
 
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RotateImmersiveAppInFullscreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RotateImmersiveAppInFullscreenTest.kt
new file mode 100644
index 0000000..ba2b3e7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RotateImmersiveAppInFullscreenTest.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.appcompat
+
+import android.os.Build
+import android.tools.common.datatypes.Rect
+import android.platform.test.annotations.Postsubmit
+import android.system.helpers.CommandsHelper
+import android.tools.common.NavBar
+import android.tools.common.Rotation
+import android.tools.common.flicker.assertions.FlickerTest
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.device.helpers.FIND_TIMEOUT
+import android.tools.device.traces.parsers.toFlickerComponent
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.helpers.LetterboxAppHelper
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Test rotating an immersive app in fullscreen.
+ *
+ * To run this test: `atest WMShellFlickerTestsOther:RotateImmersiveAppInFullscreenTest`
+ *
+ * Actions:
+ * ```
+ *     Rotate the device by 90 degrees to trigger a rotation through sensors
+ *     Verify that the button exists
+ * ```
+ *
+ * Notes:
+ * ```
+ *     Some default assertions that are inherited from
+ *     the `BaseTest` are ignored due to the nature of the immersive apps.
+ *
+ *     This test only works with Cuttlefish devices.
+ * ```
+ */
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+class RotateImmersiveAppInFullscreenTest(flicker: LegacyFlickerTest) : BaseAppCompat(flicker) {
+
+    private val immersiveApp = LetterboxAppHelper(instrumentation,
+            launcherName = ActivityOptions.PortraitImmersiveActivity.LABEL,
+            component =
+            ActivityOptions.PortraitImmersiveActivity.COMPONENT.toFlickerComponent())
+
+    private val cmdHelper: CommandsHelper = CommandsHelper.getInstance(instrumentation)
+    private val execAdb: (String) -> String = { cmd -> cmdHelper.executeShellCommand(cmd) }
+
+    protected val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
+
+    private val isCuttlefishDevice: Boolean = Build.MODEL.contains("Cuttlefish")
+
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            setup {
+                setStartRotation()
+                immersiveApp.launchViaIntent(wmHelper)
+                startDisplayBounds =
+                        wmHelper.currentState.layerState.physicalDisplayBounds
+                                ?: error("Display not found")
+            }
+            transitions {
+                if (isCuttlefishDevice) {
+                    // Simulates a device rotation through sensors because the rotation button
+                    // only appears in a rotation event through sensors
+                    execAdb("/vendor/bin/cuttlefish_sensor_injection rotate 0")
+                    // verify rotation button existence
+                    val rotationButtonSelector = By.res(LAUNCHER_PACKAGE, "rotate_suggestion")
+                    uiDevice.wait(Until.hasObject(rotationButtonSelector), FIND_TIMEOUT)
+                    uiDevice.findObject(rotationButtonSelector)
+                            ?: error("rotation button not found")
+                }
+            }
+            teardown {
+                immersiveApp.exit(wmHelper)
+            }
+        }
+
+    @Before
+    fun setUpForImmersiveAppTests() {
+        Assume.assumeTrue(isCuttlefishDevice)
+    }
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. App is in immersive mode.")
+    override fun taskBarLayerIsVisibleAtStartAndEnd() {
+    }
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. App is in immersive mode.")
+    override fun navBarLayerIsVisibleAtStartAndEnd() {
+    }
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. App is in immersive mode.")
+    override fun statusBarLayerIsVisibleAtStartAndEnd() {
+    }
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. App is in immersive mode.")
+    override fun taskBarWindowIsAlwaysVisible() {
+    }
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. App is in immersive mode.")
+    override fun navBarWindowIsAlwaysVisible() {
+    }
+
+    /** {@inheritDoc} */
+    @Test
+    @Ignore("Not applicable to this CUJ. App is in immersive mode.")
+    override fun statusBarWindowIsAlwaysVisible() {
+    }
+
+    @Test
+    @Ignore("Not applicable to this CUJ. App is in immersive mode.")
+    override fun statusBarLayerPositionAtStartAndEnd() {
+    }
+
+    @Test
+    @Ignore("Not applicable to this CUJ. App is in immersive mode.")
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
+    }
+
+    /** Test that app is fullscreen by checking status bar and task bar visibility. */
+    @Postsubmit
+    @Test
+    fun appWindowFullScreen() {
+        flicker.assertWmEnd {
+            this.isAppWindowInvisible(ComponentNameMatcher.STATUS_BAR)
+                    .isAppWindowInvisible(ComponentNameMatcher.TASK_BAR)
+                    .visibleRegion(immersiveApp).coversExactly(startDisplayBounds)
+        }
+    }
+
+    /** Test that app is in the original rotation we have set up. */
+    @Postsubmit
+    @Test
+    fun appInOriginalRotation() {
+        flicker.assertWmEnd {
+            this.hasRotation(Rotation.ROTATION_90)
+        }
+    }
+
+    companion object {
+        private var startDisplayBounds = Rect.EMPTY
+        const val LAUNCHER_PACKAGE = "com.google.android.apps.nexuslauncher"
+
+        /**
+         * Creates the test configurations.
+         *
+         * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<FlickerTest> {
+            return LegacyFlickerTestFactory.nonRotationTests(
+                    supportedRotations = listOf(Rotation.ROTATION_90),
+                    // TODO(b/292403378): 3 button mode not added as rotation button is hidden in taskbar
+                    supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index 8c81733..d1067a9 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -105,6 +105,13 @@
          screen. -->
     <item name="half_opened_bouncer_height_ratio" type="dimen" format="float">0.0</item>
 
+    <!-- Proportion of the screen height to use to set the maximum height of the bouncer to when
+     the device is in the DEVICE_POSTURE_HALF_OPENED posture.
+
+     This value is only used when motion layout bouncer is used - when flag
+     landscape.enable_lockscreen (b/293252410) is on -->
+    <item name="motion_layout_half_fold_bouncer_height_ratio" type="dimen" format="float">0.55</item>
+
     <!-- The actual amount of translation that is applied to the security when it animates from one
          side of the screen to the other in one-handed or user switcher mode. Note that it will
          always translate from the side of the screen to the other (it will "jump" closer to the
diff --git a/packages/SystemUI/res-keyguard/xml/keyguard_pattern_scene.xml b/packages/SystemUI/res-keyguard/xml/keyguard_pattern_scene.xml
index 6112411..751d6d8 100644
--- a/packages/SystemUI/res-keyguard/xml/keyguard_pattern_scene.xml
+++ b/packages/SystemUI/res-keyguard/xml/keyguard_pattern_scene.xml
@@ -10,9 +10,34 @@
         motion:duration="0"
         motion:autoTransition="none"/>
 
+    <Transition
+        motion:constraintSetStart="@id/single_constraints"
+        motion:constraintSetEnd="@+id/half_folded_single_constraints"
+        motion:duration="@integer/material_motion_duration_short_1"
+        motion:autoTransition="none"/>
+
     <!-- No changes to default layout -->
     <ConstraintSet android:id="@+id/single_constraints"/>
 
+    <ConstraintSet android:id="@+id/half_folded_single_constraints">
+
+        <Constraint
+            android:id="@+id/pattern_top_guideline"
+            androidprv:layout_constraintGuide_percent=
+                "@dimen/motion_layout_half_fold_bouncer_height_ratio"/>
+
+        <Constraint
+            android:id="@+id/keyguard_selector_fade_container"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="0dp"
+            android:layout_marginTop="@dimen/keyguard_eca_top_margin"
+            android:orientation="vertical"
+            androidprv:layout_constraintBottom_toBottomOf="parent"
+            androidprv:layout_constraintTop_toBottomOf="@+id/flow1"/>
+
+    </ConstraintSet>
+
     <ConstraintSet android:id="@+id/split_constraints">
 
         <Constraint
diff --git a/packages/SystemUI/res-keyguard/xml/keyguard_pin_scene.xml b/packages/SystemUI/res-keyguard/xml/keyguard_pin_scene.xml
index 2a1270c..cc498f4 100644
--- a/packages/SystemUI/res-keyguard/xml/keyguard_pin_scene.xml
+++ b/packages/SystemUI/res-keyguard/xml/keyguard_pin_scene.xml
@@ -26,11 +26,35 @@
         motion:constraintSetStart="@id/single_constraints"
         motion:constraintSetEnd="@+id/split_constraints"
         motion:duration="0"
-        motion:autoTransition="none"/>
+        motion:autoTransition="none" />
+
+    <Transition
+        motion:constraintSetStart="@id/single_constraints"
+        motion:constraintSetEnd="@+id/half_folded_single_constraints"
+        motion:duration="@integer/material_motion_duration_short_1" />
 
     <!-- No changes to default layout -->
     <ConstraintSet android:id="@+id/single_constraints"/>
 
+    <ConstraintSet android:id="@+id/half_folded_single_constraints">
+
+        <Constraint
+            android:id="@+id/pin_pad_top_guideline"
+            androidprv:layout_constraintGuide_percent=
+                "@dimen/motion_layout_half_fold_bouncer_height_ratio"/>
+
+        <Constraint
+            android:id="@+id/keyguard_selector_fade_container"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="0dp"
+            android:layout_marginTop="@dimen/keyguard_eca_top_margin"
+            android:orientation="vertical"
+            androidprv:layout_constraintBottom_toBottomOf="parent"
+            androidprv:layout_constraintTop_toBottomOf="@+id/flow1"/>
+
+    </ConstraintSet>
+
     <ConstraintSet android:id="@+id/split_constraints">
 
         <Constraint
@@ -68,4 +92,5 @@
             android:layout_marginTop="@dimen/keyguard_eca_top_margin" />
 
     </ConstraintSet>
+
 </MotionScene>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/bluetooth_device_item.xml b/packages/SystemUI/res/layout/bluetooth_device_item.xml
new file mode 100644
index 0000000..6dd44fb
--- /dev/null
+++ b/packages/SystemUI/res/layout/bluetooth_device_item.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!-- TODO(b/298124674) remove this root -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/bluetooth_device_list_container"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:layout_marginBottom="4dp">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/bluetooth_device_row"
+        style="@style/BluetoothTileDialog.Device"
+        android:layout_height="@dimen/bluetooth_dialog_device_height"
+        android:paddingEnd="24dp"
+        android:paddingStart="20dp"
+        android:baselineAligned="false">
+
+        <ImageView
+            android:id="@+id/bluetooth_device_icon"
+            android:contentDescription="@string/accessibility_bluetooth_device_icon"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            android:layout_gravity="center_vertical" />
+
+        <View
+            android:id="@+id/bluetooth_device"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            app:layout_constraintTop_toTopOf="@+id/bluetooth_device_name"
+            app:layout_constraintBottom_toBottomOf="@+id/bluetooth_device_summary"
+            app:layout_constraintStart_toStartOf="@+id/bluetooth_device_name"
+            app:layout_constraintEnd_toEndOf="@+id/bluetooth_device_name" />
+
+        <TextView
+            android:layout_width="0dp"
+            android:id="@+id/bluetooth_device_name"
+            style="@style/BluetoothTileDialog.DeviceName"
+            android:paddingStart="20dp"
+            android:paddingTop="10dp"
+            app:layout_constraintWidth_percent="0.7"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintStart_toEndOf="@+id/bluetooth_device_icon"
+            app:layout_constraintEnd_toStartOf="@+id/gear_icon"
+            app:layout_constraintBottom_toTopOf="@+id/bluetooth_device_summary"
+            android:gravity="center_vertical"
+            android:textSize="14sp" />
+
+        <TextView
+            android:layout_width="0dp"
+            android:id="@+id/bluetooth_device_summary"
+            style="@style/BluetoothTileDialog.DeviceSummary"
+            android:paddingStart="20dp"
+            android:paddingBottom="10dp"
+            app:layout_constraintWidth_percent="0.7"
+            app:layout_constraintTop_toBottomOf="@+id/bluetooth_device_name"
+            app:layout_constraintStart_toEndOf="@+id/bluetooth_device_icon"
+            app:layout_constraintEnd_toStartOf="@+id/gear_icon"
+            app:layout_constraintBottom_toBottomOf="parent"
+            android:gravity="center_vertical" />
+
+        <ImageView
+            android:id="@+id/gear_icon"
+            android:src="@drawable/ic_settings_24dp"
+            android:contentDescription="@string/accessibility_bluetooth_device_settings_gear"
+            android:layout_width="0dp"
+            android:layout_height="24dp"
+            app:layout_constraintStart_toEndOf="@+id/bluetooth_device_name"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintWidth_percent="0.3"
+            android:gravity="center_vertical"
+            android:paddingStart="10dp" />
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
new file mode 100644
index 0000000..16aeb95
--- /dev/null
+++ b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/root"
+    style="@style/Widget.SliceView.Panel"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
+
+    <TextView
+        android:id="@+id/bluetooth_tile_dialog_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:paddingTop="24dp"
+        android:ellipsize="end"
+        android:gravity="center_vertical|center_horizontal"
+        android:text="@string/quick_settings_bluetooth_label"
+        android:textAppearance="@style/TextAppearance.Dialog.Title"
+        android:textSize="24sp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/bluetooth_tile_dialog_subtitle"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <TextView
+        android:id="@+id/bluetooth_tile_dialog_subtitle"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="4dp"
+        android:layout_marginBottom="@dimen/bluetooth_dialog_layout_margin"
+        android:ellipsize="end"
+        android:gravity="center_vertical|center_horizontal"
+        android:maxLines="1"
+        android:text="@string/quick_settings_bluetooth_tile_subtitle"
+        android:textAppearance="@style/TextAppearance.Dialog.Body.Message"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/bluetooth_toggle_title"
+        app:layout_constraintTop_toBottomOf="@id/bluetooth_tile_dialog_title" />
+
+    <TextView
+        android:id="@+id/bluetooth_toggle_title"
+        style="@style/BluetoothTileDialog.Device"
+        android:layout_width="0dp"
+        android:layout_height="64dp"
+        android:gravity="center_vertical"
+        android:layout_marginTop="4dp"
+        android:text="@string/turn_on_bluetooth"
+        android:textAppearance="@style/TextAppearance.Dialog.Body.Message"
+        android:textSize="16sp"
+        app:layout_constraintEnd_toStartOf="@+id/bluetooth_toggle"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/bluetooth_tile_dialog_subtitle" />
+
+    <Switch
+        android:id="@+id/bluetooth_toggle"
+        style="@style/BluetoothTileDialog.Device"
+        android:layout_width="0dp"
+        android:layout_height="48dp"
+        android:gravity="center|center_vertical"
+        android:paddingEnd="24dp"
+        android:layout_marginTop="10dp"
+        android:contentDescription="@string/turn_on_bluetooth"
+        android:switchMinWidth="@dimen/settingslib_switch_track_width"
+        android:theme="@style/MainSwitch.Settingslib"
+        android:thumb="@drawable/settingslib_thumb_selector"
+        android:track="@drawable/settingslib_track_selector"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@+id/bluetooth_toggle_title"
+        app:layout_constraintTop_toBottomOf="@id/bluetooth_tile_dialog_subtitle" />
+
+    <androidx.constraintlayout.widget.Group
+        android:id="@+id/pair_new_device_layout_group"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        app:constraint_referenced_ids="ic_add,pair_new_device_text" />
+
+    <ImageView
+        android:id="@+id/ic_add"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_marginStart="36dp"
+        android:gravity="center_vertical"
+        android:importantForAccessibility="no"
+        android:src="@drawable/ic_add"
+        app:layout_constraintBottom_toTopOf="@id/device_list"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/pair_new_device_text"
+        app:layout_constraintTop_toBottomOf="@id/bluetooth_toggle_title"
+        android:tint="?android:attr/textColorPrimary" />
+
+    <TextView
+        android:id="@+id/pair_new_device_text"
+        style="@style/BluetoothTileDialog.Device"
+        android:layout_width="0dp"
+        android:layout_height="@dimen/bluetooth_dialog_device_height"
+        android:gravity="center_vertical"
+        android:layout_marginStart="0dp"
+        android:paddingStart="20dp"
+        android:text="@string/pair_new_bluetooth_devices"
+        android:textSize="14sp"
+        android:textAppearance="@style/TextAppearance.Dialog.Title"
+        app:layout_constraintBottom_toTopOf="@id/device_list"
+        app:layout_constraintStart_toEndOf="@+id/ic_add"
+        app:layout_constraintTop_toBottomOf="@id/bluetooth_toggle_title"
+        app:layout_constraintEnd_toEndOf="parent" />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/device_list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:nestedScrollingEnabled="false"
+        android:overScrollMode="never"
+        android:scrollbars="vertical"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/pair_new_device_text"
+        app:layout_constraintBottom_toTopOf="@+id/see_all_text" />
+
+    <androidx.constraintlayout.widget.Group
+        android:id="@+id/see_all_layout_group"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        app:constraint_referenced_ids="ic_arrow,see_all_text" />
+
+    <ImageView
+        android:id="@+id/ic_arrow"
+        android:layout_marginStart="36dp"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:importantForAccessibility="no"
+        android:gravity="center_vertical"
+        android:src="@drawable/ic_arrow_forward"
+        app:layout_constraintBottom_toTopOf="@+id/done_button"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/see_all_text"
+        app:layout_constraintTop_toBottomOf="@id/device_list" />
+
+    <TextView
+        android:id="@+id/see_all_text"
+        style="@style/BluetoothTileDialog.Device"
+        android:layout_width="0dp"
+        android:layout_height="@dimen/bluetooth_dialog_device_height"
+        android:gravity="center_vertical"
+        android:layout_marginStart="0dp"
+        android:paddingStart="20dp"
+        android:text="@string/see_all_bluetooth_devices"
+        android:textSize="14sp"
+        android:textAppearance="@style/TextAppearance.Dialog.Title"
+        app:layout_constraintBottom_toTopOf="@+id/done_button"
+        app:layout_constraintStart_toEndOf="@+id/ic_arrow"
+        app:layout_constraintTop_toBottomOf="@id/device_list"
+        app:layout_constraintEnd_toEndOf="parent" />
+
+    <Button
+        android:id="@+id/done_button"
+        style="@style/Widget.Dialog.Button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="@dimen/dialog_bottom_padding"
+        android:layout_marginEnd="@dimen/dialog_side_padding"
+        android:layout_marginStart="@dimen/dialog_side_padding"
+        android:clickable="true"
+        android:ellipsize="end"
+        android:focusable="true"
+        android:maxLines="1"
+        android:text="@string/inline_done_button"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/see_all_text" />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 7e9d3f5..88726af 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1643,6 +1643,11 @@
     <!-- Radius of switch track -->
     <dimen name="settingslib_switch_track_radius">35dp</dimen>
 
+    <!-- Bluetooth dialog related dimensions -->
+    <dimen name="bluetooth_dialog_layout_margin">16dp</dimen>
+    <!-- The height of the bluetooth device in bluetooth dialog. -->
+    <dimen name="bluetooth_dialog_device_height">72dp</dimen>
+
     <!-- Height percentage of the parent container occupied by the communal view -->
     <item name="communal_source_height_percentage" format="float" type="dimen">0.80</item>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 29c9767..5860806 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -459,7 +459,12 @@
 
     <!-- Content description of the bluetooth icon when connected for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="accessibility_bluetooth_connected">Bluetooth connected.</string>
-    <!-- Content description of the bluetooth icon when connecting for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
+
+    <!-- Content description of the bluetooth device icon. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_bluetooth_device_icon">Bluetooth device icon</string>
+
+    <!-- Content description of the bluetooth device settings gear icon. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_bluetooth_device_settings_gear">Bluetooth device settings gear</string>
 
     <!-- Content description of the battery when battery state is unknown for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="accessibility_battery_unknown">Battery percentage unknown.</string>
@@ -621,6 +626,19 @@
     <!-- QuickSettings: Bluetooth (Off) [CHAR LIMIT=NONE] -->
     <!-- QuickSettings: Bluetooth detail panel, text when there are no items [CHAR LIMIT=NONE] -->
     <string name="quick_settings_bluetooth_detail_empty_text">No paired devices available</string>
+    <!-- QuickSettings: Bluetooth dialog subtitle [CHAR LIMIT=NONE]-->
+    <string name="quick_settings_bluetooth_tile_subtitle">Tap a device to connect</string>
+    <!-- QuickSettings: Bluetooth dialog pair new devices [CHAR LIMIT=NONE]-->
+    <string name="pair_new_bluetooth_devices">Pair new device</string>
+    <!-- QuickSettings: Bluetooth dialog see all devices [CHAR LIMIT=NONE]-->
+    <string name="see_all_bluetooth_devices">See all</string>
+    <!-- QuickSettings: Bluetooth dialog turn on Bluetooth [CHAR LIMIT=NONE]-->
+    <string name="turn_on_bluetooth">Use Bluetooth</string>
+    <!-- QuickSettings: Bluetooth dialog device connected default summary [CHAR LIMIT=NONE]-->
+    <string name="quick_settings_bluetooth_device_connected">Connected</string>
+    <!-- QuickSettings: Bluetooth dialog device saved default summary [CHAR LIMIT=NONE]-->
+    <string name="quick_settings_bluetooth_device_saved">Saved</string>
+
     <!-- QuickSettings: Bluetooth secondary label for the battery level of a connected device [CHAR LIMIT=20]-->
     <string name="quick_settings_bluetooth_secondary_label_battery_level"><xliff:g id="battery_level_as_percentage">%s</xliff:g> battery</string>
     <!-- QuickSettings: Bluetooth secondary label for an audio device being connected [CHAR LIMIT=20]-->
@@ -3086,7 +3104,7 @@
     configured. This is shown as part of a dialog that explains to the user why they cannot select
     this shortcut for their lock screen right now. [CHAR LIMIT=NONE].
     -->
-    <string name="home_quick_affordance_unavailable_configure_the_app">&#8226; At least one device is available</string>
+    <string name="home_quick_affordance_unavailable_configure_the_app">&#8226; At least one device or device panel are available</string>
 
     <!---
     Explains that the notes app is not available. This is shown as part of a dialog that explains to
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 6991b96..084cb88 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -15,7 +15,7 @@
 -->
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
-           xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
 
     <style name="TextAppearance.StatusBar.Clock" parent="@*android:style/TextAppearance.StatusBar.Icon">
         <item name="android:textSize">@dimen/status_bar_clock_size</item>
@@ -54,10 +54,10 @@
     </style>
 
     <style name="TextAppearance.StatusBar.Expanded.EmergencyCallsOnly"
-           parent="TextAppearance.StatusBar.Expanded.AboveDateTime" />
+        parent="TextAppearance.StatusBar.Expanded.AboveDateTime" />
 
     <style name="TextAppearance.StatusBar.Expanded.ChargingInfo"
-            parent="TextAppearance.StatusBar.Expanded.AboveDateTime" />
+        parent="TextAppearance.StatusBar.Expanded.AboveDateTime" />
 
     <style name="TextAppearance.StatusBar.Expanded.UserSwitcher">
         <item name="android:textSize">@dimen/kg_user_switcher_text_size</item>
@@ -645,7 +645,7 @@
     </style>
 
     <style name="TextAppearance.HeadsUpStatusBarText"
-           parent="@*android:style/TextAppearance.DeviceDefault.Notification.Info">
+        parent="@*android:style/TextAppearance.DeviceDefault.Notification.Info">
     </style>
 
     <style name="TextAppearance.QSEdit" >
@@ -699,7 +699,7 @@
     </style>
 
     <style name="MediaPlayer.SessionAction"
-           parent="@android:style/Widget.Material.Button.Borderless.Small">
+        parent="@android:style/Widget.Material.Button.Borderless.Small">
         <item name="android:background">@drawable/qs_media_light_source</item>
         <item name="android:tint">?android:attr/textColorPrimary</item>
         <item name="android:paddingTop">12dp</item>
@@ -929,12 +929,13 @@
         <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
     </style>
 
-     <style name="Theme.SystemUI.Dialog.Control.DetailPanel" parent="@android:style/Theme.DeviceDefault.Dialog.NoActionBar">
-      <item name="android:windowFullscreen">false</item>
-      <item name="android:windowIsFloating">false</item>
-      <item name="android:windowBackground">@color/controls_task_view_bg</item>
-      <item name="android:backgroundDimEnabled">false</item>
-      <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
+    <style name="Theme.SystemUI.Dialog.Control.DetailPanel"
+           parent="@android:style/Theme.DeviceDefault.Dialog.NoActionBar">
+        <item name="android:windowFullscreen">false</item>
+        <item name="android:windowIsFloating">false</item>
+        <item name="android:windowBackground">@color/controls_task_view_bg</item>
+        <item name="android:backgroundDimEnabled">false</item>
+        <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
     </style>
 
     <style name="Control" />
@@ -1034,17 +1035,17 @@
     <style name="Wallet" />
 
     <style name="Wallet.TextAppearance">
-      <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
-      <item name="android:textColor">?android:attr/textColorPrimary</item>
-      <item name="android:singleLine">true</item>
-      <item name="android:textSize">14sp</item>
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:textSize">14sp</item>
     </style>
 
     <style name="Wallet.Theme" parent="@android:style/Theme.DeviceDefault">
-      <item name="android:colorBackground">@color/material_dynamic_neutral10</item>
-      <item name="android:itemBackground">@color/material_dynamic_neutral20</item>
-      <!-- Setting a placeholder will avoid using the SystemUI icon on the splash screen.  -->
-      <item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_blank</item>
+        <item name="android:colorBackground">@color/material_dynamic_neutral10</item>
+        <item name="android:itemBackground">@color/material_dynamic_neutral20</item>
+        <!-- Setting a placeholder will avoid using the SystemUI icon on the splash screen.  -->
+        <item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_blank</item>
     </style>
 
     <style name="Animation.InternetDialog" parent="@android:style/Animation.InputMethod">
@@ -1169,7 +1170,7 @@
     </style>
 
     <style name="TrimmedHorizontalProgressBar"
-           parent="android:Widget.Material.ProgressBar.Horizontal">
+        parent="android:Widget.Material.ProgressBar.Horizontal">
         <item name="android:indeterminateDrawable">
             @drawable/progress_indeterminate_horizontal_material_trimmed
         </item>
@@ -1254,6 +1255,36 @@
         <item name="android:textColor">?android:attr/textColorSecondary</item>
     </style>
 
+    <style name="BluetoothTileDialog">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_gravity">center_vertical|start</item>
+    </style>
+
+    <style name="BluetoothTileDialog.Device">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">88dp</item>
+        <item name="android:layout_gravity">center_vertical|start</item>
+        <item name="android:layout_marginStart">@dimen/bluetooth_dialog_layout_margin</item>
+        <item name="android:layout_marginEnd">@dimen/bluetooth_dialog_layout_margin</item>
+        <item name="android:paddingStart">22dp</item>
+        <item name="android:paddingEnd">22dp</item>
+        <item name="android:orientation">horizontal</item>
+        <item name="android:focusable">true</item>
+        <item name="android:clickable">true</item>
+    </style>
+
+    <style name="BluetoothTileDialog.DeviceName">
+        <item name="android:textSize">14sp</item>
+        <item name="android:textAppearance">@style/TextAppearance.Dialog.Title</item>
+    </style>
+
+    <style name="BluetoothTileDialog.DeviceSummary">
+        <item name="android:ellipsize">end</item>
+        <item name="android:maxLines">2</item>
+        <item name="android:textAppearance">@style/TextAppearance.Dialog.Body.Message</item>
+    </style>
+
     <style name="BroadcastDialog">
         <item name="android:layout_width">wrap_content</item>
         <item name="android:layout_height">wrap_content</item>
@@ -1397,7 +1428,7 @@
     </style>
 
     <style name="PermissionGrantButtonTop"
-           parent="@android:style/Widget.DeviceDefault.Button.Borderless.Colored">
+        parent="@android:style/Widget.DeviceDefault.Button.Borderless.Colored">
         <item name="android:layout_width">332dp</item>
         <item name="android:layout_height">56dp</item>
         <item name="android:layout_marginTop">2dp</item>
@@ -1406,7 +1437,7 @@
     </style>
 
     <style name="PermissionGrantButtonBottom"
-           parent="@android:style/Widget.DeviceDefault.Button.Borderless.Colored">
+        parent="@android:style/Widget.DeviceDefault.Button.Borderless.Colored">
         <item name="android:layout_width">332dp</item>
         <item name="android:layout_height">56dp</item>
         <item name="android:layout_marginTop">2dp</item>
@@ -1465,14 +1496,14 @@
     </style>
 
     <style name="TextAppearance.PrivacyDialog.Item.Title"
-           parent="@android:style/TextAppearance.DeviceDefault.Medium">
+        parent="@android:style/TextAppearance.DeviceDefault.Medium">
         <item name="android:textSize">14sp</item>
         <item name="android:lineHeight">20sp</item>
         <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
     </style>
 
     <style name="TextAppearance.PrivacyDialog.Item.Summary"
-           parent="@android:style/TextAppearance.DeviceDefault.Small">
+        parent="@android:style/TextAppearance.DeviceDefault.Small">
         <item name="android:textSize">14sp</item>
         <item name="android:lineHeight">20sp</item>
         <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
@@ -1481,4 +1512,4 @@
     <style name="Theme.PrivacyDialog" parent="@style/Theme.SystemUI.Dialog">
         <item name="android:colorBackground">?androidprv:attr/materialColorSurfaceContainer</item>
     </style>
-</resources>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
index 1741e30..622b67f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
@@ -17,6 +17,7 @@
 package com.android.keyguard;
 
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_PIN_APPEAR;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_PIN_DISAPPEAR;
@@ -52,6 +53,8 @@
     private final DisappearAnimationUtils mDisappearAnimationUtils;
     private final DisappearAnimationUtils mDisappearAnimationUtilsLocked;
     @Nullable private MotionLayout mContainerMotionLayout;
+    // TODO (b/293252410) - usage of mContainerConstraintLayout should be removed
+    //  when the flag is enabled/removed
     @Nullable private ConstraintLayout mContainerConstraintLayout;
     private int mDisappearYTranslation;
     private View[][] mViews;
@@ -59,7 +62,7 @@
     private int mYTransOffset;
     private View mBouncerMessageArea;
     private boolean mAlreadyUsingSplitBouncer = false;
-    private boolean mIsLockScreenLandscapeEnabled = false;
+    private boolean mIsSmallLockScreenLandscapeEnabled = false;
     @DevicePostureInt private int mLastDevicePosture = DEVICE_POSTURE_UNKNOWN;
     public static final long ANIMATION_DURATION = 650;
 
@@ -87,12 +90,12 @@
     /** Use motion layout (new bouncer implementation) if LOCKSCREEN_ENABLE_LANDSCAPE flag is
      *  enabled, instead of constraint layout (old bouncer implementation) */
     public void setIsLockScreenLandscapeEnabled(boolean isLockScreenLandscapeEnabled) {
-        mIsLockScreenLandscapeEnabled = isLockScreenLandscapeEnabled;
+        mIsSmallLockScreenLandscapeEnabled = isLockScreenLandscapeEnabled;
         findContainerLayout();
     }
 
     private void findContainerLayout() {
-        if (mIsLockScreenLandscapeEnabled) {
+        if (mIsSmallLockScreenLandscapeEnabled) {
             mContainerMotionLayout = findViewById(R.id.pin_container);
         } else {
             mContainerConstraintLayout = findViewById(R.id.pin_container);
@@ -109,7 +112,7 @@
         if (mLastDevicePosture == posture) return;
         mLastDevicePosture = posture;
 
-        if (mIsLockScreenLandscapeEnabled) {
+        if (mIsSmallLockScreenLandscapeEnabled) {
             boolean useSplitBouncerAfterFold =
                     mLastDevicePosture == DEVICE_POSTURE_CLOSED
                     && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE
@@ -166,21 +169,45 @@
             }
         }
 
+        if (mIsSmallLockScreenLandscapeEnabled) {
+            updateHalfFoldedConstraints();
+        } else {
+            updateHalfFoldedGuideline();
+        }
+    }
+
+    private void updateHalfFoldedConstraints() {
+        // Update the constraints based on the device posture...
+        if (mAlreadyUsingSplitBouncer) return;
+
+        boolean shouldCollapsePin =
+                mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED
+                        && mContext.getResources().getConfiguration().orientation
+                        == ORIENTATION_PORTRAIT;
+
+        int expectedMotionLayoutState = shouldCollapsePin
+                ? R.id.half_folded_single_constraints
+                : R.id.single_constraints;
+
+        transitionToMotionLayoutState(expectedMotionLayoutState);
+    }
+
+    // TODO (b/293252410) - this method can be removed when the flag is enabled/removed
+    private void updateHalfFoldedGuideline() {
         // Update the guideline based on the device posture...
         float halfOpenPercentage =
                 mContext.getResources().getFloat(R.dimen.half_opened_bouncer_height_ratio);
 
-        if (mIsLockScreenLandscapeEnabled) {
-            ConstraintSet cs = mContainerMotionLayout.getConstraintSet(R.id.single_constraints);
-            cs.setGuidelinePercent(R.id.pin_pad_top_guideline,
-                    mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED ? halfOpenPercentage : 0.0f);
-            cs.applyTo(mContainerMotionLayout);
-        } else {
-            ConstraintSet cs = new ConstraintSet();
-            cs.clone(mContainerConstraintLayout);
-            cs.setGuidelinePercent(R.id.pin_pad_top_guideline,
-                    mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED ? halfOpenPercentage : 0.0f);
-            cs.applyTo(mContainerConstraintLayout);
+        ConstraintSet cs = new ConstraintSet();
+        cs.clone(mContainerConstraintLayout);
+        cs.setGuidelinePercent(R.id.pin_pad_top_guideline,
+                mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED ? halfOpenPercentage : 0.0f);
+        cs.applyTo(mContainerConstraintLayout);
+    }
+
+    private void transitionToMotionLayoutState(int state) {
+        if (mContainerMotionLayout.getCurrentState() != state) {
+            mContainerMotionLayout.transitionToState(state);
         }
     }
 
@@ -189,12 +216,24 @@
      *  Only called when flag LANDSCAPE_ENABLE_LOCKSCREEN is enabled. */
     @Override
     protected void updateConstraints(boolean useSplitBouncer) {
+        if (!mIsSmallLockScreenLandscapeEnabled) return;
+
         mAlreadyUsingSplitBouncer = useSplitBouncer;
+
         if (useSplitBouncer) {
             mContainerMotionLayout.jumpToState(R.id.split_constraints);
             mContainerMotionLayout.setMaxWidth(Integer.MAX_VALUE);
         } else {
-            mContainerMotionLayout.jumpToState(R.id.single_constraints);
+            boolean useHalfFoldedConstraints =
+                    mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED
+                            && mContext.getResources().getConfiguration().orientation
+                            == ORIENTATION_PORTRAIT;
+
+            if (useHalfFoldedConstraints) {
+                mContainerMotionLayout.jumpToState(R.id.half_folded_single_constraints);
+            } else {
+                mContainerMotionLayout.jumpToState(R.id.single_constraints);
+            }
             mContainerMotionLayout.setMaxWidth(getResources()
                     .getDimensionPixelSize(R.dimen.keyguard_security_width));
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
index 802e222..5c206e9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
@@ -16,6 +16,7 @@
 package com.android.keyguard;
 
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 
 import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED;
 import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED;
@@ -81,9 +82,11 @@
     BouncerKeyguardMessageArea mSecurityMessageDisplay;
     private View mEcaView;
     @Nullable private MotionLayout mContainerMotionLayout;
+    // TODO (b/293252410) - usage of mContainerConstraintLayout should be removed
+    //  when the flag is enabled/removed
     @Nullable private ConstraintLayout mContainerConstraintLayout;
     private boolean mAlreadyUsingSplitBouncer = false;
-    private boolean mIsLockScreenLandscapeEnabled = false;
+    private boolean mIsSmallLockScreenLandscapeEnabled = false;
     @DevicePostureInt private int mLastDevicePosture = DEVICE_POSTURE_UNKNOWN;
 
     public KeyguardPatternView(Context context) {
@@ -111,12 +114,12 @@
      * enabled, instead of constraint layout (old bouncer implementation)
      */
     public void setIsLockScreenLandscapeEnabled(boolean isLockScreenLandscapeEnabled) {
-        mIsLockScreenLandscapeEnabled = isLockScreenLandscapeEnabled;
+        mIsSmallLockScreenLandscapeEnabled = isLockScreenLandscapeEnabled;
         findContainerLayout();
     }
 
     private void findContainerLayout() {
-        if (mIsLockScreenLandscapeEnabled) {
+        if (mIsSmallLockScreenLandscapeEnabled) {
             mContainerMotionLayout = findViewById(R.id.pattern_container);
         } else {
             mContainerConstraintLayout = findViewById(R.id.pattern_container);
@@ -132,7 +135,7 @@
         if (mLastDevicePosture == posture) return;
         mLastDevicePosture = posture;
 
-        if (mIsLockScreenLandscapeEnabled) {
+        if (mIsSmallLockScreenLandscapeEnabled) {
             boolean useSplitBouncerAfterFold =
                     mLastDevicePosture == DEVICE_POSTURE_CLOSED
                     && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE
@@ -147,21 +150,45 @@
     }
 
     private void updateMargins() {
+        if (mIsSmallLockScreenLandscapeEnabled) {
+            updateHalfFoldedConstraints();
+        } else {
+            updateHalfFoldedGuideline();
+        }
+    }
+
+    private void updateHalfFoldedConstraints() {
+        // Update the constraints based on the device posture...
+        if (mAlreadyUsingSplitBouncer) return;
+
+        boolean shouldCollapsePattern =
+                mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED
+                        && mContext.getResources().getConfiguration().orientation
+                        == ORIENTATION_PORTRAIT;
+
+        int expectedMotionLayoutState = shouldCollapsePattern
+                ? R.id.half_folded_single_constraints
+                : R.id.single_constraints;
+
+        transitionToMotionLayoutState(expectedMotionLayoutState);
+    }
+
+    // TODO (b/293252410) - this method can be removed when the flag is enabled/removed
+    private void updateHalfFoldedGuideline() {
         // Update the guideline based on the device posture...
         float halfOpenPercentage =
                 mContext.getResources().getFloat(R.dimen.half_opened_bouncer_height_ratio);
 
-        if (mIsLockScreenLandscapeEnabled) {
-            ConstraintSet cs = mContainerMotionLayout.getConstraintSet(R.id.single_constraints);
-            cs.setGuidelinePercent(R.id.pattern_top_guideline,
-                    mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED ? halfOpenPercentage : 0.0f);
-            cs.applyTo(mContainerMotionLayout);
-        } else {
-            ConstraintSet cs = new ConstraintSet();
-            cs.clone(mContainerConstraintLayout);
-            cs.setGuidelinePercent(R.id.pattern_top_guideline,
-                    mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED ? halfOpenPercentage : 0.0f);
-            cs.applyTo(mContainerConstraintLayout);
+        ConstraintSet cs = new ConstraintSet();
+        cs.clone(mContainerConstraintLayout);
+        cs.setGuidelinePercent(R.id.pattern_top_guideline,
+                mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED ? halfOpenPercentage : 0.0f);
+        cs.applyTo(mContainerConstraintLayout);
+    }
+
+    private void transitionToMotionLayoutState(int state) {
+        if (mContainerMotionLayout.getCurrentState() != state) {
+            mContainerMotionLayout.transitionToState(state);
         }
     }
 
@@ -172,12 +199,24 @@
      */
     @Override
     protected void updateConstraints(boolean useSplitBouncer) {
+        if (!mIsSmallLockScreenLandscapeEnabled) return;
+
         mAlreadyUsingSplitBouncer = useSplitBouncer;
+
         if (useSplitBouncer) {
             mContainerMotionLayout.jumpToState(R.id.split_constraints);
             mContainerMotionLayout.setMaxWidth(Integer.MAX_VALUE);
         } else {
-            mContainerMotionLayout.jumpToState(R.id.single_constraints);
+            boolean useHalfFoldedConstraints =
+                    mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED
+                            && mContext.getResources().getConfiguration().orientation
+                            == ORIENTATION_PORTRAIT;
+
+            if (useHalfFoldedConstraints) {
+                mContainerMotionLayout.jumpToState(R.id.half_folded_single_constraints);
+            } else {
+                mContainerMotionLayout.jumpToState(R.id.single_constraints);
+            }
             mContainerMotionLayout.setMaxWidth(getResources()
                     .getDimensionPixelSize(R.dimen.biometric_auth_pattern_view_max_size));
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
index 9c4d224..ec2999f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
@@ -139,12 +139,12 @@
                             onViewInflatedListener.onViewInflated(childController);
 
                             // Single bouncer constrains are default
-                            if (mFeatureFlags.isEnabled(LOCKSCREEN_ENABLE_LANDSCAPE)
-                                    &&
-                                    getResources().getBoolean(R.bool.update_bouncer_constraints)) {
+                            if (mFeatureFlags.isEnabled(LOCKSCREEN_ENABLE_LANDSCAPE)) {
                                 boolean useSplitBouncer =
-                                        getResources().getConfiguration().orientation
+                                        getResources().getBoolean(R.bool.update_bouncer_constraints)
+                                        && getResources().getConfiguration().orientation
                                                 == ORIENTATION_LANDSCAPE;
+
                                 updateConstraints(useSplitBouncer);
                             }
                         }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index e170849e..467e84d 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -798,7 +798,7 @@
 
     /** Enable showing a dialog when clicking on Quick Settings bluetooth tile. */
     @JvmField
-    val BLUETOOTH_QS_TILE_DIALOG = unreleasedFlag("bluetooth_qs_tile_dialog")
+    val BLUETOOTH_QS_TILE_DIALOG = unreleasedFlag("bluetooth_qs_tile_dialog", teamfood = true)
 
     // TODO(b/300995746): Tracking Bug
     /** Enable communal hub features. */
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
index cc51d21..d9b2c39 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
@@ -36,8 +36,8 @@
  *
  * @param[sliderStateListener] Listener of the slider state.
  * @param[sliderEventProducer] Producer of slider events arising from the slider.
- * @property[scope] [CoroutineScope] where the collection of slider events and the launch of timer
- *   jobs occur.
+ * @param[mainDispatcher] [CoroutineDispatcher] used to launch coroutines for the collection of
+ *   slider events and the launch of timer jobs.
  * @property[config] Configuration parameters of the slider tracker.
  */
 class SeekableSliderTracker(
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderTracker.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderTracker.kt
index e1f5708..002b5aa 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderTracker.kt
@@ -27,7 +27,8 @@
  * The tracker maintains a state machine operated by slider events coming from a
  * [SliderEventProducer]. An action is executed in each state via a [SliderListener].
  *
- * @param[scope] [CoroutineScope] to launch the collection of [SliderEvent].
+ * @property[scope] [CoroutineScope] to launch the collection of [SliderEvent] and state machine
+ *   logic.
  * @property[sliderListener] [SliderListener] to execute actions on a given [SliderState].
  * @property[eventProducer] Producer of [SliderEvent] to iterate over a state machine.
  */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index efd25d5..b506a36 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -3798,6 +3798,13 @@
     }
 
     /**
+     * Notify whether keyguard has created a remote animation runner for next app launch.
+     */
+    public void launchingActivityOverLockscreen(boolean isLaunchingActivityOverLockscreen) {
+        mKeyguardTransitions.setLaunchingActivityOverLockscreen(isLaunchingActivityOverLockscreen);
+    }
+
+    /**
      * Implementation of RemoteAnimationRunner that creates a new
      * {@link ActivityLaunchAnimator.Runner} whenever onAnimationStart is called, delegating the
      * remote animation methods to that runner.
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
index 4e71ef4..cc36961 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
@@ -78,6 +78,7 @@
             component.getControlsListingController().getOrNull()?.getCurrentServices()
         val hasFavorites =
             component.getControlsController().getOrNull()?.getFavorites()?.isNotEmpty() == true
+        val hasPanels = currentServices?.any { it.panelActivity != null } == true
         val componentPackageName = component.getPackageName()
         when {
             currentServices.isNullOrEmpty() && !componentPackageName.isNullOrEmpty() -> {
@@ -100,8 +101,8 @@
                         ),
                 )
             }
-            !hasFavorites -> {
-                // Home app installed but no favorites selected.
+            !hasFavorites && !hasPanels -> {
+                // Home app installed but no favorites selected or panel activities available.
                 val activityClass = component.getControlsUiController().get().resolveActivity()
                 return disabledPickerState(
                     explanation =
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
index 1be514d..d862f56 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
@@ -42,6 +42,8 @@
 import com.android.systemui.res.R;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.QSTile.BooleanState;
@@ -50,6 +52,7 @@
 import com.android.systemui.qs.QsEventLogger;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialogViewModel;
 import com.android.systemui.statusbar.policy.BluetoothController;
 
 import java.util.List;
@@ -72,6 +75,10 @@
 
     private final Executor mExecutor;
 
+    private final BluetoothTileDialogViewModel mDialogViewModel;
+
+    private final FeatureFlags mFeatureFlags;
+
     @Inject
     public BluetoothTile(
             QSHost host,
@@ -83,13 +90,17 @@
             StatusBarStateController statusBarStateController,
             ActivityStarter activityStarter,
             QSLogger qsLogger,
-            BluetoothController bluetoothController
+            BluetoothController bluetoothController,
+            FeatureFlags featureFlags,
+            BluetoothTileDialogViewModel dialogViewModel
     ) {
         super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger,
                 statusBarStateController, activityStarter, qsLogger);
         mController = bluetoothController;
         mController.observe(getLifecycle(), mCallback);
         mExecutor = new HandlerExecutor(mainHandler);
+        mFeatureFlags = featureFlags;
+        mDialogViewModel = dialogViewModel;
     }
 
     @Override
@@ -99,11 +110,15 @@
 
     @Override
     protected void handleClick(@Nullable View view) {
-        // Secondary clicks are header clicks, just toggle.
-        final boolean isEnabled = mState.value;
-        // Immediately enter transient enabling state when turning bluetooth on.
-        refreshState(isEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING);
-        mController.setBluetoothEnabled(!isEnabled);
+        if (mFeatureFlags.isEnabled(Flags.BLUETOOTH_QS_TILE_DIALOG)) {
+            mDialogViewModel.showDialog(mContext, view);
+        } else {
+            // Secondary clicks are header clicks, just toggle.
+            final boolean isEnabled = mState.value;
+            // Immediately enter transient enabling state when turning bluetooth on.
+            refreshState(isEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING);
+            mController.setBluetoothEnabled(!isEnabled);
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractor.kt
new file mode 100644
index 0000000..efad9ec
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractor.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import android.bluetooth.BluetoothAdapter.STATE_OFF
+import android.bluetooth.BluetoothAdapter.STATE_ON
+import com.android.settingslib.bluetooth.BluetoothCallback
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+
+/** Holds business logic for the Bluetooth Dialog's bluetooth and device connection state */
+@SysUISingleton
+internal class BluetoothStateInteractor
+@Inject
+constructor(
+    private val localBluetoothManager: LocalBluetoothManager?,
+    @Application private val coroutineScope: CoroutineScope,
+) {
+
+    internal val updateBluetoothStateFlow: StateFlow<Boolean?> =
+        conflatedCallbackFlow {
+                val listener =
+                    object : BluetoothCallback {
+                        override fun onBluetoothStateChanged(bluetoothState: Int) {
+                            if (bluetoothState == STATE_ON || bluetoothState == STATE_OFF) {
+                                super.onBluetoothStateChanged(bluetoothState)
+                                trySendWithFailureLogging(
+                                    bluetoothState == STATE_ON,
+                                    TAG,
+                                    "onBluetoothStateChanged"
+                                )
+                            }
+                        }
+                    }
+                localBluetoothManager?.eventManager?.registerCallback(listener)
+                awaitClose { localBluetoothManager?.eventManager?.unregisterCallback(listener) }
+            }
+            .stateIn(
+                coroutineScope,
+                SharingStarted.WhileSubscribed(replayExpirationMillis = 0),
+                initialValue = null
+            )
+
+    internal var isBluetoothEnabled: Boolean
+        get() = localBluetoothManager?.bluetoothAdapter?.isEnabled == true
+        set(value) {
+            if (isBluetoothEnabled != value) {
+                localBluetoothManager?.bluetoothAdapter?.apply {
+                    if (value) enable() else disable()
+                }
+            }
+        }
+
+    companion object {
+        private const val TAG = "BtStateInteractor"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
new file mode 100644
index 0000000..6815a73
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import android.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.Switch
+import android.widget.TextView
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Dialog for showing active, connected and saved bluetooth devices. */
+@SysUISingleton
+internal class BluetoothTileDialog
+constructor(
+    private val bluetoothToggleInitialValue: Boolean,
+    private val bluetoothTileDialogCallback: BluetoothTileDialogCallback,
+    private val uiEventLogger: UiEventLogger,
+    context: Context,
+) : SystemUIDialog(context, DEFAULT_THEME, DEFAULT_DISMISS_ON_DEVICE_LOCK) {
+
+    private val mutableBluetoothStateSwitchedFlow: MutableStateFlow<Boolean> =
+        MutableStateFlow(bluetoothToggleInitialValue)
+    internal val bluetoothStateSwitchedFlow
+        get() = mutableBluetoothStateSwitchedFlow.asStateFlow()
+
+    private val mutableClickedFlow: MutableSharedFlow<Pair<DeviceItem, Int>> =
+        MutableSharedFlow(extraBufferCapacity = 1)
+    internal val deviceItemClickedFlow
+        get() = mutableClickedFlow.asSharedFlow()
+
+    private val deviceItemAdapter: Adapter = Adapter(bluetoothTileDialogCallback)
+
+    private lateinit var toggleView: Switch
+    private lateinit var doneButton: View
+    private lateinit var seeAllViewGroup: View
+    private lateinit var pairNewDeviceViewGroup: View
+    private lateinit var seeAllText: View
+    private lateinit var pairNewDeviceText: View
+    private lateinit var deviceListView: RecyclerView
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TILE_DIALOG_SHOWN)
+
+        setContentView(LayoutInflater.from(context).inflate(R.layout.bluetooth_tile_dialog, null))
+
+        toggleView = requireViewById(R.id.bluetooth_toggle)
+        doneButton = requireViewById(R.id.done_button)
+        seeAllViewGroup = requireViewById(R.id.see_all_layout_group)
+        pairNewDeviceViewGroup = requireViewById(R.id.pair_new_device_layout_group)
+        seeAllText = requireViewById(R.id.see_all_text)
+        pairNewDeviceText = requireViewById(R.id.pair_new_device_text)
+        deviceListView = requireViewById<RecyclerView>(R.id.device_list)
+
+        setupToggle()
+        setupRecyclerView()
+
+        doneButton.setOnClickListener { dismiss() }
+        seeAllText.setOnClickListener { bluetoothTileDialogCallback.onSeeAllClicked(it) }
+        pairNewDeviceText.setOnClickListener {
+            bluetoothTileDialogCallback.onPairNewDeviceClicked(it)
+        }
+    }
+
+    // TODO(b/298124674): use DiffUtil or AsyncListDiffer to avoid updating the whole list
+    internal fun onDeviceItemUpdated(
+        deviceItem: List<DeviceItem>,
+        showSeeAll: Boolean,
+        showPairNewDevice: Boolean
+    ) {
+        seeAllViewGroup.visibility = if (showSeeAll) VISIBLE else GONE
+        pairNewDeviceViewGroup.visibility = if (showPairNewDevice) VISIBLE else GONE
+        deviceItemAdapter.refreshDeviceItemList(deviceItem)
+    }
+
+    internal fun onDeviceItemUpdatedAtPosition(deviceItem: DeviceItem, position: Int) {
+        deviceItemAdapter.refreshDeviceItem(deviceItem, position)
+    }
+
+    internal fun onBluetoothStateUpdated(isEnabled: Boolean) {
+        toggleView.isChecked = isEnabled
+    }
+
+    private fun setupToggle() {
+        toggleView.isChecked = bluetoothToggleInitialValue
+        toggleView.setOnCheckedChangeListener { _, isChecked ->
+            mutableBluetoothStateSwitchedFlow.value = isChecked
+            uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TOGGLE_CLICKED)
+        }
+    }
+
+    private fun setupRecyclerView() {
+        deviceListView.apply {
+            layoutManager = LinearLayoutManager(context)
+            adapter = deviceItemAdapter
+        }
+    }
+
+    internal inner class Adapter(private val onClickCallback: BluetoothTileDialogCallback) :
+        RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() {
+
+        private val deviceItem: MutableList<DeviceItem> = mutableListOf()
+
+        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceItemViewHolder {
+            val view =
+                LayoutInflater.from(parent.context)
+                    .inflate(R.layout.bluetooth_device_item, parent, false)
+            return DeviceItemViewHolder(view)
+        }
+
+        override fun getItemCount() = deviceItem.size
+
+        override fun onBindViewHolder(holder: DeviceItemViewHolder, position: Int) {
+            val item = getItem(position)
+            holder.bind(item, position, onClickCallback)
+        }
+
+        internal fun getItem(position: Int) = deviceItem[position]
+
+        internal fun refreshDeviceItemList(updated: List<DeviceItem>) {
+            deviceItem.clear()
+            deviceItem.addAll(updated)
+            notifyDataSetChanged()
+        }
+
+        internal fun refreshDeviceItem(updated: DeviceItem, position: Int) {
+            deviceItem[position] = updated
+            notifyItemChanged(position)
+        }
+
+        internal inner class DeviceItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+            private val container = view.requireViewById<View>(R.id.bluetooth_device_row)
+            private val deviceView = view.requireViewById<View>(R.id.bluetooth_device)
+            private val nameView = view.requireViewById<TextView>(R.id.bluetooth_device_name)
+            private val summaryView = view.requireViewById<TextView>(R.id.bluetooth_device_summary)
+            private val iconView = view.requireViewById<ImageView>(R.id.bluetooth_device_icon)
+            private val gearView = view.requireViewById<View>(R.id.gear_icon)
+
+            internal fun bind(
+                item: DeviceItem,
+                position: Int,
+                deviceItemOnClickCallback: BluetoothTileDialogCallback
+            ) {
+                container.apply {
+                    isEnabled = item.isEnabled
+                    alpha = item.alpha
+                    background = item.background
+                }
+                deviceView.setOnClickListener {
+                    mutableClickedFlow.tryEmit(Pair(item, position))
+                    uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_CLICKED)
+                }
+                nameView.text = item.deviceName
+                summaryView.text = item.connectionSummary
+                iconView.apply {
+                    item.iconWithDescription?.let {
+                        setImageDrawable(it.first)
+                        contentDescription = it.second
+                    }
+                }
+                gearView.setOnClickListener {
+                    deviceItemOnClickCallback.onDeviceItemGearClicked(item, it)
+                }
+            }
+        }
+    }
+
+    internal companion object {
+        const val ENABLED_ALPHA = 1.0f
+        const val DISABLED_ALPHA = 0.3f
+        const val MAX_DEVICE_ITEM_ENTRY = 3
+        const val ACTION_BLUETOOTH_DEVICE_DETAILS =
+            "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS"
+        const val ACTION_PREVIOUSLY_CONNECTED_DEVICE =
+            "com.android.settings.PREVIOUSLY_CONNECTED_DEVICE"
+        const val ACTION_PAIR_NEW_DEVICE = "android.settings.BLUETOOTH_PAIRING_SETTINGS"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogRepository.kt
new file mode 100644
index 0000000..ea51bee
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogRepository.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import android.bluetooth.BluetoothAdapter
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+/** Repository to get CachedBluetoothDevices for the Bluetooth Dialog. */
+@SysUISingleton
+internal class BluetoothTileDialogRepository
+@Inject
+constructor(
+    private val localBluetoothManager: LocalBluetoothManager?,
+    private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
+) {
+    internal val cachedDevices: Collection<CachedBluetoothDevice>
+        get() {
+            return if (
+                localBluetoothManager == null ||
+                    bluetoothAdapter == null ||
+                    !bluetoothAdapter.isEnabled
+            ) {
+                emptyList()
+            } else {
+                localBluetoothManager.cachedDeviceManager.cachedDevicesCopy
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt
new file mode 100644
index 0000000..2865ad7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import com.android.internal.logging.UiEvent
+import com.android.internal.logging.UiEventLogger
+
+/** UI Events for the bluetooth tile dialog. */
+enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum {
+    @UiEvent(doc = "The bluetooth tile dialog is shown") BLUETOOTH_TILE_DIALOG_SHOWN(1493),
+    @UiEvent(doc = "The master toggle is clicked") BLUETOOTH_TOGGLE_CLICKED(1494),
+    @UiEvent(doc = "Pair new device is clicked") PAIR_NEW_DEVICE_CLICKED(1495),
+    @UiEvent(doc = "See all is clicked") SEE_ALL_CLICKED(1496),
+    @UiEvent(doc = "Gear icon clicked") DEVICE_GEAR_CLICKED(1497),
+    @UiEvent(doc = "Device clicked") DEVICE_CLICKED(1498),
+    @UiEvent(doc = "Connected device clicked to active") CONNECTED_DEVICE_SET_ACTIVE(1499),
+    @UiEvent(doc = "Saved clicked to connect") SAVED_DEVICE_CONNECT(1500);
+
+    override fun getId() = metricId
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
new file mode 100644
index 0000000..012484f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.ACTION_BLUETOOTH_DEVICE_DETAILS
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.ACTION_PAIR_NEW_DEVICE
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.MAX_DEVICE_ITEM_ENTRY
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+/** ViewModel for Bluetooth Dialog after clicking on the Bluetooth QS tile. */
+@SysUISingleton
+internal class BluetoothTileDialogViewModel
+@Inject
+constructor(
+    private val deviceItemInteractor: DeviceItemInteractor,
+    private val bluetoothStateInteractor: BluetoothStateInteractor,
+    private val dialogLaunchAnimator: DialogLaunchAnimator,
+    private val activityStarter: ActivityStarter,
+    private val uiEventLogger: UiEventLogger,
+    @Application private val coroutineScope: CoroutineScope,
+    @Main private val mainDispatcher: CoroutineDispatcher,
+) : BluetoothTileDialogCallback {
+
+    private var job: Job? = null
+
+    @VisibleForTesting internal var dialog: BluetoothTileDialog? = null
+
+    /**
+     * Shows the dialog.
+     *
+     * @param context The context in which the dialog is displayed.
+     * @param view The view from which the dialog is shown.
+     */
+    fun showDialog(context: Context, view: View?) {
+        dismissDialog()
+
+        var updateDeviceItemJob: Job? = null
+
+        job =
+            coroutineScope.launch(mainDispatcher) {
+                dialog = createBluetoothTileDialog(context)
+                view?.let { dialogLaunchAnimator.showFromView(dialog!!, it) } ?: dialog!!.show()
+                updateDeviceItemJob?.cancel()
+                updateDeviceItemJob = launch { deviceItemInteractor.updateDeviceItems(context) }
+
+                bluetoothStateInteractor.updateBluetoothStateFlow
+                    .filterNotNull()
+                    .onEach {
+                        dialog!!.onBluetoothStateUpdated(it)
+                        updateDeviceItemJob?.cancel()
+                        updateDeviceItemJob = launch {
+                            deviceItemInteractor.updateDeviceItems(context)
+                        }
+                    }
+                    .launchIn(this)
+
+                deviceItemInteractor.updateDeviceItemsFlow
+                    .onEach {
+                        updateDeviceItemJob?.cancel()
+                        updateDeviceItemJob = launch {
+                            deviceItemInteractor.updateDeviceItems(context)
+                        }
+                    }
+                    .launchIn(this)
+
+                deviceItemInteractor.deviceItemFlow
+                    .filterNotNull()
+                    .onEach {
+                        dialog!!.onDeviceItemUpdated(
+                            it.take(MAX_DEVICE_ITEM_ENTRY),
+                            showSeeAll = it.size > MAX_DEVICE_ITEM_ENTRY,
+                            showPairNewDevice = bluetoothStateInteractor.isBluetoothEnabled
+                        )
+                    }
+                    .launchIn(this)
+
+                dialog!!
+                    .bluetoothStateSwitchedFlow
+                    .onEach { bluetoothStateInteractor.isBluetoothEnabled = it }
+                    .launchIn(this)
+
+                dialog!!
+                    .deviceItemClickedFlow
+                    .onEach {
+                        if (deviceItemInteractor.updateDeviceItemOnClick(it.first)) {
+                            dialog!!.onDeviceItemUpdatedAtPosition(it.first, it.second)
+                        }
+                    }
+                    .launchIn(this)
+            }
+    }
+
+    private fun createBluetoothTileDialog(context: Context): BluetoothTileDialog {
+        return BluetoothTileDialog(
+                bluetoothStateInteractor.isBluetoothEnabled,
+                this@BluetoothTileDialogViewModel,
+                uiEventLogger,
+                context
+            )
+            .apply { SystemUIDialog.registerDismissListener(this) { dismissDialog() } }
+    }
+
+    override fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View) {
+        uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_GEAR_CLICKED)
+        val intent =
+            Intent(ACTION_BLUETOOTH_DEVICE_DETAILS).apply {
+                putExtra(
+                    ":settings:show_fragment_args",
+                    Bundle().apply {
+                        putString("device_address", deviceItem.cachedBluetoothDevice.address)
+                    }
+                )
+            }
+        startSettingsActivity(intent, view)
+    }
+
+    override fun onSeeAllClicked(view: View) {
+        uiEventLogger.log(BluetoothTileDialogUiEvent.SEE_ALL_CLICKED)
+        startSettingsActivity(Intent(ACTION_PREVIOUSLY_CONNECTED_DEVICE), view)
+    }
+
+    override fun onPairNewDeviceClicked(view: View) {
+        uiEventLogger.log(BluetoothTileDialogUiEvent.PAIR_NEW_DEVICE_CLICKED)
+        startSettingsActivity(Intent(ACTION_PAIR_NEW_DEVICE), view)
+    }
+
+    private fun dismissDialog() {
+        job?.cancel()
+        job = null
+        dialog?.dismiss()
+        dialog = null
+    }
+
+    private fun startSettingsActivity(intent: Intent, view: View) {
+        dialog?.run {
+            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+            activityStarter.postStartActivityDismissingKeyguard(
+                intent,
+                0,
+                dialogLaunchAnimator.createActivityLaunchController(view)
+            )
+        }
+    }
+}
+
+internal interface BluetoothTileDialogCallback {
+    fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View)
+    fun onSeeAllClicked(view: View)
+    fun onPairNewDeviceClicked(view: View)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItem.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItem.kt
new file mode 100644
index 0000000..03ae5e8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItem.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import android.graphics.drawable.Drawable
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.ENABLED_ALPHA
+
+enum class DeviceItemType {
+    AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
+    CONNECTED_BLUETOOTH_DEVICE,
+    SAVED_BLUETOOTH_DEVICE,
+}
+
+data class DeviceItem(
+    val type: DeviceItemType,
+    val cachedBluetoothDevice: CachedBluetoothDevice,
+    val deviceName: String = "",
+    val connectionSummary: String = "",
+    val iconWithDescription: Pair<Drawable, String>? = null,
+    val background: Drawable? = null,
+    var isEnabled: Boolean = true,
+    var alpha: Float = ENABLED_ALPHA
+)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt
new file mode 100644
index 0000000..a16a9f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import android.bluetooth.BluetoothDevice
+import android.content.Context
+import android.media.AudioManager
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.res.R
+
+private val backgroundOn = R.drawable.settingslib_switch_bar_bg_on
+private val connected = R.string.quick_settings_bluetooth_device_connected
+private val saved = R.string.quick_settings_bluetooth_device_saved
+
+/** Factories to create different types of Bluetooth device items from CachedBluetoothDevice. */
+internal abstract class DeviceItemFactory {
+    abstract fun isFilterMatched(
+        cachedDevice: CachedBluetoothDevice,
+        audioManager: AudioManager?
+    ): Boolean
+
+    abstract fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem
+}
+
+internal class AvailableMediaDeviceItemFactory : DeviceItemFactory() {
+    override fun isFilterMatched(
+        cachedDevice: CachedBluetoothDevice,
+        audioManager: AudioManager?
+    ): Boolean {
+        return BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager)
+    }
+
+    // TODO(b/298124674): move create() to the abstract class to reduce duplicate code
+    override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem {
+        return DeviceItem(
+            type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
+            cachedBluetoothDevice = cachedDevice,
+            deviceName = cachedDevice.name,
+            connectionSummary = cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() }
+                    ?: context.getString(connected),
+            iconWithDescription =
+                BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice).let { p ->
+                    Pair(p.first, p.second)
+                },
+            background = context.getDrawable(backgroundOn),
+            isEnabled = !cachedDevice.isBusy,
+            alpha =
+                if (cachedDevice.isBusy) BluetoothTileDialog.DISABLED_ALPHA
+                else BluetoothTileDialog.ENABLED_ALPHA,
+        )
+    }
+}
+
+internal class ConnectedDeviceItemFactory : DeviceItemFactory() {
+    override fun isFilterMatched(
+        cachedDevice: CachedBluetoothDevice,
+        audioManager: AudioManager?
+    ): Boolean {
+        return BluetoothUtils.isConnectedBluetoothDevice(cachedDevice, audioManager)
+    }
+
+    override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem {
+        return DeviceItem(
+            type = DeviceItemType.CONNECTED_BLUETOOTH_DEVICE,
+            cachedBluetoothDevice = cachedDevice,
+            deviceName = cachedDevice.name,
+            connectionSummary = cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() }
+                    ?: context.getString(connected),
+            iconWithDescription =
+                BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice).let { p ->
+                    Pair(p.first, p.second)
+                },
+            isEnabled = !cachedDevice.isBusy,
+            alpha =
+                if (cachedDevice.isBusy) BluetoothTileDialog.DISABLED_ALPHA
+                else BluetoothTileDialog.ENABLED_ALPHA,
+        )
+    }
+}
+
+internal class SavedDeviceItemFactory : DeviceItemFactory() {
+    override fun isFilterMatched(
+        cachedDevice: CachedBluetoothDevice,
+        audioManager: AudioManager?
+    ): Boolean {
+        return cachedDevice.bondState == BluetoothDevice.BOND_BONDED && !cachedDevice.isConnected
+    }
+
+    override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem {
+        return DeviceItem(
+            type = DeviceItemType.SAVED_BLUETOOTH_DEVICE,
+            cachedBluetoothDevice = cachedDevice,
+            deviceName = cachedDevice.name,
+            connectionSummary = cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() }
+                    ?: context.getString(saved),
+            iconWithDescription =
+                BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice).let { p ->
+                    Pair(p.first, p.second)
+                },
+            isEnabled = !cachedDevice.isBusy,
+            alpha =
+                if (cachedDevice.isBusy) BluetoothTileDialog.DISABLED_ALPHA
+                else BluetoothTileDialog.ENABLED_ALPHA,
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt
new file mode 100644
index 0000000..fcd0ce6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.content.Context
+import android.media.AudioManager
+import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.bluetooth.BluetoothCallback
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.withContext
+
+/** Holds business logic for the Bluetooth Dialog after clicking on the Bluetooth QS tile. */
+@SysUISingleton
+internal class DeviceItemInteractor
+@Inject
+constructor(
+    private val bluetoothTileDialogRepository: BluetoothTileDialogRepository,
+    private val audioManager: AudioManager,
+    private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter(),
+    private val localBluetoothManager: LocalBluetoothManager?,
+    private val uiEventLogger: UiEventLogger,
+    @Application private val coroutineScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+) {
+
+    private val mutableDeviceItemFlow: MutableStateFlow<List<DeviceItem>?> = MutableStateFlow(null)
+    internal val deviceItemFlow
+        get() = mutableDeviceItemFlow.asStateFlow()
+
+    internal val updateDeviceItemsFlow: SharedFlow<Unit> =
+        conflatedCallbackFlow {
+                val listener =
+                    object : BluetoothCallback {
+                        override fun onActiveDeviceChanged(
+                            activeDevice: CachedBluetoothDevice?,
+                            bluetoothProfile: Int
+                        ) {
+                            super.onActiveDeviceChanged(activeDevice, bluetoothProfile)
+                            trySendWithFailureLogging(Unit, TAG, "onActiveDeviceChanged")
+                        }
+
+                        override fun onConnectionStateChanged(
+                            cachedDevice: CachedBluetoothDevice?,
+                            state: Int
+                        ) {
+                            super.onConnectionStateChanged(cachedDevice, state)
+                            trySendWithFailureLogging(Unit, TAG, "onConnectionStateChanged")
+                        }
+
+                        override fun onDeviceAdded(cachedDevice: CachedBluetoothDevice) {
+                            super.onDeviceAdded(cachedDevice)
+                            trySendWithFailureLogging(Unit, TAG, "onDeviceAdded")
+                        }
+
+                        override fun onProfileConnectionStateChanged(
+                            cachedDevice: CachedBluetoothDevice,
+                            state: Int,
+                            bluetoothProfile: Int
+                        ) {
+                            super.onProfileConnectionStateChanged(
+                                cachedDevice,
+                                state,
+                                bluetoothProfile
+                            )
+                            trySendWithFailureLogging(Unit, TAG, "onProfileConnectionStateChanged")
+                        }
+                    }
+                localBluetoothManager?.eventManager?.registerCallback(listener)
+                awaitClose { localBluetoothManager?.eventManager?.unregisterCallback(listener) }
+            }
+            .shareIn(coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0))
+
+    private var deviceItemFactoryList: List<DeviceItemFactory> =
+        listOf(
+            AvailableMediaDeviceItemFactory(),
+            ConnectedDeviceItemFactory(),
+            SavedDeviceItemFactory()
+        )
+
+    private var displayPriority: List<DeviceItemType> =
+        listOf(
+            DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
+            DeviceItemType.CONNECTED_BLUETOOTH_DEVICE,
+            DeviceItemType.SAVED_BLUETOOTH_DEVICE,
+        )
+
+    internal suspend fun updateDeviceItems(context: Context) {
+        withContext(backgroundDispatcher) {
+            val mostRecentlyConnectedDevices = bluetoothAdapter?.mostRecentlyConnectedDevices
+
+            mutableDeviceItemFlow.value =
+                bluetoothTileDialogRepository.cachedDevices
+                    .mapNotNull { cachedDevice ->
+                        deviceItemFactoryList
+                            .firstOrNull { it.isFilterMatched(cachedDevice, audioManager) }
+                            ?.create(context, cachedDevice)
+                    }
+                    .sort(displayPriority, mostRecentlyConnectedDevices)
+        }
+    }
+
+    private fun List<DeviceItem>.sort(
+        displayPriority: List<DeviceItemType>,
+        mostRecentlyConnectedDevices: List<BluetoothDevice>?
+    ): List<DeviceItem> {
+        return this.sortedWith(
+            compareBy<DeviceItem> { displayPriority.indexOf(it.type) }
+                .thenBy {
+                    mostRecentlyConnectedDevices?.indexOf(it.cachedBluetoothDevice.device) ?: 0
+                }
+        )
+    }
+
+    internal fun updateDeviceItemOnClick(deviceItem: DeviceItem): Boolean {
+        var isClicked = false
+        when (deviceItem.type) {
+            DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> {
+                if (!BluetoothUtils.isActiveMediaDevice(deviceItem.cachedBluetoothDevice)) {
+                    deviceItem.cachedBluetoothDevice.setActive()
+                    uiEventLogger.log(BluetoothTileDialogUiEvent.CONNECTED_DEVICE_SET_ACTIVE)
+                    isClicked = true
+                }
+            }
+            DeviceItemType.CONNECTED_BLUETOOTH_DEVICE -> {}
+            DeviceItemType.SAVED_BLUETOOTH_DEVICE -> {
+                deviceItem.cachedBluetoothDevice.connect()
+                uiEventLogger.log(BluetoothTileDialogUiEvent.SAVED_DEVICE_CONNECT)
+                isClicked = true
+            }
+        }
+        if (isClicked) {
+            deviceItem.isEnabled = false
+            deviceItem.alpha = BluetoothTileDialog.DISABLED_ALPHA
+        }
+        return isClicked
+    }
+
+    internal fun setDeviceItemFactoryListForTesting(list: List<DeviceItemFactory>) {
+        deviceItemFactoryList = list
+    }
+
+    internal fun setDisplayPriorityForTesting(list: List<DeviceItemType>) {
+        displayPriority = list
+    }
+
+    companion object {
+        private const val TAG = "DeviceItemInteractor"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/TEST_MAPPING b/packages/SystemUI/src/com/android/systemui/statusbar/TEST_MAPPING
index 8849d6e..10e7573 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/TEST_MAPPING
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/TEST_MAPPING
@@ -13,7 +13,7 @@
           "exclude-annotation": "org.junit.Ignore"
         },
         {
-          "exclude-annotation": "android.platform.test.annotations.LargeTest"
+          "exclude-annotation": "androidx.test.filters.LargeTest"
         },
         {
           "exclude-annotation": "androidx.test.filters.LargeTest"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 3a88504..6f69ea81 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -3429,6 +3429,7 @@
     @Override
     public void setIsLaunchingActivityOverLockscreen(boolean isLaunchingActivityOverLockscreen) {
         mIsLaunchingActivityOverLockscreen = isLaunchingActivityOverLockscreen;
+        mKeyguardViewMediator.launchingActivityOverLockscreen(mIsLaunchingActivityOverLockscreen);
     }
 
     @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
index 12fa4c8..477f455 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
@@ -176,7 +176,7 @@
                     !isFeatureEnabled ->
                         KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice::class
                             .java
-                    hasServiceInfos && hasFavorites ->
+                    hasServiceInfos && (hasFavorites || hasPanels) ->
                         KeyguardQuickAffordanceConfig.PickerScreenState.Default::class.java
                     else -> KeyguardQuickAffordanceConfig.PickerScreenState.Disabled::class.java
                 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
index 623a8e0..82ee99a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
@@ -14,6 +14,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.flags.FeatureFlagsClassic
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.qs.QSTile
@@ -22,6 +23,7 @@
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialogViewModel
 import com.android.systemui.statusbar.policy.BluetoothController
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
@@ -50,6 +52,8 @@
     @Mock private lateinit var activityStarter: ActivityStarter
     @Mock private lateinit var bluetoothController: BluetoothController
     @Mock private lateinit var uiEventLogger: QsEventLogger
+    @Mock private lateinit var featureFlags: FeatureFlagsClassic
+    @Mock private lateinit var bluetoothTileDialogViewModel: BluetoothTileDialogViewModel
 
     private lateinit var testableLooper: TestableLooper
     private lateinit var tile: FakeBluetoothTile
@@ -73,6 +77,8 @@
                 activityStarter,
                 qsLogger,
                 bluetoothController,
+                featureFlags,
+                bluetoothTileDialogViewModel
             )
 
         tile.initialize()
@@ -220,6 +226,8 @@
         activityStarter: ActivityStarter,
         qsLogger: QSLogger,
         bluetoothController: BluetoothController,
+        featureFlags: FeatureFlagsClassic,
+        bluetoothTileDialogViewModel: BluetoothTileDialogViewModel
     ) :
         BluetoothTile(
             qsHost,
@@ -232,6 +240,8 @@
             activityStarter,
             qsLogger,
             bluetoothController,
+            featureFlags,
+            bluetoothTileDialogViewModel
         ) {
         var restrictionChecked: String? = null
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractorTest.kt
new file mode 100644
index 0000000..fc2b7a64
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothStateInteractorTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.LocalBluetoothAdapter
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class BluetoothStateInteractorTest : SysuiTestCase() {
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+    private val testScope = TestScope()
+
+    private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
+
+    @Mock private lateinit var bluetoothAdapter: LocalBluetoothAdapter
+    @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
+
+    @Before
+    fun setUp() {
+        bluetoothStateInteractor =
+            BluetoothStateInteractor(localBluetoothManager, testScope.backgroundScope)
+        `when`(localBluetoothManager.bluetoothAdapter).thenReturn(bluetoothAdapter)
+    }
+
+    @Test
+    fun testGet_isBluetoothEnabled() {
+        testScope.runTest {
+            `when`(bluetoothAdapter.isEnabled).thenReturn(true)
+
+            assertThat(bluetoothStateInteractor.isBluetoothEnabled).isTrue()
+        }
+    }
+
+    @Test
+    fun testGet_isBluetoothDisabled() {
+        testScope.runTest {
+            `when`(bluetoothAdapter.isEnabled).thenReturn(false)
+
+            assertThat(bluetoothStateInteractor.isBluetoothEnabled).isFalse()
+        }
+    }
+
+    @Test
+    fun testSet_bluetoothEnabled() {
+        testScope.runTest {
+            `when`(bluetoothAdapter.isEnabled).thenReturn(false)
+
+            bluetoothStateInteractor.isBluetoothEnabled = true
+            verify(bluetoothAdapter).enable()
+        }
+    }
+
+    @Test
+    fun testSet_bluetoothNoChange() {
+        testScope.runTest {
+            `when`(bluetoothAdapter.isEnabled).thenReturn(false)
+
+            bluetoothStateInteractor.isBluetoothEnabled = false
+            verify(bluetoothAdapter, never()).enable()
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogRepositoryTest.kt
new file mode 100644
index 0000000..da8f60a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogRepositoryTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import android.bluetooth.BluetoothAdapter
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class BluetoothTileDialogRepositoryTest : SysuiTestCase() {
+
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
+
+    @Mock private lateinit var bluetoothAdapter: BluetoothAdapter
+
+    @Mock private lateinit var cachedDeviceManager: CachedBluetoothDeviceManager
+
+    @Mock private lateinit var cachedDevicesCopy: Collection<CachedBluetoothDevice>
+
+    private lateinit var repository: BluetoothTileDialogRepository
+
+    @Before
+    fun setUp() {
+        `when`(localBluetoothManager.cachedDeviceManager).thenReturn(cachedDeviceManager)
+        `when`(cachedDeviceManager.cachedDevicesCopy).thenReturn(cachedDevicesCopy)
+
+        repository = BluetoothTileDialogRepository(localBluetoothManager, bluetoothAdapter)
+    }
+
+    @Test
+    fun testCachedDevices_bluetoothOff_emptyList() {
+        `when`(bluetoothAdapter.isEnabled).thenReturn(false)
+
+        val result = repository.cachedDevices
+
+        assertThat(result).isEmpty()
+    }
+
+    @Test
+    fun testCachedDevices_bluetoothOn_returnDevice() {
+        `when`(bluetoothAdapter.isEnabled).thenReturn(true)
+
+        val result = repository.cachedDevices
+
+        assertThat(result).isEqualTo(cachedDevicesCopy)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
new file mode 100644
index 0000000..89fa55b3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import android.graphics.drawable.Drawable
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.DISABLED_ALPHA
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.ENABLED_ALPHA
+import com.android.systemui.res.R
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class BluetoothTileDialogTest : SysuiTestCase() {
+    companion object {
+        const val DEVICE_NAME = "device"
+        const val DEVICE_CONNECTION_SUMMARY = "active"
+        const val ENABLED = true
+    }
+
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice
+
+    @Mock private lateinit var bluetoothTileDialogCallback: BluetoothTileDialogCallback
+
+    @Mock private lateinit var drawable: Drawable
+
+    @Mock private lateinit var uiEventLogger: UiEventLogger
+
+    private lateinit var icon: Pair<Drawable, String>
+    private lateinit var bluetoothTileDialog: BluetoothTileDialog
+    private lateinit var deviceItem: DeviceItem
+
+    @Before
+    fun setUp() {
+        bluetoothTileDialog =
+            BluetoothTileDialog(ENABLED, bluetoothTileDialogCallback, uiEventLogger, mContext)
+        icon = Pair(drawable, DEVICE_NAME)
+        deviceItem =
+            DeviceItem(
+                type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
+                cachedBluetoothDevice = cachedBluetoothDevice,
+                deviceName = DEVICE_NAME,
+                connectionSummary = DEVICE_CONNECTION_SUMMARY,
+                iconWithDescription = icon,
+                background = null
+            )
+        `when`(cachedBluetoothDevice.isBusy).thenReturn(false)
+    }
+
+    @Test
+    fun testShowDialog_createRecyclerViewWithAdapter() {
+        bluetoothTileDialog.show()
+
+        val recyclerView = bluetoothTileDialog.requireViewById<RecyclerView>(R.id.device_list)
+
+        assertThat(bluetoothTileDialog.isShowing).isTrue()
+        assertThat(recyclerView).isNotNull()
+        assertThat(recyclerView?.visibility).isEqualTo(VISIBLE)
+        assertThat(recyclerView?.adapter).isNotNull()
+        assertThat(recyclerView?.layoutManager is LinearLayoutManager).isTrue()
+    }
+
+    @Test
+    fun testShowDialog_displayBluetoothDevice() {
+        bluetoothTileDialog =
+            BluetoothTileDialog(ENABLED, bluetoothTileDialogCallback, uiEventLogger, mContext)
+        bluetoothTileDialog.show()
+        bluetoothTileDialog.onDeviceItemUpdated(
+            listOf(deviceItem),
+            showSeeAll = false,
+            showPairNewDevice = false
+        )
+
+        val recyclerView = bluetoothTileDialog.requireViewById<RecyclerView>(R.id.device_list)
+        val adapter = recyclerView?.adapter as BluetoothTileDialog.Adapter
+        assertThat(adapter.itemCount).isEqualTo(1)
+        assertThat(adapter.getItem(0).deviceName).isEqualTo(DEVICE_NAME)
+        assertThat(adapter.getItem(0).connectionSummary).isEqualTo(DEVICE_CONNECTION_SUMMARY)
+        assertThat(adapter.getItem(0).iconWithDescription).isEqualTo(icon)
+    }
+
+    @Test
+    fun testDeviceItemViewHolder_cachedDeviceNotBusy() {
+        deviceItem.isEnabled = true
+        deviceItem.alpha = ENABLED_ALPHA
+
+        val view =
+            LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
+        val viewHolder =
+            BluetoothTileDialog(ENABLED, bluetoothTileDialogCallback, uiEventLogger, mContext)
+                .Adapter(bluetoothTileDialogCallback)
+                .DeviceItemViewHolder(view)
+        viewHolder.bind(deviceItem, 0, bluetoothTileDialogCallback)
+        val container = view.requireViewById<View>(R.id.bluetooth_device)
+        val deviceView = view.requireViewById<View>(R.id.bluetooth_device)
+
+        assertThat(container).isNotNull()
+        assertThat(container.isEnabled).isTrue()
+        assertThat(container.alpha).isEqualTo(ENABLED_ALPHA)
+        assertThat(deviceView.hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun testDeviceItemViewHolder_cachedDeviceBusy() {
+        deviceItem.isEnabled = false
+        deviceItem.alpha = DISABLED_ALPHA
+
+        val view =
+            LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
+        val viewHolder =
+            BluetoothTileDialog(ENABLED, bluetoothTileDialogCallback, uiEventLogger, mContext)
+                .Adapter(bluetoothTileDialogCallback)
+                .DeviceItemViewHolder(view)
+        viewHolder.bind(deviceItem, 0, bluetoothTileDialogCallback)
+        val container = view.requireViewById<View>(R.id.bluetooth_device_row)
+        val deviceView = view.requireViewById<View>(R.id.bluetooth_device)
+
+        assertThat(container).isNotNull()
+        assertThat(container.isEnabled).isFalse()
+        assertThat(container.alpha).isEqualTo(DISABLED_ALPHA)
+        assertThat(deviceView.hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun testOnDeviceUpdated_hideSeeAll_showPairNew() {
+        bluetoothTileDialog =
+            BluetoothTileDialog(ENABLED, bluetoothTileDialogCallback, uiEventLogger, mContext)
+        bluetoothTileDialog.show()
+        bluetoothTileDialog.onDeviceItemUpdated(
+            listOf(deviceItem),
+            showSeeAll = false,
+            showPairNewDevice = true
+        )
+
+        val seeAllLayout = bluetoothTileDialog.requireViewById<View>(R.id.see_all_layout_group)
+        val pairNewLayout =
+            bluetoothTileDialog.requireViewById<View>(R.id.pair_new_device_layout_group)
+        val recyclerView = bluetoothTileDialog.requireViewById<RecyclerView>(R.id.device_list)
+        val adapter = recyclerView?.adapter as BluetoothTileDialog.Adapter
+
+        assertThat(seeAllLayout).isNotNull()
+        assertThat(seeAllLayout.visibility).isEqualTo(GONE)
+        assertThat(pairNewLayout).isNotNull()
+        assertThat(pairNewLayout.visibility).isEqualTo(VISIBLE)
+        assertThat(adapter.itemCount).isEqualTo(1)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
new file mode 100644
index 0000000..7157cce
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import android.widget.LinearLayout
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class BluetoothTileDialogViewModelTest : SysuiTestCase() {
+
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+    private val fakeSystemClock = FakeSystemClock()
+    private val backgroundExecutor = FakeExecutor(fakeSystemClock)
+
+    private lateinit var bluetoothTileDialogViewModel: BluetoothTileDialogViewModel
+
+    @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
+
+    @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
+
+    @Mock private lateinit var activityStarter: ActivityStarter
+
+    @Mock private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
+
+    @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice
+
+    @Mock private lateinit var deviceItem: DeviceItem
+
+    @Mock private lateinit var uiEventLogger: UiEventLogger
+
+    private lateinit var scheduler: TestCoroutineScheduler
+    private lateinit var dispatcher: CoroutineDispatcher
+    private lateinit var testScope: TestScope
+
+    @Before
+    fun setUp() {
+        scheduler = TestCoroutineScheduler()
+        dispatcher = UnconfinedTestDispatcher(scheduler)
+        testScope = TestScope(dispatcher)
+        bluetoothTileDialogViewModel =
+            BluetoothTileDialogViewModel(
+                deviceItemInteractor,
+                bluetoothStateInteractor,
+                dialogLaunchAnimator,
+                activityStarter,
+                uiEventLogger,
+                testScope.backgroundScope,
+                dispatcher,
+            )
+        `when`(deviceItemInteractor.deviceItemFlow).thenReturn(MutableStateFlow(null).asStateFlow())
+        `when`(bluetoothStateInteractor.updateBluetoothStateFlow)
+            .thenReturn(MutableStateFlow(null).asStateFlow())
+        `when`(deviceItemInteractor.updateDeviceItemsFlow)
+            .thenReturn(MutableStateFlow(Unit).asStateFlow())
+        `when`(bluetoothStateInteractor.isBluetoothEnabled).thenReturn(true)
+    }
+
+    @Test
+    fun testShowDialog_noAnimation() {
+        testScope.runTest {
+            bluetoothTileDialogViewModel.showDialog(context, null)
+
+            assertThat(bluetoothTileDialogViewModel.dialog).isNotNull()
+            verify(dialogLaunchAnimator, never()).showFromView(any(), any(), any(), any())
+            assertThat(bluetoothTileDialogViewModel.dialog?.isShowing).isTrue()
+            verify(uiEventLogger).log(BluetoothTileDialogUiEvent.BLUETOOTH_TILE_DIALOG_SHOWN)
+        }
+    }
+
+    @Test
+    fun testShowDialog_animated() {
+        testScope.runTest {
+            bluetoothTileDialogViewModel.showDialog(mContext, LinearLayout(mContext))
+
+            assertThat(bluetoothTileDialogViewModel.dialog).isNotNull()
+            verify(dialogLaunchAnimator).showFromView(any(), any(), nullable(), anyBoolean())
+        }
+    }
+
+    @Test
+    fun testShowDialog_animated_callInBackgroundThread() {
+        testScope.runTest {
+            backgroundExecutor.execute {
+                bluetoothTileDialogViewModel.showDialog(mContext, LinearLayout(mContext))
+
+                assertThat(bluetoothTileDialogViewModel.dialog).isNotNull()
+                verify(dialogLaunchAnimator).showFromView(any(), any(), nullable(), anyBoolean())
+            }
+        }
+    }
+
+    @Test
+    fun testShowDialog_fetchDeviceItem() {
+        testScope.runTest {
+            bluetoothTileDialogViewModel.showDialog(context, null)
+
+            assertThat(bluetoothTileDialogViewModel.dialog).isNotNull()
+            verify(deviceItemInteractor).deviceItemFlow
+        }
+    }
+
+    @Test
+    fun testShowDialog_withBluetoothStateValue() {
+        testScope.runTest {
+            bluetoothTileDialogViewModel.showDialog(context, null)
+
+            assertThat(bluetoothTileDialogViewModel.dialog).isNotNull()
+            verify(bluetoothStateInteractor).updateBluetoothStateFlow
+        }
+    }
+
+    @Test
+    fun testStartSettingsActivity_activityLaunched_dialogDismissed() {
+        `when`(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
+        bluetoothTileDialogViewModel.showDialog(context, null)
+
+        val clickedView = View(context)
+        bluetoothTileDialogViewModel.onPairNewDeviceClicked(clickedView)
+
+        verify(uiEventLogger).log(BluetoothTileDialogUiEvent.PAIR_NEW_DEVICE_CLICKED)
+        verify(activityStarter).postStartActivityDismissingKeyguard(any(), anyInt(), nullable())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactoryTest.kt
new file mode 100644
index 0000000..3451902
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactoryTest.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class DeviceItemFactoryTest : SysuiTestCase() {
+
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var cachedDevice: CachedBluetoothDevice
+
+    private val availableMediaDeviceItemFactory = AvailableMediaDeviceItemFactory()
+    private val connectedDeviceItemFactory = ConnectedDeviceItemFactory()
+    private val savedDeviceItemFactory = SavedDeviceItemFactory()
+
+    @Before
+    fun setup() {
+        `when`(cachedDevice.name).thenReturn(DEVICE_NAME)
+        `when`(cachedDevice.connectionSummary).thenReturn(CONNECTION_SUMMARY)
+    }
+
+    @Test
+    fun testAvailableMediaDeviceItemFactory_createFromCachedDevice() {
+        val deviceItem = availableMediaDeviceItemFactory.create(context, cachedDevice)
+
+        assertDeviceItem(deviceItem, DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE)
+    }
+
+    @Test
+    fun testConnectedDeviceItemFactory_createFromCachedDevice() {
+        val deviceItem = connectedDeviceItemFactory.create(context, cachedDevice)
+
+        assertDeviceItem(deviceItem, DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+    }
+
+    @Test
+    fun testSavedDeviceItemFactory_createFromCachedDevice() {
+        val deviceItem = savedDeviceItemFactory.create(context, cachedDevice)
+
+        assertDeviceItem(deviceItem, DeviceItemType.SAVED_BLUETOOTH_DEVICE)
+        assertThat(deviceItem.background).isNull()
+    }
+
+    private fun assertDeviceItem(deviceItem: DeviceItem?, deviceItemType: DeviceItemType) {
+        assertThat(deviceItem).isNotNull()
+        assertThat(deviceItem!!.type).isEqualTo(deviceItemType)
+        assertThat(deviceItem.cachedBluetoothDevice).isEqualTo(cachedDevice)
+        assertThat(deviceItem.deviceName).isEqualTo(DEVICE_NAME)
+        assertThat(deviceItem.connectionSummary).isEqualTo(CONNECTION_SUMMARY)
+    }
+
+    companion object {
+        const val DEVICE_NAME = "DeviceName"
+        const val CONNECTION_SUMMARY = "ConnectionSummary"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt
new file mode 100644
index 0000000..07a95ae
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.dialog.bluetooth
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.content.Context
+import android.media.AudioManager
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class DeviceItemInteractorTest : SysuiTestCase() {
+
+    @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Mock private lateinit var bluetoothTileDialogRepository: BluetoothTileDialogRepository
+
+    @Mock private lateinit var cachedDevice1: CachedBluetoothDevice
+
+    @Mock private lateinit var cachedDevice2: CachedBluetoothDevice
+
+    @Mock private lateinit var device1: BluetoothDevice
+
+    @Mock private lateinit var device2: BluetoothDevice
+
+    @Mock private lateinit var deviceItem1: DeviceItem
+
+    @Mock private lateinit var deviceItem2: DeviceItem
+
+    @Mock private lateinit var audioManager: AudioManager
+
+    @Mock private lateinit var adapter: BluetoothAdapter
+
+    @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
+
+    @Mock private lateinit var uiEventLogger: UiEventLogger
+
+    private lateinit var interactor: DeviceItemInteractor
+
+    private lateinit var dispatcher: CoroutineDispatcher
+
+    private lateinit var testScope: TestScope
+
+    @Before
+    fun setUp() {
+        dispatcher = StandardTestDispatcher()
+        testScope = TestScope(dispatcher)
+        interactor =
+            DeviceItemInteractor(
+                bluetoothTileDialogRepository,
+                audioManager,
+                adapter,
+                localBluetoothManager,
+                uiEventLogger,
+                testScope.backgroundScope,
+                dispatcher
+            )
+
+        `when`(deviceItem1.cachedBluetoothDevice).thenReturn(cachedDevice1)
+        `when`(deviceItem2.cachedBluetoothDevice).thenReturn(cachedDevice2)
+        `when`(cachedDevice1.device).thenReturn(device1)
+        `when`(cachedDevice2.device).thenReturn(device2)
+        `when`(bluetoothTileDialogRepository.cachedDevices)
+            .thenReturn(listOf(cachedDevice1, cachedDevice2))
+    }
+
+    @Test
+    fun testUpdateDeviceItems_noCachedDevice_returnEmpty() {
+        testScope.runTest {
+            `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(emptyList())
+            interactor.setDeviceItemFactoryListForTesting(
+                listOf(createFactory({ true }, deviceItem1))
+            )
+
+            interactor.updateDeviceItems(mContext)
+
+            assertThat(interactor.deviceItemFlow.value).isEmpty()
+        }
+    }
+
+    @Test
+    fun testUpdateDeviceItems_hasCachedDevice_filterNotMatch_returnEmpty() {
+        testScope.runTest {
+            `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(listOf(cachedDevice1))
+            interactor.setDeviceItemFactoryListForTesting(
+                listOf(createFactory({ false }, deviceItem1))
+            )
+
+            interactor.updateDeviceItems(mContext)
+
+            assertThat(interactor.deviceItemFlow.value).isEmpty()
+        }
+    }
+
+    @Test
+    fun testUpdateDeviceItems_hasCachedDevice_filterMatch_returnDeviceItem() {
+        testScope.runTest {
+            `when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(listOf(cachedDevice1))
+            interactor.setDeviceItemFactoryListForTesting(
+                listOf(createFactory({ true }, deviceItem1))
+            )
+
+            interactor.updateDeviceItems(mContext)
+
+            assertThat(interactor.deviceItemFlow.value).hasSize(1)
+            assertThat(interactor.deviceItemFlow.value!![0]).isEqualTo(deviceItem1)
+        }
+    }
+
+    @Test
+    fun testUpdateDeviceItems_hasCachedDevice_filterMatch_returnMultipleDeviceItem() {
+        testScope.runTest {
+            `when`(adapter.mostRecentlyConnectedDevices).thenReturn(null)
+            interactor.setDeviceItemFactoryListForTesting(
+                listOf(createFactory({ false }, deviceItem1), createFactory({ true }, deviceItem2))
+            )
+
+            interactor.updateDeviceItems(mContext)
+
+            assertThat(interactor.deviceItemFlow.value).hasSize(2)
+            assertThat(interactor.deviceItemFlow.value!![0]).isEqualTo(deviceItem2)
+            assertThat(interactor.deviceItemFlow.value!![1]).isEqualTo(deviceItem2)
+        }
+    }
+
+    @Test
+    fun testUpdateDeviceItems_sortByDisplayPriority() {
+        testScope.runTest {
+            `when`(adapter.mostRecentlyConnectedDevices).thenReturn(null)
+            interactor.setDeviceItemFactoryListForTesting(
+                listOf(
+                    createFactory({ cachedDevice -> cachedDevice.device == device1 }, deviceItem1),
+                    createFactory({ cachedDevice -> cachedDevice.device == device2 }, deviceItem2)
+                )
+            )
+            interactor.setDisplayPriorityForTesting(
+                listOf(
+                    DeviceItemType.SAVED_BLUETOOTH_DEVICE,
+                    DeviceItemType.CONNECTED_BLUETOOTH_DEVICE
+                )
+            )
+            `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+            `when`(deviceItem2.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE)
+
+            interactor.updateDeviceItems(mContext)
+
+            assertThat(interactor.deviceItemFlow.value).isEqualTo(listOf(deviceItem2, deviceItem1))
+        }
+    }
+
+    @Test
+    fun testUpdateDeviceItems_sameType_sortByRecentlyConnected() {
+        testScope.runTest {
+            `when`(adapter.mostRecentlyConnectedDevices).thenReturn(listOf(device2, device1))
+            interactor.setDeviceItemFactoryListForTesting(
+                listOf(
+                    createFactory({ cachedDevice -> cachedDevice.device == device1 }, deviceItem1),
+                    createFactory({ cachedDevice -> cachedDevice.device == device2 }, deviceItem2)
+                )
+            )
+            interactor.setDisplayPriorityForTesting(
+                listOf(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+            )
+            `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+            `when`(deviceItem2.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+
+            interactor.updateDeviceItems(mContext)
+
+            assertThat(interactor.deviceItemFlow.value).isEqualTo(listOf(deviceItem2, deviceItem1))
+        }
+    }
+
+    private fun createFactory(
+        isFilterMatchFunc: (CachedBluetoothDevice) -> Boolean,
+        deviceItem: DeviceItem
+    ): DeviceItemFactory {
+        return object : DeviceItemFactory() {
+            override fun isFilterMatched(
+                cachedDevice: CachedBluetoothDevice,
+                audioManager: AudioManager?
+            ) = isFilterMatchFunc(cachedDevice)
+
+            override fun create(context: Context, cachedDevice: CachedBluetoothDevice) = deviceItem
+        }
+    }
+}
diff --git a/services/Android.bp b/services/Android.bp
index 9264172..f237095 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -34,17 +34,18 @@
     },
 }
 
-// Opt-in config for optimizing and shrinking the services target using R8.
-// Enabled via `export SYSTEM_OPTIMIZE_JAVA=true`, or explicitly in Make via the
+// Config to control optimizing and shrinking the services target using R8.
+// Set via `export SYSTEM_OPTIMIZE_JAVA=true|false`, or explicitly in Make via the
 // `SOONG_CONFIG_ANDROID_SYSTEM_OPTIMIZE_JAVA` variable.
-// TODO(b/196084106): Enable optimizations by default after stabilizing and
-// building out retrace infrastructure.
 soong_config_module_type {
     name: "system_optimized_java_defaults",
     module_type: "java_defaults",
     config_namespace: "ANDROID",
     bool_variables: ["SYSTEM_OPTIMIZE_JAVA"],
-    properties: ["optimize"],
+    properties: [
+        "optimize",
+        "dxflags",
+    ],
 }
 
 system_optimized_java_defaults {
@@ -75,6 +76,9 @@
                     // permission subpackage to prune unused jarjar'ed Kotlin dependencies.
                     proguard_flags_files: ["proguard_permission.flags"],
                 },
+                // Explicitly configure R8 to preserve debug info, as this path should
+                // really only allow stripping of permission-specific code and deps.
+                dxflags: ["--debug"],
             },
         },
     },
diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
index 102c262..a3ccb16 100644
--- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
+++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
@@ -133,6 +133,7 @@
 
     @GuardedBy("mGenericWindowPolicyControllerLock")
     private boolean mShowTasksInHostDeviceRecents;
+    @Nullable private final ComponentName mCustomHomeComponent;
 
     /**
      * Creates a window policy controller that is generic to the different use cases of virtual
@@ -157,6 +158,10 @@
      * @param intentListenerCallback Callback that is called to intercept intents when matching
      *   passed in filters.
      * @param showTasksInHostDeviceRecents whether to show activities in recents on the host device.
+     * @param customHomeComponent The component acting as a home activity on the virtual display. If
+     *   {@code null}, then the system-default secondary home activity will be used. This is only
+     *   applicable to displays that support home activities, i.e. they're created with the relevant
+     *   virtual display flag.
      */
     public GenericWindowPolicyController(
             int windowFlags,
@@ -172,7 +177,8 @@
             @Nullable SecureWindowCallback secureWindowCallback,
             @Nullable IntentListenerCallback intentListenerCallback,
             @NonNull Set<String> displayCategories,
-            boolean showTasksInHostDeviceRecents) {
+            boolean showTasksInHostDeviceRecents,
+            @Nullable ComponentName customHomeComponent) {
         super();
         mAllowedUsers = allowedUsers;
         mActivityLaunchAllowedByDefault = activityLaunchAllowedByDefault;
@@ -187,6 +193,7 @@
         mIntentListenerCallback = intentListenerCallback;
         mDisplayCategories = displayCategories;
         mShowTasksInHostDeviceRecents = showTasksInHostDeviceRecents;
+        mCustomHomeComponent = customHomeComponent;
     }
 
     /**
@@ -384,6 +391,11 @@
         return false;
     }
 
+    @Override
+    public @Nullable ComponentName getCustomHomeComponent() {
+        return mCustomHomeComponent;
+    }
+
     /**
      * Returns true if an app with the given UID has an activity running on the virtual display for
      * this controller.
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index f328b22..203a152 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -53,7 +53,7 @@
 import android.companion.virtual.sensor.VirtualSensor;
 import android.companion.virtual.sensor.VirtualSensorEvent;
 import android.compat.annotation.ChangeId;
-import android.compat.annotation.EnabledSince;
+import android.compat.annotation.EnabledAfter;
 import android.content.AttributionSource;
 import android.content.ComponentName;
 import android.content.Context;
@@ -131,7 +131,7 @@
      * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT
      */
     @ChangeId
-    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public static final long MAKE_VIRTUAL_DISPLAY_FLAGS_CONSISTENT_WITH_DISPLAY_MANAGER =
             294837146L;
 
@@ -938,6 +938,8 @@
                 mParams.getDefaultNavigationPolicy() == NAVIGATION_POLICY_DEFAULT_ALLOWED;
         final boolean showTasksInHostDeviceRecents =
                 getDevicePolicy(POLICY_TYPE_RECENTS) == DEVICE_POLICY_DEFAULT;
+        final ComponentName homeComponent =
+                Flags.vdmCustomHome() ? mParams.getHomeComponent() : null;
 
         final GenericWindowPolicyController gwpc = new GenericWindowPolicyController(
                 FLAG_SECURE,
@@ -955,7 +957,8 @@
                 this::onSecureWindowShown,
                 this::shouldInterceptIntent,
                 displayCategories,
-                showTasksInHostDeviceRecents);
+                showTasksInHostDeviceRecents,
+                homeComponent);
         gwpc.registerRunningAppsChangedListener(/* listener= */ this);
         return gwpc;
     }
diff --git a/services/core/java/com/android/server/display/ColorFade.java b/services/core/java/com/android/server/display/ColorFade.java
index cd867f6..2e5f2dc 100644
--- a/services/core/java/com/android/server/display/ColorFade.java
+++ b/services/core/java/com/android/server/display/ColorFade.java
@@ -408,12 +408,6 @@
         }
     }
 
-    void stop() {
-        if (mEglContext != null && mEglDisplay != null) {
-            EGL14.eglDestroyContext(mEglDisplay, mEglContext);
-        }
-    }
-
     /**
      * Draws an animation frame showing the color fade activated at the
      * specified level.
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index bc81491..83f4df9 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -3577,8 +3577,7 @@
 
         DisplayPowerState getDisplayPowerState(DisplayBlanker blanker, ColorFade colorFade,
                 int displayId, int displayState) {
-            return new DisplayPowerState(blanker, colorFade, displayId, displayState,
-                    new Handler(/*async=*/ true));
+            return new DisplayPowerState(blanker, colorFade, displayId, displayState);
         }
 
         DualRampAnimator<DisplayPowerState> getDualRampAnimator(DisplayPowerState dps,
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 90a8490..b0d293a 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -324,6 +324,8 @@
     // Must only be accessed on the handler thread.
     private DisplayPowerState mPowerState;
 
+
+
     // The currently active screen on unblocker.  This field is non-null whenever
     // we are waiting for a callback to release it and unblock the screen.
     private ScreenOnUnblocker mPendingScreenOnUnblocker;
@@ -2914,8 +2916,7 @@
 
         DisplayPowerState getDisplayPowerState(DisplayBlanker blanker, ColorFade colorFade,
                 int displayId, int displayState) {
-            return new DisplayPowerState(blanker, colorFade, displayId, displayState,
-                    new Handler(/*async=*/ true));
+            return new DisplayPowerState(blanker, colorFade, displayId, displayState);
         }
 
         DualRampAnimator<DisplayPowerState> getDualRampAnimator(DisplayPowerState dps,
diff --git a/services/core/java/com/android/server/display/DisplayPowerState.java b/services/core/java/com/android/server/display/DisplayPowerState.java
index 85c6a6d..2c257a1 100644
--- a/services/core/java/com/android/server/display/DisplayPowerState.java
+++ b/services/core/java/com/android/server/display/DisplayPowerState.java
@@ -74,9 +74,8 @@
     private volatile boolean mStopped;
 
     DisplayPowerState(
-            DisplayBlanker blanker, ColorFade colorFade, int displayId, int displayState,
-            Handler handler) {
-        mHandler = handler;
+            DisplayBlanker blanker, ColorFade colorFade, int displayId, int displayState) {
+        mHandler = new Handler(true /*async*/);
         mChoreographer = Choreographer.getInstance();
         mBlanker = blanker;
         mColorFade = colorFade;
@@ -318,7 +317,6 @@
         mStopped = true;
         mPhotonicModulator.interrupt();
         dismissColorFade();
-        stopColorFade();
         mCleanListener = null;
         mHandler.removeCallbacksAndMessages(null);
     }
@@ -378,11 +376,6 @@
         }
     }
 
-    // Clears up color fade resources.
-    private void stopColorFade() {
-        if (mColorFade != null) mColorFade.stop();
-    }
-
     private final Runnable mScreenUpdateRunnable = new Runnable() {
         @Override
         public void run() {
diff --git a/services/core/java/com/android/server/display/mode/VotesStorage.java b/services/core/java/com/android/server/display/mode/VotesStorage.java
index dadcebe..bdd2ab7 100644
--- a/services/core/java/com/android/server/display/mode/VotesStorage.java
+++ b/services/core/java/com/android/server/display/mode/VotesStorage.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.os.Trace;
 import android.util.Slog;
 import android.util.SparseArray;
 
@@ -104,6 +105,9 @@
                 votes.remove(priority);
             }
         }
+        Trace.traceCounter(Trace.TRACE_TAG_POWER,
+                TAG + "." + displayId + ":" + Vote.priorityToString(priority),
+                getMaxPhysicalRefreshRate(vote));
         if (mLoggingEnabled) {
             Slog.i(TAG, "Updated votes for display=" + displayId + " votes=" + votes);
         }
@@ -146,6 +150,15 @@
         }
     }
 
+    private int getMaxPhysicalRefreshRate(@Nullable Vote vote) {
+        if (vote == null) {
+            return -1;
+        } else if (vote.refreshRateRanges.physical.max == Float.POSITIVE_INFINITY) {
+            return 1000; // for visualisation, otherwise e.g. -1 -> 60 will be unnoticeable
+        }
+        return (int) vote.refreshRateRanges.physical.max;
+    }
+
     interface Listener {
         void onChanged();
     }
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
index 7045e65..c01bc20 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
@@ -868,6 +868,28 @@
     }
 
     @ServiceThreadOnly
+    void removeUnusedLocalDevices(ArrayList<HdmiCecLocalDevice> allocatedDevices) {
+        ArrayList<Integer> deviceTypesToRemove = new ArrayList<>();
+        for (int i = 0; i < mLocalDevices.size(); i++) {
+            int deviceType = mLocalDevices.keyAt(i);
+            boolean shouldRemoveLocalDevice = allocatedDevices.stream().noneMatch(
+                    localDevice -> localDevice.getDeviceInfo() != null
+                    && localDevice.getDeviceInfo().getDeviceType() == deviceType);
+            if (shouldRemoveLocalDevice) {
+                deviceTypesToRemove.add(deviceType);
+            }
+        }
+        for (Integer deviceType : deviceTypesToRemove) {
+            mLocalDevices.remove(deviceType);
+        }
+    }
+
+    @ServiceThreadOnly
+    void removeLocalDeviceWithType(int deviceType) {
+        mLocalDevices.remove(deviceType);
+    }
+
+    @ServiceThreadOnly
     public void clearDeviceList() {
         assertRunOnServiceThread();
         for (HdmiDeviceInfo info : HdmiUtils.sparseArrayToList(mDeviceInfos)) {
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 232bc47..429db5e 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -1313,9 +1313,6 @@
             localDevice.init();
             localDevices.add(localDevice);
         }
-        // It's now safe to flush existing local devices from mCecController since they were
-        // already moved to 'localDevices'.
-        clearCecLocalDevices();
         mHdmiCecNetwork.clearDeviceList();
         allocateLogicalAddress(localDevices, initiatedBy);
     }
@@ -1344,6 +1341,7 @@
                             if (logicalAddress == Constants.ADDR_UNREGISTERED) {
                                 Slog.e(TAG, "Failed to allocate address:[device_type:" + deviceType
                                         + "]");
+                                mHdmiCecNetwork.removeLocalDeviceWithType(deviceType);
                             } else {
                                 // Set POWER_STATUS_ON to all local devices because they share
                                 // lifetime
@@ -1352,6 +1350,8 @@
                                         deviceType,
                                         HdmiControlManager.POWER_STATUS_ON, getCecVersion());
                                 localDevice.setDeviceInfo(deviceInfo);
+                                // If a local device of the same type already exists, it will be
+                                // replaced.
                                 mHdmiCecNetwork.addLocalDevice(deviceType, localDevice);
                                 mHdmiCecNetwork.addCecDevice(localDevice.getDeviceInfo());
                                 mCecController.addLogicalAddress(logicalAddress);
@@ -1367,6 +1367,10 @@
                                     // since we reallocate the logical address only.
                                     onInitializeCecComplete(initiatedBy);
                                 }
+                                // We remove local devices here, instead of before the start of
+                                // address allocation, to prevent multiple local devices of the
+                                // same type from existing simultaneously.
+                                mHdmiCecNetwork.removeUnusedLocalDevices(allocatedDevices);
                                 mAddressAllocated = true;
                                 notifyAddressAllocated(allocatedDevices, initiatedBy);
                                 // Reinvoke the saved display status callback once the local
@@ -1386,9 +1390,19 @@
         }
     }
 
+    /**
+     * Notifies local devices that address allocation finished.
+     * @param devices - list of local devices allocated.
+     * @param initiatedBy - reason for the address allocation.
+     */
+    @VisibleForTesting
     @ServiceThreadOnly
-    private void notifyAddressAllocated(ArrayList<HdmiCecLocalDevice> devices, int initiatedBy) {
+    public void notifyAddressAllocated(ArrayList<HdmiCecLocalDevice> devices, int initiatedBy) {
         assertRunOnServiceThread();
+        if (devices == null || devices.isEmpty()) {
+            Slog.w(TAG, "No local device to notify.");
+            return;
+        }
         List<HdmiCecMessage> bufferedMessages = mCecMessageBuffer.getBuffer();
         for (HdmiCecLocalDevice device : devices) {
             int address = device.getDeviceInfo().getLogicalAddress();
diff --git a/services/core/java/com/android/server/notification/TEST_MAPPING b/services/core/java/com/android/server/notification/TEST_MAPPING
index 59b2bc1..7db2e8b 100644
--- a/services/core/java/com/android/server/notification/TEST_MAPPING
+++ b/services/core/java/com/android/server/notification/TEST_MAPPING
@@ -13,7 +13,7 @@
           "exclude-annotation": "org.junit.Ignore"
         },
         {
-          "exclude-annotation": "android.platform.test.annotations.LargeTest"
+          "exclude-annotation": "androidx.test.filters.LargeTest"
         },
         {
           "exclude-annotation": "androidx.test.filters.LargeTest"
@@ -33,7 +33,7 @@
           "exclude-annotation": "org.junit.Ignore"
         },
         {
-          "exclude-annotation": "android.platform.test.annotations.LargeTest"
+          "exclude-annotation": "androidx.test.filters.LargeTest"
         },
         {
           "exclude-annotation": "androidx.test.filters.LargeTest"
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 700fae9..33cb85c 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -4681,9 +4681,12 @@
             @UserIdInt int userId, @NonNull String recentCallingPackage,
             @NonNull String debugInfo) {
         synchronized (mLock) {
-            final PackageUserStateInternal userState = mSettings.getPackageLPr(
-                    packageName).getUserStateOrDefault(userId);
-            if (userState.isQuarantined()) {
+            final PackageSetting pkgSetting = mSettings.getPackageLPr(packageName);
+            // If the package doesn't exist, don't need to proceed to setPackageStoppedState.
+            if (pkgSetting == null) {
+                return;
+            }
+            if (pkgSetting.getUserStateOrDefault(userId).isQuarantined()) {
                 Slog.i(TAG,
                         "Component is quarantined+suspended but being used: "
                                 + packageName + " by " + recentCallingPackage + ", debugInfo: "
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 097656c..dc75a98 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -1983,6 +1983,7 @@
             public void run() {
                 if (mPendingHomeKeyEvent != null) {
                     handleShortPressOnHome(mPendingHomeKeyEvent);
+                    mPendingHomeKeyEvent.recycle();
                     mPendingHomeKeyEvent = null;
                 }
             }
@@ -2027,7 +2028,7 @@
                     if (mDoubleTapOnHomeBehavior != DOUBLE_TAP_HOME_PIP_MENU
                             || mPictureInPictureVisible) {
                         mHandler.removeCallbacks(mHomeDoubleTapTimeoutRunnable); // just in case
-                        mPendingHomeKeyEvent = event;
+                        mPendingHomeKeyEvent = KeyEvent.obtain(event);
                         mHandler.postDelayed(mHomeDoubleTapTimeoutRunnable,
                                 ViewConfiguration.getDoubleTapTimeout());
                         return true;
@@ -2035,7 +2036,11 @@
                 }
 
                 // Post to main thread to avoid blocking input pipeline.
-                mHandler.post(() -> handleShortPressOnHome(event));
+                final KeyEvent shortPressEvent = KeyEvent.obtain(event);
+                mHandler.post(() -> {
+                    handleShortPressOnHome(shortPressEvent);
+                    shortPressEvent.recycle();
+                });
                 return true;
             }
 
@@ -2062,9 +2067,14 @@
             if (repeatCount == 0) {
                 mHomePressed = true;
                 if (mPendingHomeKeyEvent != null) {
+                    mPendingHomeKeyEvent.recycle();
                     mPendingHomeKeyEvent = null;
                     mHandler.removeCallbacks(mHomeDoubleTapTimeoutRunnable);
-                    mHandler.post(() -> handleDoubleTapOnHome(event));
+                    final KeyEvent doublePressEvent = KeyEvent.obtain(event);
+                    mHandler.post(() -> {
+                        handleDoubleTapOnHome(doublePressEvent);
+                        doublePressEvent.recycle();
+                    });
                 // TODO(multi-display): Remove display id check once we support recents on
                 // multi-display
                 } else if (mDoubleTapOnHomeBehavior == DOUBLE_TAP_HOME_RECENT_SYSTEM_UI
@@ -2074,7 +2084,11 @@
             } else if ((event.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0) {
                 if (!keyguardOn) {
                     // Post to main thread to avoid blocking input pipeline.
-                    mHandler.post(() -> handleLongPressOnHome(event));
+                    final KeyEvent longPressEvent = KeyEvent.obtain(event);
+                    mHandler.post(() -> {
+                        handleLongPressOnHome(longPressEvent);
+                        longPressEvent.recycle();
+                    });
                 }
             }
             return true;
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index d824534..a0c7870 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2887,7 +2887,7 @@
         final boolean animate;
         if (mStartingData != null) {
             if (mStartingData.mWaitForSyncTransactionCommit
-                    || mTransitionController.inCollectingTransition(startingWindow)) {
+                    || mTransitionController.isCollecting(this)) {
                 mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_REMOVE_DIRECTLY;
                 mStartingData.mPrepareRemoveAnimation = prepareAnimation;
                 return;
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 1ad5798..901975b 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -795,10 +795,19 @@
             return false;
         }
 
-        // Try pausing the existing resumed activity in the Task if any.
         final Task task = r.getTask();
-        if (task.pauseActivityIfNeeded(r, "realStart")) {
-            return false;
+        if (andResume) {
+            // Try pausing the existing resumed activity in the Task if any.
+            if (task.pauseActivityIfNeeded(r, "realStart")) {
+                return false;
+            }
+            final TaskFragment taskFragment = r.getTaskFragment();
+            if (taskFragment != null && taskFragment.getResumedActivity() != null) {
+                if (taskFragment.startPausing(mUserLeaving, false /* uiSleeping */, r,
+                        "realStart")) {
+                    return false;
+                }
+            }
         }
 
         final Task rootTask = task.getRootTask();
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 4309e72..ca42400 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -2160,6 +2160,16 @@
     }
 
     /**
+     * @see DisplayWindowPolicyController#getCustomHomeComponent() ()
+     */
+    @Nullable ComponentName getCustomHomeComponent() {
+        if (!supportsSystemDecorations() || mDwpcHelper == null) {
+            return null;
+        }
+        return mDwpcHelper.getCustomHomeComponent();
+    }
+
+    /**
      * Applies the rotation transaction. This must be called after {@link #updateRotationUnchecked}
      * (if it returned {@code true}) to actually finish the rotation.
      *
diff --git a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
index 6b33746..e0d69b0 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.WindowConfiguration;
+import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.os.Process;
@@ -200,6 +201,17 @@
         return mDisplayWindowPolicyController.isEnteringPipAllowed(uid);
     }
 
+    /**
+     * @see DisplayWindowPolicyController#getCustomHomeComponent
+     */
+    @Nullable
+    public ComponentName getCustomHomeComponent() {
+        if (mDisplayWindowPolicyController == null) {
+            return null;
+        }
+        return mDisplayWindowPolicyController.getCustomHomeComponent();
+    }
+
     void dump(String prefix, PrintWriter pw) {
         if (mDisplayWindowPolicyController != null) {
             pw.println();
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 2fdfec0..2a33918 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -1513,10 +1513,30 @@
             throw new IllegalArgumentException(
                     "resolveSecondaryHomeActivity: Should not be default task container");
         }
-        // Resolve activities in the same package as currently selected primary home activity.
+
         Intent homeIntent = mService.getHomeIntent();
         ActivityInfo aInfo = resolveHomeActivity(userId, homeIntent);
-        if (aInfo != null) {
+        boolean lookForSecondaryHomeActivityInPrimaryHomePackage = aInfo != null;
+
+        if (android.companion.virtual.flags.Flags.vdmCustomHome()) {
+            // Resolve the externally set home activity for this display, if any. If it is unset or
+            // we fail to resolve it, fallback to the default secondary home activity.
+            final ComponentName customHomeComponent =
+                    taskDisplayArea.getDisplayContent() != null
+                            ? taskDisplayArea.getDisplayContent().getCustomHomeComponent()
+                            : null;
+            if (customHomeComponent != null) {
+                homeIntent.setComponent(customHomeComponent);
+                ActivityInfo customHomeActivityInfo = resolveHomeActivity(userId, homeIntent);
+                if (customHomeActivityInfo != null) {
+                    aInfo = customHomeActivityInfo;
+                    lookForSecondaryHomeActivityInPrimaryHomePackage = false;
+                }
+            }
+        }
+
+        if (lookForSecondaryHomeActivityInPrimaryHomePackage) {
+            // Resolve activities in the same package as currently selected primary home activity.
             if (ResolverActivity.class.getName().equals(aInfo.name)) {
                 // Always fallback to secondary home component if default home is not set.
                 aInfo = null;
diff --git a/services/core/java/com/android/server/wm/StartingData.java b/services/core/java/com/android/server/wm/StartingData.java
index 2d281c4..07ffa69e 100644
--- a/services/core/java/com/android/server/wm/StartingData.java
+++ b/services/core/java/com/android/server/wm/StartingData.java
@@ -108,4 +108,13 @@
     boolean hasImeSurface() {
         return false;
     }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "{"
+                + Integer.toHexString(System.identityHashCode(this))
+                + " waitForSyncTransactionCommit=" + mWaitForSyncTransactionCommit
+                + " removeAfterTransaction= " + mRemoveAfterTransaction
+                + "}";
+    }
 }
diff --git a/services/core/jni/TEST_MAPPING b/services/core/jni/TEST_MAPPING
index ea44d06..eb9db70 100644
--- a/services/core/jni/TEST_MAPPING
+++ b/services/core/jni/TEST_MAPPING
@@ -6,7 +6,7 @@
       ],
       "name": "CtsVibratorTestCases",
       "options": [
-        {"exclude-annotation": "android.platform.test.annotations.LargeTest"},
+        {"exclude-annotation": "androidx.test.filters.LargeTest"},
         {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
         {"exclude-annotation": "androidx.test.filters.FlakyTest"},
         {"exclude-annotation": "org.junit.Ignore"}
diff --git a/services/devicepolicy/TEST_MAPPING b/services/devicepolicy/TEST_MAPPING
index 72bba11..fccd1ec 100644
--- a/services/devicepolicy/TEST_MAPPING
+++ b/services/devicepolicy/TEST_MAPPING
@@ -7,7 +7,7 @@
           "exclude-annotation": "android.platform.test.annotations.FlakyTest"
         },
         {
-          "exclude-annotation": "android.platform.test.annotations.LargeTest"
+          "exclude-annotation": "androidx.test.filters.LargeTest"
         }
       ]
     }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 84d1a45..f604932 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -16017,16 +16017,20 @@
     }
 
     @Override
-    public PackagePolicy getCredentialManagerPolicy() {
+    public PackagePolicy getCredentialManagerPolicy(int userId) {
         if (!mHasFeature) {
             return null;
         }
         final CallerIdentity caller = getCallerIdentity();
         Preconditions.checkCallAuthorization(
                 canWriteCredentialManagerPolicy(caller) || canQueryAdminPolicy(caller));
+        if (userId != caller.getUserId()) {
+            Preconditions.checkCallAuthorization(
+                    hasCallingOrSelfPermission(permission.INTERACT_ACROSS_USERS));
+        }
 
         synchronized (getLockObject()) {
-            ActiveAdmin admin = getProfileOwnerOrDeviceOwnerLocked(caller.getUserId());
+            ActiveAdmin admin = getProfileOwnerOrDeviceOwnerLocked(userId);
             return (admin != null) ? admin.mCredentialManagerPolicy : null;
         }
     }
diff --git a/services/foldables/devicestateprovider/OWNERS b/services/foldables/devicestateprovider/OWNERS
new file mode 100644
index 0000000..b2dcd0c
--- /dev/null
+++ b/services/foldables/devicestateprovider/OWNERS
@@ -0,0 +1,6 @@
+akulian@google.com
+kennethford@google.com
+jiamingliu@google.com
+kchyn@google.com
+nickchameyev@google.com
+nicomazz@google.com
\ No newline at end of file
diff --git a/services/incremental/TEST_MAPPING b/services/incremental/TEST_MAPPING
index 4af880d..4c9403c 100644
--- a/services/incremental/TEST_MAPPING
+++ b/services/incremental/TEST_MAPPING
@@ -12,7 +12,7 @@
       "name": "CtsPackageManagerIncrementalStatsHostTestCases",
       "options": [
         {
-          "exclude-annotation": "android.platform.test.annotations.LargeTest"
+          "exclude-annotation": "androidx.test.filters.LargeTest"
         }
       ]
     },
@@ -55,7 +55,7 @@
       "name": "CtsPackageManagerIncrementalStatsHostTestCases",
       "options": [
         {
-          "include-annotation": "android.platform.test.annotations.LargeTest"
+          "include-annotation": "androidx.test.filters.LargeTest"
         }
       ]
     }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
index 9174899..a56b59a 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
@@ -1255,21 +1255,6 @@
     }
 
     @Test
-    public void testPowerStateStopsOnDpcStop() {
-        // Set up
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1);
-
-        // Stop dpc
-        mHolder.dpc.stop();
-        advanceTime(1);
-
-        // Ensure dps has stopped
-        verify(mHolder.displayPowerState, times(1)).stop();
-    }
-
-    @Test
     public void testRampRateForHdrContent_HdrClamperOff() {
         float hdrBrightness = 0.8f;
         float clampedBrightness = 0.6f;
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 412b65f..0572117 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -1191,21 +1191,6 @@
     }
 
     @Test
-    public void testPowerStateStopsOnDpcStop() {
-        // Set up
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1);
-
-        // Stop dpc
-        mHolder.dpc.stop();
-        advanceTime(1);
-
-        // Ensure dps has stopped
-        verify(mHolder.displayPowerState, times(1)).stop();
-    }
-
-    @Test
     public void testDisplayBrightnessHdr_SkipAnimationOnHdrAppearance() {
         Settings.System.putInt(mContext.getContentResolver(),
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.java
deleted file mode 100644
index 167a412..0000000
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerStateTest.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.display;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-
-import static org.mockito.Mockito.times;
-
-import android.os.Handler;
-import android.os.test.TestLooper;
-import android.view.Display;
-
-import androidx.test.filters.SmallTest;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-
-@SmallTest
-public class DisplayPowerStateTest {
-    private static final int DISPLAY_ID = 123;
-
-    private DisplayPowerState mDisplayPowerState;
-    private TestLooper mTestLooper;
-    @Mock
-    private DisplayBlanker mDisplayBlankerMock;
-    @Mock
-    private ColorFade mColorFadeMock;
-
-    @Rule
-    public final MockitoRule mMockitoRule = MockitoJUnit.rule();
-
-    @Before
-    public void setUp() {
-        mTestLooper = new TestLooper();
-        mDisplayPowerState = new DisplayPowerState(
-                mDisplayBlankerMock, mColorFadeMock, DISPLAY_ID, Display.STATE_ON,
-                new Handler(mTestLooper.getLooper()));
-    }
-
-    @Test
-    public void testColorFadeStopsOnDpsStop() {
-        mDisplayPowerState.stop();
-        verify(mColorFadeMock, times(1)).stop();
-    }
-}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index 552b59c..a250ac7 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -33,7 +33,6 @@
 import static com.android.server.job.JobSchedulerService.WORKING_INDEX;
 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
 import static com.android.server.job.JobSchedulerService.sSystemClock;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
@@ -75,11 +74,11 @@
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.SystemClock;
-import android.platform.test.annotations.LargeTest;
 import android.provider.DeviceConfig;
 import android.util.ArraySet;
 import android.util.SparseBooleanArray;
 
+import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.ArrayUtils;
@@ -2597,7 +2596,9 @@
     @Test
     public void testIsWithinEJQuotaLocked_TempAllowlisting_Restricted() {
         setDischarging();
-        JobStatus js = createExpeditedJobStatus("testIsWithinEJQuotaLocked_TempAllowlisting_Restricted", 1);
+        JobStatus js =
+                createExpeditedJobStatus(
+                        "testIsWithinEJQuotaLocked_TempAllowlisting_Restricted", 1);
         setStandbyBucket(RESTRICTED_INDEX, js);
         setDeviceConfigLong(QcConstants.KEY_EJ_LIMIT_FREQUENT_MS, 10 * MINUTE_IN_MILLIS);
         final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
@@ -6088,7 +6089,8 @@
         Handler handler = mQuotaController.getHandler();
         spyOn(handler);
 
-        JobStatus job = createExpeditedJobStatus("testEJTimerTracking_TempAllowlisting_Restricted", 1);
+        JobStatus job =
+                createExpeditedJobStatus("testEJTimerTracking_TempAllowlisting_Restricted", 1);
         setStandbyBucket(RESTRICTED_INDEX, job);
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeStartTrackingJobLocked(job, null);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationThumbnailTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationThumbnailTest.java
index 8faddf8..fcfe48e 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationThumbnailTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationThumbnailTest.java
@@ -17,7 +17,6 @@
 package com.android.server.accessibility.magnification;
 
 import static com.google.common.truth.Truth.assertThat;
-
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
@@ -27,13 +26,13 @@
 
 import android.graphics.Rect;
 import android.os.Handler;
-import android.platform.test.annotations.LargeTest;
 import android.testing.TestableContext;
 import android.view.WindowInsets;
 import android.view.WindowManager;
 import android.view.WindowMetrics;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Before;
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
index c40ad28..1c48b8a 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
@@ -88,7 +88,8 @@
                         /* secureWindowCallback= */ null,
                         /* intentListenerCallback= */ null,
                         /* displayCategories= */ new ArraySet<>(),
-                        /* showTasksInHostDeviceRecents= */ true);
+                        /* showTasksInHostDeviceRecents= */ true,
+                        /* customHomeComponent= */ null);
     }
 
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index 0d172fdb..708ee35 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -21,8 +21,15 @@
 
 import static com.android.server.SystemService.PHASE_BOOT_COMPLETED;
 import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
+import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
+import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
+import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_2;
+import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_3;
 import static com.android.server.hdmi.HdmiControlService.DEVICE_CLEANUP_TIMEOUT;
 import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
+import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_HOTPLUG;
+import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_SCREEN_ON;
+import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_SOUNDBAR_MODE;
 import static com.android.server.hdmi.HdmiControlService.WAKE_UP_SCREEN_ON;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -51,6 +58,7 @@
 import android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener;
 import android.hardware.hdmi.IHdmiControlStatusChangeListener;
 import android.hardware.hdmi.IHdmiVendorCommandListener;
+import android.hardware.tv.cec.V1_0.SendMessageResult;
 import android.os.Binder;
 import android.os.Looper;
 import android.os.RemoteException;
@@ -71,6 +79,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Optional;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Tests for {@link HdmiControlService} class.
@@ -137,7 +146,7 @@
 
         mLocalDevices.add(mAudioSystemDeviceSpy);
         mLocalDevices.add(mPlaybackDeviceSpy);
-        mHdmiPortInfo = new HdmiPortInfo[4];
+        mHdmiPortInfo = new HdmiPortInfo[5];
         mHdmiPortInfo[0] =
                 new HdmiPortInfo.Builder(1, HdmiPortInfo.PORT_INPUT, 0x2100)
                         .setCecSupported(true)
@@ -166,6 +175,13 @@
                         .setArcSupported(false)
                         .setEarcSupported(false)
                         .build();
+        mHdmiPortInfo[4] =
+                new HdmiPortInfo.Builder(4, HdmiPortInfo.PORT_OUTPUT, 0x3000)
+                        .setCecSupported(true)
+                        .setMhlSupported(false)
+                        .setArcSupported(false)
+                        .setEarcSupported(false)
+                        .build();
         mNativeWrapper.setPortInfo(mHdmiPortInfo);
         mHdmiControlServiceSpy.initService();
         mWakeLockSpy = spy(new FakePowerManagerWrapper.FakeWakeLockWrapper());
@@ -1396,6 +1412,207 @@
     }
 
     @Test
+    public void triggerMultipleAddressAllocations_uniqueLocalDevicePerDeviceType() {
+        long allocationDelay = TimeUnit.SECONDS.toMillis(60);
+        mHdmiCecController.setLogicalAddressAllocationDelay(allocationDelay);
+        mTestLooper.dispatchAll();
+        Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+        // Wake up process that will trigger the address allocation to start.
+        mHdmiControlServiceSpy.onWakeUp(HdmiControlService.WAKE_UP_SCREEN_ON);
+        verify(mHdmiControlServiceSpy, times(1))
+                .allocateLogicalAddress(any(), eq(INITIATED_BY_SCREEN_ON));
+        Mockito.clearInvocations(mHdmiControlServiceSpy);
+        mTestLooper.dispatchAll();
+
+        mTestLooper.moveTimeForward(allocationDelay / 2);
+        mTestLooper.dispatchAll();
+        // Hotplug In will trigger the address allocation to start.
+        mHdmiControlServiceSpy.onHotplug(4, true);
+        verify(mHdmiControlServiceSpy, times(1))
+                .allocateLogicalAddress(any(), eq(INITIATED_BY_HOTPLUG));
+        Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+        mTestLooper.moveTimeForward(allocationDelay / 2);
+        mTestLooper.dispatchAll();
+        // The first allocation finished. The second allocation is still in progress.
+        HdmiCecLocalDevicePlayback firstAllocatedPlayback = mHdmiControlServiceSpy.playback();
+        verify(mHdmiControlServiceSpy, times(1))
+                .notifyAddressAllocated(any(), eq(INITIATED_BY_SCREEN_ON));
+        Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+        mTestLooper.moveTimeForward(allocationDelay / 2);
+        mTestLooper.dispatchAll();
+        // The second allocation finished.
+        HdmiCecLocalDevicePlayback secondAllocatedPlayback = mHdmiControlServiceSpy.playback();
+        verify(mHdmiControlServiceSpy, times(1))
+                .notifyAddressAllocated(any(), eq(INITIATED_BY_HOTPLUG));
+        // Local devices have the same identity.
+        assertTrue(firstAllocatedPlayback == secondAllocatedPlayback);
+    }
+
+    @Test
+    public void triggerMultipleAddressAllocations_keepLastAllocatedAddress() {
+        // First logical address for playback is free.
+        mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_1, SendMessageResult.NACK);
+        mTestLooper.dispatchAll();
+
+        long allocationDelay = TimeUnit.SECONDS.toMillis(60);
+        mHdmiCecController.setLogicalAddressAllocationDelay(allocationDelay);
+        mTestLooper.dispatchAll();
+        Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+        // Wake up process that will trigger the address allocation to start.
+        mHdmiControlServiceSpy.onWakeUp(HdmiControlService.WAKE_UP_SCREEN_ON);
+        verify(mHdmiControlServiceSpy, times(1))
+                .allocateLogicalAddress(any(), eq(INITIATED_BY_SCREEN_ON));
+        Mockito.clearInvocations(mHdmiControlServiceSpy);
+        mTestLooper.dispatchAll();
+
+        mTestLooper.moveTimeForward(allocationDelay / 2);
+        mTestLooper.dispatchAll();
+
+        // First logical address for playback is busy.
+        mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_1, SendMessageResult.SUCCESS);
+        mTestLooper.dispatchAll();
+
+        mHdmiControlServiceSpy.onWakeUp(HdmiControlService.WAKE_UP_SCREEN_ON);
+        verify(mHdmiControlServiceSpy, times(1))
+                .allocateLogicalAddress(any(), eq(INITIATED_BY_SCREEN_ON));
+        Mockito.clearInvocations(mHdmiControlServiceSpy);
+        mTestLooper.dispatchAll();
+
+        mTestLooper.moveTimeForward(allocationDelay / 2);
+        mTestLooper.dispatchAll();
+        // The first allocation finished. The second allocation is still in progress.
+        verify(mHdmiControlServiceSpy, times(1))
+                .notifyAddressAllocated(any(), eq(INITIATED_BY_SCREEN_ON));
+        Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+        mTestLooper.moveTimeForward(allocationDelay / 2);
+        mTestLooper.dispatchAll();
+        // The second allocation finished. Second logical address for playback is used.
+        HdmiCecLocalDevicePlayback allocatedPlayback = mHdmiControlServiceSpy.playback();
+        verify(mHdmiControlServiceSpy, times(1))
+                .notifyAddressAllocated(any(), eq(INITIATED_BY_SCREEN_ON));
+        assertThat(allocatedPlayback.getDeviceInfo().getLogicalAddress())
+                .isEqualTo(ADDR_PLAYBACK_2);
+    }
+
+    @Test
+    public void triggerMultipleAddressAllocations_toggleSoundbarMode_addThenRemoveAudioSystem() {
+        mHdmiControlServiceSpy.setPowerStatus(HdmiControlManager.POWER_STATUS_ON);
+        long allocationDelay = TimeUnit.SECONDS.toMillis(60);
+        mHdmiCecController.setLogicalAddressAllocationDelay(allocationDelay);
+        mTestLooper.dispatchAll();
+        Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+        // Enabling Dynamic soundbar mode will trigger address allocation.
+        mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
+                HdmiControlManager.SOUNDBAR_MODE_ENABLED);
+        mTestLooper.dispatchAll();
+        verify(mHdmiControlServiceSpy, times(1))
+                .allocateLogicalAddress(any(), eq(INITIATED_BY_SOUNDBAR_MODE));
+        Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+        mTestLooper.moveTimeForward(allocationDelay / 2);
+        mTestLooper.dispatchAll();
+        // Disabling Dynamic soundbar mode will trigger another address allocation.
+        mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
+                HdmiControlManager.SOUNDBAR_MODE_DISABLED);
+        mTestLooper.dispatchAll();
+        verify(mHdmiControlServiceSpy, times(1))
+                .allocateLogicalAddress(any(), eq(INITIATED_BY_SOUNDBAR_MODE));
+        Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+        mTestLooper.moveTimeForward(allocationDelay / 2);
+        mTestLooper.dispatchAll();
+        // The first allocation finished. The second allocation is still in progress.
+        // The audio system is present in the network.
+        verify(mHdmiControlServiceSpy, times(1))
+                .notifyAddressAllocated(any(), eq(INITIATED_BY_SOUNDBAR_MODE));
+        assertThat(mHdmiControlServiceSpy.audioSystem()).isNotNull();
+        Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+        mTestLooper.moveTimeForward(allocationDelay / 2);
+        mTestLooper.dispatchAll();
+        // The second allocation finished. The audio system is not present in the network.
+        verify(mHdmiControlServiceSpy, times(1))
+                .notifyAddressAllocated(any(), eq(INITIATED_BY_SOUNDBAR_MODE));
+        assertThat(mHdmiControlServiceSpy.audioSystem()).isNull();
+    }
+
+    @Test
+    public void triggerMultipleAddressAllocations_toggleSoundbarMode_removeThenAddAudioSystem() {
+        mHdmiControlServiceSpy.setPowerStatus(HdmiControlManager.POWER_STATUS_ON);
+        // Enable the setting and check if the audio system local device is found in the network.
+        mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
+                HdmiControlManager.SOUNDBAR_MODE_ENABLED);
+        mTestLooper.dispatchAll();
+        assertThat(mHdmiControlServiceSpy.audioSystem()).isNotNull();
+
+        long allocationDelay = TimeUnit.SECONDS.toMillis(60);
+        mHdmiCecController.setLogicalAddressAllocationDelay(allocationDelay);
+        mTestLooper.dispatchAll();
+        Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+        // Disabling Dynamic soundbar mode will trigger address allocation.
+        mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
+                HdmiControlManager.SOUNDBAR_MODE_DISABLED);
+        mTestLooper.dispatchAll();
+        verify(mHdmiControlServiceSpy, times(1))
+                .allocateLogicalAddress(any(), eq(INITIATED_BY_SOUNDBAR_MODE));
+        Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+        mTestLooper.moveTimeForward(allocationDelay / 2);
+        mTestLooper.dispatchAll();
+        // Enabling Dynamic soundbar mode will trigger another address allocation.
+        mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
+                HdmiControlManager.SOUNDBAR_MODE_ENABLED);
+        mTestLooper.dispatchAll();
+        verify(mHdmiControlServiceSpy, times(1))
+                .allocateLogicalAddress(any(), eq(INITIATED_BY_SOUNDBAR_MODE));
+        Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+        mTestLooper.moveTimeForward(allocationDelay / 2);
+        mTestLooper.dispatchAll();
+        // The first allocation finished. The second allocation is still in progress.
+        // The audio system is not present in the network.
+        verify(mHdmiControlServiceSpy, times(1))
+                .notifyAddressAllocated(any(), eq(INITIATED_BY_SOUNDBAR_MODE));
+        assertThat(mHdmiControlServiceSpy.audioSystem()).isNull();
+        Mockito.clearInvocations(mHdmiControlServiceSpy);
+
+        mTestLooper.moveTimeForward(allocationDelay / 2);
+        mTestLooper.dispatchAll();
+        // The second allocation finished. The audio system is present in the network.
+        verify(mHdmiControlServiceSpy, times(1))
+                .notifyAddressAllocated(any(), eq(INITIATED_BY_SOUNDBAR_MODE));
+        assertThat(mHdmiControlServiceSpy.audioSystem()).isNotNull();
+    }
+
+    @Test
+    public void failedAddressAllocation_noLocalDevice() {
+        mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_1, SendMessageResult.SUCCESS);
+        mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_2, SendMessageResult.SUCCESS);
+        mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_3, SendMessageResult.SUCCESS);
+        mNativeWrapper.setPollAddressResponse(ADDR_AUDIO_SYSTEM, SendMessageResult.SUCCESS);
+        mTestLooper.dispatchAll();
+
+        mHdmiControlServiceSpy.clearCecLocalDevices();
+        mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        mTestLooper.dispatchAll();
+
+        assertThat(mHdmiControlServiceSpy.playback()).isNull();
+        assertThat(mHdmiControlServiceSpy.audioSystem()).isNull();
+    }
+
+    @Test
     public void earcIdle_blocksArcConnection() {
         mHdmiControlServiceSpy.clearEarcLocalDevice();
         HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
diff --git a/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java b/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java
index be13753..213e05e 100644
--- a/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java
@@ -18,7 +18,6 @@
 
 import static android.app.job.JobInfo.NETWORK_TYPE_ANY;
 import static android.app.job.JobInfo.NETWORK_TYPE_NONE;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -28,13 +27,14 @@
 
 import android.app.job.JobInfo;
 import android.content.ComponentName;
-import android.platform.test.annotations.LargeTest;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArrayMap;
 import android.util.SparseBooleanArray;
 import android.util.SparseLongArray;
 
+import androidx.test.filters.LargeTest;
+
 import com.android.server.job.controllers.JobStatus;
 
 import org.junit.Test;
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
index 8db09f9..61c4d06 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
@@ -46,7 +46,6 @@
 import static java.util.Collections.unmodifiableMap;
 
 import android.content.Context;
-import android.os.Looper;
 import android.os.SystemClock;
 import android.util.ArrayMap;
 import android.view.InputDevice;
@@ -99,10 +98,6 @@
      *      settings values.
      */
     protected final void setUpPhoneWindowManager(boolean supportSettingsUpdate) {
-        if (Looper.myLooper() == null) {
-            Looper.prepare();
-        }
-
         doReturn(mSettingsProviderRule.mockContentResolver(mContext))
                 .when(mContext).getContentResolver();
         mPhoneWindowManager = new TestPhoneWindowManager(mContext, supportSettingsUpdate);
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index ef28ffa..6e2c4bd 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -70,7 +70,6 @@
 import android.hardware.input.InputManager;
 import android.media.AudioManagerInternal;
 import android.os.Handler;
-import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.PowerManager;
 import android.os.PowerManagerInternal;
@@ -78,6 +77,7 @@
 import android.os.UserHandle;
 import android.os.Vibrator;
 import android.os.VibratorInfo;
+import android.os.test.TestLooper;
 import android.service.dreams.DreamManagerInternal;
 import android.telecom.TelecomManager;
 import android.util.FeatureFlagUtils;
@@ -160,8 +160,8 @@
     @Mock private KeyguardServiceDelegate mKeyguardServiceDelegate;
 
     private StaticMockitoSession mMockitoSession;
-    private HandlerThread mHandlerThread;
     private Handler mHandler;
+    private TestLooper mTestLooper;
 
     private class TestInjector extends PhoneWindowManager.Injector {
         TestInjector(Context context, WindowManagerPolicy.WindowManagerFuncs funcs) {
@@ -184,12 +184,11 @@
 
     TestPhoneWindowManager(Context context, boolean supportSettingsUpdate) {
         MockitoAnnotations.initMocks(this);
-        mHandlerThread = new HandlerThread("fake window manager");
-        mHandlerThread.start();
-        mHandler = new Handler(mHandlerThread.getLooper());
+        mTestLooper = new TestLooper();
+        mHandler = new Handler(mTestLooper.getLooper());
         mContext = mockingDetails(context).isSpy() ? context : spy(context);
-        mHandler.runWithScissors(() -> setUp(supportSettingsUpdate),  0 /* timeout */);
-        waitForIdle();
+        mHandler.post(() -> setUp(supportSettingsUpdate));
+        mTestLooper.dispatchAll();
     }
 
     private void setUp(boolean supportSettingsUpdate) {
@@ -301,7 +300,6 @@
     }
 
     void tearDown() {
-        mHandlerThread.quitSafely();
         LocalServices.removeServiceForTest(InputMethodManagerInternal.class);
         Mockito.reset(mPhoneWindowManager);
         mMockitoSession.finishMocking();
@@ -328,7 +326,7 @@
     }
 
     void waitForIdle() {
-        mHandler.runWithScissors(() -> { }, 0 /* timeout */);
+        mTestLooper.dispatchAll();
     }
 
     /**
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
index 30a8941..cf620fe 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
@@ -275,5 +275,10 @@
         public boolean isEnteringPipAllowed(int uid) {
             return true;
         }
+
+        @Override
+        public ComponentName getCustomHomeComponent() {
+            return null;
+        }
     }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt
index 030b292..b3c9c96 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt
@@ -31,7 +31,8 @@
 @RunWith(FlickerServiceJUnit4ClassRunner::class)
 class CloseAppBackButton3ButtonLandscape :
     CloseAppBackButton(NavBar.MODE_3BUTTON, Rotation.ROTATION_90) {
-    @ExpectedScenarios(["APP_CLOSE_TO_HOME"])
+    // TODO: Missing CUJ (b/300078127)
+    @ExpectedScenarios(["ENTIRE_TRACE"])
     @Test
     override fun closeAppBackButtonTest() = super.closeAppBackButtonTest()
 
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt
index 770da143..29c11a4 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt
@@ -31,7 +31,8 @@
 @RunWith(FlickerServiceJUnit4ClassRunner::class)
 class CloseAppBackButton3ButtonPortrait :
     CloseAppBackButton(NavBar.MODE_3BUTTON, Rotation.ROTATION_0) {
-    @ExpectedScenarios(["APP_CLOSE_TO_HOME"])
+    // TODO: Missing CUJ (b/300078127)
+    @ExpectedScenarios(["ENTIRE_TRACE"])
     @Test
     override fun closeAppBackButtonTest() = super.closeAppBackButtonTest()
 
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt
index 4b2206d7..1bb4a25 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt
@@ -31,7 +31,8 @@
 @RunWith(FlickerServiceJUnit4ClassRunner::class)
 class CloseAppBackButtonGesturalNavLandscape :
     CloseAppBackButton(NavBar.MODE_GESTURAL, Rotation.ROTATION_90) {
-    @ExpectedScenarios(["APP_CLOSE_TO_HOME"])
+    // TODO: Missing CUJ (b/300078127)
+    @ExpectedScenarios(["ENTIRE_TRACE"])
     @Test
     override fun closeAppBackButtonTest() = super.closeAppBackButtonTest()
 
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt
index 54b471e..17cb6e1 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt
@@ -31,7 +31,8 @@
 @RunWith(FlickerServiceJUnit4ClassRunner::class)
 class CloseAppBackButtonGesturalNavPortrait :
     CloseAppBackButton(NavBar.MODE_GESTURAL, Rotation.ROTATION_0) {
-    @ExpectedScenarios(["APP_CLOSE_TO_HOME"])
+    // TODO: Missing CUJ (b/300078127)
+    @ExpectedScenarios(["ENTIRE_TRACE"])
     @Test
     override fun closeAppBackButtonTest() = super.closeAppBackButtonTest()
 
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchBetweenTwoAppsBack.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchBetweenTwoAppsBack.kt
index 93130c4..a1708fe 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchBetweenTwoAppsBack.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/service/quickswitch/scenarios/QuickSwitchBetweenTwoAppsBack.kt
@@ -19,6 +19,7 @@
 import android.app.Instrumentation
 import android.tools.common.NavBar
 import android.tools.common.Rotation
+import android.tools.device.flicker.rules.ChangeDisplayOrientationRule
 import android.tools.device.traces.parsers.WindowManagerStateHelper
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.tapl.LauncherInstrumentation
@@ -46,7 +47,9 @@
         tapl.setExpectedRotation(rotation.value)
         tapl.setIgnoreTaskbarVisibility(true)
         testApp1.launchViaIntent(wmHelper)
+        ChangeDisplayOrientationRule.setRotation(rotation)
         testApp2.launchViaIntent(wmHelper)
+        ChangeDisplayOrientationRule.setRotation(rotation)
     }
 
     @Test
diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
index f867c98..9198ae1 100644
--- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
@@ -102,6 +102,20 @@
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
+        <activity android:name=".PortraitImmersiveActivity"
+                  android:taskAffinity="com.android.server.wm.flicker.testapp.PortraitImmersiveActivity"
+                  android:immersive="true"
+                  android:resizeableActivity="true"
+                  android:screenOrientation="portrait"
+                  android:theme="@android:style/Theme.NoTitleBar"
+                  android:configChanges="screenSize"
+                  android:label="PortraitImmersiveActivity"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
         <activity android:name=".LaunchTransparentActivity"
                   android:resizeableActivity="false"
                   android:screenOrientation="portrait"
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
index 7c5e9a3..8b334c0 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
@@ -73,6 +73,12 @@
                 FLICKER_APP_PACKAGE + ".NonResizeablePortraitActivity");
     }
 
+    public static class PortraitImmersiveActivity {
+        public static final String LABEL = "PortraitImmersiveActivity";
+        public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
+                FLICKER_APP_PACKAGE + ".PortraitImmersiveActivity");
+    }
+
     public static class TransparentActivity {
         public static final String LABEL = "TransparentActivity";
         public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PortraitImmersiveActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PortraitImmersiveActivity.java
new file mode 100644
index 0000000..0a7f81c
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PortraitImmersiveActivity.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.testapp;
+
+public class PortraitImmersiveActivity extends GameActivity {
+}
diff --git a/tests/NetworkSecurityConfigTest/res/raw/ca_certs_der.der b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_der.der
index 235bd47..fd888ec 100644
--- a/tests/NetworkSecurityConfigTest/res/raw/ca_certs_der.der
+++ b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_der.der
Binary files differ
diff --git a/tests/NetworkSecurityConfigTest/res/raw/ca_certs_pem.pem b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_pem.pem
index 413e3c0..66f7bfd 100644
--- a/tests/NetworkSecurityConfigTest/res/raw/ca_certs_pem.pem
+++ b/tests/NetworkSecurityConfigTest/res/raw/ca_certs_pem.pem
@@ -1,35 +1,31 @@
 -----BEGIN CERTIFICATE-----
-MIIDfTCCAuagAwIBAgIDErvmMA0GCSqGSIb3DQEBBQUAME4xCzAJBgNVBAYTAlVT
-MRAwDgYDVQQKEwdFcXVpZmF4MS0wKwYDVQQLEyRFcXVpZmF4IFNlY3VyZSBDZXJ0
-aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDIwNTIxMDQwMDAwWhcNMTgwODIxMDQwMDAw
-WjBCMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UE
-AxMSR2VvVHJ1c3QgR2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
-CgKCAQEA2swYYzD99BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9m
-OSm9BXiLnTjoBbdqfnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIu
-T8rxh0PBFpVXLVDviS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6c
-JmTM386DGXHKTubU1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmR
-Cw7+OC7RHQWa9k0+bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5asz
-PeE4uwc2hGKceeoWMPRfwCvocWvk+QIDAQABo4HwMIHtMB8GA1UdIwQYMBaAFEjm
-aPkr0rKV10fYIyAQTzOYkJ/UMB0GA1UdDgQWBBTAephojYn7qwVkDBF9qn1luMrM
-TjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjA6BgNVHR8EMzAxMC+g
-LaArhilodHRwOi8vY3JsLmdlb3RydXN0LmNvbS9jcmxzL3NlY3VyZWNhLmNybDBO
-BgNVHSAERzBFMEMGBFUdIAAwOzA5BggrBgEFBQcCARYtaHR0cHM6Ly93d3cuZ2Vv
-dHJ1c3QuY29tL3Jlc291cmNlcy9yZXBvc2l0b3J5MA0GCSqGSIb3DQEBBQUAA4GB
-AHbhEm5OSxYShjAGsoEIz/AIx8dxfmbuwu3UOx//8PDITtZDOLC5MH0Y0FWDomrL
-NhGc6Ehmo21/uBPUR/6LWlxz/K7ZGzIZOKuXNBSqltLroxwUCEm2u+WR74M26x1W
-b8ravHNjkOR/ez4iyz0H7V84dJzjA1BOoa+Y7mHyhD8S
------END CERTIFICATE-----
------BEGIN CERTIFICATE-----
-MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkG
-A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz
-cyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2
-MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV
-BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmlt
-YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN
-ADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhE
-BarsAx94f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/is
-I19wKTakyYbnsZogy1Olhec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0G
-CSqGSIb3DQEBBQUAA4GBABByUqkFFBkyCEHwxWsKzH4PIRnN5GfcX6kb5sroc50i
-2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWXbj9T/UWZYB2oK0z5XqcJ
-2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/D/xwzoiQ
+MIIFYjCCBEqgAwIBAgIQd70NbNs2+RrqIQ/E8FjTDTANBgkqhkiG9w0BAQsFADBX
+MQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UE
+CxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIwMDYx
+OTAwMDA0MloXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoT
+GUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFIx
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAthECix7joXebO9y/lD63
+ladAPKH9gvl9MgaCcfb2jH/76Nu8ai6Xl6OMS/kr9rH5zoQdsfnFl97vufKj6bwS
+iV6nqlKr+CMny6SxnGPb15l+8Ape62im9MZaRw1NEDPjTrETo8gYbEvs/AmQ351k
+KSUjB6G00j0uYODP0gmHu81I8E3CwnqIiru6z1kZ1q+PsAewnjHxgsHA3y6mbWwZ
+DrXYfiYaRQM9sHmklCitD38m5agI/pboPGiUU+6DOogrFZYJsuB6jC511pzrp1Zk
+j5ZPaK49l8KEj8C8QMALXL32h7M1bKwYUH+E4EzNktMg6TO8UpmvMrUpsyUqtEj5
+cuHKZPfmghCN6J3Cioj6OGaK/GP5Afl4/Xtcd/p2h/rs37EOeZVXtL0m79YB0esW
+CruOC7XFxYpVq9Os6pFLKcwZpDIlTirxZUTQAs6qzkm06p98g7BAe+dDq6dso499
+iYH6TKX/1Y7DzkvgtdizjkXPdsDtQCv9Uw+wp9U7DbGKogPeMa3Md+pvez7W35Ei
+Eua++tgy/BBjFFFy3l3WFpO9KWgz7zpm7AeKJt8T11dleCfeXkkUAKIAf5qoIbap
+sZWwpbkNFhHax2xIPEDgfg1azVY80ZcFuctL7TlLnMQ/0lUTbiSw1nH69MG6zO0b
+9f6BQdgAmD06yK56mDcYBZUCAwEAAaOCATgwggE0MA4GA1UdDwEB/wQEAwIBhjAP
+BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTkrysmcRorSCeFL1JmLO/wiRNxPjAf
+BgNVHSMEGDAWgBRge2YaRQ2XyolQL30EzTSo//z9SzBgBggrBgEFBQcBAQRUMFIw
+JQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnBraS5nb29nL2dzcjEwKQYIKwYBBQUH
+MAKGHWh0dHA6Ly9wa2kuZ29vZy9nc3IxL2dzcjEuY3J0MDIGA1UdHwQrMCkwJ6Al
+oCOGIWh0dHA6Ly9jcmwucGtpLmdvb2cvZ3NyMS9nc3IxLmNybDA7BgNVHSAENDAy
+MAgGBmeBDAECATAIBgZngQwBAgIwDQYLKwYBBAHWeQIFAwIwDQYLKwYBBAHWeQIF
+AwMwDQYJKoZIhvcNAQELBQADggEBADSkHrEoo9C0dhemMXoh6dFSPsjbdBZBiLg9
+NR3t5P+T4Vxfq7vqfM/b5A3Ri1fyJm9bvhdGaJQ3b2t6yMAYN/olUazsaL+yyEn9
+WprKASOshIArAoyZl+tJaox118fessmXn1hIVw41oeQa1v1vg4Fv74zPl6/AhSrw
+9U5pCZEt4Wi4wStz6dTZ/CLANx8LZh1J7QJVj2fhMtfTJr9w4z30Z209fOU0iOMy
++qduBmpvvYuR7hZL6Dupszfnw0Skfths18dG9ZKb59UhvmaSGZRVbNQpsg3BZlvi
+d0lIKO2d1xozclOzgjXPYovJJIultzkMu34qQb9Sz/yilrbCgj8=
 -----END CERTIFICATE-----
diff --git a/tests/NetworkSecurityConfigTest/res/xml/domain_whitespace.xml b/tests/NetworkSecurityConfigTest/res/xml/domain_whitespace.xml
index 5d23d36e..99106ad 100644
--- a/tests/NetworkSecurityConfigTest/res/xml/domain_whitespace.xml
+++ b/tests/NetworkSecurityConfigTest/res/xml/domain_whitespace.xml
@@ -5,7 +5,7 @@
     </domain>
     <domain>   developer.android.com    </domain>
     <pin-set>
-      <pin digest="SHA-256">  7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=  </pin>
+      <pin digest="SHA-256">  zCTnfLwLKbS9S2sbp+uFz4KZOocFvXxkV06Ce9O5M2w=  </pin>
     </pin-set>
   </domain-config>
 </network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml b/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml
index d45fd77..232f88f 100644
--- a/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml
+++ b/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml
@@ -9,7 +9,7 @@
       <domain-config>
           <domain>developer.android.com</domain>
           <pin-set>
-              <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
+              <pin digest="SHA-256">zCTnfLwLKbS9S2sbp+uFz4KZOocFvXxkV06Ce9O5M2w=</pin>
           </pin-set>
       </domain-config>
     </domain-config>
diff --git a/tests/NetworkSecurityConfigTest/res/xml/pins1.xml b/tests/NetworkSecurityConfigTest/res/xml/pins1.xml
index 1773d280..7cc81b0 100644
--- a/tests/NetworkSecurityConfigTest/res/xml/pins1.xml
+++ b/tests/NetworkSecurityConfigTest/res/xml/pins1.xml
@@ -3,7 +3,7 @@
   <domain-config>
     <domain>android.com</domain>
     <pin-set>
-      <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
+      <pin digest="SHA-256">zCTnfLwLKbS9S2sbp+uFz4KZOocFvXxkV06Ce9O5M2w=</pin>
     </pin-set>
   </domain-config>
 </network-security-config>
diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java
index 047be16..0494f17 100644
--- a/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java
+++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java
@@ -22,23 +22,17 @@
 import android.test.ActivityUnitTestCase;
 import android.util.ArraySet;
 import android.util.Pair;
+
+import com.android.org.conscrypt.TrustedCertificateStore;
+
 import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.net.Socket;
-import java.net.URL;
 import java.security.cert.Certificate;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLHandshakeException;
-import javax.net.ssl.TrustManager;
 
-import com.android.org.conscrypt.TrustedCertificateStore;
+import javax.net.ssl.SSLContext;
 
 public class NetworkSecurityConfigTests extends ActivityUnitTestCase<Activity> {
 
@@ -46,9 +40,9 @@
         super(Activity.class);
     }
 
-    // SHA-256 of the G2 intermediate CA for android.com (as of 10/2015).
-    private static final byte[] G2_SPKI_SHA256
-            = hexToBytes("ec722969cb64200ab6638f68ac538e40abab5b19a6485661042a1061c4612776");
+    // SHA-256 of the GTS intermediate CA (CN = GTS CA 1C3) for android.com (as of 09/2023).
+    private static final byte[] GTS_INTERMEDIATE_SPKI_SHA256 =
+        hexToBytes("cc24e77cbc0b29b4bd4b6b1ba7eb85cf82993a8705bd7c64574e827bd3b9336c");
 
     private static final byte[] TEST_CA_BYTES
             = hexToBytes(
@@ -161,7 +155,7 @@
 
     public void testGoodPin() throws Exception {
         ArraySet<Pin> pins = new ArraySet<Pin>();
-        pins.add(new Pin("SHA-256", G2_SPKI_SHA256));
+        pins.add(new Pin("SHA-256", GTS_INTERMEDIATE_SPKI_SHA256));
         NetworkSecurityConfig domain = new NetworkSecurityConfig.Builder()
                 .setPinSet(new PinSet(pins, Long.MAX_VALUE))
                 .addCertificatesEntryRef(
@@ -247,7 +241,7 @@
 
     public void testWithUrlConnection() throws Exception {
         ArraySet<Pin> pins = new ArraySet<Pin>();
-        pins.add(new Pin("SHA-256", G2_SPKI_SHA256));
+        pins.add(new Pin("SHA-256", GTS_INTERMEDIATE_SPKI_SHA256));
         NetworkSecurityConfig domain = new NetworkSecurityConfig.Builder()
                 .setPinSet(new PinSet(pins, Long.MAX_VALUE))
                 .addCertificatesEntryRef(
@@ -304,7 +298,7 @@
         } finally {
             // Delete the user added CA. We don't know the alias so just delete them all.
             for (String alias : store.aliases()) {
-                if (store.isUser(alias)) {
+                if (TrustedCertificateStore.isUser(alias)) {
                     try {
                         store.deleteCertificateEntry(alias);
                     } catch (Exception ignored) {
diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java
index 9dec21b..39b5cb4c 100644
--- a/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java
+++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java
@@ -16,19 +16,20 @@
 
 package android.security.net.config;
 
+import static org.junit.Assert.fail;
+
 import android.content.pm.ApplicationInfo;
 import android.os.Build;
-import java.net.Socket;
+
 import java.net.URL;
+
 import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLHandshakeException;
-import javax.net.ssl.TrustManager;
+import javax.net.ssl.SSLSocket;
 import javax.net.ssl.TrustManagerFactory;
 
-import junit.framework.Assert;
-
-public final class TestUtils extends Assert {
+public final class TestUtils {
 
     private TestUtils() {
     }
@@ -36,8 +37,8 @@
     public static void assertConnectionFails(SSLContext context, String host, int port)
             throws Exception {
         try {
-            Socket s = context.getSocketFactory().createSocket(host, port);
-            s.getInputStream();
+            SSLSocket s = (SSLSocket) context.getSocketFactory().createSocket(host, port);
+            s.startHandshake();
             fail("Expected connection to " + host + ":" + port + " to fail.");
         } catch (SSLHandshakeException expected) {
         }
@@ -45,7 +46,8 @@
 
     public static void assertConnectionSucceeds(SSLContext context, String host, int port)
             throws Exception {
-        Socket s = context.getSocketFactory().createSocket(host, port);
+        SSLSocket s = (SSLSocket) context.getSocketFactory().createSocket(host, port);
+        s.startHandshake();
         s.getInputStream();
     }
 
diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java
index 4b7a014..81e05c1 100644
--- a/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java
+++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/XmlConfigTests.java
@@ -16,26 +16,18 @@
 
 package android.security.net.config;
 
-import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.test.AndroidTestCase;
 import android.test.MoreAsserts;
-import android.util.ArraySet;
-import android.util.Pair;
+
 import java.io.IOException;
 import java.net.InetAddress;
-import java.net.Socket;
-import java.net.URL;
 import java.security.KeyStore;
 import java.security.Provider;
-import java.security.Security;
 import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.Set;
-import javax.net.ssl.HttpsURLConnection;
+
 import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLHandshakeException;
 import javax.net.ssl.SSLSocket;
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.TrustManagerFactory;
@@ -52,7 +44,7 @@
         NetworkSecurityConfig config = appConfig.getConfigForHostname("");
         assertNotNull(config);
         // Check defaults.
-        assertTrue(config.isCleartextTrafficPermitted());
+        assertFalse(config.isCleartextTrafficPermitted());
         assertFalse(config.isHstsEnforced());
         assertFalse(config.getTrustAnchors().isEmpty());
         PinSet pinSet = config.getPins();
@@ -72,7 +64,7 @@
         NetworkSecurityConfig config = appConfig.getConfigForHostname("");
         assertNotNull(config);
         // Check defaults.
-        assertTrue(config.isCleartextTrafficPermitted());
+        assertFalse(config.isCleartextTrafficPermitted());
         assertFalse(config.isHstsEnforced());
         assertTrue(config.getTrustAnchors().isEmpty());
         PinSet pinSet = config.getPins();
@@ -91,14 +83,14 @@
         NetworkSecurityConfig config = appConfig.getConfigForHostname("");
         assertNotNull(config);
         // Check defaults.
-        assertTrue(config.isCleartextTrafficPermitted());
+        assertFalse(config.isCleartextTrafficPermitted());
         assertFalse(config.isHstsEnforced());
         assertTrue(config.getTrustAnchors().isEmpty());
         PinSet pinSet = config.getPins();
         assertTrue(pinSet.pins.isEmpty());
         // Check android.com.
         config = appConfig.getConfigForHostname("android.com");
-        assertTrue(config.isCleartextTrafficPermitted());
+        assertFalse(config.isCleartextTrafficPermitted());
         assertFalse(config.isHstsEnforced());
         assertFalse(config.getTrustAnchors().isEmpty());
         pinSet = config.getPins();
@@ -188,7 +180,7 @@
         ApplicationConfig appConfig = new ApplicationConfig(source);
         assertTrue(appConfig.hasPerDomainConfigs());
         NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
-        assertTrue(config.isCleartextTrafficPermitted());
+        assertFalse(config.isCleartextTrafficPermitted());
         assertFalse(config.isHstsEnforced());
         assertFalse(config.getTrustAnchors().isEmpty());
         PinSet pinSet = config.getPins();
@@ -250,9 +242,9 @@
         ApplicationConfig appConfig = new ApplicationConfig(source);
         // Check android.com.
         NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
-        assertTrue(config.isCleartextTrafficPermitted());
+        assertFalse(config.isCleartextTrafficPermitted());
         assertFalse(config.isHstsEnforced());
-        assertEquals(2, config.getTrustAnchors().size());
+        assertEquals(1, config.getTrustAnchors().size());
         // Try connections.
         SSLContext context = TestUtils.getSSLContext(source);
         TestUtils.assertConnectionSucceeds(context, "android.com", 443);
@@ -267,9 +259,9 @@
         ApplicationConfig appConfig = new ApplicationConfig(source);
         // Check android.com.
         NetworkSecurityConfig config = appConfig.getConfigForHostname("android.com");
-        assertTrue(config.isCleartextTrafficPermitted());
+        assertFalse(config.isCleartextTrafficPermitted());
         assertFalse(config.isHstsEnforced());
-        assertEquals(2, config.getTrustAnchors().size());
+        assertEquals(1, config.getTrustAnchors().size());
         // Try connections.
         SSLContext context = TestUtils.getSSLContext(source);
         TestUtils.assertConnectionSucceeds(context, "android.com", 443);
diff --git a/tests/utils/testutils/TEST_MAPPING b/tests/utils/testutils/TEST_MAPPING
index d9eb44f..6468d8c 100644
--- a/tests/utils/testutils/TEST_MAPPING
+++ b/tests/utils/testutils/TEST_MAPPING
@@ -4,7 +4,7 @@
       "name": "frameworks-base-testutils-tests",
       "options": [
         {
-          "exclude-annotation": "android.platform.test.annotations.LargeTest"
+          "exclude-annotation": "androidx.test.filters.LargeTest"
         },
         {
           "exclude-annotation": "android.platform.test.annotations.FlakyTest"