Synchronize window config updates (9/n)

Refactor to call scheduleTransaction through ClientLifecycleManager.

Bug: 260873529
Test: atest WmTests:ClientLifecycleManagerTests
Change-Id: I1ffc8e78b4a7c7c0363e385efbf404aff5f181a2
diff --git a/core/java/android/app/servertransaction/ActivityLifecycleItem.java b/core/java/android/app/servertransaction/ActivityLifecycleItem.java
index 06bff5d..48db18f 100644
--- a/core/java/android/app/servertransaction/ActivityLifecycleItem.java
+++ b/core/java/android/app/servertransaction/ActivityLifecycleItem.java
@@ -59,7 +59,7 @@
     }
 
     @Override
-    boolean isActivityLifecycleItem() {
+    public boolean isActivityLifecycleItem() {
         return true;
     }
 
diff --git a/core/java/android/app/servertransaction/ClientTransactionItem.java b/core/java/android/app/servertransaction/ClientTransactionItem.java
index f94e22d..a8d61db 100644
--- a/core/java/android/app/servertransaction/ClientTransactionItem.java
+++ b/core/java/android/app/servertransaction/ClientTransactionItem.java
@@ -75,7 +75,7 @@
     /**
      * Whether this is a {@link ActivityLifecycleItem}.
      */
-    boolean isActivityLifecycleItem() {
+    public boolean isActivityLifecycleItem() {
         return false;
     }
 
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
index 443dcb4..2315a58 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
@@ -31,6 +31,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -59,6 +60,8 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 import java.util.Arrays;
 import java.util.Collections;
@@ -80,20 +83,32 @@
 @Presubmit
 public class TransactionExecutorTests {
 
+    @Mock
+    private ClientTransactionHandler mTransactionHandler;
+    @Mock
+    private ActivityLifecycleItem mActivityLifecycleItem;
+    @Mock
+    private IBinder mActivityToken;
+    @Mock
+    private Activity mActivity;
+
     private TransactionExecutor mExecutor;
     private TransactionExecutorHelper mExecutorHelper;
-    private ClientTransactionHandler mTransactionHandler;
     private ActivityClientRecord mClientRecord;
 
     @Before
     public void setUp() throws Exception {
-        mTransactionHandler = mock(ClientTransactionHandler.class);
+        MockitoAnnotations.initMocks(this);
 
         mClientRecord = new ActivityClientRecord();
         when(mTransactionHandler.getActivityClient(any())).thenReturn(mClientRecord);
 
         mExecutor = spy(new TransactionExecutor(mTransactionHandler));
         mExecutorHelper = new TransactionExecutorHelper();
+
+        doReturn(true).when(mActivityLifecycleItem).isActivityLifecycleItem();
+        doReturn(mActivityToken).when(mActivityLifecycleItem).getActivityToken();
+        doReturn(mActivity).when(mTransactionHandler).getActivity(mActivityToken);
     }
 
     @Test
@@ -229,23 +244,21 @@
         when(callback1.getPostExecutionState()).thenReturn(UNDEFINED);
         ClientTransactionItem callback2 = mock(ClientTransactionItem.class);
         when(callback2.getPostExecutionState()).thenReturn(UNDEFINED);
-        ActivityLifecycleItem stateRequest = mock(ActivityLifecycleItem.class);
-        IBinder token = mock(IBinder.class);
-        when(stateRequest.getActivityToken()).thenReturn(token);
-        when(mTransactionHandler.getActivity(token)).thenReturn(mock(Activity.class));
 
         ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
         transaction.addCallback(callback1);
         transaction.addCallback(callback2);
-        transaction.setLifecycleStateRequest(stateRequest);
+        transaction.setLifecycleStateRequest(mActivityLifecycleItem);
 
         transaction.preExecute(mTransactionHandler);
         mExecutor.execute(transaction);
 
-        InOrder inOrder = inOrder(mTransactionHandler, callback1, callback2, stateRequest);
+        InOrder inOrder = inOrder(mTransactionHandler, callback1, callback2,
+                mActivityLifecycleItem);
         inOrder.verify(callback1).execute(eq(mTransactionHandler), any());
         inOrder.verify(callback2).execute(eq(mTransactionHandler), any());
-        inOrder.verify(stateRequest).execute(eq(mTransactionHandler), eq(mClientRecord), any());
+        inOrder.verify(mActivityLifecycleItem).execute(eq(mTransactionHandler), eq(mClientRecord),
+                any());
     }
 
     @Test
@@ -254,23 +267,21 @@
         when(callback1.getPostExecutionState()).thenReturn(UNDEFINED);
         ClientTransactionItem callback2 = mock(ClientTransactionItem.class);
         when(callback2.getPostExecutionState()).thenReturn(UNDEFINED);
-        ActivityLifecycleItem stateRequest = mock(ActivityLifecycleItem.class);
-        IBinder token = mock(IBinder.class);
-        when(stateRequest.getActivityToken()).thenReturn(token);
-        when(mTransactionHandler.getActivity(token)).thenReturn(mock(Activity.class));
 
         ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
         transaction.addTransactionItem(callback1);
         transaction.addTransactionItem(callback2);
-        transaction.addTransactionItem(stateRequest);
+        transaction.addTransactionItem(mActivityLifecycleItem);
 
         transaction.preExecute(mTransactionHandler);
         mExecutor.execute(transaction);
 
-        InOrder inOrder = inOrder(mTransactionHandler, callback1, callback2, stateRequest);
+        InOrder inOrder = inOrder(mTransactionHandler, callback1, callback2,
+                mActivityLifecycleItem);
         inOrder.verify(callback1).execute(eq(mTransactionHandler), any());
         inOrder.verify(callback2).execute(eq(mTransactionHandler), any());
-        inOrder.verify(stateRequest).execute(eq(mTransactionHandler), eq(mClientRecord), any());
+        inOrder.verify(mActivityLifecycleItem).execute(eq(mTransactionHandler), eq(mClientRecord),
+                any());
     }
 
     @Test
@@ -536,42 +547,36 @@
 
     @Test
     public void testActivityItemExecute() {
-        final IBinder token = mock(IBinder.class);
         final ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
         final ActivityTransactionItem activityItem = mock(ActivityTransactionItem.class);
         when(activityItem.getPostExecutionState()).thenReturn(UNDEFINED);
-        when(activityItem.getActivityToken()).thenReturn(token);
+        when(activityItem.getActivityToken()).thenReturn(mActivityToken);
         transaction.addCallback(activityItem);
-        final ActivityLifecycleItem stateRequest = mock(ActivityLifecycleItem.class);
-        transaction.setLifecycleStateRequest(stateRequest);
-        when(stateRequest.getActivityToken()).thenReturn(token);
-        when(mTransactionHandler.getActivity(token)).thenReturn(mock(Activity.class));
+        transaction.setLifecycleStateRequest(mActivityLifecycleItem);
 
         mExecutor.execute(transaction);
 
-        final InOrder inOrder = inOrder(activityItem, stateRequest);
+        final InOrder inOrder = inOrder(activityItem, mActivityLifecycleItem);
         inOrder.verify(activityItem).execute(eq(mTransactionHandler), eq(mClientRecord), any());
-        inOrder.verify(stateRequest).execute(eq(mTransactionHandler), eq(mClientRecord), any());
+        inOrder.verify(mActivityLifecycleItem).execute(eq(mTransactionHandler), eq(mClientRecord),
+                any());
     }
 
     @Test
     public void testExecuteTransactionItems_activityItemExecute() {
-        final IBinder token = mock(IBinder.class);
         final ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
         final ActivityTransactionItem activityItem = mock(ActivityTransactionItem.class);
         when(activityItem.getPostExecutionState()).thenReturn(UNDEFINED);
-        when(activityItem.getActivityToken()).thenReturn(token);
+        when(activityItem.getActivityToken()).thenReturn(mActivityToken);
         transaction.addTransactionItem(activityItem);
-        final ActivityLifecycleItem stateRequest = mock(ActivityLifecycleItem.class);
-        transaction.addTransactionItem(stateRequest);
-        when(stateRequest.getActivityToken()).thenReturn(token);
-        when(mTransactionHandler.getActivity(token)).thenReturn(mock(Activity.class));
+        transaction.addTransactionItem(mActivityLifecycleItem);
 
         mExecutor.execute(transaction);
 
-        final InOrder inOrder = inOrder(activityItem, stateRequest);
+        final InOrder inOrder = inOrder(activityItem, mActivityLifecycleItem);
         inOrder.verify(activityItem).execute(eq(mTransactionHandler), eq(mClientRecord), any());
-        inOrder.verify(stateRequest).execute(eq(mTransactionHandler), eq(mClientRecord), any());
+        inOrder.verify(mActivityLifecycleItem).execute(eq(mTransactionHandler), eq(mClientRecord),
+                any());
     }
 
     private static int[] shuffledArray(int[] inputArray) {
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index 7b399c8..faccca8 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -70,7 +70,6 @@
 import android.app.PictureInPictureParams;
 import android.app.PictureInPictureUiState;
 import android.app.compat.CompatChanges;
-import android.app.servertransaction.ClientTransaction;
 import android.app.servertransaction.EnterPipRequestedItem;
 import android.app.servertransaction.PipStateTransactionItem;
 import android.compat.annotation.ChangeId;
@@ -1018,7 +1017,7 @@
         }
 
         try {
-            mService.getLifecycleManager().scheduleTransaction(r.app.getThread(),
+            mService.getLifecycleManager().scheduleTransactionItem(r.app.getThread(),
                     EnterPipRequestedItem.obtain(r.token));
             return true;
         } catch (Exception e) {
@@ -1038,9 +1037,8 @@
         }
 
         try {
-            final ClientTransaction transaction = ClientTransaction.obtain(r.app.getThread());
-            transaction.addCallback(PipStateTransactionItem.obtain(r.token, pipState));
-            mService.getLifecycleManager().scheduleTransaction(transaction);
+            mService.getLifecycleManager().scheduleTransactionItem(r.app.getThread(),
+                    PipStateTransactionItem.obtain(r.token, pipState));
         } catch (Exception e) {
             Slog.w(TAG, "Failed to send pip state transaction item: "
                     + r.intent.getComponent(), e);
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index eeeca10..1f905ea 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -277,7 +277,6 @@
 import android.app.servertransaction.ActivityLifecycleItem;
 import android.app.servertransaction.ActivityRelaunchItem;
 import android.app.servertransaction.ActivityResultItem;
-import android.app.servertransaction.ClientTransaction;
 import android.app.servertransaction.ClientTransactionItem;
 import android.app.servertransaction.DestroyActivityItem;
 import android.app.servertransaction.MoveToDisplayItem;
@@ -1449,7 +1448,7 @@
                     + "display, activityRecord=%s, displayId=%d, config=%s", this, displayId,
                     config);
 
-            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(),
+            mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(),
                     MoveToDisplayItem.obtain(token, displayId, config));
         } catch (RemoteException e) {
             // If process died, whatever.
@@ -1466,7 +1465,7 @@
             ProtoLog.v(WM_DEBUG_CONFIGURATION, "Sending new config to %s, "
                     + "config: %s", this, config);
 
-            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(),
+            mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(),
                     ActivityConfigurationChangeItem.obtain(token, config));
         } catch (RemoteException e) {
             // If process died, whatever.
@@ -1487,7 +1486,7 @@
             ProtoLog.v(WM_DEBUG_STATES, "Sending position change to %s, onTop: %b",
                     this, onTop);
 
-            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(),
+            mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(),
                     TopResumedActivityChangeItem.obtain(token, onTop));
         } catch (RemoteException e) {
             // If process died, whatever.
@@ -2744,7 +2743,7 @@
         }
         try {
             mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_ATTACH_TO_CLIENT;
-            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(),
+            mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(),
                     TransferSplashScreenViewStateItem.obtain(token, parcelable,
                             windowAnimationLeash));
             scheduleTransferSplashScreenTimeout();
