Add hierarchy operations to container transaction

Also add a way to query for child containers of a container.

Combined, these provide enough flexibility to re-arrange
containers to system-ui. For now, it is locked-down a bit so
it only provides children of TaskTiles and can only reparent
ActivityStacks. Eventually, this can be expanded for nested
tasks or display areas.

Bug: 133381284
Test: Added TaskTileTest#testHierarchyTransaction
Change-Id: Id0e89a4e4b9352d9392349c4e1fe7d69b4f9f212
diff --git a/core/java/android/app/ITaskOrganizerController.aidl b/core/java/android/app/ITaskOrganizerController.aidl
index bfc42ef..5d5956e 100644
--- a/core/java/android/app/ITaskOrganizerController.aidl
+++ b/core/java/android/app/ITaskOrganizerController.aidl
@@ -51,6 +51,9 @@
     /** Deletes a persistent root task in WM */
     boolean deleteRootTask(IWindowContainer task);
 
+    /** Gets direct child tasks (ordered from top-to-bottom) */
+    List<ActivityManager.RunningTaskInfo> getChildTasks(in IWindowContainer parent);
+
     /** Get the root task which contains the current ime target */
     IWindowContainer getImeTarget(int display);
 
diff --git a/core/java/android/view/WindowContainerTransaction.java b/core/java/android/view/WindowContainerTransaction.java
index 33f21f2..cf34b0b 100644
--- a/core/java/android/view/WindowContainerTransaction.java
+++ b/core/java/android/view/WindowContainerTransaction.java
@@ -16,6 +16,8 @@
 
 package android.view;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.WindowConfiguration;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
@@ -25,6 +27,8 @@
 import android.os.Parcelable;
 import android.util.ArrayMap;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -36,10 +40,14 @@
 public class WindowContainerTransaction implements Parcelable {
     private final ArrayMap<IBinder, Change> mChanges = new ArrayMap<>();
 
+    // Flat list because re-order operations are order-dependent
+    private final ArrayList<HierarchyOp> mHierarchyOps = new ArrayList<>();
+
     public WindowContainerTransaction() {}
 
     protected WindowContainerTransaction(Parcel in) {
         in.readMap(mChanges, null /* loader */);
+        in.readList(mHierarchyOps, null /* loader */);
     }
 
     private Change getOrCreateChange(IBinder token) {
@@ -97,10 +105,39 @@
         return this;
     }
 
+    /**
+     * Reparents a container into another one. The effect of a {@code null} parent can vary. For
+     * example, reparenting a stack to {@code null} will reparent it to its display.
+     *
+     * @param onTop When {@code true}, the child goes to the top of parent; otherwise it goes to
+     *              the bottom.
+     */
+    public WindowContainerTransaction reparent(@NonNull IWindowContainer child,
+            @Nullable IWindowContainer parent, boolean onTop) {
+        mHierarchyOps.add(new HierarchyOp(child.asBinder(),
+                parent == null ? null : parent.asBinder(), onTop));
+        return this;
+    }
+
+    /**
+     * Reorders a container within its parent.
+     *
+     * @param onTop When {@code true}, the child goes to the top of parent; otherwise it goes to
+     *              the bottom.
+     */
+    public WindowContainerTransaction reorder(@NonNull IWindowContainer child, boolean onTop) {
+        mHierarchyOps.add(new HierarchyOp(child.asBinder(), onTop));
+        return this;
+    }
+
     public Map<IBinder, Change> getChanges() {
         return mChanges;
     }
 
+    public List<HierarchyOp> getHierarchyOps() {
+        return mHierarchyOps;
+    }
+
     @Override
     public String toString() {
         return "WindowContainerTransaction { changes = " + mChanges + " }";
@@ -109,6 +146,7 @@
     @Override
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeMap(mChanges);
+        dest.writeList(mHierarchyOps);
     }
 
     @Override
@@ -249,4 +287,88 @@
             }
         };
     }
