Merge "Notify organizer when the last activity in TaskFragment enter PiP" into tm-dev am: 88671a050f am: 38f42547e4

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/17713423

Change-Id: I3439bb17ed2f7438133f2bbf01036761a0c7bfaf
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/core/java/android/window/TaskFragmentInfo.java b/core/java/android/window/TaskFragmentInfo.java
index a118f9a..f72164e 100644
--- a/core/java/android/window/TaskFragmentInfo.java
+++ b/core/java/android/window/TaskFragmentInfo.java
@@ -52,9 +52,6 @@
     @NonNull
     private final Configuration mConfiguration = new Configuration();
 
-    /** Whether the TaskFragment contains any child Window Container. */
-    private final boolean mIsEmpty;
-
     /** The number of the running activities in the TaskFragment. */
     private final int mRunningActivityCount;
 
@@ -77,21 +74,27 @@
      */
     private final boolean mIsTaskClearedForReuse;
 
+    /**
+     * Whether the last running activity in the TaskFragment was reparented to a different Task
+     * because it is entering PiP.
+     */
+    private final boolean mIsTaskFragmentClearedForPip;
+
     /** @hide */
     public TaskFragmentInfo(
             @NonNull IBinder fragmentToken, @NonNull WindowContainerToken token,
-            @NonNull Configuration configuration, boolean isEmpty, int runningActivityCount,
+            @NonNull Configuration configuration, int runningActivityCount,
             boolean isVisible, @NonNull List<IBinder> activities, @NonNull Point positionInParent,
-            boolean isTaskClearedForReuse) {
+            boolean isTaskClearedForReuse, boolean isTaskFragmentClearedForPip) {
         mFragmentToken = requireNonNull(fragmentToken);
         mToken = requireNonNull(token);
         mConfiguration.setTo(configuration);
-        mIsEmpty = isEmpty;
         mRunningActivityCount = runningActivityCount;
         mIsVisible = isVisible;
         mActivities.addAll(activities);
         mPositionInParent = requireNonNull(positionInParent);
         mIsTaskClearedForReuse = isTaskClearedForReuse;
+        mIsTaskFragmentClearedForPip = isTaskFragmentClearedForPip;
     }
 
     @NonNull
@@ -110,7 +113,7 @@
     }
 
     public boolean isEmpty() {
-        return mIsEmpty;
+        return mRunningActivityCount == 0;
     }
 
     public boolean hasRunningActivity() {
@@ -140,6 +143,11 @@
         return mIsTaskClearedForReuse;
     }
 
+    /** @hide */
+    public boolean isTaskFragmentClearedForPip() {
+        return mIsTaskFragmentClearedForPip;
+    }
+
     @WindowingMode
     public int getWindowingMode() {
         return mConfiguration.windowConfiguration.getWindowingMode();
@@ -156,25 +164,25 @@
 
         return mFragmentToken.equals(that.mFragmentToken)
                 && mToken.equals(that.mToken)
-                && mIsEmpty == that.mIsEmpty
                 && mRunningActivityCount == that.mRunningActivityCount
                 && mIsVisible == that.mIsVisible
                 && getWindowingMode() == that.getWindowingMode()
                 && mActivities.equals(that.mActivities)
                 && mPositionInParent.equals(that.mPositionInParent)
-                && mIsTaskClearedForReuse == that.mIsTaskClearedForReuse;
+                && mIsTaskClearedForReuse == that.mIsTaskClearedForReuse
+                && mIsTaskFragmentClearedForPip == that.mIsTaskFragmentClearedForPip;
     }
 
     private TaskFragmentInfo(Parcel in) {
         mFragmentToken = in.readStrongBinder();
         mToken = in.readTypedObject(WindowContainerToken.CREATOR);
         mConfiguration.readFromParcel(in);
-        mIsEmpty = in.readBoolean();
         mRunningActivityCount = in.readInt();
         mIsVisible = in.readBoolean();
         in.readBinderList(mActivities);
         mPositionInParent = requireNonNull(in.readTypedObject(Point.CREATOR));
         mIsTaskClearedForReuse = in.readBoolean();
+        mIsTaskFragmentClearedForPip = in.readBoolean();
     }
 
     /** @hide */