@@ -3908,7 +3907,7 @@
 
             try {
                 if (DEBUG_SWITCH) Slog.i(TAG_SWITCH, "Destroying: " + this);
-                mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(),
+                mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(),
                         DestroyActivityItem.obtain(token, finishing, configChangeFlags));
             } catch (Exception e) {
                 // We can just ignore exceptions here...  if the process has crashed, our death
@@ -4819,7 +4818,7 @@
             try {
                 final ArrayList<ResultInfo> list = new ArrayList<>();
                 list.add(new ResultInfo(resultWho, requestCode, resultCode, data));
-                mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(),
+                mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(),
                         ActivityResultItem.obtain(token, list));
                 return;
             } catch (Exception e) {
@@ -4830,22 +4829,23 @@
         // Schedule sending results now for Media Projection setup.
         if (forceSendForMediaProjection && attachedToProcess() && isState(STARTED, PAUSING, PAUSED,
                 STOPPING, STOPPED)) {
-            final ClientTransaction transaction = ClientTransaction.obtain(app.getThread());
             // Build result to be returned immediately.
-            transaction.addCallback(ActivityResultItem.obtain(
-                    token, List.of(new ResultInfo(resultWho, requestCode, resultCode, data))));
+            final ActivityResultItem activityResultItem = ActivityResultItem.obtain(
+                    token, List.of(new ResultInfo(resultWho, requestCode, resultCode, data)));
             // When the activity result is delivered, the activity will transition to RESUMED.
             // Since the activity is only resumed so the result can be immediately delivered,
             // return it to its original lifecycle state.
-            ActivityLifecycleItem lifecycleItem = getLifecycleItemForCurrentStateForResult();
-            if (lifecycleItem != null) {
-                transaction.setLifecycleStateRequest(lifecycleItem);
-            } else {
-                Slog.w(TAG, "Unable to get the lifecycle item for state " + mState
-                        + " so couldn't immediately send result");
-            }
+            final ActivityLifecycleItem lifecycleItem = getLifecycleItemForCurrentStateForResult();
             try {
-                mAtmService.getLifecycleManager().scheduleTransaction(transaction);
+                if (lifecycleItem != null) {
+                    mAtmService.getLifecycleManager().scheduleTransactionAndLifecycleItems(
+                            app.getThread(), activityResultItem, lifecycleItem);
+                } else {
+                    Slog.w(TAG, "Unable to get the lifecycle item for state " + mState
+                            + " so couldn't immediately send result");
+                    mAtmService.getLifecycleManager().scheduleTransactionItem(
+                            app.getThread(), activityResultItem);
+                }
             } catch (RemoteException e) {
                 Slog.w(TAG, "Exception thrown sending result to " + this, e);
             }
@@ -4925,7 +4925,7 @@
                 // Making sure the client state is RESUMED after transaction completed and doing
                 // so only if activity is currently RESUMED. Otherwise, client may have extra
                 // life-cycle calls to RESUMED (and PAUSED later).
-                mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(),
+                mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(),
                         NewIntentItem.obtain(token, ar, mState == RESUMED));
                 unsent = false;
             } catch (RemoteException e) {
@@ -6150,7 +6150,7 @@
             EventLogTags.writeWmPauseActivity(mUserId, System.identityHashCode(this),
                     shortComponentName, "userLeaving=false", "make-active");
             try {
-                mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(),
+                mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(),
                         PauseActivityItem.obtain(token, finishing, false /* userLeaving */,
                                 configChangeFlags, false /* dontReport */, mAutoEnteringPip));
             } catch (Exception e) {
@@ -6163,7 +6163,7 @@
             setState(STARTED, "makeActiveIfNeeded");
 
             try {
-                mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(),
+                mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(),
                         StartActivityItem.obtain(token, takeOptions()));
             } catch (Exception e) {
                 Slog.w(TAG, "Exception thrown sending start: " + intent.getComponent(), e);
@@ -6461,7 +6461,7 @@
             }
             EventLogTags.writeWmStopActivity(
                     mUserId, System.identityHashCode(this), shortComponentName);
