Merge "Migrate WindowContext#onConfigurationChanged to ClientTransaction (2/n)" into main
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 853528f..12ffdb3 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -65,6 +65,7 @@
 import android.app.servertransaction.ResumeActivityItem;
 import android.app.servertransaction.TransactionExecutor;
 import android.app.servertransaction.TransactionExecutorHelper;
+import android.app.servertransaction.WindowTokenClientController;
 import android.bluetooth.BluetoothFrameworkInitializer;
 import android.companion.virtual.VirtualDeviceManager;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -6221,6 +6222,18 @@
                 false /* clearPending */);
     }
 
+    @Override
+    public void handleWindowContextConfigurationChanged(@NonNull IBinder clientToken,
+            @NonNull Configuration configuration, int displayId) {
+        WindowTokenClientController.getInstance().onWindowContextConfigurationChanged(clientToken,
+                configuration, displayId);
+    }
+
+    @Override
+    public void handleWindowContextWindowRemoval(@NonNull IBinder clientToken) {
+        WindowTokenClientController.getInstance().onWindowContextWindowRemoved(clientToken);
+    }
+
     /**
      * Sends windowing mode change callbacks to {@link Activity} if applicable.
      *
diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java
index 49fb794..f7a43f4 100644
--- a/core/java/android/app/ClientTransactionHandler.java
+++ b/core/java/android/app/ClientTransactionHandler.java
@@ -163,6 +163,13 @@
     public abstract void handleActivityConfigurationChanged(@NonNull ActivityClientRecord r,
             Configuration overrideConfig, int displayId);
 
+    /** Deliver {@link android.window.WindowContext} configuration change. */
+    public abstract void handleWindowContextConfigurationChanged(@NonNull IBinder clientToken,
+            @NonNull Configuration configuration, int displayId);
+
+    /** Deliver {@link android.window.WindowContext} window removal event. */
+    public abstract void handleWindowContextWindowRemoval(@NonNull IBinder clientToken);
+
     /** Deliver result from another activity. */
     public abstract void handleSendResult(
             @NonNull ActivityClientRecord r, List<ResultInfo> results, String reason);