+
+    /**
+     * Holds information about a reparent/reorder operation in the hierarchy. This is separate from
+     * Changes because they must be executed in the same order that they are added.
+     */
+    public static class HierarchyOp implements Parcelable {
+        private final IBinder mContainer;
+
+        // If this is same as mContainer, then only change position, don't reparent.
+        private final IBinder mReparent;
+
+        // Moves/reparents to top of parent when {@code true}, otherwise moves/reparents to bottom.
+        private final boolean mToTop;
+
+        public HierarchyOp(@NonNull IBinder container, @Nullable IBinder reparent, boolean toTop) {
+            mContainer = container;
+            mReparent = reparent;
+            mToTop = toTop;
+        }
+
+        public HierarchyOp(@NonNull IBinder container, boolean toTop) {
+            mContainer = container;
+            mReparent = container;
+            mToTop = toTop;
+        }
+
+        protected HierarchyOp(Parcel in) {
+            mContainer = in.readStrongBinder();
+            mReparent = in.readStrongBinder();
+            mToTop = in.readBoolean();
+        }
+
+        public boolean isReparent() {
+            return mContainer != mReparent;
+        }
+
+        @Nullable
+        public IBinder getNewParent() {
+            return mReparent;
+        }
+
+        @NonNull
+        public IBinder getContainer() {
+            return mContainer;
+        }
+
+        public boolean getToTop() {
+            return mToTop;
+        }
+
+        @Override
+        public String toString() {
+            if (isReparent()) {
+                return "{reparent: " + mContainer + " to " + (mToTop ? "top of " : "bottom of ")
+                        + mReparent + "}";
+            } else {
+                return "{reorder: " + mContainer + " to " + (mToTop ? "top" : "bottom") + "}";
+            }
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeStrongBinder(mContainer);
+            dest.writeStrongBinder(mReparent);
+            dest.writeBoolean(mToTop);
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        public static final Creator<HierarchyOp> CREATOR = new Creator<HierarchyOp>() {
+            @Override
+            public HierarchyOp createFromParcel(Parcel in) {
+                return new HierarchyOp(in);
+            }
+
+            @Override
+            public HierarchyOp[] newArray(int size) {
+                return new HierarchyOp[size];
+            }
+        };
+    }
 }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 87c91ef..b1db9d7 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -79,11 +79,6 @@
 import static com.android.server.wm.ActivityTaskManagerService.TAG_STACK;
 import static com.android.server.wm.DragResizeMode.DRAG_RESIZE_MODE_DOCKED_DIVIDER;
 import static com.android.server.wm.ProtoLogGroup.WM_DEBUG_ADD_REMOVE;
-import static com.android.server.wm.TaskProto.DISPLAYED_BOUNDS;
-import static com.android.server.wm.TaskProto.FILLS_PARENT;
-import static com.android.server.wm.TaskProto.SURFACE_HEIGHT;
-import static com.android.server.wm.TaskProto.SURFACE_WIDTH;
-import static com.android.server.wm.TaskProto.WINDOW_CONTAINER;
 import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN;
 import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_STACK;
@@ -125,7 +120,6 @@
 import android.service.voice.IVoiceInteractionSession;
 import android.util.DisplayMetrics;
 import android.util.Slog;
-import android.util.proto.ProtoOutputStream;
 import android.view.DisplayInfo;
 import android.view.ITaskOrganizer;
 import android.view.RemoteAnimationTarget;
@@ -3195,12 +3189,16 @@
         info.lastActiveTime = lastActiveTime;
         info.taskDescription = new ActivityManager.TaskDescription(getTaskDescription());
         info.supportsSplitScreenMultiWindow = supportsSplitScreenWindowingMode();
-        info.resizeMode = mResizeMode;
         info.configuration.setTo(getConfiguration());
         info.token = mRemoteToken;
         // Get's the first non-undefined activity type among this and children. Can't use
         // configuration.windowConfiguration because that would only be this level.
         info.topActivityType = getActivityType();
+
+        //TODO (AM refactor): Just use local once updateEffectiveIntent is run during all child
+        //                    order changes.
+        final Task top = getTopMostTask();
+        info.resizeMode = top != null ? top.mResizeMode : mResizeMode;
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java
index 096541f..0a0530c9 100644
--- a/services/core/java/com/android/server/wm/TaskOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java
@@ -23,6 +23,8 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
 
 import static com.android.server.wm.ActivityStackSupervisor.PRESERVE_WINDOWS;
+import static com.android.server.wm.WindowContainer.POSITION_BOTTOM;
+import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
 import android.annotation.Nullable;
 import android.app.ActivityManager.RunningTaskInfo;
@@ -47,6 +49,7 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.WeakHashMap;
 
@@ -375,6 +378,45 @@
         }
     }
 