-            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(),
+            mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(),
                     StopActivityItem.obtain(token, configChangeFlags));
 
             mAtmService.mH.postDelayed(mStopTimeoutRunnable, STOP_TIMEOUT);
@@ -9895,10 +9895,8 @@
             } else {
                 lifecycleItem = PauseActivityItem.obtain(token);
             }
-            final ClientTransaction transaction = ClientTransaction.obtain(app.getThread());
-            transaction.addCallback(callbackItem);
-            transaction.setLifecycleStateRequest(lifecycleItem);
-            mAtmService.getLifecycleManager().scheduleTransaction(transaction);
+            mAtmService.getLifecycleManager().scheduleTransactionAndLifecycleItems(
+                    app.getThread(), callbackItem, lifecycleItem);
             startRelaunching();
             // Note: don't need to call pauseIfSleepingLocked() here, because the caller will only
             // request resume if this activity is currently resumed, which implies we aren't
@@ -9990,7 +9988,7 @@
         // The process will be killed until the activity reports stopped with saved state (see
         // {@link ActivityTaskManagerService.activityStopped}).
         try {
-            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(),
+            mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(),
                     StopActivityItem.obtain(token, 0 /* configChanges */));
         } catch (RemoteException e) {
             Slog.w(TAG, "Exception thrown during restart " + this, e);
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index e196d46..ce8d051 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -98,7 +98,6 @@
 import android.app.TaskInfo;
 import android.app.WaitResult;
 import android.app.servertransaction.ActivityLifecycleItem;
-import android.app.servertransaction.ClientTransaction;
 import android.app.servertransaction.LaunchActivityItem;
 import android.app.servertransaction.PauseActivityItem;
 import android.app.servertransaction.ResumeActivityItem;
@@ -928,14 +927,10 @@
                 }
 
                 // Create activity launch transaction.
