Use current fullscreen opaque window as insets control target

This fixes stats bar blinking when switching from an app that hides
status bar to another that shows status bar. It is caused by the
TopFullscreenOpaqueWindow in DisplayPolicy will be updated according
to visibility. There may be an intermediate state that the insets
control target is still the previous window, then the transient
showing bar will be hidden a while.

It is tempting to make canAffectSystemUiFlags accept the activity
with mVisibleRequested=true so TopFullscreenOpaqueWindow can be
updated earlier. But that will break status bar hiding animation
when switching from an app that shows the bar to another that hides
the bar.

Also fix:
 - The original insets source is modified when getting insets state
   for client because the default "new InsetsState" will reuse the
   given insets sources.
 - Redundant invocation of updateBarControlTarget when calling
   abortTransient from updateBarControlTarget.

Fixes: 168023253
Bug: 160458371
Test: InsetsPolicyTest#testControlsForDispatch_topAppHidesStatusBar
Change-Id: I60f29dba81b0273a189ddd0a48a2e84155177f5b
Merged-In: I60f29dba81b0273a189ddd0a48a2e84155177f5b
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index fd67db1..be1d0fc 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -36,6 +36,7 @@
 import android.view.InsetsAnimationControlImpl;
 import android.view.InsetsAnimationControlRunner;
 import android.view.InsetsController;
+import android.view.InsetsSource;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
 import android.view.InsetsState.InternalInsetsType;
@@ -212,11 +213,21 @@
      * @see InsetsStateController#getInsetsForDispatch
      */
     InsetsState getInsetsForDispatch(WindowState target) {
-        InsetsState originalState = mStateController.getInsetsForDispatch(target);
+        final InsetsState originalState = mStateController.getInsetsForDispatch(target);
         InsetsState state = originalState;
         for (int i = mShowingTransientTypes.size() - 1; i >= 0; i--) {
-            state = new InsetsState(state);
-            state.setSourceVisible(mShowingTransientTypes.get(i), false);
+            final int type = mShowingTransientTypes.get(i);
+            final InsetsSource originalSource = state.peekSource(type);
+            if (originalSource != null && originalSource.isVisible()) {
+                if (state == originalState) {
+                    // The source will be modified, create a non-deep copy to store the new one.
+                    state = new InsetsState(originalState);
+                }
+                // Replace the source with a copy in invisible state.
+                final InsetsSource source = new InsetsSource(originalSource);
+                source.setVisible(false);
+                state.addSource(source);
+            }
         }
         return state;
     }
@@ -252,11 +263,14 @@
         }
     }
 
+    /**
+     * If the caller is not {@link #updateBarControlTarget}, it should call
+     * updateBarControlTarget(mFocusedWin) after this invocation.
+     */
     private void abortTransient() {
         mPolicy.getStatusBarManagerInternal().abortTransient(mDisplayContent.getDisplayId(),
                 mShowingTransientTypes.toArray());
         mShowingTransientTypes.clear();
-        updateBarControlTarget(mFocusedWin);
     }
 
     private @Nullable InsetsControlTarget getFakeControlTarget(@Nullable WindowState focused,
@@ -290,7 +304,7 @@
             // fake control to the client, so that it can re-show the bar during this scenario.
             return mDummyControlTarget;
         }
