Merge "Modernize LightsOutNotifController" into main
diff --git a/cmds/gpu_counter_producer/Android.bp b/cmds/gpu_counter_producer/Android.bp
index 2232345..d645d06 100644
--- a/cmds/gpu_counter_producer/Android.bp
+++ b/cmds/gpu_counter_producer/Android.bp
@@ -19,6 +19,4 @@
         "-Wunused",
         "-Wunreachable-code",
     ],
-
-    soc_specific: true,
 }
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 275fe77..c282e4b6 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -307,7 +307,7 @@
     field public static final String RECOVER_KEYSTORE = "android.permission.RECOVER_KEYSTORE";
     field public static final String REGISTER_CALL_PROVIDER = "android.permission.REGISTER_CALL_PROVIDER";
     field public static final String REGISTER_CONNECTION_MANAGER = "android.permission.REGISTER_CONNECTION_MANAGER";
-    field public static final String REGISTER_NSD_OFFLOAD_ENGINE = "android.permission.REGISTER_NSD_OFFLOAD_ENGINE";
+    field @FlaggedApi("com.android.net.flags.register_nsd_offload_engine") public static final String REGISTER_NSD_OFFLOAD_ENGINE = "android.permission.REGISTER_NSD_OFFLOAD_ENGINE";
     field public static final String REGISTER_SIM_SUBSCRIPTION = "android.permission.REGISTER_SIM_SUBSCRIPTION";
     field public static final String REGISTER_STATS_PULL_ATOM = "android.permission.REGISTER_STATS_PULL_ATOM";
     field public static final String REMOTE_DISPLAY_PROVIDER = "android.permission.REMOTE_DISPLAY_PROVIDER";
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index b5f7f23..6c10f49 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -7604,15 +7604,17 @@
      * @param taskDescription The TaskDescription properties that describe the task with this activity
      */
     public void setTaskDescription(ActivityManager.TaskDescription taskDescription) {
-        if (mTaskDescription != taskDescription) {
-            mTaskDescription.copyFromPreserveHiddenFields(taskDescription);
-            // Scale the icon down to something reasonable if it is provided
-            if (taskDescription.getIconFilename() == null && taskDescription.getIcon() != null) {
-                final int size = ActivityManager.getLauncherLargeIconSizeInner(this);
-                final Bitmap icon = Bitmap.createScaledBitmap(taskDescription.getIcon(), size, size,
-                        true);
-                mTaskDescription.setIcon(Icon.createWithBitmap(icon));
-            }
+        if (taskDescription == null || mTaskDescription.equals(taskDescription)) {
+            return;
+        }
+
+        mTaskDescription.copyFromPreserveHiddenFields(taskDescription);
+        // Scale the icon down to something reasonable if it is provided
+        if (taskDescription.getIconFilename() == null && taskDescription.getIcon() != null) {
+            final int size = ActivityManager.getLauncherLargeIconSizeInner(this);
+            final Bitmap icon = Bitmap.createScaledBitmap(taskDescription.getIcon(), size, size,
+                    true);
+            mTaskDescription.setIcon(Icon.createWithBitmap(icon));
         }
         ActivityClient.getInstance().setTaskDescription(mToken, mTaskDescription);
     }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 7b075e6..4d208c6 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2301,6 +2301,7 @@
     <!-- Allows system apps to call methods to register itself as a mDNS offload engine.
         <p>Not for use by third-party or privileged applications.
         @SystemApi
+        @FlaggedApi("com.android.net.flags.register_nsd_offload_engine")
         @hide This should only be used by system apps.
     -->
     <permission android:name="android.permission.REGISTER_NSD_OFFLOAD_ENGINE"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
index b528089d15..5c02dbc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
@@ -17,6 +17,7 @@
 package com.android.wm.shell.transition;
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
 
 import static com.android.wm.shell.transition.Transitions.TransitionObserver;
 
@@ -61,9 +62,10 @@
             }
 
             final int mode = change.getMode();
+            final boolean isBackGesture = change.hasFlags(FLAG_BACK_GESTURE_ANIMATED);
             if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME
-                    && TransitionUtil.isOpenOrCloseMode(mode)) {
-                notifyHomeVisibilityChanged(TransitionUtil.isOpeningType(mode));
+                    && (TransitionUtil.isOpenOrCloseMode(mode) || isBackGesture)) {
+                notifyHomeVisibilityChanged(TransitionUtil.isOpeningType(mode) || isBackGesture);
             }
         }
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
index a5629c8..b355ab0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
@@ -14,8 +14,6 @@
 import android.window.TransitionInfo
 import android.window.TransitionInfo.FLAG_IS_WALLPAPER
 import androidx.test.filters.SmallTest
-import com.android.server.testutils.any
-import com.android.server.testutils.mock
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.ShellTestCase
 import com.android.wm.shell.TestRunningTaskInfoBuilder