-                final ClientTransaction clientTransaction = ClientTransaction.obtain(
-                        proc.getThread());
-
                 final boolean isTransitionForward = r.isTransitionForward();
                 final IBinder fragmentToken = r.getTaskFragment().getFragmentToken();
-
                 final int deviceId = getDeviceIdForDisplayId(r.getDisplayId());
-                clientTransaction.addCallback(LaunchActivityItem.obtain(r.token,
+                final LaunchActivityItem launchActivityItem = LaunchActivityItem.obtain(r.token,
                         r.intent, System.identityHashCode(r), r.info,
                         // TODO: Have this take the merged configuration instead of separate global
                         // and override configs.
@@ -945,7 +940,7 @@
                         proc.getReportedProcState(), r.getSavedState(), r.getPersistentSavedState(),
                         results, newIntents, r.takeOptions(), isTransitionForward,
                         proc.createProfilerInfoIfNeeded(), r.assistToken, activityClientController,
-                        r.shareableActivityToken, r.getLaunchedFromBubble(), fragmentToken));
+                        r.shareableActivityToken, r.getLaunchedFromBubble(), fragmentToken);
 
                 // Set desired final state.
                 final ActivityLifecycleItem lifecycleItem;
@@ -955,10 +950,10 @@
                 } else {
                     lifecycleItem = PauseActivityItem.obtain(r.token);
                 }
