Letterboxing: ensure bar contrast

Ensures that any bars that draw over a letterbox draw their own
background such that they always have contrast.

This fixes an issue, where if a light status / navigation bar app was
letterboxed, the bar would still draw dark icons over the now black
letterbox, making the icons unreadable.

To do so, splits the letterbox into a layout and an apply surface
changes phase.

Change-Id: Ia8afa3386d75d9a72434d701b867c3ebc35cc36f
Fixes: 72696928
Test: Open a letterboxed app with a white navigation bar, verify it is visible
Test: ApiDemos > App > Activity > Max Aspect Ratio > 1:1, verify navbar is visible
diff --git a/services/core/java/com/android/server/policy/BarController.java b/services/core/java/com/android/server/policy/BarController.java
index c906705..cf88bd5 100644
--- a/services/core/java/com/android/server/policy/BarController.java
+++ b/services/core/java/com/android/server/policy/BarController.java
@@ -20,6 +20,7 @@
 import static com.android.server.wm.proto.BarControllerProto.TRANSIENT_STATE;
 
 import android.app.StatusBarManager;
+import android.graphics.Rect;
 import android.os.Handler;
 import android.os.Message;
 import android.os.SystemClock;
@@ -68,6 +69,7 @@
     private boolean mShowTransparent;
     private boolean mSetUnHideFlagWhenNextTransparent;
     private boolean mNoAnimationOnNextShow;
+    private final Rect mContentFrame = new Rect();
 
     private OnBarVisibilityChangedListener mVisibilityChangeListener;
 
@@ -87,6 +89,15 @@
         mWin = win;
     }
 