diff --git a/core/java/android/app/servertransaction/WindowContextConfigurationChangeItem.java b/core/java/android/app/servertransaction/WindowContextConfigurationChangeItem.java
new file mode 100644
index 0000000..3ac642f
--- /dev/null
+++ b/core/java/android/app/servertransaction/WindowContextConfigurationChangeItem.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.servertransaction;
+
+import static android.view.Display.INVALID_DISPLAY;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ClientTransactionHandler;
+import android.content.res.Configuration;
+import android.os.IBinder;
+import android.os.Parcel;
+
+import java.util.Objects;
+
+/**
+ * {@link android.window.WindowContext} configuration change message.
+ * @hide
+ */
+public class WindowContextConfigurationChangeItem extends ClientTransactionItem {
+
+    @Nullable
+    private IBinder mClientToken;
+    @Nullable
+    private Configuration mConfiguration;
+    private int mDisplayId;
+
+    @Override
+    public void execute(@NonNull ClientTransactionHandler client, @NonNull IBinder token,
+            @NonNull PendingTransactionActions pendingActions) {
+        client.handleWindowContextConfigurationChanged(mClientToken, mConfiguration, mDisplayId);
+    }
+
+    // ObjectPoolItem implementation
+
+    private WindowContextConfigurationChangeItem() {}
+
+    /** Obtains an instance initialized with provided params. */
+    public static WindowContextConfigurationChangeItem obtain(
+            @NonNull IBinder clientToken, @NonNull Configuration config, int displayId) {
+        WindowContextConfigurationChangeItem instance =
+                ObjectPool.obtain(WindowContextConfigurationChangeItem.class);
+        if (instance == null) {
+            instance = new WindowContextConfigurationChangeItem();
+        }
+        instance.mClientToken = requireNonNull(clientToken);
+        instance.mConfiguration = requireNonNull(config);
+        instance.mDisplayId = displayId;
+
+        return instance;
+    }
+
+    @Override
+    public void recycle() {
+        mClientToken = null;
+        mConfiguration = null;
+        mDisplayId = INVALID_DISPLAY;
+        ObjectPool.recycle(this);
+    }
+
+    // Parcelable implementation
+
+    /** Writes to Parcel. */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeStrongBinder(mClientToken);
+        dest.writeTypedObject(mConfiguration, flags);
+        dest.writeInt(mDisplayId);
+    }
+
+    /** Reads from Parcel. */
+    private WindowContextConfigurationChangeItem(@NonNull Parcel in) {
+        mClientToken = in.readStrongBinder();
+        mConfiguration = in.readTypedObject(Configuration.CREATOR);
+        mDisplayId = in.readInt();
+    }
+
+    public static final @NonNull Creator<WindowContextConfigurationChangeItem> CREATOR =
+            new Creator<>() {
+                public WindowContextConfigurationChangeItem createFromParcel(Parcel in) {
+                    return new WindowContextConfigurationChangeItem(in);
+                }
+
+                public WindowContextConfigurationChangeItem[] newArray(int size) {
+                    return new WindowContextConfigurationChangeItem[size];
+                }
+    };
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        final WindowContextConfigurationChangeItem other = (WindowContextConfigurationChangeItem) o;
+        return Objects.equals(mClientToken, other.mClientToken)
+                && Objects.equals(mConfiguration, other.mConfiguration)
+                && mDisplayId == other.mDisplayId;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = 17;
+        result = 31 * result + Objects.hashCode(mClientToken);
+        result = 31 * result + Objects.hashCode(mConfiguration);
+        result = 31 * result + mDisplayId;
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "WindowContextConfigurationChangeItem{clientToken=" + mClientToken
+                + ", config=" + mConfiguration
+                + ", displayId=" + mDisplayId
+                + "}";
+    }
+}
diff --git a/core/java/android/app/servertransaction/WindowContextWindowRemovalItem.java b/core/java/android/app/servertransaction/WindowContextWindowRemovalItem.java
new file mode 100644
index 0000000..ed52a64
--- /dev/null
+++ b/core/java/android/app/servertransaction/WindowContextWindowRemovalItem.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.servertransaction;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ClientTransactionHandler;
+import android.os.IBinder;
+import android.os.Parcel;
+
+import java.util.Objects;
+
+/**
+ * {@link android.window.WindowContext} window removal message.
+ * @hide
+ */
+public class WindowContextWindowRemovalItem extends ClientTransactionItem {
+
+    @Nullable
+    private IBinder mClientToken;
+
+    @Override
+    public void execute(@NonNull ClientTransactionHandler client, @NonNull IBinder token,
+            @NonNull PendingTransactionActions pendingActions) {
+        client.handleWindowContextWindowRemoval(mClientToken);
+    }
+
+    // ObjectPoolItem implementation
+
+    private WindowContextWindowRemovalItem() {}
+
+    /** Obtains an instance initialized with provided params. */
+    public static WindowContextWindowRemovalItem obtain(@NonNull IBinder clientToken) {
+        WindowContextWindowRemovalItem instance =
+                ObjectPool.obtain(WindowContextWindowRemovalItem.class);
+        if (instance == null) {
+            instance = new WindowContextWindowRemovalItem();
+        }
+        instance.mClientToken = requireNonNull(clientToken);
+
+        return instance;
+    }
+
+    @Override
+    public void recycle() {
+        mClientToken = null;
+        ObjectPool.recycle(this);
+    }
+
+    // Parcelable implementation
+
+    /** Writes to Parcel. */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeStrongBinder(mClientToken);
+    }
+
+    /** Reads from Parcel. */
+    private WindowContextWindowRemovalItem(@NonNull Parcel in) {
+        mClientToken = in.readStrongBinder();
+    }
+
+    public static final @NonNull Creator<WindowContextWindowRemovalItem> CREATOR = new Creator<>() {
+        public WindowContextWindowRemovalItem createFromParcel(Parcel in) {
+            return new WindowContextWindowRemovalItem(in);
+        }
+
+        public WindowContextWindowRemovalItem[] newArray(int size) {
+            return new WindowContextWindowRemovalItem[size];
+        }
+    };
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        final WindowContextWindowRemovalItem other = (WindowContextWindowRemovalItem) o;
+        return Objects.equals(mClientToken, other.mClientToken);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = 17;
+        result = 31 * result + Objects.hashCode(mClientToken);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "WindowContextWindowRemovalItem{clientToken=" + mClientToken + "}";
+    }
+}
diff --git a/core/java/android/app/servertransaction/WindowTokenClientController.java b/core/java/android/app/servertransaction/WindowTokenClientController.java
index 28e2040..5d123a0 100644
--- a/core/java/android/app/servertransaction/WindowTokenClientController.java
+++ b/core/java/android/app/servertransaction/WindowTokenClientController.java
@@ -27,6 +27,7 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.ArrayMap;
+import android.util.Log;
 import android.view.IWindowManager;
 import android.window.WindowContext;
 import android.window.WindowTokenClient;