-                clientTransaction.setLifecycleStateRequest(lifecycleItem);
 
                 // Schedule transaction.
-                mService.getLifecycleManager().scheduleTransaction(clientTransaction);
+                mService.getLifecycleManager().scheduleTransactionAndLifecycleItems(
+                        proc.getThread(), launchActivityItem, lifecycleItem);
 
                 if (procConfig.seq > mRootWindowContainer.getConfiguration().seq) {
                     // If the seq is increased, there should be something changed (e.g. registered
diff --git a/services/core/java/com/android/server/wm/ClientLifecycleManager.java b/services/core/java/com/android/server/wm/ClientLifecycleManager.java
index ef31837..28f656e 100644
--- a/services/core/java/com/android/server/wm/ClientLifecycleManager.java
+++ b/services/core/java/com/android/server/wm/ClientLifecycleManager.java
@@ -40,35 +40,51 @@
      * @throws RemoteException
      *
      * @see ClientTransaction
+     * @deprecated use {@link #scheduleTransactionItem(IApplicationThread, ClientTransactionItem)}.
      */
+    @Deprecated
     void scheduleTransaction(@NonNull ClientTransaction transaction) throws RemoteException {
         final IApplicationThread client = transaction.getClient();
-        transaction.schedule();
-        if (!(client instanceof Binder)) {
-            // If client is not an instance of Binder - it's a remote call and at this point it is
-            // safe to recycle the object. All objects used for local calls will be recycled after
-            // the transaction is executed on client in ActivityThread.
-            transaction.recycle();
+        try {
+            transaction.schedule();
+        } finally {
+            if (!(client instanceof Binder)) {
+                // If client is not an instance of Binder - it's a remote call and at this point it
+                // is safe to recycle the object. All objects used for local calls will be recycled
+                // after the transaction is executed on client in ActivityThread.
+                transaction.recycle();
+            }
         }
     }
 
     /**
      * Schedules a single transaction item, either a callback or a lifecycle request, delivery to
      * client application.
-     * @param client Target client.
-     * @param transactionItem A transaction item to deliver a message.
      * @throws RemoteException
-     *
      * @see ClientTransactionItem
      */
-    void scheduleTransaction(@NonNull IApplicationThread client,
+    void scheduleTransactionItem(@NonNull IApplicationThread client,
             @NonNull ClientTransactionItem transactionItem) throws RemoteException {
         final ClientTransaction clientTransaction = ClientTransaction.obtain(client);
-        if (transactionItem instanceof ActivityLifecycleItem) {
+        if (transactionItem.isActivityLifecycleItem()) {
             clientTransaction.setLifecycleStateRequest((ActivityLifecycleItem) transactionItem);
         } else {
             clientTransaction.addCallback(transactionItem);
         }
         scheduleTransaction(clientTransaction);
     }
+
+    /**
+     * Schedules a single transaction item with a lifecycle request, delivery to client application.
+     * @throws RemoteException
+     * @see ClientTransactionItem
+     */
+    void scheduleTransactionAndLifecycleItems(@NonNull IApplicationThread client,
+            @NonNull ClientTransactionItem transactionItem,
+            @NonNull ActivityLifecycleItem lifecycleItem) throws RemoteException {
+        final ClientTransaction clientTransaction = ClientTransaction.obtain(client);
+        clientTransaction.addCallback(transactionItem);
+        clientTransaction.setLifecycleStateRequest(lifecycleItem);
+        scheduleTransaction(clientTransaction);
+    }
 }
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index 534cdc2..e808dec 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -38,7 +38,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.StringRes;
-import android.app.servertransaction.ClientTransaction;
 import android.app.servertransaction.RefreshCallbackItem;
 import android.app.servertransaction.ResumeActivityItem;
 import android.content.pm.ActivityInfo.ScreenOrientation;
@@ -226,13 +225,12 @@
             ProtoLog.v(WM_DEBUG_STATES,
                     "Refreshing activity for camera compatibility treatment, "
                             + "activityRecord=%s", activity);
-            final ClientTransaction transaction = ClientTransaction.obtain(
-                    activity.app.getThread());
-            transaction.addCallback(RefreshCallbackItem.obtain(activity.token,
-                            cycleThroughStop ? ON_STOP : ON_PAUSE));
-            transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(activity.token,
-                    /* isForward */ false, /* shouldSendCompatFakeFocus */ false));
-            activity.mAtmService.getLifecycleManager().scheduleTransaction(transaction);
+            final RefreshCallbackItem refreshCallbackItem = RefreshCallbackItem.obtain(
+                    activity.token, cycleThroughStop ? ON_STOP : ON_PAUSE);
+            final ResumeActivityItem resumeActivityItem = ResumeActivityItem.obtain(
+                    activity.token, /* isForward */ false, /* shouldSendCompatFakeFocus */ false);
+            activity.mAtmService.getLifecycleManager().scheduleTransactionAndLifecycleItems(
+                    activity.app.getThread(), refreshCallbackItem, resumeActivityItem);
             mHandler.postDelayed(
                     () -> onActivityRefreshed(activity),
                     REFRESH_CALLBACK_TIMEOUT_MS);
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 197edc3..8bc461f 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -1793,7 +1793,7 @@
             EventLogTags.writeWmPauseActivity(prev.mUserId, System.identityHashCode(prev),
                     prev.shortComponentName, "userLeaving=" + userLeaving, reason);
 
-            mAtmService.getLifecycleManager().scheduleTransaction(prev.app.getThread(),
+            mAtmService.getLifecycleManager().scheduleTransactionItem(prev.app.getThread(),
                     PauseActivityItem.obtain(prev.token, prev.finishing, userLeaving,
                             prev.configChangeFlags, pauseImmediately, autoEnteringPip));
         } catch (Exception e) {
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index a74a707..192d960 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -1666,7 +1666,7 @@
     private void scheduleClientTransactionItem(@NonNull IApplicationThread thread,
             @NonNull ClientTransactionItem transactionItem) {
         try {
-            mAtm.getLifecycleManager().scheduleTransaction(thread, transactionItem);
+            mAtm.getLifecycleManager().scheduleTransactionItem(thread, transactionItem);
         } catch (Exception e) {
             Slog.e(TAG_CONFIGURATION, "Failed to schedule ClientTransactionItem="
                     + transactionItem + " owner=" + mOwner, e);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 1776ba5..786432a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -490,7 +490,7 @@
         ensureActivityConfiguration(activity);
 
         verify(mClientLifecycleManager, never())
-                .scheduleTransaction(any(), isA(ActivityConfigurationChangeItem.class));
+                .scheduleTransactionItem(any(), isA(ActivityConfigurationChangeItem.class));
     }
 
     @Test
@@ -519,7 +519,7 @@
         // The configuration change is still sent to the activity, even if it doesn't relaunch.
         final ActivityConfigurationChangeItem expected =
                 ActivityConfigurationChangeItem.obtain(activity.token, newConfig);
-        verify(mClientLifecycleManager).scheduleTransaction(
+        verify(mClientLifecycleManager).scheduleTransactionItem(
                 eq(activity.app.getThread()), eq(expected));
     }
 
@@ -592,7 +592,7 @@
         assertEquals(expectedOrientation, currentConfig.orientation);
         final ActivityConfigurationChangeItem expected =
                 ActivityConfigurationChangeItem.obtain(activity.token, currentConfig);
-        verify(mClientLifecycleManager).scheduleTransaction(activity.app.getThread(), expected);
+        verify(mClientLifecycleManager).scheduleTransactionItem(activity.app.getThread(), expected);
         verify(displayRotation).onSetRequestedOrientation();
     }
 
@@ -812,7 +812,8 @@
             final ActivityConfigurationChangeItem expected =
                     ActivityConfigurationChangeItem.obtain(activity.token,
                             activity.getConfiguration());
-            verify(mClientLifecycleManager).scheduleTransaction(activity.app.getThread(), expected);
+            verify(mClientLifecycleManager).scheduleTransactionItem(
+                    activity.app.getThread(), expected);
         } finally {
             stack.getDisplayArea().removeChild(stack);
         }
@@ -1785,7 +1786,7 @@
             clearInvocations(mClientLifecycleManager);
             activity.getTask().removeImmediately("test");
             try {
-                verify(mClientLifecycleManager).scheduleTransaction(any(),
+                verify(mClientLifecycleManager).scheduleTransactionItem(any(),
                         isA(DestroyActivityItem.class));
             } catch (RemoteException ignored) {
             }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
index 3c027ff..d2c731c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
@@ -127,7 +127,7 @@
 
         final ArgumentCaptor<ClientTransactionItem> clientTransactionItemCaptor =
                 ArgumentCaptor.forClass(ClientTransactionItem.class);
-        verify(mockLifecycleManager).scheduleTransaction(any(),
+        verify(mockLifecycleManager).scheduleTransactionItem(any(),
                 clientTransactionItemCaptor.capture());
         final ClientTransactionItem transactionItem = clientTransactionItemCaptor.getValue();
         // Check that only an enter pip request item callback was scheduled.
@@ -144,7 +144,7 @@
 
         mAtm.mActivityClientController.requestPictureInPictureMode(activity);
 
-        verify(mClientLifecycleManager, never()).scheduleTransaction(any(), any());
+        verify(mClientLifecycleManager, never()).scheduleTransactionItem(any(), any());
     }
 
     @Test
@@ -156,7 +156,7 @@
 
         mAtm.mActivityClientController.requestPictureInPictureMode(activity);
 
-        verify(mClientLifecycleManager, never()).scheduleTransaction(any(), any());
+        verify(mClientLifecycleManager, never()).scheduleTransactionItem(any(), any());
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java b/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java
index a18dbaf..04aa981 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java
@@ -16,18 +16,32 @@
 
 package com.android.server.wm;
 
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+
 import android.app.IApplicationThread;
+import android.app.servertransaction.ActivityLifecycleItem;
 import android.app.servertransaction.ClientTransaction;
+import android.app.servertransaction.ClientTransactionItem;
+import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.filters.SmallTest;
 
+import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 /**
  * Build/Install/Run:
@@ -37,23 +51,77 @@
 @Presubmit
 public class ClientLifecycleManagerTests {
 
-    @Test
-    public void testScheduleAndRecycleBinderClientTransaction() throws Exception {
-        ClientTransaction item = spy(ClientTransaction.obtain(mock(IApplicationThread.class)));
+    @Mock
+    private IApplicationThread mClient;
+    @Mock
+    private IApplicationThread.Stub mNonBinderClient;
+    @Mock
+    private ClientTransactionItem mTransactionItem;
+    @Mock
+    private ActivityLifecycleItem mLifecycleItem;
+    @Captor
+    private ArgumentCaptor<ClientTransaction> mTransactionCaptor;
 
-        ClientLifecycleManager clientLifecycleManager = new ClientLifecycleManager();
-        clientLifecycleManager.scheduleTransaction(item);
+    private ClientLifecycleManager mLifecycleManager;
 
-        verify(item, times(1)).recycle();
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        mLifecycleManager = spy(new ClientLifecycleManager());
+
+        doReturn(true).when(mLifecycleItem).isActivityLifecycleItem();
     }
 
     @Test
-    public void testScheduleNoRecycleNonBinderClientTransaction() throws Exception {
-        ClientTransaction item = spy(ClientTransaction.obtain(mock(IApplicationThread.Stub.class)));
+    public void testScheduleTransaction_recycleBinderClientTransaction() throws Exception {
+        final ClientTransaction item = spy(ClientTransaction.obtain(mClient));
 
-        ClientLifecycleManager clientLifecycleManager = new ClientLifecycleManager();
-        clientLifecycleManager.scheduleTransaction(item);
+        mLifecycleManager.scheduleTransaction(item);
 
-        verify(item, times(0)).recycle();
+        verify(item).recycle();
+    }
+
+    @Test
+    public void testScheduleTransaction_notRecycleNonBinderClientTransaction() throws Exception {
+        final ClientTransaction item = spy(ClientTransaction.obtain(mNonBinderClient));
+
+        mLifecycleManager.scheduleTransaction(item);
+
+        verify(item, never()).recycle();
+    }
+
+    @Test
+    public void testScheduleTransactionItem() throws RemoteException {
+        doNothing().when(mLifecycleManager).scheduleTransaction(any());
+        mLifecycleManager.scheduleTransactionItem(mClient, mTransactionItem);
+
+        verify(mLifecycleManager).scheduleTransaction(mTransactionCaptor.capture());
+        ClientTransaction transaction = mTransactionCaptor.getValue();
+        assertEquals(1, transaction.getCallbacks().size());
+        assertEquals(mTransactionItem, transaction.getCallbacks().get(0));
+        assertNull(transaction.getLifecycleStateRequest());
+        assertNull(transaction.getTransactionItems());
+
+        clearInvocations(mLifecycleManager);
+        mLifecycleManager.scheduleTransactionItem(mClient, mLifecycleItem);
+
+        verify(mLifecycleManager).scheduleTransaction(mTransactionCaptor.capture());
+        transaction = mTransactionCaptor.getValue();
+        assertNull(transaction.getCallbacks());
+        assertEquals(mLifecycleItem, transaction.getLifecycleStateRequest());
+    }
+
+    @Test
+    public void testScheduleTransactionAndLifecycleItems() throws RemoteException {
+        doNothing().when(mLifecycleManager).scheduleTransaction(any());
+        mLifecycleManager.scheduleTransactionAndLifecycleItems(mClient, mTransactionItem,
+                mLifecycleItem);
+
+        verify(mLifecycleManager).scheduleTransaction(mTransactionCaptor.capture());
+        final ClientTransaction transaction = mTransactionCaptor.getValue();
+        assertEquals(1, transaction.getCallbacks().size());
+        assertEquals(mTransactionItem, transaction.getCallbacks().get(0));
+        assertEquals(mLifecycleItem, transaction.getLifecycleStateRequest());
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
index 2af6745..e7ac33f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
@@ -43,12 +43,9 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
 
-import android.app.servertransaction.ClientTransaction;
 import android.app.servertransaction.RefreshCallbackItem;
 import android.app.servertransaction.ResumeActivityItem;
 import android.content.ComponentName;
@@ -529,8 +526,8 @@
     public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp()
             throws Exception {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
-        when(mActivity.mLetterboxUiController.shouldRefreshActivityViaPauseForCameraCompat())
-                .thenReturn(true);
+        doReturn(true).when(mActivity.mLetterboxUiController)
+                .shouldRefreshActivityViaPauseForCameraCompat();
 
         mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
         callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
@@ -571,14 +568,14 @@
         verify(mActivity.mLetterboxUiController, times(refreshRequested ? 1 : 0))
                 .setIsRefreshAfterRotationRequested(true);
 
-        final ClientTransaction transaction = ClientTransaction.obtain(mActivity.app.getThread());
-        transaction.addCallback(RefreshCallbackItem.obtain(mActivity.token,
-                cycleThroughStop ? ON_STOP : ON_PAUSE));
-        transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(mActivity.token,
-                /* isForward */ false, /* shouldSendCompatFakeFocus */ false));
+        final RefreshCallbackItem refreshCallbackItem = RefreshCallbackItem.obtain(mActivity.token,
+                cycleThroughStop ? ON_STOP : ON_PAUSE);
+        final ResumeActivityItem resumeActivityItem = ResumeActivityItem.obtain(mActivity.token,
+                /* isForward */ false, /* shouldSendCompatFakeFocus */ false);
 
         verify(mActivity.mAtmService.getLifecycleManager(), times(refreshRequested ? 1 : 0))
-                .scheduleTransaction(eq(transaction));
+                .scheduleTransactionAndLifecycleItems(mActivity.app.getThread(),
+                        refreshCallbackItem, resumeActivityItem);
     }
 
     private void assertNoForceRotationOrRefresh() throws Exception {
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 46cff8b..e152feb 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).scheduleTransaction(any(), any());
+        doNothing().when(mClientLifecycleManager).scheduleTransactionItem(any(), any());
         final IApplicationThread thread = mWpc.getThread();
         final Configuration newConfig = new Configuration(mWpc.getConfiguration());
         newConfig.densityDpi += 100;
@@ -314,20 +314,20 @@
         mWpc.setReportedProcState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
         clearInvocations(mClientLifecycleManager);
         mWpc.onConfigurationChanged(newConfig);
-        verify(mClientLifecycleManager).scheduleTransaction(eq(thread), any());
+        verify(mClientLifecycleManager).scheduleTransactionItem(eq(thread), any());
 
         // Cached state won't send the change.
         clearInvocations(mClientLifecycleManager);
         mWpc.setReportedProcState(ActivityManager.PROCESS_STATE_CACHED_ACTIVITY);
         newConfig.densityDpi += 100;
         mWpc.onConfigurationChanged(newConfig);
-        verify(mClientLifecycleManager, never()).scheduleTransaction(eq(thread), any());
+        verify(mClientLifecycleManager, never()).scheduleTransactionItem(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).scheduleTransaction(eq(thread), captor.capture());
+        verify(mClientLifecycleManager).scheduleTransactionItem(eq(thread), captor.capture());
         final ClientTransactionHandler client = mock(ClientTransactionHandler.class);
         captor.getValue().preExecute(client);
         final ArgumentCaptor<Configuration> configCaptor =