+    /**
+     * Sets the frame within which the bar will display its content.
+     *
+     * This is used to determine if letterboxes interfere with the display of such content.
+     */
+    public void setContentFrame(Rect frame) {
+        mContentFrame.set(frame);
+    }
+
     public void setShowTransparent(boolean transparent) {
         if (transparent != mShowTransparent) {
             mShowTransparent = transparent;
@@ -135,7 +146,8 @@
                 } else {
                     vis &= ~mTranslucentFlag;
                 }
-                if ((fl & WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0) {
+                if ((fl & WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
+                        && isTransparentAllowed(win)) {
                     vis |= mTransparentFlag;
                 } else {
                     vis &= ~mTransparentFlag;
@@ -148,6 +160,10 @@
         return vis;
     }
 
+    boolean isTransparentAllowed(WindowState win) {
+        return win == null || !win.isLetterboxedOverlappingWith(mContentFrame);
+    }
+
     public boolean setBarShowingLw(final boolean show) {
         if (mWin == null) return false;
         if (show && mTransientBarState == TRANSIENT_BAR_HIDING) {
@@ -328,6 +344,7 @@
             pw.println(StatusBarManager.windowStateToString(mState));
             pw.print(prefix); pw.print("  "); pw.print("mTransientBar"); pw.print('=');
             pw.println(transientBarStateToString(mTransientBarState));
+            pw.print(prefix); pw.print("  mContentFrame="); pw.println(mContentFrame);
         }
     }
 
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 3cd79e1..c997d6e 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -4705,6 +4705,13 @@
         displayFrames.mStable.top = displayFrames.mUnrestricted.top
                 + mStatusBarHeightForRotation[displayFrames.mRotation];
 
+        // Tell the bar controller where the collapsed status bar content is
+        mTmpRect.set(mStatusBar.getContentFrameLw());
+        mTmpRect.intersect(displayFrames.mDisplayCutoutSafe);
+        mTmpRect.top = mStatusBar.getContentFrameLw().top;  // Ignore top display cutout inset
+        mTmpRect.bottom = displayFrames.mStable.top;  // Use collapsed status bar size
+        mStatusBarController.setContentFrame(mTmpRect);
+
         boolean statusBarTransient = (sysui & View.STATUS_BAR_TRANSIENT) != 0;
         boolean statusBarTranslucent = (sysui
                 & (View.STATUS_BAR_TRANSLUCENT | View.STATUS_BAR_TRANSPARENT)) != 0;
@@ -4838,6 +4845,8 @@
                 mTmpNavigationFrame, displayFrames.mDisplayCutoutSafe, mTmpNavigationFrame, dcf,
                 mTmpNavigationFrame, displayFrames.mDisplayCutoutSafe,
                 displayFrames.mDisplayCutout);
+        mNavigationBarController.setContentFrame(mNavigationBar.getContentFrameLw());
+
         if (DEBUG_LAYOUT) Slog.i(TAG, "mNavigationBar frame: " + mTmpNavigationFrame);
         return mNavigationBarController.checkHiddenLw();
     }
@@ -8097,15 +8106,6 @@
         return vis;
     }
 
-    private boolean drawsSystemBarBackground(WindowState win) {
-        return win == null || (win.getAttrs().flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0;
-    }
-
-    private boolean forcesDrawStatusBarBackground(WindowState win) {
-        return win == null || (win.getAttrs().privateFlags
-                & PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND) != 0;
-    }
-
     private int updateSystemBarsLw(WindowState win, int oldVis, int vis) {
         final boolean dockedStackVisible =
                 mWindowManagerInternal.isStackVisible(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
@@ -8129,13 +8129,9 @@
                 mTopDockedOpaqueWindowState, 0, 0);
 
         final boolean fullscreenDrawsStatusBarBackground =
-                (drawsSystemBarBackground(mTopFullscreenOpaqueWindowState)
-                        && (vis & View.STATUS_BAR_TRANSLUCENT) == 0)
-                || forcesDrawStatusBarBackground(mTopFullscreenOpaqueWindowState);
+                drawsStatusBarBackground(vis, mTopFullscreenOpaqueWindowState);
         final boolean dockedDrawsStatusBarBackground =
-                (drawsSystemBarBackground(mTopDockedOpaqueWindowState)
-                        && (dockedVis & View.STATUS_BAR_TRANSLUCENT) == 0)
-                || forcesDrawStatusBarBackground(mTopDockedOpaqueWindowState);
+                drawsStatusBarBackground(dockedVis, mTopDockedOpaqueWindowState);
 
         // prevent status bar interaction from clearing certain flags
         int type = win.getAttrs().type;
@@ -8238,6 +8234,22 @@
         return vis;
     }
 
+    private boolean drawsStatusBarBackground(int vis, WindowState win) {
+        if (!mStatusBarController.isTransparentAllowed(win)) {
+            return false;
+        }
+        if (win == null) {
+            return true;
+        }
+
+        final boolean drawsSystemBars =
+                (win.getAttrs().flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0;
+        final boolean forceDrawsSystemBars =
+                (win.getAttrs().privateFlags & PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND) != 0;
+
+        return forceDrawsSystemBars || drawsSystemBars && (vis & View.STATUS_BAR_TRANSLUCENT) == 0;
+    }
+
     /**
      * @return the current visibility flags with the nav-bar opacity related flags toggled based
      *         on the nav bar opacity rules chosen by {@link #mNavBarOpacityMode}.
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index b0d5e1a..9904216 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -447,6 +447,14 @@
             return false;
         }
 
+        /**
+         * Returns true if the window has a letterbox and any part of that letterbox overlaps with
+         * the given {@code rect}.
+         */
+        default boolean isLetterboxedOverlappingWith(Rect rect) {
+            return false;
+        }
+
         /** @return the current windowing mode of this window. */
         int getWindowingMode();
 
diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java
index 1f71b8f..c15893b 100644
--- a/services/core/java/com/android/server/wm/AppWindowToken.java
+++ b/services/core/java/com/android/server/wm/AppWindowToken.java
@@ -24,7 +24,6 @@
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.SurfaceControl.HIDDEN;
 import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
 import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
@@ -712,7 +711,7 @@
         if (destroyedSomething) {
             final DisplayContent dc = getDisplayContent();
             dc.assignWindowLayers(true /*setLayoutNeeded*/);
-            updateLetterbox(null);
+            updateLetterboxSurface(null);
         }
     }
 
@@ -979,7 +978,7 @@
     void removeChild(WindowState child) {
         super.removeChild(child);
         checkKeyguardFlagsChanged();
-        updateLetterbox(child);
+        updateLetterboxSurface(child);
     }
 
     private boolean waitingForReplacement() {
@@ -1470,7 +1469,7 @@
         return isInterestingAndDrawn;
     }
 
-    void updateLetterbox(WindowState winHint) {
+    void layoutLetterbox(WindowState winHint) {
         final WindowState w = findMainWindow();
         if (w != winHint && winHint != null && w != null) {
             return;
@@ -1481,19 +1480,20 @@
             if (mLetterbox == null) {
                 mLetterbox = new Letterbox(() -> makeChildSurface(null));
             }
-            mLetterbox.setDimensions(mPendingTransaction, getParent().getBounds(), w.mFrame);
+            mLetterbox.layout(getParent().getBounds(), w.mFrame);
         } else if (mLetterbox != null) {
-            final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
-            // Make sure we have a transaction here, in case we're called outside of a transaction.
-            // This does not use mPendingTransaction, because SurfaceAnimator uses a
-            // global transaction in onAnimationEnd.
-            SurfaceControl.openTransaction();
-            try {
-                mLetterbox.hide(t);
-            } finally {
-                SurfaceControl.mergeToGlobalTransaction(t);
-                SurfaceControl.closeTransaction();
-            }
+            mLetterbox.hide();
+        }
+    }
+
+    void updateLetterboxSurface(WindowState winHint) {
+        final WindowState w = findMainWindow();
+        if (w != winHint && winHint != null && w != null) {
+            return;
+        }
+        layoutLetterbox(winHint);
+        if (mLetterbox != null && mLetterbox.needsApplySurfaceChanges()) {
+            mLetterbox.applySurfaceChanges(mPendingTransaction);
         }
     }
 
@@ -2156,4 +2156,12 @@
             return new Rect();
         }
     }
+
+    /**
+     * @eturn true if there is a letterbox and any part of that letterbox overlaps with
+     * the given {@code rect}.
+     */
+    boolean isLetterboxOverlappingWith(Rect rect) {
+        return mLetterbox != null && mLetterbox.isOverlappingWith(rect);
+    }
 }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 59babcf..c35c05d 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -80,7 +80,6 @@
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TOKEN_MOVEMENT;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER_LIGHT;
-import static com.android.server.wm.WindowManagerDebugConfig.SHOW_LIGHT_TRANSACTIONS;
 import static com.android.server.wm.WindowManagerDebugConfig.SHOW_STACK_CRAWLS;
 import static com.android.server.wm.WindowManagerDebugConfig.SHOW_TRANSACTIONS;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -558,6 +557,10 @@
                     w.updateLastInsetValues();
                 }
 