@@ -41,6 +42,7 @@
  */
 public class WindowTokenClientController {
 
+    private static final String TAG = WindowTokenClientController.class.getSimpleName();
     private static WindowTokenClientController sController;
 
     private final Object mLock = new Object();
@@ -61,7 +63,7 @@
 
     /** Overrides the {@link #getInstance()} for test only. */
     @VisibleForTesting
-    public static void overrideInstance(@NonNull WindowTokenClientController controller) {
+    public static void overrideForTesting(@NonNull WindowTokenClientController controller) {
         synchronized (WindowTokenClientController.class) {
             sController = controller;
         }
@@ -90,7 +92,7 @@
         if (configuration == null) {
             return false;
         }
-        onWindowContainerTokenAttached(client, displayId, configuration);
+        onWindowContextTokenAttached(client, displayId, configuration);
         return true;
     }
 
@@ -116,7 +118,7 @@
         if (configuration == null) {
             return false;
         }
-        onWindowContainerTokenAttached(client, displayId, configuration);
+        onWindowContextTokenAttached(client, displayId, configuration);
         return true;
     }
 
@@ -153,7 +155,7 @@
         }
     }
 
-    private void onWindowContainerTokenAttached(@NonNull WindowTokenClient client, int displayId,
+    private void onWindowContextTokenAttached(@NonNull WindowTokenClient client, int displayId,
             @NonNull Configuration configuration) {
         synchronized (mLock) {
             mWindowTokenClientMap.put(client.asBinder(), client);
@@ -161,4 +163,33 @@
         client.onConfigurationChanged(configuration, displayId,
                 false /* shouldReportConfigChange */);
     }
+
+    /** Called when receives {@link WindowContextConfigurationChangeItem}. */
+    public void onWindowContextConfigurationChanged(@NonNull IBinder clientToken,
+            @NonNull Configuration configuration, int displayId) {
+        final WindowTokenClient windowTokenClient = getWindowTokenClient(clientToken);
+        if (windowTokenClient != null) {
+            windowTokenClient.onConfigurationChanged(configuration, displayId);
+        }
+    }
+
+    /** Called when receives {@link WindowContextWindowRemovalItem}. */
+    public void onWindowContextWindowRemoved(@NonNull IBinder clientToken) {
+        final WindowTokenClient windowTokenClient = getWindowTokenClient(clientToken);
+        if (windowTokenClient != null) {
+            windowTokenClient.onWindowTokenRemoved();
+        }
+    }
+
+    @Nullable
+    private WindowTokenClient getWindowTokenClient(@NonNull IBinder clientToken) {
+        final WindowTokenClient windowTokenClient;
+        synchronized (mLock) {
+            windowTokenClient = mWindowTokenClientMap.get(clientToken);
+        }
+        if (windowTokenClient == null) {
+            Log.w(TAG, "Can't find attached WindowTokenClient for " + clientToken);
+        }
+        return windowTokenClient;
+    }
 }
diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java
index 99a4f6b..6aa8506 100644
--- a/core/java/android/view/WindowManagerGlobal.java
+++ b/core/java/android/view/WindowManagerGlobal.java
@@ -34,6 +34,7 @@
 import android.util.Log;
 import android.view.inputmethod.InputMethodManager;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.FastPrintWriter;
 
 import java.io.FileDescriptor;
@@ -184,6 +185,15 @@
         }
     }
 