-        if (mPolicy.topAppHidesStatusBar()) {
+        if (!canBeTopFullscreenOpaqueWindow(focusedWin) && mPolicy.topAppHidesStatusBar()) {
             // Non-fullscreen focused window should not break the state that the top-fullscreen-app
             // window hides status bar.
             return mPolicy.getTopFullscreenOpaqueWindow();
@@ -298,6 +312,16 @@
         return focusedWin;
     }
 
+    private static boolean canBeTopFullscreenOpaqueWindow(@Nullable WindowState win) {
+        // The condition doesn't use WindowState#canAffectSystemUiFlags because the window may
+        // haven't drawn or committed the visibility.
+        final boolean nonAttachedAppWindow = win != null
+                && win.mAttrs.type >= WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW
+                && win.mAttrs.type <= WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
+        return nonAttachedAppWindow && win.mAttrs.isFullscreen() && !win.isFullyTransparent()
+                && !win.inMultiWindowMode();
+    }
+
     private @Nullable InsetsControlTarget getNavControlTarget(@Nullable WindowState focusedWin,
             boolean forceShowsSystemBarsForWindowingMode) {
         if (mShowingTransientTypes.indexOf(ITYPE_NAVIGATION_BAR) != -1) {
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index e85049c..3c0cb17 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -480,9 +480,7 @@
         final int color = ColorUtils.setAlphaComponent(
                 task.getTaskDescription().getBackgroundColor(), 255);
         final LayoutParams attrs = mainWindow.getAttrs();
-        final InsetsPolicy insetsPolicy = mainWindow.getDisplayContent().getInsetsPolicy();
-        final InsetsState insetsState =
-                new InsetsState(insetsPolicy.getInsetsForDispatch(mainWindow));
+        final InsetsState insetsState = new InsetsState(mainWindow.getInsetsState());
         mergeInsetsSources(insetsState, mainWindow.getRequestedInsetsState());
         final Rect systemBarInsets = getSystemBarInsets(mainWindow.getFrameLw(), insetsState);
         final SystemBarBackgroundPainter decorPainter = new SystemBarBackgroundPainter(attrs.flags,
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotSurface.java b/services/core/java/com/android/server/wm/TaskSnapshotSurface.java
index 6cc0ba5..94229b9 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotSurface.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotSurface.java
@@ -245,11 +245,7 @@
             task.getBounds(taskBounds);
             currentOrientation = topFullscreenOpaqueWindow.getConfiguration().orientation;
             activityType = activity.getActivityType();
-
-            final InsetsPolicy insetsPolicy = topFullscreenOpaqueWindow.getDisplayContent()
-                    .getInsetsPolicy();
-            insetsState =
-                    new InsetsState(insetsPolicy.getInsetsForDispatch(topFullscreenOpaqueWindow));
+            insetsState = new InsetsState(topFullscreenOpaqueWindow.getInsetsState());
             mergeInsetsSources(insetsState, topFullscreenOpaqueWindow.getRequestedInsetsState());
         }
         try {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 451aa4c..bab83dc 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1525,6 +1525,10 @@
         return getDisplayContent().getDisplayInfo();
     }
 
+    /**
+     * Returns the insets state for the client. Its sources may be the copies with visibility
+     * modification according to the state of transient bars.
+     */
     InsetsState getInsetsState() {
         return getDisplayContent().getInsetsPolicy().getInsetsForDispatch(this);
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
index 90af8a1..7d84920 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
@@ -38,10 +38,13 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
 
+import android.app.StatusBarManager;
 import android.platform.test.annotations.Presubmit;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
@@ -49,6 +52,8 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.server.statusbar.StatusBarManagerInternal;
+
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
@@ -222,6 +227,23 @@
         assertNotNull(fullscreenAppControls);
         assertEquals(1, fullscreenAppControls.length);
         assertEquals(ITYPE_STATUS_BAR, fullscreenAppControls[0].getType());
+
+        // Assume mFocusedWindow is updated but mTopFullscreenOpaqueWindowState hasn't.
+        final WindowState newFocusedFullscreenApp = addWindow(TYPE_APPLICATION, "newFullscreenApp");
+        final InsetsState newRequestedState = new InsetsState();
+        newRequestedState.getSource(ITYPE_STATUS_BAR).setVisible(true);
+        newFocusedFullscreenApp.updateRequestedInsetsState(newRequestedState);
+        // Make sure status bar is hidden by previous insets state.
+        mDisplayContent.getInsetsPolicy().updateBarControlTarget(fullscreenApp);
+
+        final StatusBarManagerInternal sbmi =
+                mDisplayContent.getDisplayPolicy().getStatusBarManagerInternal();
+        clearInvocations(sbmi);
+        mDisplayContent.getInsetsPolicy().updateBarControlTarget(newFocusedFullscreenApp);
+        // The status bar should be shown by newFocusedFullscreenApp even
+        // mTopFullscreenOpaqueWindowState is still fullscreenApp.
+        verify(sbmi).setWindowState(mDisplayContent.mDisplayId, StatusBarManager.WINDOW_STATUS_BAR,
+                StatusBarManager.WINDOW_STATE_SHOWING);
     }
 
     @Test
@@ -309,6 +331,15 @@
         final InsetsState state = policy.getInsetsForDispatch(mAppWindow);
         state.setSourceVisible(ITYPE_STATUS_BAR, true);
         state.setSourceVisible(ITYPE_NAVIGATION_BAR, true);
+
+        final InsetsState clientState = mAppWindow.getInsetsState();
+        // The transient bar states for client should be invisible.
+        assertFalse(clientState.getSource(ITYPE_STATUS_BAR).isVisible());
+        assertFalse(clientState.getSource(ITYPE_NAVIGATION_BAR).isVisible());
+        // The original state shouldn't be modified.
+        assertTrue(state.getSource(ITYPE_STATUS_BAR).isVisible());
+        assertTrue(state.getSource(ITYPE_NAVIGATION_BAR).isVisible());
+
         policy.onInsetsModified(mAppWindow, state);
         waitUntilWindowAnimatorIdle();