+                if (w.mAppToken != null) {
+                    w.mAppToken.layoutLetterbox(w);
+                }
+
                 if (DEBUG_LAYOUT) Slog.v(TAG, "  LAYOUT: mFrame=" + w.mFrame
                         + " mContainingFrame=" + w.mContainingFrame
                         + " mDisplayFrame=" + w.mDisplayFrame);
@@ -697,7 +700,7 @@
 
         final AppWindowToken atoken = w.mAppToken;
         if (atoken != null) {
-            atoken.updateLetterbox(w);
+            atoken.updateLetterboxSurface(w);
             final boolean updateAllDrawn = atoken.updateDrawnWindowStates(w);
             if (updateAllDrawn && !mTmpUpdateAllDrawn.contains(atoken)) {
                 mTmpUpdateAllDrawn.add(atoken);
diff --git a/services/core/java/com/android/server/wm/Letterbox.java b/services/core/java/com/android/server/wm/Letterbox.java
index 0f9735d..4eb021c 100644
--- a/services/core/java/com/android/server/wm/Letterbox.java
+++ b/services/core/java/com/android/server/wm/Letterbox.java
@@ -49,24 +49,26 @@
     }
 
     /**
-     * Sets the dimensions of the the letterbox, such that the area between the outer and inner
+     * Lays out the letterbox, such that the area between the outer and inner
      * frames will be covered by black color surfaces.
      *
-     * @param t     a transaction in which to set the dimensions
+     * The caller must use {@link #applySurfaceChanges} to apply the new layout to the surface.
+     *
      * @param outer the outer frame of the letterbox (this frame will be black, except the area
      *              that intersects with the {code inner} frame).
      * @param inner the inner frame of the letterbox (this frame will be clear)
      */
-    public void setDimensions(SurfaceControl.Transaction t, Rect outer, Rect inner) {
+    public void layout(Rect outer, Rect inner) {
         mOuter.set(outer);
         mInner.set(inner);
 
-        mTop.setRect(t, outer.left, outer.top, inner.right, inner.top);
-        mLeft.setRect(t, outer.left, inner.top, inner.left, outer.bottom);
-        mBottom.setRect(t, inner.left, inner.bottom, outer.right, outer.bottom);
-        mRight.setRect(t, inner.right, outer.top, outer.right, inner.bottom);
+        mTop.layout(outer.left, outer.top, inner.right, inner.top);
+        mLeft.layout(outer.left, inner.top, inner.left, outer.bottom);
+        mBottom.layout(inner.left, inner.bottom, outer.right, outer.bottom);
+        mRight.layout(inner.right, outer.top, outer.right, inner.bottom);
     }
 
+
     /**
      * Gets the insets between the outer and inner rects.
      */
@@ -79,12 +81,20 @@
     }
 
     /**
+     * Returns true if any part of the letterbox overlaps with the given {@code rect}.
+     */
+    public boolean isOverlappingWith(Rect rect) {
+        return mTop.isOverlappingWith(rect) || mLeft.isOverlappingWith(rect)
+                || mBottom.isOverlappingWith(rect) || mRight.isOverlappingWith(rect);
+    }
+
+    /**
      * Hides the letterbox.
      *
-     * @param t a transaction in which to hide the letterbox
+     * The caller must use {@link #applySurfaceChanges} to apply the new layout to the surface.
      */
-    public void hide(SurfaceControl.Transaction t) {
-        setDimensions(t, EMPTY_RECT, EMPTY_RECT);
+    public void hide() {
+        layout(EMPTY_RECT, EMPTY_RECT);
     }
 
     /**
@@ -100,43 +110,40 @@
         mRight.destroy();
     }
 
+    /** Returns whether a call to {@link #applySurfaceChanges} would change the surface. */
+    public boolean needsApplySurfaceChanges() {
+        return mTop.needsApplySurfaceChanges()
+                || mLeft.needsApplySurfaceChanges()
+                || mBottom.needsApplySurfaceChanges()
+                || mRight.needsApplySurfaceChanges();
+    }
+
+    public void applySurfaceChanges(SurfaceControl.Transaction t) {
+        mTop.applySurfaceChanges(t);
+        mLeft.applySurfaceChanges(t);
+        mBottom.applySurfaceChanges(t);
+        mRight.applySurfaceChanges(t);
+    }
+
     private class LetterboxSurface {
 
         private final String mType;
         private SurfaceControl mSurface;
 
-        private int mLastLeft = 0;
-        private int mLastTop = 0;
-        private int mLastRight = 0;
-        private int mLastBottom = 0;
+        private final Rect mSurfaceFrame = new Rect();
+        private final Rect mLayoutFrame = new Rect();
 
         public LetterboxSurface(String type) {
             mType = type;
         }
 
-        public void setRect(SurfaceControl.Transaction t,
-                int left, int top, int right, int bottom) {
-            if (mLastLeft == left && mLastTop == top
-                    && mLastRight == right && mLastBottom == bottom) {
+        public void layout(int left, int top, int right, int bottom) {
+            if (mLayoutFrame.left == left && mLayoutFrame.top == top
+                    && mLayoutFrame.right == right && mLayoutFrame.bottom == bottom) {
                 // Nothing changed.
                 return;
             }
-
-            if (left < right && top < bottom) {
-                if (mSurface == null) {
-                    createSurface();
-                }
-                t.setPosition(mSurface, left, top);
-                t.setSize(mSurface, right - left, bottom - top);
-                t.show(mSurface);
-            } else if (mSurface != null) {
-                t.hide(mSurface);
-            }
-
-            mLastLeft = left;
-            mLastTop = top;
-            mLastRight = right;
-            mLastBottom = bottom;
+            mLayoutFrame.set(left, top, right, bottom);
         }
 
         private void createSurface() {
@@ -154,11 +161,40 @@
         }
 
         public int getWidth() {
-            return Math.max(0, mLastRight - mLastLeft);
+            return Math.max(0, mLayoutFrame.width());
         }
 
         public int getHeight() {
-            return Math.max(0, mLastBottom - mLastTop);
+            return Math.max(0, mLayoutFrame.height());
+        }
+
+        public boolean isOverlappingWith(Rect rect) {
+            if (getWidth() <= 0 || getHeight() <= 0) {
+                return false;
+            }
+            return Rect.intersects(rect, mLayoutFrame);
+        }
+
+        public void applySurfaceChanges(SurfaceControl.Transaction t) {
+            if (mSurfaceFrame.equals(mLayoutFrame)) {
+                // Nothing changed.
+                return;
+            }
+            mSurfaceFrame.set(mLayoutFrame);
+            if (!mSurfaceFrame.isEmpty()) {
+                if (mSurface == null) {
+                    createSurface();
+                }
+                t.setPosition(mSurface, mSurfaceFrame.left, mSurfaceFrame.top);
+                t.setSize(mSurface, mSurfaceFrame.width(), mSurfaceFrame.height());
+                t.show(mSurface);
+            } else if (mSurface != null) {
+                t.hide(mSurface);
+            }
+        }
+
+        public boolean needsApplySurfaceChanges() {
+            return !mSurfaceFrame.equals(mLayoutFrame);
         }
     }
 }
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 91cd4bb..5912897 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -3074,6 +3074,11 @@
         return (fl & FLAG_FULLSCREEN) != 0 || (sysui & (SYSTEM_UI_FLAG_FULLSCREEN)) != 0;
     }
 
+    @Override
+    public boolean isLetterboxedOverlappingWith(Rect rect) {
+        return mAppToken != null && mAppToken.isLetterboxOverlappingWith(rect);
+    }
+
     boolean isDragResizeChanged() {
         return mDragResizing != computeDragResizing();
     }