+    /** Overrides the {@link #getWindowManagerService()} for test only. */
+    @VisibleForTesting
+    public static void overrideWindowManagerServiceForTesting(
+            @NonNull IWindowManager windowManager) {
+        synchronized (WindowManagerGlobal.class) {
+            sWindowManagerService = windowManager;
+        }
+    }
+
     @UnsupportedAppUsage
     public static IWindowSession getWindowSession() {
         synchronized (WindowManagerGlobal.class) {
diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java
index 55b823b..47d3df87 100644
--- a/core/java/android/window/WindowTokenClient.java
+++ b/core/java/android/window/WindowTokenClient.java
@@ -20,7 +20,6 @@
 import static android.window.ConfigurationHelper.shouldUpdateResources;
 
 import android.annotation.AnyThread;
-import android.annotation.BinderThread;
 import android.annotation.MainThread;
 import android.annotation.NonNull;
 import android.app.ActivityThread;
@@ -97,9 +96,10 @@
      * @param newConfig the updated {@link Configuration}
      * @param newDisplayId the updated {@link android.view.Display} ID
      */
-    @BinderThread
+    @AnyThread
     @Override
     public void onConfigurationChanged(Configuration newConfig, int newDisplayId) {
+        // TODO(b/290876897): No need to post on mHandler after migrating to ClientTransaction
         mHandler.post(PooledLambda.obtainRunnable(this::onConfigurationChanged, newConfig,
                 newDisplayId, true /* shouldReportConfigChange */).recycleOnUse());
     }
@@ -188,9 +188,10 @@
         }
     }
 
-    @BinderThread
+    @AnyThread
     @Override
     public void onWindowTokenRemoved() {
+        // TODO(b/290876897): No need to post on mHandler after migrating to ClientTransaction
         mHandler.post(PooledLambda.obtainRunnable(
                 WindowTokenClient::onWindowTokenRemovedInner, this).recycleOnUse());
     }
diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
index 4857741..c1b55cd 100644
--- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
+++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
@@ -21,6 +21,7 @@
 import static android.content.Intent.ACTION_VIEW;
 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -29,6 +30,8 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 
 import android.annotation.Nullable;
 import android.app.Activity;
@@ -46,6 +49,7 @@
 import android.app.servertransaction.NewIntentItem;
 import android.app.servertransaction.ResumeActivityItem;
 import android.app.servertransaction.StopActivityItem;
+import android.app.servertransaction.WindowTokenClientController;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.CompatibilityInfo;
@@ -70,6 +74,7 @@
 import com.android.internal.content.ReferrerIntent;
 
 import org.junit.After;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -83,7 +88,7 @@
 /**
  * Test for verifying {@link android.app.ActivityThread} class.
  * Build/Install/Run:
- *  atest FrameworksCoreTests:android.app.activity.ActivityThreadTest
+ *  atest FrameworksCoreTests:ActivityThreadTest
  */
 @RunWith(AndroidJUnit4.class)
 @MediumTest
@@ -100,14 +105,24 @@
             new ActivityTestRule<>(TestActivity.class, true /* initialTouchMode */,
                     false /* launchActivity */);
 
+    private WindowTokenClientController mOriginalWindowTokenClientController;
+
     private ArrayList<VirtualDisplay> mCreatedVirtualDisplays;
 
+    @Before
+    public void setup() {
+        // Keep track of the original controller, so that it can be used to restore in tearDown()
+        // when there is override in some test cases.
+        mOriginalWindowTokenClientController = WindowTokenClientController.getInstance();
+    }
+
     @After
     public void tearDown() {
         if (mCreatedVirtualDisplays != null) {
             mCreatedVirtualDisplays.forEach(VirtualDisplay::release);
             mCreatedVirtualDisplays = null;
         }
+        WindowTokenClientController.overrideForTesting(mOriginalWindowTokenClientController);
     }
 
     @Test
@@ -730,6 +745,39 @@
         assertFalse(activity.enterPipSkipped());
     }
 