@@ -28,8 +26,10 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
+import org.mockito.kotlin.mock
 import org.mockito.kotlin.never
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.verifyZeroInteractions
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
index ea7c0d9..421c445 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
@@ -18,8 +18,10 @@
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
@@ -145,6 +147,26 @@
         verify(mListener, times(0)).onHomeVisibilityChanged(anyBoolean());
     }
 
+    @Test
+    public void testHomeActivityWithBackGestureNotifiesHomeIsVisible() throws RemoteException {
+        TransitionInfo info = mock(TransitionInfo.class);
+        TransitionInfo.Change change = mock(TransitionInfo.Change.class);
+        ActivityManager.RunningTaskInfo taskInfo = mock(ActivityManager.RunningTaskInfo.class);
+        when(change.getTaskInfo()).thenReturn(taskInfo);
+        when(info.getChanges()).thenReturn(new ArrayList<>(List.of(change)));
+
+        when(change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)).thenReturn(true);
+        setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_CHANGE);
+
+        mHomeTransitionObserver.onTransitionReady(mock(IBinder.class),
+                info,
+                mock(SurfaceControl.Transaction.class),
+                mock(SurfaceControl.Transaction.class));
+
+        verify(mListener, times(1)).onHomeVisibilityChanged(true);
+    }
+
+
     /**
      * Helper class to initialize variables for the rest.
      */
diff --git a/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm b/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm
index 93a5082..071f9f4 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm
@@ -116,6 +116,9 @@
     base:                               'w'
     shift, capslock:                    'W'
     shift+capslock:                     'w'