+    @Override
+    public List<RunningTaskInfo> getChildTasks(IWindowContainer parent) {
+        enforceStackPermission("getChildTasks()");
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            synchronized (mGlobalLock) {
+                if (parent == null) {
+                    throw new IllegalArgumentException("Can't get children of null parent");
+                }
+                final WindowContainer container = WindowContainer.fromBinder(parent.asBinder());
+                if (container == null) {
+                    Slog.e(TAG, "Can't get children of " + parent + " because it is not valid.");
+                    return null;
+                }
+                // For now, only support returning children of persistent root tasks (of which the
+                // only current implementation is TaskTile).
+                if (!(container instanceof TaskTile)) {
+                    Slog.w(TAG, "Can only get children of root tasks created via createRootTask");
+                    return null;
+                }
+                ArrayList<RunningTaskInfo> out = new ArrayList<>();
+                // Tiles aren't real parents, so we need to go through stacks on the display to
+                // ensure correct ordering.
+                final DisplayContent dc = container.getDisplayContent();
+                for (int i = dc.getStackCount() - 1; i >= 0; --i) {
+                    final ActivityStack as = dc.getStackAt(i);
+                    if (as.getTile() == container) {
+                        final RunningTaskInfo info = new RunningTaskInfo();
+                        as.fillTaskInfo(info);
+                        out.add(info);
+                    }
+                }
+                return out;
+            }
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
     private int sanitizeAndApplyChange(WindowContainer container,
             WindowContainerTransaction.Change change) {
         if (!(container instanceof Task)) {
@@ -405,6 +447,54 @@
         return effects;
     }
 
+    private int sanitizeAndApplyHierarchyOp(WindowContainer container,
+            WindowContainerTransaction.HierarchyOp hop) {
+        if (!(container instanceof Task)) {
+            throw new IllegalArgumentException("Invalid container in hierarchy op");
+        }
+        if (hop.isReparent()) {
+            // special case for tiles since they are "virtual" parents
+            if (container instanceof ActivityStack && ((ActivityStack) container).isRootTask()) {
+                ActivityStack as = (ActivityStack) container;
+                TaskTile newParent = hop.getNewParent() == null ? null
+                        : (TaskTile) WindowContainer.fromBinder(hop.getNewParent());
+                if (as.getTile() != newParent) {
+                    if (as.getTile() != null) {
+                        as.getTile().removeChild(as);
+                    }
+                    if (newParent != null) {
+                        if (!as.affectedBySplitScreenResize()) {
+                            return 0;
+                        }
+                        newParent.addChild(as, POSITION_TOP);
+                    }
+                }
+                if (hop.getToTop()) {
+                    as.getDisplay().positionStackAtTop(as, false /* includingParents */);
+                } else {
+                    as.getDisplay().positionStackAtBottom(as);
+                }
+            } else if (container instanceof Task) {
+                throw new RuntimeException("Reparenting leaf Tasks is not supported now.");
+            }
+        } else {
+            // Ugh, of course ActivityStack has its own special reorder logic...
+            if (container instanceof ActivityStack && ((ActivityStack) container).isRootTask()) {
+                ActivityStack as = (ActivityStack) container;
+                if (hop.getToTop()) {
+                    as.getDisplay().positionStackAtTop(as, false /* includingParents */);
+                } else {
+                    as.getDisplay().positionStackAtBottom(as);
+                }
+            } else {
+                container.getParent().positionChildAt(
+                        hop.getToTop() ? POSITION_TOP : POSITION_BOTTOM,
+                        container, false /* includingParents */);
+            }
+        }
+        return TRANSACT_EFFECTS_LIFECYCLE;
+    }
+
     private void resizePinnedStackIfNeeded(ConfigurationContainer container, int configMask,
             int windowMask, Configuration config) {
         if ((container instanceof ActivityStack)
@@ -470,8 +560,7 @@
                     while (entries.hasNext()) {
                         final Map.Entry<IBinder, WindowContainerTransaction.Change> entry =
                                 entries.next();
-                        final WindowContainer wc = WindowContainer.RemoteToken.fromBinder(
-                                entry.getKey()).getContainer();
+                        final WindowContainer wc = WindowContainer.fromBinder(entry.getKey());
                         int containerEffect = applyWindowContainerChange(wc, entry.getValue());
                         effects |= containerEffect;
 
@@ -484,6 +573,13 @@
                             mBLASTSyncEngine.addToSyncSet(syncId, wc);
                         }
                     }
+                    // Hierarchy changes
+                    final List<WindowContainerTransaction.HierarchyOp> hops = t.getHierarchyOps();
+                    for (int i = 0, n = hops.size(); i < n; ++i) {
+                        final WindowContainerTransaction.HierarchyOp hop = hops.get(i);
+                        final WindowContainer wc = WindowContainer.fromBinder(hop.getContainer());
+                        effects |= sanitizeAndApplyHierarchyOp(wc, hop);
+                    }
                     if ((effects & TRANSACT_EFFECTS_LIFECYCLE) != 0) {
                         // Already calls ensureActivityConfig
                         mService.mRootWindowContainer.ensureActivitiesVisible(
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 9acb660..504aa2d 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -2296,6 +2296,10 @@
         return mRemoteToken;
     }
 
+    static WindowContainer fromBinder(IBinder binder) {
+        return RemoteToken.fromBinder(binder).getContainer();
+    }
+
     static class RemoteToken extends IWindowContainer.Stub {
         final WeakReference<WindowContainer> mWeakRef;
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskOrganizerTests.java
index 7172a1b..f7aa3cc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskOrganizerTests.java
@@ -29,10 +29,10 @@
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -45,8 +45,10 @@
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.os.Binder;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
+import android.util.ArrayMap;
 import android.view.Display;
 import android.view.ITaskOrganizer;
 import android.view.IWindowContainer;
@@ -357,6 +359,78 @@
         assertEquals(ACTIVITY_TYPE_UNDEFINED, lastReportedTiles.get(0).topActivityType);
     }
 
+    @Test
+    public void testHierarchyTransaction() {
+        final ArrayMap<IBinder, RunningTaskInfo> lastReportedTiles = new ArrayMap<>();
+        ITaskOrganizer listener = new ITaskOrganizer.Stub() {
+            @Override
+            public void taskAppeared(RunningTaskInfo taskInfo) { }
+
+            @Override
+            public void taskVanished(IWindowContainer container) { }
+
+            @Override
+            public void transactionReady(int id, SurfaceControl.Transaction t) { }
+
+            @Override
+            public void onTaskInfoChanged(RunningTaskInfo info) {
+                lastReportedTiles.put(info.token.asBinder(), info);
+            }
+        };
+        mWm.mAtmService.mTaskOrganizerController.registerTaskOrganizer(
+                listener, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+        RunningTaskInfo info1 = mWm.mAtmService.mTaskOrganizerController.createRootTask(
+                mDisplayContent.mDisplayId, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+        RunningTaskInfo info2 = mWm.mAtmService.mTaskOrganizerController.createRootTask(
+                mDisplayContent.mDisplayId, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+
+        final ActivityStack stack = createTaskStackOnDisplay(
+                WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_STANDARD, mDisplayContent);
+        final ActivityStack stack2 = createTaskStackOnDisplay(
+                WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_HOME, mDisplayContent);
+
+        lastReportedTiles.clear();
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        wct.reparent(stack.mRemoteToken, info1.token, true /* onTop */);
+        wct.reparent(stack2.mRemoteToken, info2.token, true /* onTop */);
+        mWm.mAtmService.mTaskOrganizerController.applyContainerTransaction(wct,
+                null /* organizer */);
+        assertFalse(lastReportedTiles.isEmpty());
+        assertEquals(ACTIVITY_TYPE_STANDARD,
+                lastReportedTiles.get(info1.token.asBinder()).topActivityType);
+        assertEquals(ACTIVITY_TYPE_HOME,
+                lastReportedTiles.get(info2.token.asBinder()).topActivityType);
+
+        lastReportedTiles.clear();
+        wct = new WindowContainerTransaction();
+        wct.reparent(stack2.mRemoteToken, info1.token, false /* onTop */);
+        mWm.mAtmService.mTaskOrganizerController.applyContainerTransaction(wct,
+                null /* organizer */);
+        assertFalse(lastReportedTiles.isEmpty());
+        // Standard should still be on top of tile 1, so no change there
+        assertFalse(lastReportedTiles.containsKey(info1.token.asBinder()));
+        // But tile 2 has no children, so should become undefined
+        assertEquals(ACTIVITY_TYPE_UNDEFINED,
+                lastReportedTiles.get(info2.token.asBinder()).topActivityType);
+
+        // Check the getChildren call
+        List<RunningTaskInfo> children =
+                mWm.mAtmService.mTaskOrganizerController.getChildTasks(info1.token);
+        assertEquals(2, children.size());
+        children = mWm.mAtmService.mTaskOrganizerController.getChildTasks(info2.token);
+        assertEquals(0, children.size());
+
+        lastReportedTiles.clear();
+        wct = new WindowContainerTransaction();
+        wct.reorder(stack2.mRemoteToken, true /* onTop */);
+        mWm.mAtmService.mTaskOrganizerController.applyContainerTransaction(wct,
+                null /* organizer */);
+        // Home should now be on top. No change occurs in second tile, so not reported
+        assertEquals(1, lastReportedTiles.size());
+        assertEquals(ACTIVITY_TYPE_HOME,
+                lastReportedTiles.get(info1.token.asBinder()).topActivityType);
+    }
+
     private List<TaskTile> getTaskTiles(DisplayContent dc) {
         ArrayList<TaskTile> out = new ArrayList<>();
         for (int i = dc.getStackCount() - 1; i >= 0; --i) {