+    @Test
+    public void testHandleWindowContextConfigurationChanged() {
+        final Activity activity = mActivityTestRule.launchActivity(new Intent());
+        final ActivityThread activityThread = activity.getActivityThread();
+        final WindowTokenClientController windowTokenClientController =
+                mock(WindowTokenClientController.class);
+        WindowTokenClientController.overrideForTesting(windowTokenClientController);
+        final IBinder clientToken = mock(IBinder.class);
+        final Configuration configuration = new Configuration();
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> activityThread
+                .handleWindowContextConfigurationChanged(
+                        clientToken, configuration, DEFAULT_DISPLAY));
+
+        verify(windowTokenClientController).onWindowContextConfigurationChanged(
+                clientToken, configuration, DEFAULT_DISPLAY);
+    }
+
+    @Test
+    public void testHandleWindowContextWindowRemoval() {
+        final Activity activity = mActivityTestRule.launchActivity(new Intent());
+        final ActivityThread activityThread = activity.getActivityThread();
+        final WindowTokenClientController windowTokenClientController =
+                mock(WindowTokenClientController.class);
+        WindowTokenClientController.overrideForTesting(windowTokenClientController);
+        final IBinder clientToken = mock(IBinder.class);
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> activityThread
+                .handleWindowContextWindowRemoval(clientToken));
+
+        verify(windowTokenClientController).onWindowContextWindowRemoved(clientToken);
+    }
+
     /**
      * Calls {@link ActivityThread#handleActivityConfigurationChanged(ActivityClientRecord,
      * Configuration, int)} to try to push activity configuration to the activity for the given
diff --git a/core/tests/coretests/src/android/app/servertransaction/WindowContextConfigurationChangeItemTest.java b/core/tests/coretests/src/android/app/servertransaction/WindowContextConfigurationChangeItemTest.java
new file mode 100644
index 0000000..7811e1a
--- /dev/null
+++ b/core/tests/coretests/src/android/app/servertransaction/WindowContextConfigurationChangeItemTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.servertransaction;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static org.mockito.Mockito.verify;
+
+import android.app.ClientTransactionHandler;
+import android.content.res.Configuration;
+import android.os.IBinder;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for {@link WindowContextConfigurationChangeItem}.
+ *
+ * Build/Install/Run:
+ *  atest FrameworksCoreTests:WindowContextConfigurationChangeItemTest
+ */
+public class WindowContextConfigurationChangeItemTest {
+
+    @Mock
+    private ClientTransactionHandler mHandler;
+    @Mock
+    private IBinder mToken;
+    @Mock
+    private PendingTransactionActions mPendingActions;
+    @Mock
+    private IBinder mClientToken;
+    // Can't mock final class.
+    private final Configuration mConfiguration = new Configuration();
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void testExecute() {
+        final WindowContextConfigurationChangeItem item = WindowContextConfigurationChangeItem
+                .obtain(mClientToken, mConfiguration, DEFAULT_DISPLAY);
+        item.execute(mHandler, mToken, mPendingActions);
+
+        verify(mHandler).handleWindowContextConfigurationChanged(mClientToken, mConfiguration,
+                DEFAULT_DISPLAY);
+    }
+}
diff --git a/core/tests/coretests/src/android/app/servertransaction/WindowContextWindowRemovalItemTest.java b/core/tests/coretests/src/android/app/servertransaction/WindowContextWindowRemovalItemTest.java
new file mode 100644
index 0000000..2c83c70
--- /dev/null
+++ b/core/tests/coretests/src/android/app/servertransaction/WindowContextWindowRemovalItemTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.servertransaction;
+
+import static org.mockito.Mockito.verify;
+
+import android.app.ClientTransactionHandler;
+import android.os.IBinder;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for {@link WindowContextWindowRemovalItem}.
+ *
+ * Build/Install/Run:
+ *  atest FrameworksCoreTests:WindowContextWindowRemovalItemTest
+ */
+public class WindowContextWindowRemovalItemTest {
+
+    @Mock
+    private ClientTransactionHandler mHandler;
+    @Mock
+    private IBinder mToken;
+    @Mock
+    private PendingTransactionActions mPendingActions;
+    @Mock
+    private IBinder mClientToken;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void testExecute() {
+        final WindowContextWindowRemovalItem item = WindowContextWindowRemovalItem.obtain(
+                mClientToken);
+        item.execute(mHandler, mToken, mPendingActions);
+
+        verify(mHandler).handleWindowContextWindowRemoval(mClientToken);
+    }
+}
diff --git a/core/tests/coretests/src/android/app/servertransaction/WindowTokenClientControllerTest.java b/core/tests/coretests/src/android/app/servertransaction/WindowTokenClientControllerTest.java
new file mode 100644
index 0000000..3b2fe58
--- /dev/null
+++ b/core/tests/coretests/src/android/app/servertransaction/WindowTokenClientControllerTest.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.servertransaction;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import android.content.res.Configuration;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+import android.view.IWindowManager;
+import android.view.WindowManagerGlobal;
+import android.window.WindowTokenClient;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for {@link WindowTokenClientController}.
+ *
+ * Build/Install/Run:
+ *  atest FrameworksCoreTests:WindowTokenClientControllerTest
+ */
+@SmallTest
+@Presubmit
+public class WindowTokenClientControllerTest {
+
+    @Mock
+    private IWindowManager mWindowManagerService;
+    @Mock
+    private WindowTokenClient mWindowTokenClient;
+    @Mock
+    private IBinder mClientToken;
+    @Mock
+    private IBinder mWindowToken;
+    // Can't mock final class.
+    private final Configuration mConfiguration = new Configuration();
+
+    private IWindowManager mOriginalWindowManagerService;
+
+    private WindowTokenClientController mController;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mOriginalWindowManagerService = WindowManagerGlobal.getWindowManagerService();
+        WindowManagerGlobal.overrideWindowManagerServiceForTesting(mWindowManagerService);
+        doReturn(mClientToken).when(mWindowTokenClient).asBinder();
+        mController = spy(WindowTokenClientController.getInstance());
+    }
+
+    @After
+    public void tearDown() {
+        WindowManagerGlobal.overrideWindowManagerServiceForTesting(mOriginalWindowManagerService);
+    }
+
+    @Test
+    public void testAttachToDisplayArea() throws RemoteException {
+        doReturn(null).when(mWindowManagerService).attachWindowContextToDisplayArea(
+                any(), anyInt(), anyInt(), any());
+
+        assertFalse(mController.attachToDisplayArea(mWindowTokenClient, TYPE_APPLICATION_OVERLAY,
+                DEFAULT_DISPLAY, null /* options */));
+        verify(mWindowManagerService).attachWindowContextToDisplayArea(mWindowTokenClient,
+                TYPE_APPLICATION_OVERLAY, DEFAULT_DISPLAY, null /* options */);
+        verify(mWindowTokenClient, never()).onConfigurationChanged(any(), anyInt(), anyBoolean());
+
+        doReturn(mConfiguration).when(mWindowManagerService).attachWindowContextToDisplayArea(
+                any(), anyInt(), anyInt(), any());
+
+        assertTrue(mController.attachToDisplayArea(mWindowTokenClient, TYPE_APPLICATION_OVERLAY,
+                DEFAULT_DISPLAY, null /* options */));
+        verify(mWindowTokenClient).onConfigurationChanged(mConfiguration, DEFAULT_DISPLAY,
+                false /* shouldReportConfigChange */);
+    }
+
+    @Test
+    public void testAttachToDisplayArea_detachIfNeeded() throws RemoteException {
+        mController.detachIfNeeded(mWindowTokenClient);
+
+        verify(mWindowManagerService, never()).detachWindowContextFromWindowContainer(any());
+
+        doReturn(null).when(mWindowManagerService).attachWindowContextToDisplayArea(
+                any(), anyInt(), anyInt(), any());
+        mController.attachToDisplayArea(mWindowTokenClient, TYPE_APPLICATION_OVERLAY,
+                DEFAULT_DISPLAY, null /* options */);
+        mController.detachIfNeeded(mWindowTokenClient);
+
+        verify(mWindowManagerService, never()).detachWindowContextFromWindowContainer(any());
+
+        doReturn(mConfiguration).when(mWindowManagerService).attachWindowContextToDisplayArea(
+                any(), anyInt(), anyInt(), any());
+        mController.attachToDisplayArea(mWindowTokenClient, TYPE_APPLICATION_OVERLAY,
+                DEFAULT_DISPLAY, null /* options */);
+        mController.detachIfNeeded(mWindowTokenClient);
+
+        verify(mWindowManagerService).detachWindowContextFromWindowContainer(any());
+    }
+
+    @Test
+    public void testAttachToDisplayContent() throws RemoteException {
+        doReturn(null).when(mWindowManagerService).attachToDisplayContent(
+                any(), anyInt());
+
+        assertFalse(mController.attachToDisplayContent(mWindowTokenClient, DEFAULT_DISPLAY));
+        verify(mWindowManagerService).attachToDisplayContent(mWindowTokenClient, DEFAULT_DISPLAY);
+        verify(mWindowTokenClient, never()).onConfigurationChanged(any(), anyInt(), anyBoolean());
+
+        doReturn(mConfiguration).when(mWindowManagerService).attachToDisplayContent(
+                any(), anyInt());
+
+        assertTrue(mController.attachToDisplayContent(mWindowTokenClient, DEFAULT_DISPLAY));
+        verify(mWindowTokenClient).onConfigurationChanged(mConfiguration, DEFAULT_DISPLAY,
+                false /* shouldReportConfigChange */);
+    }
+
+    @Test
+    public void testAttachToDisplayContent_detachIfNeeded() throws RemoteException {
+        mController.detachIfNeeded(mWindowTokenClient);
+
+        verify(mWindowManagerService, never()).detachWindowContextFromWindowContainer(any());
+
+        doReturn(null).when(mWindowManagerService).attachToDisplayContent(
+                any(), anyInt());
+        mController.attachToDisplayContent(mWindowTokenClient, DEFAULT_DISPLAY);
+        mController.detachIfNeeded(mWindowTokenClient);
+
+        verify(mWindowManagerService, never()).detachWindowContextFromWindowContainer(any());
+
+        doReturn(mConfiguration).when(mWindowManagerService).attachToDisplayContent(
+                any(), anyInt());
+        mController.attachToDisplayContent(mWindowTokenClient, DEFAULT_DISPLAY);
+        mController.detachIfNeeded(mWindowTokenClient);
+
+        verify(mWindowManagerService).detachWindowContextFromWindowContainer(any());
+    }
+
+    @Test
+    public void testAttachToWindowToken() throws RemoteException {
+        mController.attachToWindowToken(mWindowTokenClient, mWindowToken);
+
+        verify(mWindowManagerService).attachWindowContextToWindowToken(mWindowTokenClient,
+                mWindowToken);
+    }
+
+    @Test
+    public void testAttachToWindowToken_detachIfNeeded() throws RemoteException {
+        mController.detachIfNeeded(mWindowTokenClient);
+
+        verify(mWindowManagerService, never()).detachWindowContextFromWindowContainer(any());
+
+        mController.attachToWindowToken(mWindowTokenClient, mWindowToken);
+        mController.detachIfNeeded(mWindowTokenClient);
+
+        verify(mWindowManagerService).detachWindowContextFromWindowContainer(any());
+    }
+
+    @Test
+    public void testOnWindowContextConfigurationChanged() {
+        mController.onWindowContextConfigurationChanged(
+                mClientToken, mConfiguration, DEFAULT_DISPLAY);
+
+        verify(mWindowTokenClient, never()).onConfigurationChanged(any(), anyInt());
+
+        mController.attachToWindowToken(mWindowTokenClient, mWindowToken);
+
+        mController.onWindowContextConfigurationChanged(
+                mClientToken, mConfiguration, DEFAULT_DISPLAY);
+
+        verify(mWindowTokenClient).onConfigurationChanged(mConfiguration, DEFAULT_DISPLAY);
+    }
+
+    @Test
+    public void testOnWindowContextWindowRemoved() {
+        mController.onWindowContextWindowRemoved(mClientToken);
+
+        verify(mWindowTokenClient, never()).onWindowTokenRemoved();
+
+        mController.attachToWindowToken(mWindowTokenClient, mWindowToken);
+
+        mController.onWindowContextWindowRemoved(mClientToken);
+
+        verify(mWindowTokenClient).onWindowTokenRemoved();
+    }
+}
diff --git a/core/tests/coretests/src/android/window/WindowContextControllerTest.java b/core/tests/coretests/src/android/window/WindowContextControllerTest.java
index 813c360..5f2aecc 100644
--- a/core/tests/coretests/src/android/window/WindowContextControllerTest.java
+++ b/core/tests/coretests/src/android/window/WindowContextControllerTest.java
@@ -71,14 +71,14 @@
         mController = new WindowContextController(mMockToken);
         doNothing().when(mMockToken).onConfigurationChanged(any(), anyInt(), anyBoolean());
         mOriginalController = WindowTokenClientController.getInstance();
-        WindowTokenClientController.overrideInstance(mWindowTokenClientController);
+        WindowTokenClientController.overrideForTesting(mWindowTokenClientController);
         doReturn(true).when(mWindowTokenClientController).attachToDisplayArea(
                 eq(mMockToken), anyInt(), anyInt(), any());
     }
 
     @After
     public void tearDown() {
-        WindowTokenClientController.overrideInstance(mOriginalController);
+        WindowTokenClientController.overrideForTesting(mOriginalController);
     }
 
     @Test(expected = IllegalStateException.class)