+    ralt:                               '\u1e83'
+    shift+ralt, capslock+ralt:          '\u1e82'
+    shift+capslock+ralt:                '\u1e83'
 }
 
 key E {
@@ -147,6 +150,9 @@
     base:                               'y'
     shift, capslock:                    'Y'
     shift+capslock:                     'y'
+    ralt:                               '\u00fd'
+    shift+ralt, capslock+ralt:          '\u00dd'
+    shift+capslock+ralt:                '\u00fd'
 }
 
 key U {
@@ -313,6 +319,9 @@
     base:                               'c'
     shift, capslock:                    'C'
     shift+capslock:                     'c'
+    ralt:                               '\u00e7'
+    shift+ralt, capslock+ralt:          '\u00c7'
+    shift+capslock+ralt:                '\u00e7'
 }
 
 key V {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_french.kcm b/packages/InputDevices/res/raw/keyboard_layout_french.kcm
index 4906304..636f98d 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_french.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_french.kcm
@@ -44,7 +44,7 @@
     label:                              '2'
     base:                               '\u00e9'
     shift:                              '2'
-    ralt:                               '~'
+    ralt:                               '\u0303'
 }
 
 key 3 {
@@ -79,7 +79,7 @@
     label:                              '7'
     base:                               '\u00e8'
     shift:                              '7'
-    ralt:                               '`'
+    ralt:                               '\u0300'
 }
 
 key 8 {
diff --git a/packages/InputDevices/res/xml/keyboard_layouts.xml b/packages/InputDevices/res/xml/keyboard_layouts.xml
index 7f23f74..ee49b23 100644
--- a/packages/InputDevices/res/xml/keyboard_layouts.xml
+++ b/packages/InputDevices/res/xml/keyboard_layouts.xml
@@ -94,7 +94,7 @@
         android:name="keyboard_layout_swiss_german"
         android:label="@string/keyboard_layout_swiss_german_label"
         android:keyboardLayout="@raw/keyboard_layout_swiss_german"
-        android:keyboardLocale="de-Latn-CH"
+        android:keyboardLocale="de-Latn-CH|gsw-Latn-CH"
         android:keyboardLayoutType="qwertz" />
 
     <keyboard-layout
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index d65a69c..9ee3d22 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -64,13 +64,13 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.settingslib.volume.MediaSessions;
 import com.android.systemui.Dumpable;
-import com.android.systemui.res.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.plugins.VolumeDialogController;
 import com.android.systemui.qs.tiles.DndTile;
+import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.util.RingerModeLiveData;
@@ -99,6 +99,7 @@
 @SysUISingleton
 public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpable {
     private static final String TAG = Util.logTag(VolumeDialogControllerImpl.class);
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private static final int TOUCH_FEEDBACK_TIMEOUT_MS = 1000;
     private static final int DYNAMIC_STREAM_START_INDEX = 100;
@@ -1339,14 +1340,24 @@
 
         private boolean showForSession(Token token) {
             if (mVolumeAdjustmentForRemoteGroupSessions) {
+                if (DEBUG) {
+                    Log.d(TAG, "Volume adjustment for remote group sessions allowed,"
+                            + " showForSession: true");
+                }
                 return true;
             }
             MediaController ctr = new MediaController(mContext, token);
             String packageName = ctr.getPackageName();
             List<RoutingSessionInfo> sessions =
                     mRouter2Manager.getRoutingSessions(packageName);
-
+            if (DEBUG) {
+                Log.d(TAG, "Found " + sessions.size() + " routing sessions for package name "
+                        + packageName);
+            }
             for (RoutingSessionInfo session : sessions) {
+                if (DEBUG) {
+                    Log.d(TAG, "Found routingSessionInfo: " + session);
+                }
                 if (!session.isSystemSession()
                         && session.getVolumeHandling() != MediaRoute2Info.PLAYBACK_VOLUME_FIXED) {
                     return true;
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 215970e..e51ef29 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -864,6 +864,23 @@
         }
 
         @Override
+        public @NonNull Set<String> getAllPersistentDeviceIds() {
+            Set<String> persistentIds = new ArraySet<>();
+            synchronized (mVirtualDeviceManagerLock) {
+                for (int i = 0; i < mActiveAssociations.size(); ++i) {
+                    AssociationInfo associationInfo = mActiveAssociations.get(i);
+                    if (VIRTUAL_DEVICE_COMPANION_DEVICE_PROFILES.contains(
+                            associationInfo.getDeviceProfile())) {
+                        persistentIds.add(
+                                VirtualDeviceImpl.createPersistentDeviceId(
+                                        associationInfo.getId()));
+                    }
+                }
+            }
+            return persistentIds;
+        }
+
+        @Override
         public void registerAppsOnVirtualDeviceListener(
                 @NonNull AppsOnVirtualDeviceListener listener) {
             synchronized (mVirtualDeviceManagerLock) {
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
index a570d09..6940ffe 100644
--- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
+++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
@@ -69,6 +69,11 @@
             }
 
             @Override
+            public void onProcessCaptureRequest(int streamId, int frameId) throws RemoteException {
+                camera.onProcessCaptureRequest(streamId, frameId, /*metadata=*/ null);
+            }
+
+            @Override
             public void onStreamClosed(int streamId) throws RemoteException {
                 camera.onStreamClosed(streamId);
             }
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index ea92154..61e4f36 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -19,6 +19,9 @@
 import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES;
 import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_UNKNOWN;
 import static android.media.AudioSystem.isBluetoothDevice;
+import static android.media.AudioSystem.isBluetoothLeDevice;
+
+import static com.android.media.audio.Flags.dsaOverBtLeAudio;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -1625,10 +1628,10 @@
     }
 
     private int getHeadSensorHandleUpdateTracker() {
-        int headHandle = -1;
+        Sensor htSensor = null;
         if (sRoutingDevices.isEmpty()) {
             logloge("getHeadSensorHandleUpdateTracker: no device, no head tracker");
-            return headHandle;
+            return  -1;
         }
         final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0);
         List<String> deviceAddresses = mAudioService.getDeviceAddresses(currentDevice);
@@ -1642,27 +1645,86 @@
         for (String address : deviceAddresses) {
             UUID routingDeviceUuid = UuidUtils.uuidFromAudioDeviceAttributes(
                     new AudioDeviceAttributes(currentDevice.getInternalType(), address));
-            for (Sensor sensor : sensors) {
-                final UUID uuid = sensor.getUuid();
-                if (uuid.equals(routingDeviceUuid)) {
-                    headHandle = sensor.getHandle();
-                    if (!setHasHeadTracker(currentDevice)) {
-                        headHandle = -1;
+            if (dsaOverBtLeAudio()) {
+                for (Sensor sensor : sensors) {
+                    final UUID uuid = sensor.getUuid();
+                    if (uuid.equals(routingDeviceUuid)) {
+                        htSensor = sensor;
+                        HeadtrackerInfo info = new HeadtrackerInfo(sensor);
+                        if (isBluetoothLeDevice(currentDevice.getInternalType())) {
+                            if (info.getMajorVersion() == 2) {
+                                // Version 2 is used only by LE Audio profile
+                                break;
+                            }
+                            // we do not break, as this could be a match on the A2DP sensor
+                            // for a dual mode headset.
+                        } else if (info.getMajorVersion() == 1) {
+                            // Version 1 is used only by A2DP profile
+                            break;
+                        }
                     }
+                    if (htSensor == null && uuid.equals(UuidUtils.STANDALONE_UUID)) {
+                        htSensor = sensor;
+                        // we do not break, perhaps we find a head tracker on device.
+                    }
+                }
+                if (htSensor != null) {
+                    if (htSensor.getUuid().equals(UuidUtils.STANDALONE_UUID)) {
+                        break;
+                    }
+                    if (setHasHeadTracker(currentDevice)) {
+                        break;
+                    } else {
+                        htSensor = null;
+                    }
+                }
+            } else {
+                for (Sensor sensor : sensors) {
+                    final UUID uuid = sensor.getUuid();
+                    if (uuid.equals(routingDeviceUuid)) {
+                        htSensor = sensor;
+                        if (!setHasHeadTracker(currentDevice)) {
+                            htSensor = null;
+                        }
+                        break;
+                    }
+                    if (uuid.equals(UuidUtils.STANDALONE_UUID)) {
+                        htSensor = sensor;
+                        // we do not break, perhaps we find a head tracker on device.
+                    }
+                }
+                if (htSensor != null) {
                     break;
                 }
-                if (uuid.equals(UuidUtils.STANDALONE_UUID)) {
-                    headHandle = sensor.getHandle();
-                    // we do not break, perhaps we find a head tracker on device.
-                }
-            }
-            if (headHandle != -1) {
-                break;
             }
         }
-        return headHandle;
+        return htSensor != null ? htSensor.getHandle() : -1;
     }
 
+    /**
+     * Contains the information parsed from the head tracker sensor version.
+     * See platform/hardware/libhardware/modules/sensors/dynamic_sensor/HidRawSensor.h
+     * for the definition of version and capability fields.
+     */
+    private static class HeadtrackerInfo {
+        private final int mVersion;
+        HeadtrackerInfo(Sensor sensor) {
+            mVersion = sensor.getVersion();
+        }
+        int getMajorVersion() {
+            return (mVersion & 0xFF000000) >> 24;
+        }
+        int getMinorVersion() {
+            return (mVersion & 0xFF0000) >> 16;
+        }
+        boolean hasAclTransport() {
+            return getMajorVersion() == 2 ? ((mVersion & 0x1) != 0) : false;
+        }
+        boolean hasIsoTransport() {
+            return getMajorVersion() == 2 ? ((mVersion & 0x2) != 0) : false;
+        }
+    };
+
     private int getScreenSensorHandle() {
         int screenHandle = -1;
         Sensor screenSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
index c629b2b..be78ea2 100644
--- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
+++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
@@ -157,4 +157,10 @@
      * @see VirtualDevice#getPersistentDeviceId()
      */
     public abstract @Nullable String getPersistentIdForDevice(int deviceId);
+
+    /**
+     * Returns all current persistent device IDs, including the ones for which no virtual device
+     * exists, as long as one may have existed or can be created.
+     */
+    public abstract @NonNull Set<String> getAllPersistentDeviceIds();
 }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 0492f43..c2b5964 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -184,6 +184,7 @@
 import android.companion.ICompanionDeviceManager;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledAfter;
+import android.compat.annotation.EnabledSince;
 import android.compat.annotation.LoggingOnly;
 import android.content.AttributionSource;
 import android.content.BroadcastReceiver;
@@ -555,7 +556,7 @@
      * creation and activation of an implicit {@link android.app.AutomaticZenRule}.
      */
     @ChangeId
-    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     static final long MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES = 308670109L;
 
     private static final Duration POST_WAKE_LOCK_TIMEOUT = Duration.ofSeconds(30);
diff --git a/services/core/java/com/android/server/wm/ClientLifecycleManager.java b/services/core/java/com/android/server/wm/ClientLifecycleManager.java
index 28f656e..8b282dd3 100644
--- a/services/core/java/com/android/server/wm/ClientLifecycleManager.java
+++ b/services/core/java/com/android/server/wm/ClientLifecycleManager.java
@@ -58,6 +58,23 @@
     }
 
     /**
+     * Similar to {@link #scheduleTransactionItem}, but is called without WM lock.
+     *
+     * @see WindowProcessController#setReportedProcState(int)
+     */
+    void scheduleTransactionItemUnlocked(@NonNull IApplicationThread client,
+            @NonNull ClientTransactionItem transactionItem) throws RemoteException {
+        // Immediately dispatching to client, and must not access WMS.
+        final ClientTransaction clientTransaction = ClientTransaction.obtain(client);
+        if (transactionItem.isActivityLifecycleItem()) {
+            clientTransaction.setLifecycleStateRequest((ActivityLifecycleItem) transactionItem);
+        } else {
+            clientTransaction.addCallback(transactionItem);
+        }
+        scheduleTransaction(clientTransaction);
+    }
+
+    /**
      * Schedules a single transaction item, either a callback or a lifecycle request, delivery to
      * client application.
      * @throws RemoteException
@@ -65,6 +82,7 @@
      */
     void scheduleTransactionItem(@NonNull IApplicationThread client,
             @NonNull ClientTransactionItem transactionItem) throws RemoteException {
+        // TODO(b/260873529): queue the transaction items.
         final ClientTransaction clientTransaction = ClientTransaction.obtain(client);
         if (transactionItem.isActivityLifecycleItem()) {
             clientTransaction.setLifecycleStateRequest((ActivityLifecycleItem) transactionItem);
@@ -82,6 +100,7 @@
     void scheduleTransactionAndLifecycleItems(@NonNull IApplicationThread client,
             @NonNull ClientTransactionItem transactionItem,
             @NonNull ActivityLifecycleItem lifecycleItem) throws RemoteException {
+        // TODO(b/260873529): replace with #scheduleTransactionItem after launch for cleanup.
         final ClientTransaction clientTransaction = ClientTransaction.obtain(client);
         clientTransaction.addCallback(transactionItem);
         clientTransaction.setLifecycleStateRequest(lifecycleItem);
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 558bf9d..2b18f07 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -388,13 +388,22 @@
         final IApplicationThread thread = mThread;
         if (prevProcState >= CACHED_CONFIG_PROC_STATE && repProcState < CACHED_CONFIG_PROC_STATE
                 && thread != null && mHasCachedConfiguration) {
-            final Configuration config;
+            final ConfigurationChangeItem configurationChangeItem;
             synchronized (mLastReportedConfiguration) {
-                config = new Configuration(mLastReportedConfiguration);
+                onConfigurationChangePreScheduled(mLastReportedConfiguration);
+                configurationChangeItem = ConfigurationChangeItem.obtain(
+                        mLastReportedConfiguration, mLastTopActivityDeviceId);
             }
             // Schedule immediately to make sure the app component (e.g. receiver, service) can get
             // the latest configuration in their lifecycle callbacks (e.g. onReceive, onCreate).
-            scheduleConfigurationChange(thread, config);
+            try {
+                // No WM lock here.
+                mAtm.getLifecycleManager().scheduleTransactionItemUnlocked(
+                        thread, configurationChangeItem);
+            } catch (Exception e) {
+                Slog.e(TAG_CONFIGURATION, "Failed to schedule ConfigurationChangeItem="
+                        + configurationChangeItem + " owner=" + mOwner, e);
+            }
         }
     }
 
@@ -1634,11 +1643,12 @@
             }
         }
 
-        scheduleConfigurationChange(thread, config);
+        onConfigurationChangePreScheduled(config);
+        scheduleClientTransactionItem(thread, ConfigurationChangeItem.obtain(
+                config, mLastTopActivityDeviceId));
     }
 
-    private void scheduleConfigurationChange(@NonNull IApplicationThread thread,
-            @NonNull Configuration config) {
+    private void onConfigurationChangePreScheduled(@NonNull Configuration config) {
         ProtoLog.v(WM_DEBUG_CONFIGURATION, "Sending to proc %s new config %s", mName,
                 config);
         if (Build.IS_DEBUGGABLE && mHasImeService) {
@@ -1646,8 +1656,6 @@
             Slog.v(TAG_CONFIGURATION, "Sending to IME proc " + mName + " new config " + config);
         }
         mHasCachedConfiguration = false;
-        scheduleClientTransactionItem(thread, ConfigurationChangeItem.obtain(
-                config, mLastTopActivityDeviceId));
     }
 
     @VisibleForTesting
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index d87b8d1..b5ba322 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -773,6 +773,30 @@
     }
 
     @Test
+    public void getAllPersistentDeviceIds_respectsCurrentAssociations() {
+        mVdms.onCdmAssociationsChanged(List.of(mAssociationInfo));
+        TestableLooper.get(this).processAllMessages();
+
+        assertThat(mLocalService.getAllPersistentDeviceIds())
+                .containsExactly(mDeviceImpl.getPersistentDeviceId());
+
+        mVdms.onCdmAssociationsChanged(List.of(
+                createAssociationInfo(2, AssociationRequest.DEVICE_PROFILE_APP_STREAMING),
+                createAssociationInfo(3, AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION),
+                createAssociationInfo(4, AssociationRequest.DEVICE_PROFILE_WATCH)));
+        TestableLooper.get(this).processAllMessages();
+
+        assertThat(mLocalService.getAllPersistentDeviceIds()).containsExactly(
+                VirtualDeviceImpl.createPersistentDeviceId(2),
+                VirtualDeviceImpl.createPersistentDeviceId(3));
+
+        mVdms.onCdmAssociationsChanged(Collections.emptyList());
+        TestableLooper.get(this).processAllMessages();
+
+        assertThat(mLocalService.getAllPersistentDeviceIds()).isEmpty();
+    }
+
+    @Test
     public void onAppsOnVirtualDeviceChanged_singleVirtualDevice_listenersNotified() {
         ArraySet<Integer> uids = new ArraySet<>(Arrays.asList(UID_1, UID_2));
         mLocalService.registerAppsOnVirtualDeviceListener(mAppsOnVirtualDeviceListener);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
index e152feb..e31ee11 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
@@ -306,7 +306,7 @@
 
     @Test
     public void testCachedStateConfigurationChange() throws RemoteException {
-        doNothing().when(mClientLifecycleManager).scheduleTransactionItem(any(), any());
+        doNothing().when(mClientLifecycleManager).scheduleTransactionItemUnlocked(any(), any());
         final IApplicationThread thread = mWpc.getThread();
         final Configuration newConfig = new Configuration(mWpc.getConfiguration());
         newConfig.densityDpi += 100;
@@ -322,18 +322,17 @@
         newConfig.densityDpi += 100;
         mWpc.onConfigurationChanged(newConfig);
         verify(mClientLifecycleManager, never()).scheduleTransactionItem(eq(thread), any());
+        verify(mClientLifecycleManager, never()).scheduleTransactionItemUnlocked(eq(thread), any());
 
         // Cached -> non-cached will send the previous deferred config immediately.
         mWpc.setReportedProcState(ActivityManager.PROCESS_STATE_RECEIVER);
         final ArgumentCaptor<ConfigurationChangeItem> captor =
                 ArgumentCaptor.forClass(ConfigurationChangeItem.class);
-        verify(mClientLifecycleManager).scheduleTransactionItem(eq(thread), captor.capture());
+        verify(mClientLifecycleManager).scheduleTransactionItemUnlocked(
+                eq(thread), captor.capture());
         final ClientTransactionHandler client = mock(ClientTransactionHandler.class);
         captor.getValue().preExecute(client);
-        final ArgumentCaptor<Configuration> configCaptor =
-                ArgumentCaptor.forClass(Configuration.class);
-        verify(client).updatePendingConfiguration(configCaptor.capture());
-        assertEquals(newConfig, configCaptor.getValue());
+        verify(client).updatePendingConfiguration(newConfig);
     }
 
     @Test
diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
index b44f1a6..c49f8fe 100644
--- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
+++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
@@ -23,10 +23,13 @@
 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.traces.parsers.toFlickerComponent
 import androidx.test.filters.FlakyTest
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeAppHelper
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.flicker.testapp.ActivityOptions.Ime.Default.ACTION_FINISH_ACTIVITY
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -53,7 +56,12 @@
             testApp.launchViaIntent(wmHelper)
             testApp.openIME(wmHelper)
         }
-        transitions { testApp.finishActivity(wmHelper) }
+        transitions {
+            broadcastActionTrigger.doAction(ACTION_FINISH_ACTIVITY)
+            wmHelper.StateSyncBuilder()
+                    .withActivityRemoved(ActivityOptions.Ime.Default.COMPONENT.toFlickerComponent())
+                    .waitForAndVerify()
+        }
         teardown { simpleApp.exit(wmHelper) }
     }
 
diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
index 976ac82..994edc5 100644
--- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
+++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
@@ -28,6 +28,7 @@
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
+import com.android.server.wm.flicker.testapp.ActivityOptions.Ime.Default.ACTION_TOGGLE_ORIENTATION
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -53,7 +54,11 @@
             // Enable letterbox when the app calls setRequestedOrientation
             device.executeShellCommand("cmd window set-ignore-orientation-request true")
         }
-        transitions { testApp.toggleFixPortraitOrientation(wmHelper) }
+        transitions {
+            broadcastActionTrigger.doAction(ACTION_TOGGLE_ORIENTATION)
+            // Ensure app relaunching transition finished and the IME was shown
+            testApp.waitIMEShown(wmHelper)
+        }
         teardown {
             testApp.exit()
             device.executeShellCommand("cmd window set-ignore-orientation-request false")
diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt
index aff8e65..6ee5a9a 100644
--- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt
+++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt
@@ -104,7 +104,7 @@
 
     @Presubmit
     @Test
-    open fun imeLayerIsVisibleWhenSwitchingToImeApp() {
+    fun imeLayerIsVisibleWhenSwitchingToImeApp() {
         flicker.assertLayersStart { isVisible(ComponentNameMatcher.IME) }
         flicker.assertLayersTag(TAG_IME_VISIBLE) { isVisible(ComponentNameMatcher.IME) }
         flicker.assertLayersEnd { isVisible(ComponentNameMatcher.IME) }
diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt
index 4ffdcea..1ad5c0d 100644
--- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt
+++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt
@@ -93,7 +93,7 @@
         }
         transitions {
             testApp.launchViaIntent(wmHelper)
-            wmHelper.StateSyncBuilder().withImeShown().waitForAndVerify()
+            testApp.waitIMEShown(wmHelper)
         }
     }
 
diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt
index 6ad235c..181a2a2 100644
--- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt
+++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt
@@ -23,11 +23,14 @@
 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.traces.parsers.toFlickerComponent
 import android.view.WindowInsets.Type.ime
 import android.view.WindowInsets.Type.navigationBars
 import android.view.WindowInsets.Type.statusBars
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.flicker.testapp.ActivityOptions.Ime.Default.ACTION_START_DIALOG_THEMED_ACTIVITY
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.FixMethodOrder
@@ -50,8 +53,12 @@
     override val transition: FlickerBuilder.() -> Unit = {
         setup {
             testApp.launchViaIntent(wmHelper)
-            wmHelper.StateSyncBuilder().withImeShown().waitForAndVerify()
-            testApp.startDialogThemedActivity(wmHelper)
+            testApp.waitIMEShown(wmHelper)
+            broadcastActionTrigger.doAction(ACTION_START_DIALOG_THEMED_ACTIVITY)
+            wmHelper.StateSyncBuilder()
+                    .withFullScreenApp(
+                        ActivityOptions.DialogThemedActivity.COMPONENT.toFlickerComponent())
+                    .waitForAndVerify()
             // Verify IME insets isn't visible on dialog since it's non-IME focusable window
             assertFalse(testApp.getInsetsVisibleFromDialog(ime()))
             assertTrue(testApp.getInsetsVisibleFromDialog(statusBars()))
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
index 7c9c05d..ad272a0 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
@@ -17,6 +17,7 @@
 package com.android.server.wm.flicker
 
 import android.app.Instrumentation
+import android.content.Intent
 import android.platform.test.annotations.Presubmit
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerBuilderProvider
@@ -50,6 +51,19 @@
     /** Specification of the test transition to execute */
     abstract val transition: FlickerBuilder.() -> Unit
 
+    protected val broadcastActionTrigger = BroadcastActionTrigger(instrumentation)
+
+    // Helper class to process test actions by broadcast.
+    protected class BroadcastActionTrigger(private val instrumentation: Instrumentation) {
+        private fun createIntentWithAction(broadcastAction: String): Intent {
+            return Intent(broadcastAction).setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+        }
+
+        fun doAction(broadcastAction: String) {
+            instrumentation.context.sendBroadcast(createIntentWithAction(broadcastAction))
+        }
+    }
+
     /**
      * Entry point for the test runner. It will use this method to initialize and cache flicker
      * executions
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt
index 252f7d3..cb1aab0 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt
@@ -50,7 +50,7 @@
         waitIMEShown(wmHelper)
     }
 
-    protected fun waitIMEShown(wmHelper: WindowManagerStateHelper) {
+    fun waitIMEShown(wmHelper: WindowManagerStateHelper) {
         wmHelper.StateSyncBuilder().withImeShown().waitForAndVerify()
     }
 
@@ -63,17 +63,4 @@
         uiDevice.pressBack()
         wmHelper.StateSyncBuilder().withImeGone().waitForAndVerify()
     }
-
-    open fun finishActivity(wmHelper: WindowManagerStateHelper) {
-        val finishButton =
-            uiDevice.wait(
-                Until.findObject(By.res(packageName, "finish_activity_btn")),
-                FIND_TIMEOUT
-            )
-        requireNotNull(finishButton) {
-            "Finish activity button not found, probably IME activity is not on the screen?"
-        }
-        finishButton.click()
-        wmHelper.StateSyncBuilder().withActivityRemoved(this).waitForAndVerify()
-    }
 }
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt
index d3cee64..0ee7aee 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt
@@ -74,24 +74,6 @@
         open(expectedPackage)
     }
 
-    fun startDialogThemedActivity(wmHelper: WindowManagerStateHelper) {
-        val button =
-            uiDevice.wait(
-                Until.findObject(By.res(packageName, "start_dialog_themed_activity_btn")),
-                FIND_TIMEOUT
-            )
-
-        requireNotNull(button) {
-            "Button not found, this usually happens when the device " +
-                "was left in an unknown state (e.g. Screen turned off)"
-        }
-        button.click()
-        wmHelper
-            .StateSyncBuilder()
-            .withFullScreenApp(ActivityOptions.DialogThemedActivity.COMPONENT.toFlickerComponent())
-            .waitForAndVerify()
-    }
-
     fun dismissDialog(wmHelper: WindowManagerStateHelper) {
         val dialog = uiDevice.wait(Until.findObject(By.text("Dialog for test")), FIND_TIMEOUT)
 
@@ -126,20 +108,4 @@
         }
         return false
     }
-
-    fun toggleFixPortraitOrientation(wmHelper: WindowManagerStateHelper) {
-        val button =
-            uiDevice.wait(
-                Until.findObject(By.res(packageName, "toggle_fixed_portrait_btn")),
-                FIND_TIMEOUT
-            )
-        require(button != null) {
-            "Button not found, this usually happens when the device " +
-                "was left in an unknown state (e.g. Screen turned off)"
-        }
-        button.click()
-        instrumentation.waitForIdleSync()
-        // Ensure app relaunching transition finish and the IME has shown
-        waitIMEShown(wmHelper)
-    }
 }
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
index fa73e2c..507c1b6 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
  Copyright 2018 The Android Open Source Project
 
  Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,39 +13,17 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 -->
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:orientation="vertical"
+    android:background="@android:color/holo_green_light"
     android:focusableInTouchMode="true"
-    android:background="@android:color/holo_green_light">
-    <EditText android:id="@+id/plain_text_input"
-              android:layout_height="wrap_content"
-              android:layout_width="match_parent"
-	      android:imeOptions="flagNoExtractUi"
-              android:inputType="text"/>
-    <LinearLayout
-        xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical">
+
+    <EditText
+        android:id="@+id/plain_text_input"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="horizontal">
-        <Button
-            android:id="@+id/finish_activity_btn"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="Finish activity" />
-        <Button
-            android:id="@+id/start_dialog_themed_activity_btn"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="Dialog themed activity" />
-        <ToggleButton
-            android:id="@+id/toggle_fixed_portrait_btn"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:textOn="Portrait (On)"
-            android:textOff="Portrait (Off)"
-        />
-    </LinearLayout>
+        android:layout_height="wrap_content"
+        android:imeOptions="flagNoExtractUi"
+        android:inputType="text" />
 </LinearLayout>
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 8b334c0..80c1dd0 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
@@ -40,6 +40,18 @@
             public static final String LABEL = "ImeActivity";
             public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
                     FLICKER_APP_PACKAGE + ".ImeActivity");
+
+            /** Intent action used to finish the test activity. */
+            public static final String ACTION_FINISH_ACTIVITY =
+                    FLICKER_APP_PACKAGE + ".ImeActivity.FINISH_ACTIVITY";
+
+            /** Intent action used to start a {@link DialogThemedActivity}. */
+            public static final String ACTION_START_DIALOG_THEMED_ACTIVITY =
+                    FLICKER_APP_PACKAGE + ".ImeActivity.START_DIALOG_THEMED_ACTIVITY";
+
+            /** Intent action used to toggle activity orientation. */
+            public static final String ACTION_TOGGLE_ORIENTATION =
+                    FLICKER_APP_PACKAGE + ".ImeActivity.TOGGLE_ORIENTATION";
         }
 
         public static class AutoFocusActivity {
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivity.java
index d7ee2af..4418b5a 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivity.java
@@ -16,12 +16,51 @@
 
 package com.android.server.wm.flicker.testapp;
 
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+
+import static com.android.server.wm.flicker.testapp.ActivityOptions.Ime.Default.ACTION_FINISH_ACTIVITY;
+import static com.android.server.wm.flicker.testapp.ActivityOptions.Ime.Default.ACTION_START_DIALOG_THEMED_ACTIVITY;
+import static com.android.server.wm.flicker.testapp.ActivityOptions.Ime.Default.ACTION_TOGGLE_ORIENTATION;
+
 import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.os.Bundle;
+import android.util.Log;
 import android.view.WindowManager;
-import android.widget.Button;
 
 public class ImeActivity extends Activity {
+
+    private static final String TAG = "ImeActivity";
+
+    /** Receiver used to handle actions coming from the test helper methods. */
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            switch (intent.getAction()) {
+                case ACTION_FINISH_ACTIVITY -> finish();
+                case ACTION_START_DIALOG_THEMED_ACTIVITY -> startActivity(
+                        new Intent(context, DialogThemedActivity.class));
+                case ACTION_TOGGLE_ORIENTATION -> {
+                    mIsPortrait = !mIsPortrait;
+                    setRequestedOrientation(mIsPortrait
+                            ? SCREEN_ORIENTATION_PORTRAIT
+                            : SCREEN_ORIENTATION_UNSPECIFIED);
+                }
+                default -> Log.w(TAG, "Unhandled action=" + intent.getAction());
+            }
+        }
+    };
+
+    /**
+     * Used to toggle activity orientation between portrait when {@code true} and
+     * unspecified otherwise.
+     */
+    private boolean mIsPortrait = false;
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -30,9 +69,17 @@
                 .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
         getWindow().setAttributes(p);
         setContentView(R.layout.activity_ime);
-        Button button = findViewById(R.id.finish_activity_btn);
-        button.setOnClickListener(view -> {
-            finish();
-        });
+
+        final var filter = new IntentFilter();
+        filter.addAction(ACTION_FINISH_ACTIVITY);
+        filter.addAction(ACTION_START_DIALOG_THEMED_ACTIVITY);
+        filter.addAction(ACTION_TOGGLE_ORIENTATION);
+        registerReceiver(mBroadcastReceiver, filter, Context.RECEIVER_EXPORTED);
+    }
+
+    @Override
+    protected void onDestroy() {
+        unregisterReceiver(mBroadcastReceiver);
+        super.onDestroy();
     }
 }
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java
index 7ee8deb..cd711f7 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java
@@ -16,29 +16,12 @@
 
 package com.android.server.wm.flicker.testapp;
 
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
-
-import android.content.Intent;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.ToggleButton;
-
 public class ImeActivityAutoFocus extends ImeActivity {
     @Override
     protected void onStart() {
         super.onStart();
 
-        Button startThemedActivityButton = findViewById(R.id.start_dialog_themed_activity_btn);
-        startThemedActivityButton.setOnClickListener(
-                button -> startActivity(new Intent(this, DialogThemedActivity.class)));
-
-        ToggleButton toggleFixedPortraitButton = findViewById(R.id.toggle_fixed_portrait_btn);
-        toggleFixedPortraitButton.setOnCheckedChangeListener(
-                (button, isChecked) -> setRequestedOrientation(
-                        isChecked ? SCREEN_ORIENTATION_PORTRAIT : SCREEN_ORIENTATION_UNSPECIFIED));
-
-        EditText editTextField = findViewById(R.id.plain_text_input);
+        final var editTextField = findViewById(R.id.plain_text_input);
         editTextField.requestFocus();
     }
 }