Compatible behavior for non-resizable activity (2/N)

Scale and offset activity with compatibility bounds horizontally.
This reuses the existing scaling flow of compatibility mode,
so the insets scaling is also handled when computing frames.

Bug: 112288258
Test: atest AppWindowTokenTests#testSizeCompatBounds
Change-Id: I42a3fae7cd27f3ab6d79d885bedbea2e34874d01
diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java
index bcf6aba..92346e9 100644
--- a/services/core/java/com/android/server/wm/AppWindowToken.java
+++ b/services/core/java/com/android/server/wm/AppWindowToken.java
@@ -239,6 +239,17 @@
     ArrayDeque<Rect> mFrozenBounds = new ArrayDeque<>();
     ArrayDeque<Configuration> mFrozenMergedConfig = new ArrayDeque<>();
 
+    /**
+     * The scale to fit at least one side of the activity to its parent. If the activity uses
+     * 1920x1080, and the actually size on the screen is 960x540, then the scale is 0.5.
+     */
+    private float mSizeCompatScale = 1f;
+    /**
+     * The bounds in global coordinates for activity in size compatibility mode.
+     * @see ActivityRecord#inSizeCompatMode
+     */
+    private Rect mSizeCompatBounds;
+
     private boolean mDisablePreviewScreenshots;
 
     private Task mLastParent;
@@ -1555,11 +1566,52 @@
         return mOrientation;
     }
 