@@ -183,12 +191,12 @@
         dest.writeStrongBinder(mFragmentToken);
         dest.writeTypedObject(mToken, flags);
         mConfiguration.writeToParcel(dest, flags);
-        dest.writeBoolean(mIsEmpty);
         dest.writeInt(mRunningActivityCount);
         dest.writeBoolean(mIsVisible);
         dest.writeBinderList(mActivities);
         dest.writeTypedObject(mPositionInParent, flags);
         dest.writeBoolean(mIsTaskClearedForReuse);
+        dest.writeBoolean(mIsTaskFragmentClearedForPip);
     }
 
     @NonNull
@@ -210,12 +218,12 @@
         return "TaskFragmentInfo{"
                 + " fragmentToken=" + mFragmentToken
                 + " token=" + mToken
-                + " isEmpty=" + mIsEmpty
                 + " runningActivityCount=" + mRunningActivityCount
                 + " isVisible=" + mIsVisible
                 + " activities=" + mActivities
                 + " positionInParent=" + mPositionInParent
                 + " isTaskClearedForReuse=" + mIsTaskClearedForReuse
+                + " isTaskFragmentClearedForPip" + mIsTaskFragmentClearedForPip
                 + "}";
     }
 
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 3ec8843..33a41ec 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -155,13 +155,18 @@
         // Check if there are no running activities - consider the container empty if there are no
         // non-finishing activities left.
         if (!taskFragmentInfo.hasRunningActivity()) {
-            // TODO(b/225371112): Don't finish dependent if the last activity is moved to the PIP
-            // Task.
-            // Do not finish the dependents if this TaskFragment was cleared due to launching
-            // activity in the Task.
-            final boolean shouldFinishDependent =
-                    !taskFragmentInfo.isTaskClearedForReuse();
-            mPresenter.cleanupContainer(container, shouldFinishDependent);
+            if (taskFragmentInfo.isTaskFragmentClearedForPip()) {
+                // Do not finish the dependents if the last activity is reparented to PiP.
+                // Instead, the original split should be cleanup, and the dependent may be expanded
+                // to fullscreen.
+                cleanupForEnterPip(container);
+                mPresenter.cleanupContainer(container, false /* shouldFinishDependent */);
+            } else {
+                // Do not finish the dependents if this TaskFragment was cleared due to launching
+                // activity in the Task.
+                final boolean shouldFinishDependent = !taskFragmentInfo.isTaskClearedForReuse();
+                mPresenter.cleanupContainer(container, shouldFinishDependent);
+            }
         } else if (wasInPip && isInPip) {
             // No update until exit PIP.
             return;
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 3cb9a56..1e1cbaf 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2006,6 +2006,7 @@
             // of the activity entering PIP
             r.getDisplayContent().prepareAppTransition(TRANSIT_NONE);
 
+            final TaskFragment organizedTf = r.getOrganizedTaskFragment();
             // TODO: Does it make sense to only count non-finishing activities?
             final boolean singleActivity = task.getActivityCount() == 1;
             if (singleActivity) {
@@ -2042,6 +2043,14 @@
                     task.clearLastRecentsAnimationTransaction(false /* forceRemoveOverlay */);
                 }
 
+                // The organized TaskFragment is becoming empty because this activity is reparented
+                // to a new PIP Task. In this case, we should notify the organizer about why the
+                // TaskFragment becomes empty.
+                if (organizedTf != null && organizedTf.getNonFinishingActivityCount() == 1
+                        && organizedTf.getTopNonFinishingActivity() == r) {
+                    organizedTf.mClearedTaskFragmentForPip = true;
+                }
+
                 // There are multiple activities in the task and moving the top activity should
                 // reveal/leave the other activities in their original task.
                 // On the other hand, ActivityRecord#onParentChanged takes care of setting the
@@ -2102,6 +2111,13 @@
             // Reset the state that indicates it can enter PiP while pausing after we've moved it
             // to the root pinned task
             r.supportsEnterPipOnTaskSwitch = false;
+
+            if (organizedTf != null && organizedTf.mClearedTaskFragmentForPip) {
+                // Dispatch the pending info to TaskFragmentOrganizer before PIP animation.
+                // Otherwise, it will keep waiting for the empty TaskFragment to be non-empty.
+                mService.mTaskFragmentOrganizerController.dispatchPendingInfoChangedEvent(
+                        organizedTf);
+            }
         } finally {
             mService.continueWindowLayout();
             try {
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 83bd979..4e0d84c 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -193,6 +193,12 @@
     boolean mClearedTaskForReuse;
 
     /**
+     * The last running activity of the TaskFragment was reparented to a different Task because it
+     * is entering PiP.
+     */
+    boolean mClearedTaskFragmentForPip;
+
+    /**
      * When we are in the process of pausing an activity, before starting the
      * next one, this variable holds the activity that is currently being paused.
      *
@@ -847,6 +853,16 @@
         return getActivity((r) -> r.canBeTopRunning() && r.getTask() == this.getTask());
     }
 
+    int getNonFinishingActivityCount() {
+        final int[] runningActivityCount = new int[1];
+        forAllActivities(a -> {
+            if (!a.finishing) {
+                runningActivityCount[0]++;
+            }
+        });
+        return runningActivityCount[0];
+    }
+
     boolean isTopActivityFocusable() {
         final ActivityRecord r = topRunningActivity();
         return r != null ? r.isFocusable()
@@ -1709,6 +1725,7 @@
     void addChild(WindowContainer child, int index) {
         ActivityRecord r = topRunningActivity();
         mClearedTaskForReuse = false;
+        mClearedTaskFragmentForPip = false;
 
         boolean isAddingActivity = child.asActivityRecord() != null;
         final Task task = isAddingActivity ? getTask() : null;
@@ -2253,22 +2270,16 @@
         }
         final Point positionInParent = new Point();
         getRelativePosition(positionInParent);
-        final int[] runningActivityCount = new int[1];
-        forAllActivities(a -> {
-            if (!a.finishing) {
-                runningActivityCount[0]++;
-            }
-        });
         return new TaskFragmentInfo(
                 mFragmentToken,
                 mRemoteToken.toWindowContainerToken(),
                 getConfiguration(),
-                runningActivityCount[0] == 0,
-                runningActivityCount[0],
+                getNonFinishingActivityCount(),
                 isVisible(),
                 childActivities,
                 positionInParent,
-                mClearedTaskForReuse);
+                mClearedTaskForReuse,
+                mClearedTaskFragmentForPip);
     }
 
     @Nullable
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 30c2413..54fa4e4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -209,7 +209,7 @@
     }
 
     @Test
-    public void testEmbeddedTaskFragmentEnterPip_resetOrganizerOverrideConfig() {
+    public void testEmbeddedTaskFragmentEnterPip_singleActivity_resetOrganizerOverrideConfig() {
         final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
                 .setOrganizer(mOrganizer)
                 .setFragmentToken(new Binder())
@@ -242,6 +242,38 @@
     }
 
     @Test
+    public void testEmbeddedTaskFragmentEnterPip_multiActivities_notifyOrganizer() {
+        final Task task = createTask(mDisplayContent);
+        final TaskFragment taskFragment0 = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(new Binder())
+                .createActivityCount(1)
+                .build();
+        final TaskFragment taskFragment1 = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(new Binder())
+                .createActivityCount(1)
+                .build();
+        final ActivityRecord activity0 = taskFragment0.getTopMostActivity();
+        spyOn(mAtm.mTaskFragmentOrganizerController);
+
+        // Move activity to pinned.
+        mRootWindowContainer.moveActivityToPinnedRootTask(activity0,
+                null /* launchIntoPipHostActivity */, "test");
+
+        // Ensure taskFragment requested config is reset.
+        assertTrue(taskFragment0.mClearedTaskFragmentForPip);
+        assertFalse(taskFragment1.mClearedTaskFragmentForPip);
+        final TaskFragmentInfo info = taskFragment0.getTaskFragmentInfo();
+        assertTrue(info.isTaskFragmentClearedForPip());
+        assertTrue(info.isEmpty());
+        verify(mAtm.mTaskFragmentOrganizerController)
+                .dispatchPendingInfoChangedEvent(taskFragment0);
+    }
+
+    @Test
     public void testActivityHasOverlayOverUntrustedModeEmbedded() {
         final Task rootTask = createTask(mDisplayContent, WINDOWING_MODE_MULTI_WINDOW,
                 ACTIVITY_TYPE_STANDARD);