Remove token behind fullscreen activity from unknown visibility

When launching activity while keyguard is locked, their window tokens
will be added into unknown visibility controller to smooth the transition.
But if there are multiple starting activities without attached process,
only the last started activity will be resumed. Then the prior activities
are left in unknown visibility and cause transition timeout.

It is resolved by removing the unknown visibility records that won't be
visible when resuming top activity. Also consolidate shouldBeVisible and
shouldBeVisibleIgnoringKeyguard to reduce duplicated checking.

Bug: 123540470
Bug: 141826987
Test: atest ActivityStackTests# \
            testClearUnknownAppVisibilityBehindFullscreenActivity

Change-Id: Ie6d11e856a2b81a39222a0d96644712ce8ddf955
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 0d95706..eeee062 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -4439,34 +4439,30 @@
         removeStartingWindow();
     }
 
-    void notifyUnknownVisibilityLaunched() {
-
+    /**
+     * Suppress transition until the new activity becomes ready, otherwise the keyguard can appear
+     * for a short amount of time before the new process with the new activity had the ability to
+     * set its showWhenLocked flags.
+     */
+    void notifyUnknownVisibilityLaunchedForKeyguardTransition() {
         // No display activities never add a window, so there is no point in waiting them for
         // relayout.
-        if (!noDisplay && getDisplayContent() != null) {
-            getDisplayContent().mUnknownAppVisibilityController.notifyLaunched(this);
-        }
-    }
-
-    /**
-     * @return true if the input activity should be made visible, ignoring any effect Keyguard
-     * might have on the visibility
-     *
-     * TODO(b/123540470): Combine this method and {@link #shouldBeVisible(boolean)}.
-     *
-     * @see {@link ActivityStack#checkKeyguardVisibility}
-     */
-    boolean shouldBeVisibleIgnoringKeyguard(boolean behindFullscreenActivity) {
-        if (!okToShowLocked()) {
-            return false;
+        if (noDisplay || !mStackSupervisor.getKeyguardController().isKeyguardLocked()) {
+            return;
         }
 
-        return !behindFullscreenActivity || mLaunchTaskBehind;
+        mDisplayContent.mUnknownAppVisibilityController.notifyLaunched(this);
     }
 
-    boolean shouldBeVisible(boolean behindFullscreenActivity) {
+    /** @return {@code true} if this activity should be made visible. */
+    boolean shouldBeVisible(boolean behindFullscreenActivity, boolean ignoringKeyguard) {
         // Check whether activity should be visible without Keyguard influence
-        visibleIgnoringKeyguard = shouldBeVisibleIgnoringKeyguard(behindFullscreenActivity);
+        visibleIgnoringKeyguard = (!behindFullscreenActivity || mLaunchTaskBehind)
+                && okToShowLocked();
+
+        if (ignoringKeyguard) {
+            return visibleIgnoringKeyguard;
+        }
 
         final ActivityStack stack = getActivityStack();
         if (stack == null) {
@@ -4495,9 +4491,9 @@
             return false;
         }
 
-        // TODO: Use real value of behindFullscreenActivity calculated using the same logic in
-        // ActivityStack#ensureActivitiesVisibleLocked().
-        return shouldBeVisible(!stack.shouldBeVisible(null /* starting */));
+        final boolean behindFullscreenActivity = stack.checkBehindFullscreenActivity(
+                this, null /* handleBehindFullscreenActivity */);
+        return shouldBeVisible(behindFullscreenActivity, false /* ignoringKeyguard */);
     }
 
     void makeVisibleIfNeeded(ActivityRecord starting, boolean reportToClient) {
@@ -5538,12 +5534,26 @@
         }
     }
 
-    void removeOrphanedStartingWindow(boolean behindFullscreenActivity) {
-        if (mStartingWindowState == STARTING_WINDOW_SHOWN && behindFullscreenActivity) {
+    /**
+     * If any activities below the top running one are in the INITIALIZING state and they have a
+     * starting window displayed then remove that starting window. It is possible that the activity
+     * in this state will never resumed in which case that starting window will be orphaned.
+     * <p>
+     * It should only be called if this activity is behind other fullscreen activity.
+     */
+    void cancelInitializing() {
+        if (mStartingWindowState == STARTING_WINDOW_SHOWN) {
+            // Remove orphaned starting window.
             if (DEBUG_VISIBILITY) Slog.w(TAG_VISIBILITY, "Found orphaned starting window " + this);
             mStartingWindowState = STARTING_WINDOW_REMOVED;
             removeStartingWindow();
         }
+        if (isState(INITIALIZING) && !shouldBeVisible(
+                true /* behindFullscreenActivity */, true /* ignoringKeyguard */)) {
+            // Remove the unknown visibility record because an invisible activity shouldn't block
+            // the keyguard transition.
+            mDisplayContent.mUnknownAppVisibilityController.appRemovedOrHidden(this);
+        }
     }
 
     void postWindowRemoveStartingWindowCleanup(WindowState win) {
diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index a2353a0..8e3995bf 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -162,6 +162,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.function.Consumer;
 
 /**
  * State and management of a single stack of activities.
@@ -2118,14 +2119,21 @@
                     }
                     aboveTop = false;
 
+                    final boolean reallyVisible = r.shouldBeVisible(behindFullscreenActivity,
+                            false /* ignoringKeyguard */);
                     // Check whether activity should be visible without Keyguard influence
-                    final boolean visibleIgnoringKeyguard = r.shouldBeVisibleIgnoringKeyguard(
-                            behindFullscreenActivity);
-                    final boolean reallyVisible = r.shouldBeVisible(behindFullscreenActivity);
-                    if (visibleIgnoringKeyguard) {
-                        behindFullscreenActivity = updateBehindFullscreen(!stackShouldBeVisible,
-                                behindFullscreenActivity, r);
+                    if (r.visibleIgnoringKeyguard) {
+                        if (r.occludesParent()) {
+                            // At this point, nothing else needs to be shown in this task.
+                            if (DEBUG_VISIBILITY) Slog.v(TAG_VISIBILITY, "Fullscreen: at " + r
+                                    + " stackVisible=" + stackShouldBeVisible
+                                    + " behindFullscreen=" + behindFullscreenActivity);
+                            behindFullscreenActivity = true;
+                        } else {
+                            behindFullscreenActivity = false;
+                        }
                     }
+
                     if (reallyVisible) {
                         if (r.finishing) {
                             continue;
@@ -2338,18 +2346,6 @@
         return false;
     }
 
-    private boolean updateBehindFullscreen(boolean stackInvisible, boolean behindFullscreenActivity,
-            ActivityRecord r) {
-        if (r.occludesParent()) {
-            if (DEBUG_VISIBILITY) Slog.v(TAG_VISIBILITY, "Fullscreen: at " + r
-                        + " stackInvisible=" + stackInvisible
-                        + " behindFullscreenActivity=" + behindFullscreenActivity);
-            // At this point, nothing else needs to be shown in this task.
-            behindFullscreenActivity = true;
-        }
-        return behindFullscreenActivity;
-    }
-
     void convertActivityToTranslucent(ActivityRecord r) {
         mTranslucentActivityWaiting = r;
         mUndrawnActivitiesBelowTopTranslucent.clear();
@@ -2400,14 +2396,22 @@
         }
     }
 
-    /** If any activities below the top running one are in the INITIALIZING state and they have a
-     * starting window displayed then remove that starting window. It is possible that the activity
-     * in this state will never resumed in which case that starting window will be orphaned. */
+    /** @see ActivityRecord#cancelInitializing() */
     void cancelInitializingActivities() {
-        final ActivityRecord topActivity = topRunningActivityLocked();
-        boolean aboveTop = true;
         // We don't want to clear starting window for activities that aren't behind fullscreen
         // activities as we need to display their starting window until they are done initializing.
+        checkBehindFullscreenActivity(null /* toCheck */, ActivityRecord::cancelInitializing);
+    }
+
+    /**
+     * If an activity {@param toCheck} is given, this method returns {@code true} if the activity
+     * is occluded by any fullscreen activity. If there is no {@param toCheck} and the handling
+     * function {@param handleBehindFullscreenActivity} is given, this method will pass all occluded
+     * activities to the function.
+     */
+    boolean checkBehindFullscreenActivity(ActivityRecord toCheck,
+            Consumer<ActivityRecord> handleBehindFullscreenActivity) {
+        boolean aboveTop = true;
         boolean behindFullscreenActivity = false;
 
         if (!shouldBeVisible(null)) {
@@ -2417,22 +2421,40 @@
             behindFullscreenActivity = true;
         }
 
+        final boolean handlingOccluded = toCheck == null && handleBehindFullscreenActivity != null;
+        if (!handlingOccluded && behindFullscreenActivity) {
+            return true;
+        }
+
+        final ActivityRecord topActivity = topRunningActivityLocked();
         for (int taskNdx = mTaskHistory.size() - 1; taskNdx >= 0; --taskNdx) {
             final TaskRecord task = mTaskHistory.get(taskNdx);
             for (int activityNdx = task.getChildCount() - 1; activityNdx >= 0; --activityNdx) {
                 final ActivityRecord r = task.getChildAt(activityNdx);
                 if (aboveTop) {
                     if (r == topActivity) {
+                        if (r == toCheck) {
+                            // It is the top activity in a visible stack.
+                            return false;
+                        }
                         aboveTop = false;
                     }
                     behindFullscreenActivity |= r.occludesParent();
                     continue;
                 }
 
-                r.removeOrphanedStartingWindow(behindFullscreenActivity);
+                if (handlingOccluded) {
+                    handleBehindFullscreenActivity.accept(r);
+                } else if (r == toCheck) {
+                    return behindFullscreenActivity;
+                } else if (behindFullscreenActivity) {
+                    // It is occluded before {@param toCheck} is found.
+                    return true;
+                }
                 behindFullscreenActivity |= r.occludesParent();
             }
         }
+        return behindFullscreenActivity;
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
index 191e870..64351fb 100644
--- a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
@@ -752,9 +752,7 @@
                 andResume = false;
             }
 
-            if (getKeyguardController().isKeyguardLocked()) {
-                r.notifyUnknownVisibilityLaunched();
-            }
+            r.notifyUnknownVisibilityLaunchedForKeyguardTransition();
 
             // Have the window manager re-evaluate the orientation of the screen based on the new
             // activity order.  Note that as a result of this, it can call back into the activity
@@ -992,12 +990,7 @@
             knownToBeDead = true;
         }
 
-        // Suppress transition until the new activity becomes ready, otherwise the keyguard can
-        // appear for a short amount of time before the new process with the new activity had the
-        // ability to set its showWhenLocked flags.
-        if (getKeyguardController().isKeyguardLocked()) {
-            r.notifyUnknownVisibilityLaunched();
-        }
+        r.notifyUnknownVisibilityLaunchedForKeyguardTransition();
 
         final boolean isTop = andResume && r.isTopRunningActivity();
         mService.startProcessAsync(r, knownToBeDead, isTop, isTop ? "top-activity" : "activity");
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java
index 2835c1f..fcebb81 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStackTests.java
@@ -1140,12 +1140,50 @@
     }
 
     @Test
+    public void testClearUnknownAppVisibilityBehindFullscreenActivity() {
+        final UnknownAppVisibilityController unknownAppVisibilityController =
+                mDefaultDisplay.mDisplayContent.mUnknownAppVisibilityController;
+        final KeyguardController keyguardController = mSupervisor.getKeyguardController();
+        doReturn(true).when(keyguardController).isKeyguardLocked();
+
+        // Start 2 activities that their processes have not yet started.
+        final ActivityRecord[] activities = new ActivityRecord[2];
+        mSupervisor.beginDeferResume();
+        for (int i = 0; i < activities.length; i++) {
+            final ActivityRecord r = new ActivityBuilder(mService).setTask(mTask).build();
+            activities[i] = r;
+            doReturn(null).when(mService).getProcessController(
+                    eq(r.processName), eq(r.info.applicationInfo.uid));
+            r.setState(ActivityStack.ActivityState.INITIALIZING, "test");
+            // Ensure precondition that the activity is opaque.
+            assertTrue(r.occludesParent());
+            mSupervisor.startSpecificActivityLocked(r, false /* andResume */,
+                    false /* checkConfig */);
+        }
+        mSupervisor.endDeferResume();
+
+        doReturn(false).when(mService).isBooting();
+        doReturn(true).when(mService).isBooted();
+        // 2 activities are started while keyguard is locked, so they are waiting to be resolved.
+        assertFalse(unknownAppVisibilityController.allResolved());
+
+        // Assume the top activity is going to resume and
+        // {@link RootActivityContainer#cancelInitializingActivities} should clear the unknown
+        // visibility records that are occluded.
+        mStack.resumeTopActivityUncheckedLocked(null /* prev */, null /* options */);
+        // Assume the top activity relayouted, just remove it directly.
+        unknownAppVisibilityController.appRemovedOrHidden(activities[1]);
+        // All unresolved records should be removed.
+        assertTrue(unknownAppVisibilityController.allResolved());
+    }
+
+    @Test
     public void testNonTopVisibleActivityNotResume() {
         final ActivityRecord nonTopVisibleActivity =
                 new ActivityBuilder(mService).setTask(mTask).build();
         new ActivityBuilder(mService).setTask(mTask).build();
         doReturn(false).when(nonTopVisibleActivity).attachedToProcess();
-        doReturn(true).when(nonTopVisibleActivity).shouldBeVisibleIgnoringKeyguard(anyBoolean());
+        doReturn(true).when(nonTopVisibleActivity).shouldBeVisible(anyBoolean(), anyBoolean());
         doNothing().when(mSupervisor).startSpecificActivityLocked(any(), anyBoolean(),
                 anyBoolean());