+    /** @return {@code true} if the compatibility bounds is taking effect. */
+    boolean inSizeCompatMode() {
+        return mSizeCompatBounds != null;
+    }
+
+    @Override
+    float getSizeCompatScale() {
+        return inSizeCompatMode() ? mSizeCompatScale : super.getSizeCompatScale();
+    }
+
+    /**
+     * @return Non-empty bounds if the activity has override bounds.
+     * @see ActivityRecord#resolveOverrideConfiguration(Configuration)
+     */
+    Rect getResolvedOverrideBounds() {
+        // Get bounds from resolved override configuration because it is computed with orientation.
+        return getResolvedOverrideConfiguration().windowConfiguration.getBounds();
+    }
+
     @Override
     public void onConfigurationChanged(Configuration newParentConfig) {
         final int prevWinMode = getWindowingMode();
         mTmpPrevBounds.set(getBounds());
         super.onConfigurationChanged(newParentConfig);
+
+        final Task task = getTask();
+        final Rect overrideBounds = getResolvedOverrideBounds();
+        if (task != null && !overrideBounds.isEmpty()
+                // If the changes come from change-listener, the incoming parent configuration is
+                // still the old one. Make sure their orientations are the same to reduce computing
+                // the compatibility bounds for the intermediate state.
+                && getResolvedOverrideConfiguration().orientation == newParentConfig.orientation) {
+            final Rect taskBounds = task.getBounds();
+            // Since we only center the activity horizontally, if only the fixed height is smaller
+            // than its container, the override bounds don't need to take effect.
+            if ((overrideBounds.width() != taskBounds.width()
+                    || overrideBounds.height() > taskBounds.height())) {
+                calculateCompatBoundsTransformation(newParentConfig);
+                updateSurfacePosition();
+            } else if (mSizeCompatBounds != null) {
+                mSizeCompatBounds = null;
+                mSizeCompatScale = 1f;
+                updateSurfacePosition();
+            }
+        }
+
         final int winMode = getWindowingMode();
 
         if (prevWinMode == winMode) {
@@ -1671,6 +1723,54 @@
         return mThumbnail;
     }
 
+    /**
+     * Calculates the scale and offset to horizontal center the size compatibility bounds into the
+     * region which is available to application.
+     */
+    private void calculateCompatBoundsTransformation(Configuration newParentConfig) {
+        final Rect parentAppBounds = newParentConfig.windowConfiguration.getAppBounds();
+        final Rect viewportBounds = parentAppBounds != null
+                ? parentAppBounds : newParentConfig.windowConfiguration.getBounds();
+        final Rect contentBounds = getResolvedOverrideBounds();
+        final float contentW = contentBounds.width();
+        final float contentH = contentBounds.height();
+        final float viewportW = viewportBounds.width();
+        final float viewportH = viewportBounds.height();
+        // Only allow to scale down.
+        mSizeCompatScale = (contentW <= viewportW && contentH <= viewportH)
+                ? 1 : Math.min(viewportW / contentW, viewportH / contentH);
+        final int offsetX = (int) ((viewportW - contentW * mSizeCompatScale + 1) * 0.5f)
+                + viewportBounds.left;
+
+        if (mSizeCompatBounds == null) {
+            mSizeCompatBounds = new Rect();
+        }
+        mSizeCompatBounds.set(contentBounds);
+        mSizeCompatBounds.offsetTo(0, 0);
+        mSizeCompatBounds.scale(mSizeCompatScale);
+        mSizeCompatBounds.left += offsetX;
+        mSizeCompatBounds.right += offsetX;
+    }
+
+    @Override
+    public Rect getBounds() {
+        if (mSizeCompatBounds != null) {
+            return mSizeCompatBounds;
+        }
+        return super.getBounds();
+    }
+
+    @Override
+    public boolean matchParentBounds() {
+        if (super.matchParentBounds()) {
+            return true;
+        }
+        // An activity in size compatibility mode may have override bounds which equals to its
+        // parent bounds, so the exact bounds should also be checked.
+        final WindowContainer parent = getParent();
+        return parent == null || parent.getBounds().equals(getResolvedOverrideBounds());
+    }
+
     @Override
     void checkAppWindowsReadyToShow() {
         if (allDrawn == mLastAllDrawn) {
@@ -2864,6 +2964,10 @@
         if (mPendingRelaunchCount != 0) {
             pw.print(prefix); pw.print("mPendingRelaunchCount="); pw.println(mPendingRelaunchCount);
         }
+        if (mSizeCompatScale != 1f || mSizeCompatBounds != null) {
+            pw.println(prefix + "mSizeCompatScale=" + mSizeCompatScale + " mSizeCompatBounds="
+                    + mSizeCompatBounds);
+        }
         if (mRemovingFromDisplay) {
             pw.println(prefix + "mRemovingFromDisplay=" + mRemovingFromDisplay);
         }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 6c3e1f4..a748e78 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -47,7 +47,6 @@
 import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
 import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
 import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW;
-import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
 import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
@@ -1837,6 +1836,9 @@
                 return;
             }
             outDisplayFrame.set(win.getDisplayFrameLw());
+            if (win.inSizeCompatMode()) {
+                outDisplayFrame.scale(win.mInvGlobalScale);
+            }
         }
     }
 
@@ -1961,8 +1963,6 @@
             if (DEBUG_LAYOUT) Slog.v(TAG_WM, "Relayout " + win + ": viewVisibility=" + viewVisibility
                     + " req=" + requestedWidth + "x" + requestedHeight + " " + win.mAttrs);
             winAnimator.mSurfaceDestroyDeferred = (flags & RELAYOUT_DEFER_SURFACE_DESTROY) != 0;
-            win.mEnforceSizeCompat =
-                    (win.mAttrs.privateFlags & PRIVATE_FLAG_COMPATIBLE_WINDOW) != 0;
             if ((attrChanges & WindowManager.LayoutParams.ALPHA_CHANGED) != 0) {
                 winAnimator.mAlpha = attrs.alpha;
             }
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 85b251a..48cd6b6 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -246,7 +246,6 @@
     final boolean mIsWallpaper;
     private final boolean mIsFloatingLayer;
     int mSeq;
-    boolean mEnforceSizeCompat;
     int mViewVisibility;
     int mSystemUiVisibility;
     /**
@@ -505,7 +504,8 @@
      */
     private PowerManager.WakeLock mDrawLock;
 
