Add support for singleTaskInstance displays

singleTaskInstance displays will only contain on task. This is mostly
used by ActivityView for use cases like bubbles.

Bug: 121047677
Test: atest ActivityManagerMultiDisplayTests#testSingleTaskInstanceDisplay
Change-Id: I5166015d8ecfa2845b4ffaa6c16d21a30a56b8a8
diff --git a/services/core/java/com/android/server/wm/ActivityDisplay.java b/services/core/java/com/android/server/wm/ActivityDisplay.java
index 973499f..1f638c7 100644
--- a/services/core/java/com/android/server/wm/ActivityDisplay.java
+++ b/services/core/java/com/android/server/wm/ActivityDisplay.java
@@ -37,6 +37,7 @@
 import static com.android.server.am.ActivityDisplayProto.FOCUSED_STACK_ID;
 import static com.android.server.am.ActivityDisplayProto.ID;
 import static com.android.server.am.ActivityDisplayProto.RESUMED_ACTIVITY;
+import static com.android.server.am.ActivityDisplayProto.SINGLE_TASK_INSTANCE;
 import static com.android.server.am.ActivityDisplayProto.STACKS;
 import static com.android.server.wm.ActivityStack.ActivityState.RESUMED;
 import static com.android.server.wm.ActivityStackSupervisor.TAG_TASKS;
@@ -120,6 +121,9 @@
      */
     private boolean mRemoved;
 
+    /** The display can only contain one task. */
+    private boolean mSingleTaskInstance;
+
     /**
      * A focusable stack that is purposely to be positioned at the top. Although the stack may not
      * have the topmost index, it is used as a preferred candidate to prevent being unable to resume
@@ -244,6 +248,10 @@
         final ActivityStack prevFocusedStack = updateLastFocusedStackReason != null
                 ? getFocusedStack() : null;
         final boolean wasContained = mStacks.remove(stack);
+        if (mSingleTaskInstance && getChildCount() > 0) {
+            throw new IllegalStateException(
+                    "positionChildAt: Can only have one child on display=" + this);
+        }
         final int insertPosition = getTopInsertPosition(stack, position);
         mStacks.add(insertPosition, stack);
 
@@ -403,6 +411,14 @@
      */
     <T extends ActivityStack> T createStack(int windowingMode, int activityType, boolean onTop) {
 
+        if (mSingleTaskInstance && getChildCount() > 0) {
+            // Create stack on default display instead since this display can only contain 1 stack.
+            // TODO: Kinda a hack, but better that having the decision at each call point. Hoping
+            // this goes away once ActivityView is no longer using virtual displays.
+            return mRootActivityContainer.getDefaultDisplay().createStack(
+                    windowingMode, activityType, onTop);
+        }
+
         if (activityType == ACTIVITY_TYPE_UNDEFINED) {
             // Can't have an undefined stack type yet...so re-map to standard. Anyone that wants
             // anything else should be passing it in anyways...
@@ -1337,8 +1353,31 @@
         }
     }
 
+    void setDisplayToSingleTaskInstance() {
+        final int childCount = getChildCount();
+        if (childCount > 1) {
+            throw new IllegalArgumentException("Display already has multiple stacks. display="
+                    + this);
+        }
+        if (childCount > 0) {
+            final ActivityStack stack = getChildAt(0);
+            if (stack.getChildCount() > 1) {
+                throw new IllegalArgumentException("Display stack already has multiple tasks."
+                        + " display=" + this + " stack=" + stack);
+            }
+        }
+
+        mSingleTaskInstance = true;
+    }
+
+    /** Returns true if the display can only contain one task */
+    boolean isSingleTaskInstance() {
+        return mSingleTaskInstance;
+    }
+
     public void dump(PrintWriter pw, String prefix) {
-        pw.println(prefix + "displayId=" + mDisplayId + " stacks=" + mStacks.size());
+        pw.println(prefix + "displayId=" + mDisplayId + " stacks=" + mStacks.size()
+                + (mSingleTaskInstance ? " mSingleTaskInstance" : ""));
         final String myPrefix = prefix + " ";
         if (mHomeStack != null) {
             pw.println(myPrefix + "mHomeStack=" + mHomeStack);
@@ -1373,6 +1412,7 @@
         final long token = proto.start(fieldId);
         super.writeToProto(proto, CONFIGURATION_CONTAINER, false /* trim */);
         proto.write(ID, mDisplayId);
+        proto.write(SINGLE_TASK_INSTANCE, mSingleTaskInstance);
         final ActivityStack focusedStack = getFocusedStack();
         if (focusedStack != null) {
             proto.write(FOCUSED_STACK_ID, focusedStack.mStackId);
diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index 6755c73..6292c4a 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -1142,6 +1142,12 @@
         }
     }
 
+    /** @return true if the stack can only contain one task */
+    boolean isSingleTaskInstance() {
+        final ActivityDisplay display = getDisplay();
+        return display != null && display.isSingleTaskInstance();
+    }
+
     final void removeActivitiesFromLRUListLocked(TaskRecord task) {
         for (ActivityRecord r : task.mActivities) {
             mLRUActivities.remove(r);
@@ -5348,6 +5354,10 @@
             String reason) {
         // TODO: Is this remove really needed? Need to look into the call path for the other addTask
         mTaskHistory.remove(task);
+        if (isSingleTaskInstance() && !mTaskHistory.isEmpty()) {
+            throw new IllegalStateException("Can only have one child on stack=" + this);
+        }
+
         position = getAdjustedPositionForTask(task, position, null /* starting */);
         final boolean toTop = position >= mTaskHistory.size();
         final ActivityStack prevStack = preAddTask(task, reason, toTop);
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index c4be1ba537..86c5d4d 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -4569,6 +4569,26 @@
         }
     }
 
+    /**
+     * Makes the display with the given id a single task instance display. I.e the display can only
+     * contain one task.
+     */
+    @Override
+    public void setDisplayToSingleTaskInstance(int displayId) {
+        mAmInternal.enforceCallingPermission(Manifest.permission.MANAGE_ACTIVITY_STACKS,
+                "setDisplayToSingleTaskInstance");
+        final long origId = Binder.clearCallingIdentity();
+        try {
+            final ActivityDisplay display =
+                    mRootActivityContainer.getActivityDisplayOrCreate(displayId);
+            if (display != null) {
+                display.setDisplayToSingleTaskInstance();
+            }
+        } finally {
+            Binder.restoreCallingIdentity(origId);
+        }
+    }
+
     void dumpLastANRLocked(PrintWriter pw) {
         pw.println("ACTIVITY MANAGER LAST ANR (dumpsys activity lastanr)");
         if (mLastANRState == null) {
diff --git a/services/core/java/com/android/server/wm/RootActivityContainer.java b/services/core/java/com/android/server/wm/RootActivityContainer.java
index 6f92e64..b7ea1d7 100644
--- a/services/core/java/com/android/server/wm/RootActivityContainer.java
+++ b/services/core/java/com/android/server/wm/RootActivityContainer.java
@@ -915,6 +915,13 @@
                     + " to its current displayId=" + displayId);
         }
 
+        if (activityDisplay.isSingleTaskInstance() && activityDisplay.getChildCount() > 0) {
+            // We don't allow moving stacks to single instance display that already has a child.
+            Slog.e(TAG, "Can not move stack=" + stack
+                    + " to single task instance display=" + activityDisplay);
+            return;
+        }
+
         stack.reparent(activityDisplay, onTop, false /* displayRemoved */);
         // TODO(multi-display): resize stacks properly if moved from split-screen.
     }