-    final private Rect mTmpRect = new Rect();
+    private final Rect mTmpRect = new Rect();
+    private final Point mTmpPoint = new Point();
 
     /**
      * Whether the window was resized by us while it was gone for layout.
@@ -655,7 +655,6 @@
         mContext = mWmService.mContext;
         DeathRecipient deathRecipient = new DeathRecipient();
         mSeq = seq;
-        mEnforceSizeCompat = (mAttrs.privateFlags & PRIVATE_FLAG_COMPATIBLE_WINDOW) != 0;
         mPowerManagerWrapper = powerManagerWrapper;
         mForceSeamlesslyRotate = token.mRoundedCornerOverlay;
         if (localLOGV) Slog.v(
@@ -733,6 +732,18 @@
     }
 
     /**
+     * @return {@code true} if the application runs in size compatibility mode.
+     * @see android.content.res.CompatibilityInfo#supportsScreen
+     * @see ActivityRecord#inSizeCompatMode
+     */
+    boolean inSizeCompatMode() {
+        return (mAttrs.privateFlags & PRIVATE_FLAG_COMPATIBLE_WINDOW) != 0
+                || (mAppToken != null && mAppToken.inSizeCompatMode()
+                        // Exclude starting window because it is not displayed by the application.
+                        && mAttrs.type != TYPE_APPLICATION_STARTING);
+    }
+
+    /**
      * Returns whether this {@link WindowState} has been considered for drawing by its parent.
      */
     boolean getDrawnStateEvaluated() {
@@ -995,7 +1006,7 @@
         mWindowFrames.offsetFrames(-layoutXDiff, -layoutYDiff);
 
         mWindowFrames.mCompatFrame.set(mWindowFrames.mFrame);
-        if (mEnforceSizeCompat) {
+        if (inSizeCompatMode()) {
             // If there is a size compatibility scale being applied to the
             // window, we need to apply this to its insets so that they are
             // reported to the app in its coordinate space.
@@ -1354,8 +1365,8 @@
     }
 
     void prelayout() {
-        if (mEnforceSizeCompat) {
-            mGlobalScale = getDisplayContent().mCompatibleScreenScale;
+        if (inSizeCompatMode()) {
+            mGlobalScale = mToken.getSizeCompatScale();
             mInvGlobalScale = 1 / mGlobalScale;
         } else {
             mGlobalScale = mInvGlobalScale = 1;
@@ -2145,6 +2156,30 @@
 
     int getSurfaceTouchableRegion(Region region, int flags) {
         final boolean modal = (flags & (FLAG_NOT_TOUCH_MODAL | FLAG_NOT_FOCUSABLE)) == 0;
+        if (mAppToken != null && !mAppToken.getResolvedOverrideBounds().isEmpty()) {
+            // There may have touchable letterboxes around the activity, so in order to let the
+            // letterboxes are able to receive touch event and slip to activity, the activity with
+            // compatibility bounds cannot occupy full screen touchable region.
+            if (modal) {
+                // A modal window uses the whole compatibility bounds.
+                flags |= FLAG_NOT_TOUCH_MODAL;
+                mTmpRect.set(mAppToken.getResolvedOverrideBounds());
+                // TODO(b/112288258): Remove the forced offset when the override bounds always
+                // starts from zero (See {@link ActivityRecord#resolveOverrideConfiguration}).
+                mTmpRect.offsetTo(0, 0);
+            } else {
+                // Non-modal uses the application based frame.
+                mTmpRect.set(mWindowFrames.mCompatFrame);
+            }
+            // The offset of compatibility bounds is applied to surface of {@link #AppWindowToken}
+            // and frame, so it is unnecessary to translate twice in surface based coordinates.
+            final int surfaceOffsetX = mAppToken.inSizeCompatMode()
+                    ? mAppToken.getBounds().left : 0;
+            mTmpRect.offset(surfaceOffsetX - mWindowFrames.mFrame.left, -mWindowFrames.mFrame.top);
+            region.set(mTmpRect);
+            return flags;
+        }
+
         if (modal && mAppToken != null) {
             // Limit the outer touch to the activity stack region.
             flags |= FLAG_NOT_TOUCH_MODAL;
@@ -2943,7 +2978,7 @@
             if (DEBUG_ORIENTATION && mWinAnimator.mDrawState == DRAW_PENDING)
                 Slog.i(TAG, "Resizing " + this + " WITH DRAW PENDING");
 
-            final Rect frame = mWindowFrames.mFrame;
+            final Rect frame = mWindowFrames.mCompatFrame;
             final Rect overscanInsets = mWindowFrames.mLastOverscanInsets;
             final Rect contentInsets = mWindowFrames.mLastContentInsets;
             final Rect visibleInsets = mWindowFrames.mLastVisibleInsets;
@@ -3353,7 +3388,7 @@
         pw.println(prefix + "mHasSurface=" + mHasSurface
                 + " isReadyForDisplay()=" + isReadyForDisplay()
                 + " mWindowRemovalAllowed=" + mWindowRemovalAllowed);
-        if (mEnforceSizeCompat) {
+        if (inSizeCompatMode()) {
             pw.println(prefix + "mCompatFrame=" + mWindowFrames.mCompatFrame.toShortString(sTmpSB));
         }
         if (dumpAll) {
@@ -3477,17 +3512,18 @@
         float x, y;
         int w,h;
 
+        final boolean inSizeCompatMode = inSizeCompatMode();
         if ((mAttrs.flags & FLAG_SCALED) != 0) {
             if (mAttrs.width < 0) {
                 w = pw;
-            } else if (mEnforceSizeCompat) {
+            } else if (inSizeCompatMode) {
                 w = (int)(mAttrs.width * mGlobalScale + .5f);
             } else {
                 w = mAttrs.width;
             }
             if (mAttrs.height < 0) {
                 h = ph;
-            } else if (mEnforceSizeCompat) {
+            } else if (inSizeCompatMode) {
                 h = (int)(mAttrs.height * mGlobalScale + .5f);
             } else {
                 h = mAttrs.height;
@@ -3495,21 +3531,21 @@
         } else {
             if (mAttrs.width == MATCH_PARENT) {
                 w = pw;
-            } else if (mEnforceSizeCompat) {
+            } else if (inSizeCompatMode) {
                 w = (int)(mRequestedWidth * mGlobalScale + .5f);
             } else {
                 w = mRequestedWidth;
             }
             if (mAttrs.height == MATCH_PARENT) {
                 h = ph;
-            } else if (mEnforceSizeCompat) {
+            } else if (inSizeCompatMode) {
                 h = (int)(mRequestedHeight * mGlobalScale + .5f);
             } else {
                 h = mRequestedHeight;
             }
         }
 
-        if (mEnforceSizeCompat) {
+        if (inSizeCompatMode) {
             x = mAttrs.x * mGlobalScale;
             y = mAttrs.y * mGlobalScale;
         } else {
@@ -3537,7 +3573,7 @@
         // We need to make sure we update the CompatFrame as it is used for
         // cropping decisions, etc, on systems where we lack a decor layer.
         mWindowFrames.mCompatFrame.set(mWindowFrames.mFrame);
-        if (mEnforceSizeCompat) {
+        if (inSizeCompatMode) {
             // See comparable block in computeFrameLw.
             mWindowFrames.mCompatFrame.scale(mInvGlobalScale);
         }
@@ -3650,7 +3686,7 @@
 
     float translateToWindowX(float x) {
         float winX = x - mWindowFrames.mFrame.left;
-        if (mEnforceSizeCompat) {
+        if (inSizeCompatMode()) {
             winX *= mGlobalScale;
         }
         return winX;
@@ -3658,7 +3694,7 @@
 
     float translateToWindowY(float y) {
         float winY = y - mWindowFrames.mFrame.top;
-        if (mEnforceSizeCompat) {
+        if (inSizeCompatMode()) {
             winY *= mGlobalScale;
         }
         return winY;
@@ -4233,12 +4269,11 @@
      */
     void calculatePolicyCrop(Rect policyCrop) {
         final DisplayContent displayContent = getDisplayContent();
-        final DisplayInfo displayInfo = displayContent.getDisplayInfo();
 
-        if (!isDefaultDisplay()) {
+        if (!displayContent.isDefaultDisplay && !displayContent.supportsSystemDecorations()) {
             // On a different display there is no system decor. Crop the window
             // by the screen boundaries.
-            // TODO(multi-display)
+            final DisplayInfo displayInfo = displayContent.getDisplayInfo();
             policyCrop.set(0, 0, mWindowFrames.mCompatFrame.width(),
                     mWindowFrames.mCompatFrame.height());
             policyCrop.intersect(-mWindowFrames.mCompatFrame.left, -mWindowFrames.mCompatFrame.top,
@@ -4304,7 +4339,7 @@
         // scale function because we want to round things to make the crop
         // always round to a larger rect to ensure we don't crop too
         // much and hide part of the window that should be seen.
-        if (mEnforceSizeCompat && mInvGlobalScale != 1.0f) {
+        if (inSizeCompatMode() && mInvGlobalScale != 1.0f) {
             final float scale = mInvGlobalScale;
             systemDecorRect.left = (int) (systemDecorRect.left * scale - 0.5f);
             systemDecorRect.top = (int) (systemDecorRect.top * scale - 0.5f);
@@ -4664,8 +4699,9 @@
             // Since the parent was outset by its surface insets, we need to undo the outsetting
             // with insetting by the same amount.
             final WindowState parent = getParentWindow();
-            outPoint.offset(-parent.mWindowFrames.mFrame.left + parent.mAttrs.surfaceInsets.left,
-                    -parent.mWindowFrames.mFrame.top + parent.mAttrs.surfaceInsets.top);
+            transformSurfaceInsetsPosition(mTmpPoint, parent.mAttrs.surfaceInsets);
+            outPoint.offset(-parent.mWindowFrames.mFrame.left + mTmpPoint.x,
+                    -parent.mWindowFrames.mFrame.top + mTmpPoint.y);
         } else if (parentWindowContainer != null) {
             final Rect parentBounds = parentWindowContainer.getDisplayedBounds();
             outPoint.offset(-parentBounds.left, -parentBounds.top);
@@ -4683,7 +4719,22 @@
         }
 
         // Expand for surface insets. See WindowState.expandForSurfaceInsets.
-        outPoint.offset(-mAttrs.surfaceInsets.left, -mAttrs.surfaceInsets.top);
+        transformSurfaceInsetsPosition(mTmpPoint, mAttrs.surfaceInsets);
+        outPoint.offset(-mTmpPoint.x, -mTmpPoint.y);
+    }
+
+    /**
+     * The surface insets from layout parameter are in application coordinate. If the window is
+     * scaled, the insets also need to be scaled for surface position in global coordinate.
+     */
+    private void transformSurfaceInsetsPosition(Point outPos, Rect surfaceInsets) {
+        if (!inSizeCompatMode()) {
+            outPos.x = surfaceInsets.left;
+            outPos.y = surfaceInsets.top;
+            return;
+        }
+        outPos.x = (int) (surfaceInsets.left * mGlobalScale + 0.5f);
+        outPos.y = (int) (surfaceInsets.top * mGlobalScale + 0.5f);
     }
 
     boolean needsRelativeLayeringToIme() {
diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java
index 3b9b8ba..f0b9c62 100644
--- a/services/core/java/com/android/server/wm/WindowToken.java
+++ b/services/core/java/com/android/server/wm/WindowToken.java
@@ -182,6 +182,14 @@
     }
 
     /**
+     * @return The scale for applications running in compatibility mode. Multiply the size in the
+     *         application by this scale will be the size in the screen.
+     */
+    float getSizeCompatScale() {
+        return mDisplayContent.mCompatibleScreenScale;
+    }
+
+    /**
      * Returns true if the new window is considered greater than the existing window in terms of
      * z-order.
      */
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenTests.java b/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenTests.java
index bc62de1..90cda4d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenTests.java
@@ -44,6 +44,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyInt;
 
+import android.content.res.Configuration;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
@@ -211,6 +212,48 @@
     }
 
     @Test
+    public void testSizeCompatBounds() {
+        // The real surface transaction is unnecessary.
+        mToken.setSkipPrepareSurfaces(true);
+
+        final Rect fixedBounds = mToken.getRequestedOverrideConfiguration().windowConfiguration
+                .getBounds();
+        fixedBounds.set(0, 0, 1200, 1600);
+        final Configuration newParentConfig = mTask.getConfiguration();
+
+        // Change the size of the container to two times smaller with insets.
+        newParentConfig.windowConfiguration.setAppBounds(200, 0, 800, 800);
+        final Rect containerAppBounds = newParentConfig.windowConfiguration.getAppBounds();
+        final Rect containerBounds = newParentConfig.windowConfiguration.getBounds();
+        containerBounds.set(0, 0, 600, 800);
+        mToken.onConfigurationChanged(newParentConfig);
+
+        assertTrue(mToken.inSizeCompatMode());
+        assertEquals(containerAppBounds, mToken.getBounds());
+        assertEquals((float) containerAppBounds.width() / fixedBounds.width(),
+                mToken.getSizeCompatScale(), 0.0001f /* delta */);
+
+        // Change the width of the container to two times bigger.
+        containerAppBounds.set(0, 0, 2400, 1600);
+        containerBounds.set(containerAppBounds);
+        mToken.onConfigurationChanged(newParentConfig);
+
+        assertTrue(mToken.inSizeCompatMode());
+        // Don't scale up, so the bounds keep the same as the fixed width.
+        assertEquals(fixedBounds.width(), mToken.getBounds().width());
+        // Assert the position is horizontal center.
+        assertEquals((containerAppBounds.width() - fixedBounds.width()) / 2,
+                mToken.getBounds().left);
+        assertEquals(1f, mToken.getSizeCompatScale(), 0.0001f  /* delta */);
+
+        // Change the width of the container to fit the fixed bounds.
+        containerBounds.set(0, 0, 1200, 2000);
+        mToken.onConfigurationChanged(newParentConfig);
+        // Assert don't use fixed bounds because the region is enough.
+        assertFalse(mToken.inSizeCompatMode());
+    }
+
+    @Test
     @Presubmit
     public void testGetOrientation() {
         mToken.setOrientation(SCREEN_ORIENTATION_LANDSCAPE);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowFrameTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowFrameTests.java
index 0a4a8a4..fb30f8b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowFrameTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowFrameTests.java
@@ -449,8 +449,7 @@
 
         // Now simulate switch to fullscreen for letterboxed app.
         final int xInset = logicalWidth / 10;
-        final int yInset = logicalWidth / 10;
-        final Rect cf = new Rect(xInset, yInset, logicalWidth - xInset, logicalHeight - yInset);
+        final Rect cf = new Rect(xInset, 0, logicalWidth - xInset, logicalHeight);
         Configuration config = new Configuration(w.mAppToken.getRequestedOverrideConfiguration());
         config.windowConfiguration.setBounds(cf);
         w.mAppToken.onRequestedOverrideConfigurationChanged(config);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestUtils.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestUtils.java
index a494889..114eac9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestUtils.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestUtils.java
@@ -151,6 +151,7 @@
     /** Used so we can gain access to some protected members of the {@link AppWindowToken} class. */
     public static class TestAppWindowToken extends AppWindowToken {
         boolean mOnTop = false;
+        private boolean mSkipPrepareSurfaces;
         private Transaction mPendingTransactionOverride;
         boolean mSkipOnParentChanged = true;
 
@@ -213,6 +214,17 @@
             return mOnTop;
         }
 
+        @Override
+        void prepareSurfaces() {
+            if (!mSkipPrepareSurfaces) {
+                super.prepareSurfaces();
+            }
+        }
+
+        void setSkipPrepareSurfaces(boolean ignore) {
+            mSkipPrepareSurfaces = ignore;
+        }
+
         void setPendingTransaction(Transaction transaction) {
             mPendingTransactionOverride = transaction;
         }