Add TaskTile concept to Window Manager

This adds the concept of a TaskTile to the WM. Due to
complexities in the current Stack/Task relationship, tiles
can't actually be part of the hierarchy, so the Stack
level has to internally resolve configurations as if they
were.

The TaskTiles themselves *are* ActivityStacks though from
the client/sysui perspective, though.

Bug: 133381284
Test: Added TaskTileTests
Change-Id: I9baad5ec899b4fab323a36c1533a40081727a2f7
diff --git a/core/java/android/app/ActivityTaskManager.java b/core/java/android/app/ActivityTaskManager.java
index dd9a2bc..58bff7f 100644
--- a/core/java/android/app/ActivityTaskManager.java
+++ b/core/java/android/app/ActivityTaskManager.java
@@ -158,6 +158,24 @@
                 }
             };
 
+    /** @hide */
+    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS)
+    public static ITaskOrganizerController getTaskOrganizerController() {
+        return ITaskOrganizerControllerSingleton.get();
+    }
+
+    private static final Singleton<ITaskOrganizerController> ITaskOrganizerControllerSingleton =
+            new Singleton<ITaskOrganizerController>() {
+                @Override
+                protected ITaskOrganizerController create() {
+                    try {
+                        return getService().getTaskOrganizerController();
+                    } catch (RemoteException e) {
+                        return null;
+                    }
+                }
+            };
+
     /**
      * Sets the windowing mode for a specific task. Only works on tasks of type
      * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD}
diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl
index 85fa7c1..503f5c5 100644
--- a/core/java/android/app/IActivityTaskManager.aidl
+++ b/core/java/android/app/IActivityTaskManager.aidl
@@ -29,6 +29,7 @@
 import android.app.IRequestFinishCallback;
 import android.app.IServiceConnection;
 import android.app.IStopUserCallback;
+import android.app.ITaskOrganizerController;
 import android.app.ITaskStackListener;
 import android.app.IUiAutomationConnection;
 import android.app.IUidObserver;
@@ -71,7 +72,6 @@
 import android.view.ITaskOrganizer;
 import android.view.RemoteAnimationDefinition;
 import android.view.RemoteAnimationAdapter;
-import android.view.WindowContainerTransaction;
 import com.android.internal.app.IVoiceInteractor;
 import com.android.internal.os.IResultReceiver;
 import com.android.internal.policy.IKeyguardDismissCallback;
@@ -123,8 +123,6 @@
             int requestCode, int flags, in ProfilerInfo profilerInfo, in Bundle options,
             IBinder permissionToken, boolean ignoreTargetSecurity, int userId);
 
-    void registerTaskOrganizer(in ITaskOrganizer organizer, int windowingMode);
-
     boolean isActivityStartAllowedOnDisplay(int displayId, in Intent intent, in String resolvedType,
             int userId);
 
@@ -224,7 +222,6 @@
     void setTaskResizeable(int taskId, int resizeableMode);
     void toggleFreeformWindowingMode(in IBinder token);
     void resizeTask(int taskId, in Rect bounds, int resizeMode);
-    void applyContainerTransaction(in WindowContainerTransaction t);
     void moveStackToDisplay(int stackId, int displayId);
     void removeStack(int stackId);
 
@@ -364,6 +361,11 @@
             in Rect tempOtherTaskBounds, in Rect tempOtherTaskInsetBounds);
 
     /**
+     * Returns an interface enabling the management of task organizers.
+     */
+    ITaskOrganizerController getTaskOrganizerController();
+
+    /**
      * Sets whether we are currently in an interactive split screen resize operation where we
      * are changing the docked stack size.
      */
diff --git a/core/java/android/app/ITaskOrganizerController.aidl b/core/java/android/app/ITaskOrganizerController.aidl
new file mode 100644
index 0000000..168f782
--- /dev/null
+++ b/core/java/android/app/ITaskOrganizerController.aidl
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.app.ActivityManager;
+import android.view.ITaskOrganizer;
+import android.view.IWindowContainer;
+import android.view.WindowContainerTransaction;
+
+/** @hide */
+interface ITaskOrganizerController {
+
+    /**
+     * Register a TaskOrganizer to manage tasks as they enter the given windowing mode.
+     * If there was already a TaskOrganizer for this windowing mode it will be evicted
+     * and receive taskVanished callbacks in the process.
+     */
+    void registerTaskOrganizer(ITaskOrganizer organizer, int windowingMode);
+
+    /** Apply multiple WindowContainer operations at once. */
+    void applyContainerTransaction(in WindowContainerTransaction t);
+
+    /** Creates a persistent root task in WM for a particular windowing-mode. */
+    ActivityManager.RunningTaskInfo createRootTask(int displayId, int windowingMode);
+
+    /** Deletes a persistent root task in WM */
+    boolean deleteRootTask(IWindowContainer task);
+
+    /** Get the root task which contains the current ime target */
+    IWindowContainer getImeTarget(int display);
+
+    /**
+     * Set's the root task to launch new tasks into on a display. {@code null} means no launch root
+     * and thus new tasks just end up directly on the display.
+     */
+    void setLaunchRoot(int displayId, in IWindowContainer root);
+}
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index fe9c640..662ca6e 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -16,6 +16,8 @@
 
 package android.app;
 
+import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -25,6 +27,7 @@
 import android.os.Parcel;
 import android.os.RemoteException;
 import android.util.Log;
+import android.view.IWindowContainer;
 
 /**
  * Stores information about a particular Task.
@@ -138,6 +141,19 @@
     @UnsupportedAppUsage
     public final Configuration configuration = new Configuration();
 
+    /**
+     * Used as an opaque identifier for this task.
+     * @hide
+     */
+    @NonNull
+    public IWindowContainer token;
+
+    /**
+     * The activity type of the top activity in this task.
+     * @hide
+     */
+    public @WindowConfiguration.ActivityType int topActivityType;
+
     TaskInfo() {
         // Do nothing
     }
@@ -160,6 +176,11 @@
         }
     }
 
+    /** @hide */
+    public boolean isResizable() {
+        return resizeMode != RESIZE_MODE_UNRESIZEABLE;
+    }
+
     /**
      * Reads the TaskInfo from a parcel.
      */
@@ -186,6 +207,8 @@
         supportsSplitScreenMultiWindow = source.readBoolean();
         resizeMode = source.readInt();
         configuration.readFromParcel(source);
+        token = IWindowContainer.Stub.asInterface(source.readStrongBinder());
+        topActivityType = source.readInt();
     }
 
     /**
@@ -221,6 +244,8 @@
         dest.writeBoolean(supportsSplitScreenMultiWindow);
         dest.writeInt(resizeMode);
         configuration.writeToParcel(dest, flags);
+        dest.writeStrongInterface(token);
+        dest.writeInt(topActivityType);
     }
 
     @Override
@@ -234,6 +259,8 @@
                 + " numActivities=" + numActivities
                 + " lastActiveTime=" + lastActiveTime
                 + " supportsSplitScreenMultiWindow=" + supportsSplitScreenMultiWindow
-                + " resizeMode=" + resizeMode;
+                + " resizeMode=" + resizeMode
+                + " token=" + token
+                + " topActivityType=" + topActivityType;
     }
 }
diff --git a/core/java/android/view/ITaskOrganizer.aidl b/core/java/android/view/ITaskOrganizer.aidl
index e92aafe..5ccdd30 100644
--- a/core/java/android/view/ITaskOrganizer.aidl
+++ b/core/java/android/view/ITaskOrganizer.aidl
@@ -26,8 +26,7 @@
  * {@hide}
  */
 oneway interface ITaskOrganizer {
-    void taskAppeared(in IWindowContainer container,
-        in ActivityManager.RunningTaskInfo taskInfo);
+    void taskAppeared(in ActivityManager.RunningTaskInfo taskInfo);
     void taskVanished(in IWindowContainer container);
 
     /**
@@ -35,4 +34,19 @@
      * ActivityTaskManagerService#applyTaskOrganizerTransaction
      */
     void transactionReady(int id, in SurfaceControl.Transaction t);
+
+    /**
+     * Will fire when core attributes of a Task's info change. Relevant properties include the
+     * {@link WindowConfiguration.ActivityType} and whether it is resizable.
+     *
+     * This is used, for example, during split-screen. The flow for starting is: Something sends an
+     * Intent with windowingmode. Then WM finds a matching root task and launches the new task into
+     * it. This causes the root task's info to change because now it has a task when it didn't
+     * before. The default Divider implementation interprets this as a request to enter
+     * split-screen mode and will move all other Tasks into the secondary root task. When WM
+     * applies this change, it triggers an info change in the secondary root task because it now
+     * has children. The Divider impl looks at the info and can see that the secondary root task
+     * has adopted an ActivityType of HOME and proceeds to show the minimized dock UX.
+     */
+    void onTaskInfoChanged(in ActivityManager.RunningTaskInfo info);
 }
\ No newline at end of file
diff --git a/core/java/android/view/WindowContainerTransaction.java b/core/java/android/view/WindowContainerTransaction.java
index c55caa3..33f21f2 100644
--- a/core/java/android/view/WindowContainerTransaction.java
+++ b/core/java/android/view/WindowContainerTransaction.java
@@ -86,6 +86,17 @@
         return this;
     }
 
+    /**
+     * Set the smallestScreenWidth of a container.
+     */
+    public WindowContainerTransaction setSmallestScreenWidthDp(IWindowContainer container,
+            int widthDp) {
+        Change cfg = getOrCreateChange(container.asBinder());
+        cfg.mConfiguration.smallestScreenWidthDp = widthDp;
+        cfg.mConfigSetMask |= ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
+        return this;
+    }
+
     public Map<IBinder, Change> getChanges() {
         return mChanges;
     }
diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index ddf0117..0d72d84 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -341,6 +341,9 @@
 
     private static final int TRANSLUCENT_TIMEOUT_MSG = FIRST_ACTIVITY_STACK_MSG + 1;
 
+    // TODO(task-hierarchy): remove when tiles can be actual parents
+    TaskTile mTile = null;
+
     private final Handler mHandler;
 
     private class ActivityStackHandler extends Handler {
@@ -638,11 +641,20 @@
     }
 
     @Override
+    public void resolveOverrideConfiguration(Configuration newParentConfig) {
+        super.resolveOverrideConfiguration(newParentConfig);
+        if (mTile != null) {
+            // If this is a virtual child of a tile, simulate the parent-child relationship
+            mTile.updateResolvedConfig(getResolvedOverrideConfiguration());
+        }
+    }
+
+    @Override
     public void onConfigurationChanged(Configuration newParentConfig) {
         // Calling Task#onConfigurationChanged() for leaf task since the ops in this method are
         // particularly for ActivityStack, like preventing bounds changes when inheriting certain
         // windowing mode.
-        if (!isRootTask()) {
+        if (!isRootTask() || this instanceof TaskTile) {
             super.onConfigurationChanged(newParentConfig);
             return;
         }
@@ -3944,7 +3956,6 @@
                 ? ((WindowContainer) newParent).getDisplayContent() : null;
         final DisplayContent oldDisplay = oldParent != null
                 ? ((WindowContainer) oldParent).getDisplayContent() : null;
-
         super.onParentChanged(newParent, oldParent);
 
         if (display != null && inSplitScreenPrimaryWindowingMode()
@@ -3963,6 +3974,11 @@
         if (oldDisplay != null && oldDisplay.isRemoving()) {
             postReparent();
         }
+        if (mTile != null && getSurfaceControl() != null) {
+            // by now, the TaskStack should already have been reparented, so we can reparent its
+            // surface here
+            reparentSurfaceControl(getPendingTransaction(), mTile.getSurfaceControl());
+        }
     }
 
     void reparent(DisplayContent newParent, boolean onTop) {
@@ -4000,7 +4016,16 @@
 
     @Override
     void getRelativeDisplayedPosition(Point outPos) {
-        super.getRelativeDisplayedPosition(outPos);
+        // check for tile which is "virtually" a parent.
+        if (mTile != null) {
+            final Rect dispBounds = getDisplayedBounds();
+            outPos.set(dispBounds.left, dispBounds.top);
+            final Rect parentBounds = mTile.getBounds();
+            outPos.offset(-parentBounds.left, -parentBounds.top);
+        } else {
+            super.getRelativeDisplayedPosition(outPos);
+        }
+
         final int outset = getStackOutset();
         outPos.x -= outset;
         outPos.y -= outset;
@@ -4010,6 +4035,16 @@
         if (mSurfaceControl == null) {
             return;
         }
+        if (mTile != null) {
+            // Tile controls crop, so the app needs to be able to draw its background outside of
+            // the stack bounds for when the tile crop gets bigger than the stack.
+            if (mLastSurfaceSize.equals(0, 0)) {
+                return;
+            }
+            transaction.setWindowCrop(mSurfaceControl, null);
+            mLastSurfaceSize.set(0, 0);
+            return;
+        }
 
         final Rect stackBounds = getDisplayedBounds();
         int width = stackBounds.width();
@@ -4033,6 +4068,9 @@
 
     @Override
     void onDisplayChanged(DisplayContent dc) {
+        if (mTile != null && dc != mTile.getDisplay()) {
+            mTile.removeChild(this);
+        }
         super.onDisplayChanged(dc);
         if (isRootTask()) {
             updateSurfaceBounds();
@@ -4848,6 +4886,42 @@
         return shouldSleepActivities() || mAtmService.mShuttingDown;
     }
 
+    TaskTile getTile() {
+        return mTile;
+    }
+
+    /**
+     * Don't call this directly. instead use {@link TaskTile#addChild} or
+     * {@link TaskTile#removeChild}.
+     */
+    void setTile(TaskTile tile) {
+        TaskTile origTile = mTile;
+        mTile = tile;
+        final ConfigurationContainer parent = getParent();
+        if (parent != null) {
+            onConfigurationChanged(parent.getConfiguration());
+        }
+
+        // Reparent to tile surface or back to original parent
+        if (getSurfaceControl() == null) {
+            return;
+        }
+        if (mTile != null) {
+            reparentSurfaceControl(getPendingTransaction(), mTile.getSurfaceControl());
+        } else if (mTile == null && origTile != null) {
+            reparentSurfaceControl(getPendingTransaction(), getParentSurfaceControl());
+        }
+    }
+
+    @Override
+    void removeImmediately() {
+        // TODO(task-hierarchy): remove this override when tiles are in hierarchy
+        if (mTile != null) {
+            mTile.removeChild(this);
+        }
+        super.removeImmediately();
+    }
+
     @Override
     public void dumpDebug(ProtoOutputStream proto, long fieldId,
             @WindowTraceLogLevel int logLevel) {
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index f019013..d6e7077 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -144,6 +144,7 @@
 import android.app.IAssistDataReceiver;
 import android.app.INotificationManager;
 import android.app.IRequestFinishCallback;
+import android.app.ITaskOrganizerController;
 import android.app.ITaskStackListener;
 import android.app.Notification;
 import android.app.NotificationManager;
@@ -225,10 +226,8 @@
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
 import android.view.IRecentsAnimationRunner;
-import android.view.ITaskOrganizer;
 import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationDefinition;
-import android.view.WindowContainerTransaction;
 import android.view.WindowManager;
 
 import com.android.internal.R;
@@ -292,7 +291,6 @@
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -344,10 +342,6 @@
     /** This activity is being relaunched due to a free-resize operation. */
     public static final int RELAUNCH_REASON_FREE_RESIZE = 2;
 
-    /** Flag indicating that an applied transaction may have effected lifecycle */
-    private static final int TRANSACT_EFFECTS_CLIENT_CONFIG = 1;
-    private static final int TRANSACT_EFFECTS_LIFECYCLE = 1 << 1;
-
     Context mContext;
 
     /**
@@ -669,8 +663,7 @@
     /**
      * Stores the registration and state of TaskOrganizers in use.
      */
-    TaskOrganizerController mTaskOrganizerController =
-        new TaskOrganizerController(this, mGlobalLock);
+    TaskOrganizerController mTaskOrganizerController = new TaskOrganizerController(this);
 
     private int mDeviceOwnerUid = Process.INVALID_UID;
 
@@ -1286,15 +1279,6 @@
     }
 
     @Override
-    public final void registerTaskOrganizer(ITaskOrganizer organizer, int windowingMode) {
-        enforceCallerIsRecentsOrHasPermission(
-                MANAGE_ACTIVITY_STACKS, "registerTaskOrganizer()");
-        synchronized (mGlobalLock) {
-            mTaskOrganizerController.registerTaskOrganizer(organizer, windowingMode);
-        }
-    }
-
-    @Override
     public IBinder requestStartActivityPermissionToken(IBinder delegatorToken) {
         int callingUid = Binder.getCallingUid();
         if (UserHandle.getAppId(callingUid) != SYSTEM_UID) {
@@ -3304,116 +3288,6 @@
         }
     }
 
-    private int sanitizeAndApplyChange(WindowContainer container,
-            WindowContainerTransaction.Change change) {
-        if (!(container instanceof Task || container instanceof ActivityStack)) {
-            throw new RuntimeException("Invalid token in task transaction");
-        }
-        // The "client"-facing API should prevent bad changes; however, just in case, sanitize
-        // masks here.
-        int configMask = change.getConfigSetMask();
-        int windowMask = change.getWindowSetMask();
-        configMask &= ActivityInfo.CONFIG_WINDOW_CONFIGURATION
-                | ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
-        windowMask &= WindowConfiguration.WINDOW_CONFIG_BOUNDS;
-        int effects = 0;
-        if (configMask != 0) {
-            Configuration c = new Configuration(container.getRequestedOverrideConfiguration());
-            c.setTo(change.getConfiguration(), configMask, windowMask);
-            container.onRequestedOverrideConfigurationChanged(c);
-            // TODO(b/145675353): remove the following once we could apply new bounds to the
-            // pinned stack together with its children.
-            resizePinnedStackIfNeeded(container, configMask, windowMask, c);
-            effects |= TRANSACT_EFFECTS_CLIENT_CONFIG;
-        }
-        if ((change.getChangeMask() & WindowContainerTransaction.Change.CHANGE_FOCUSABLE) != 0) {
-            if (container.setFocusable(change.getFocusable())) {
-                effects |= TRANSACT_EFFECTS_LIFECYCLE;
-            }
-        }
-        return effects;
-    }
-
-    private void resizePinnedStackIfNeeded(ConfigurationContainer container, int configMask,
-            int windowMask, Configuration config) {
-        if ((container instanceof ActivityStack)
-                && ((configMask & ActivityInfo.CONFIG_WINDOW_CONFIGURATION) != 0)
-                && ((windowMask & WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)) {
-            final ActivityStack stack = (ActivityStack) container;
-            if (stack.inPinnedWindowingMode()) {
-                stack.resize(config.windowConfiguration.getBounds(),
-                        null /* tempTaskBounds */, null /* tempTaskInsetBounds */,
-                        PRESERVE_WINDOWS, true /* deferResume */);
-            }
-        }
-    }
-
-    private int applyWindowContainerChange(WindowContainer wc,
-            WindowContainerTransaction.Change c) {
-        int effects = sanitizeAndApplyChange(wc, c);
-
-        Rect enterPipBounds = c.getEnterPipBounds();
-        if (enterPipBounds != null) {
-            Task tr = (Task) wc;
-            mStackSupervisor.updatePictureInPictureMode(tr,
-                    enterPipBounds, true);
-        }
-        return effects;
-    }
-
-    @Override
-    public void applyContainerTransaction(WindowContainerTransaction t) {
-        mAmInternal.enforceCallingPermission(MANAGE_ACTIVITY_STACKS, "applyContainerTransaction()");
-        if (t == null) {
-            return;
-        }
-        long ident = Binder.clearCallingIdentity();
-        try {
-            synchronized (mGlobalLock) {
-                int effects = 0;
-                deferWindowLayout();
-                try {
-                    ArraySet<WindowContainer> haveConfigChanges = new ArraySet<>();
-                    Iterator<Map.Entry<IBinder, WindowContainerTransaction.Change>> entries =
-                            t.getChanges().entrySet().iterator();
-                    while (entries.hasNext()) {
-                        final Map.Entry<IBinder, WindowContainerTransaction.Change> entry =
-                                entries.next();
-                        final WindowContainer wc = WindowContainer.RemoteToken.fromBinder(
-                                entry.getKey()).getContainer();
-                        int containerEffect = applyWindowContainerChange(wc, entry.getValue());
-                        effects |= containerEffect;
-                        // Lifecycle changes will trigger ensureConfig for everything.
-                        if ((effects & TRANSACT_EFFECTS_LIFECYCLE) == 0
-                                && (containerEffect & TRANSACT_EFFECTS_CLIENT_CONFIG) != 0) {
-                            haveConfigChanges.add(wc);
-                        }
-                    }
-                    if ((effects & TRANSACT_EFFECTS_LIFECYCLE) != 0) {
-                        // Already calls ensureActivityConfig
-                        mRootWindowContainer.ensureActivitiesVisible(null, 0, PRESERVE_WINDOWS);
-                    } else if ((effects & TRANSACT_EFFECTS_CLIENT_CONFIG) != 0) {
-                        final PooledConsumer f = PooledLambda.obtainConsumer(
-                                ActivityRecord::ensureActivityConfiguration,
-                                PooledLambda.__(ActivityRecord.class), 0,
-                                false /* preserveWindow */);
-                        try {
-                            for (int i = haveConfigChanges.size() - 1; i >= 0; --i) {
-                                haveConfigChanges.valueAt(i).forAllActivities(f);
-                            }
-                        } finally {
-                            f.recycle();
-                        }
-                    }
-                } finally {
-                    continueWindowLayout();
-                }
-            }
-        } finally {
-            Binder.restoreCallingIdentity(ident);
-        }
-    }
-
     @Override
     public boolean releaseActivityInstance(IBinder token) {
         synchronized (mGlobalLock) {
@@ -4442,6 +4316,13 @@
         }
     }
 
+    @Override
+    public ITaskOrganizerController getTaskOrganizerController() {
+        mAmInternal.enforceCallingPermission(MANAGE_ACTIVITY_STACKS,
+                "getTaskOrganizerController()");
+        return mTaskOrganizerController;
+    }
+
     /**
      * Check that we have the features required for VR-related API calls, and throw an exception if
      * not.
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 6e479b2..9f4cd88 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -207,6 +207,7 @@
 import android.view.Gravity;
 import android.view.IDisplayWindowInsetsController;
 import android.view.ISystemGestureExclusionListener;
+import android.view.ITaskOrganizer;
 import android.view.IWindow;
 import android.view.InputChannel;
 import android.view.InputDevice;
@@ -664,6 +665,9 @@
     private final RootWindowContainer.FindTaskResult
             mTmpFindTaskResult = new RootWindowContainer.FindTaskResult();
 
+    // When non-null, new stacks get put into this tile.
+    TaskTile mLaunchTile = null;
+
     private final Consumer<WindowState> mUpdateWindowsForAnimator = w -> {
         WindowStateAnimator winAnimator = w.mWinAnimator;
         final ActivityRecord activity = w.mActivityRecord;
@@ -4275,8 +4279,15 @@
 
         @VisibleForTesting
         ActivityStack getTopStack() {
-            return mTaskContainers.getChildCount() > 0
-                    ? mTaskContainers.getChildAt(mTaskContainers.getChildCount() - 1) : null;
+            // TODO(task-hierarchy): Just grab index -1 once tiles are in hierarchy.
+            for (int i = mTaskContainers.getChildCount() - 1; i >= 0; --i) {
+                final ActivityStack child = mTaskContainers.getChildAt(i);
+                if (child instanceof TaskTile) {
+                    continue;
+                }
+                return child;
+            }
+            return null;
         }
 
         int getIndexOf(ActivityStack stack) {
@@ -4318,6 +4329,10 @@
         }
 
         private void addStackReferenceIfNeeded(ActivityStack stack) {
+            // TODO(task-hierarchy): Remove when tiles are in hierarchy.
+            if (stack instanceof TaskTile) {
+                return;
+            }
             if (stack.isActivityTypeHome()) {
                 if (mRootHomeTask != null) {
                     if (!stack.isDescendantOf(mRootHomeTask)) {
@@ -4735,6 +4750,17 @@
                 mSplitScreenDividerAnchor = null;
             }
         }
+
+        @Override
+        void onChildPositionChanged(WindowContainer child) {
+            // TODO(task-hierarchy): Move functionality to TaskTile when it's a proper parent.
+            TaskTile tile = ((ActivityStack) child).getTile();
+            if (tile == null) {
+                return;
+            }
+            mAtmService.mTaskOrganizerController.dispatchTaskInfoChanged(
+                    tile, false /* force */);
+        }
     }
 
     private final class AboveAppWindowContainers extends NonAppWindowContainers {
@@ -6271,6 +6297,10 @@
     }
 
     boolean isTopNotPinnedStack(ActivityStack stack) {
+        // TODO(task-hierarchy): Remove when tiles are in hierarchy.
+        if (stack instanceof TaskTile) {
+            return false;
+        }
         for (int i = getStackCount() - 1; i >= 0; --i) {
             final ActivityStack current = getStackAt(i);
             if (!current.inPinnedWindowingMode()) {
@@ -6685,6 +6715,19 @@
         return getHomeActivityForUser(mRootWindowContainer.mCurrentUser);
     }
 
+    // TODO(task-hierarchy): Remove when tiles are in hierarchy.
+    void addTile(TaskTile tile) {
+        mTaskContainers.addChild(tile, POSITION_BOTTOM);
+        ITaskOrganizer organizer = mAtmService.mTaskOrganizerController.getTaskOrganizer(
+                tile.getWindowingMode());
+        tile.setTaskOrganizer(organizer);
+    }
+
+    // TODO(task-hierarchy): Remove when tiles are in hierarchy.
+    void removeTile(TaskTile tile) {
+        mTaskContainers.removeChild(tile);
+    }
+
     @Nullable
     ActivityRecord getHomeActivityForUser(int userId) {
         final ActivityStack homeStack = getRootHomeTask();
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index da77314..efe79b3 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -526,7 +526,7 @@
             mService.mH.removeCallbacks(mDisplayRotationHandlerTimeout);
             mIsWaitingForRemoteRotation = false;
             mDisplayContent.sendNewConfiguration();
-            mService.mAtmService.applyContainerTransaction(t);
+            mService.mAtmService.mTaskOrganizerController.applyContainerTransaction(t);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/RecentsAnimation.java b/services/core/java/com/android/server/wm/RecentsAnimation.java
index 647be0f..9770947 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimation.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimation.java
@@ -388,11 +388,12 @@
                     // surfaces needs to be done immediately.
                     mWindowManager.executeAppTransition();
 
-                    // After reordering the stacks, reset the minimized state. At this point, either
-                    // the target activity is now top-most and we will stay minimized (if in
-                    // split-screen), or we will have returned to the app, and the minimized state
-                    // should be reset
-                    mWindowManager.checkSplitScreenMinimizedChanged(true /* animate */);
+                    if (targetStack.getTile() != null) {
+                        // Client state may have changed during the recents animation, so force
+                        // send task info so the client can synchronize its state.
+                        mService.mTaskOrganizerController.dispatchTaskInfoChanged(
+                                targetStack.mTile, true /* force */);
+                    }
                 } catch (Exception e) {
                     Slog.e(TAG, "Failed to clean up recents activity", e);
                     throw e;
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index e6fd512..a13399b 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -1012,6 +1012,9 @@
 
         mWmService.scheduleAnimationLocked();
 
+        // Send any pending task-info changes that were queued-up during a layout deferment
+        mWmService.mAtmService.mTaskOrganizerController.dispatchPendingTaskInfoChanges();
+
         if (DEBUG_WINDOW_TRACE) Slog.e(TAG,
                 "performSurfacePlacementInner exit: animating="
                         + mWmService.mAnimator.isAnimating());
@@ -2959,6 +2962,12 @@
             case ACTIVITY_TYPE_RECENTS: return r.isActivityTypeRecents();
             case ACTIVITY_TYPE_ASSISTANT: return r.isActivityTypeAssistant();
         }
+        // TODO(task-hierarchy): Find another way to differentiate tile from normal stack once it is
+        //                       part of the hierarchy
+        if (stack instanceof TaskTile) {
+            // Don't launch directly into tiles.
+            return false;
+        }
         // There is a 1-to-1 relationship between stack and task when not in
         // primary split-windowing mode.
         if (stack.getWindowingMode() == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 36cae1f..348104a 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -1257,7 +1257,7 @@
         if (affinityIntent != null) return affinityIntent;
         // Probably a task that contains other tasks, so return the intent for the top task?
         final Task topTask = getTopMostTask();
-        return topTask != null ? topTask.getBaseIntent() : null;
+        return (topTask != this && topTask != null) ? topTask.getBaseIntent() : null;
     }
 
     /** Returns the first non-finishing activity from the bottom. */
@@ -3214,7 +3214,8 @@
         info.taskId = mTaskId;
         info.displayId = getDisplayId();
         info.isRunning = getTopNonFinishingActivity() != null;
-        info.baseIntent = new Intent(getBaseIntent());
+        final Intent baseIntent = getBaseIntent();
+        info.baseIntent = baseIntent == null ? new Intent() : baseIntent;
         info.baseActivity = mReuseActivitiesReport.base != null
                 ? mReuseActivitiesReport.base.intent.getComponent()
                 : null;
@@ -3229,6 +3230,10 @@
         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();
     }
 
     /**
@@ -3375,7 +3380,7 @@
         if (affinity != null) {
             sb.append(" A=");
             sb.append(affinity);
-        } else if (intent != null) {
+        } else if (intent != null && intent.getComponent() != null) {
             sb.append(" I=");
             sb.append(intent.getComponent().flattenToShortString());
         } else if (affinityIntent != null && affinityIntent.getComponent() != null) {
@@ -3865,7 +3870,12 @@
 
     boolean isControlledByTaskOrganizer() {
         final Task rootTask = getRootTask();
-        return rootTask == this && rootTask.mTaskOrganizer != null;
+        return rootTask == this && rootTask.mTaskOrganizer != null
+                // TODO(task-hierarchy): Figure out how to control nested tasks.
+                // For now, if this is in a tile let WM drive.
+                && !(rootTask instanceof TaskTile)
+                && !(rootTask instanceof ActivityStack
+                        && ((ActivityStack) rootTask).getTile() != null);
     }
 
     @Override
@@ -3893,6 +3903,9 @@
    }
 
     void setTaskOrganizer(ITaskOrganizer organizer) {
+        if (mTaskOrganizer == organizer) {
+            return;
+        }
         // Let the old organizer know it has lost control.
         if (mTaskOrganizer != null) {
             sendTaskVanished();
diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java
index 66c65e2..44a6fc9 100644
--- a/services/core/java/com/android/server/wm/TaskOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java
@@ -16,26 +16,51 @@
 
 package com.android.server.wm;
 
-import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.Manifest.permission.MANAGE_ACTIVITY_STACKS;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
 
+import static com.android.server.wm.ActivityStackSupervisor.PRESERVE_WINDOWS;
+
+import android.annotation.Nullable;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.ITaskOrganizerController;
+import android.app.WindowConfiguration;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.util.ArraySet;
 import android.util.Slog;
 import android.view.ITaskOrganizer;
-import android.view.SurfaceControl;
+import android.view.IWindowContainer;
+import android.view.WindowContainerTransaction;
+
+import com.android.internal.util.function.pooled.PooledConsumer;
+import com.android.internal.util.function.pooled.PooledLambda;
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.WeakHashMap;
 
 /**
  * Stores the TaskOrganizers associated with a given windowing mode and
  * their associated state.
  */
-class TaskOrganizerController {
+class TaskOrganizerController extends ITaskOrganizerController.Stub {
     private static final String TAG = "TaskOrganizerController";
 
-    private WindowManagerGlobalLock mGlobalLock;
+    /** Flag indicating that an applied transaction may have effected lifecycle */
+    private static final int TRANSACT_EFFECTS_CLIENT_CONFIG = 1;
+    private static final int TRANSACT_EFFECTS_LIFECYCLE = 1 << 1;
+
+    private final WindowManagerGlobalLock mGlobalLock;
 
     private class DeathRecipient implements IBinder.DeathRecipient {
         int mWindowingMode;
@@ -87,11 +112,20 @@
 
     final HashMap<Integer, ITaskOrganizer> mTaskOrganizersByPendingSyncId = new HashMap();
 
+    private final WeakHashMap<Task, RunningTaskInfo> mLastSentTaskInfos = new WeakHashMap<>();
+    private final ArrayList<Task> mPendingTaskInfoChanges = new ArrayList<>();
+
     final ActivityTaskManagerService mService;
 
-    TaskOrganizerController(ActivityTaskManagerService atm, WindowManagerGlobalLock lock) {
+    RunningTaskInfo mTmpTaskInfo;
+
+    TaskOrganizerController(ActivityTaskManagerService atm) {
         mService = atm;
-        mGlobalLock = lock;
+        mGlobalLock = atm.mGlobalLock;
+    }
+
+    private void enforceStackPermission(String func) {
+        mService.mAmInternal.enforceCallingPermission(MANAGE_ACTIVITY_STACKS, func);
     }
 
     private void clearIfNeeded(int windowingMode) {
@@ -106,26 +140,35 @@
      * If there was already a TaskOrganizer for this windowing mode it will be evicted
      * and receive taskVanished callbacks in the process.
      */
-    void registerTaskOrganizer(ITaskOrganizer organizer, int windowingMode) {
-        if (windowingMode != WINDOWING_MODE_PINNED &&
-            windowingMode != WINDOWING_MODE_MULTI_WINDOW) {
-            throw new UnsupportedOperationException(
-                    "As of now only Pinned and Multiwindow windowing modes are"
-                    + " supported for registerTaskOrganizer");
-
+    @Override
+    public void registerTaskOrganizer(ITaskOrganizer organizer, int windowingMode) {
+        if (windowingMode != WINDOWING_MODE_PINNED
+                && windowingMode != WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
+                && windowingMode != WINDOWING_MODE_SPLIT_SCREEN_SECONDARY
+                && windowingMode != WINDOWING_MODE_MULTI_WINDOW) {
+            throw new UnsupportedOperationException("As of now only Pinned/Split/Multiwindow"
+                    + " windowing modes are supported for registerTaskOrganizer");
         }
-        clearIfNeeded(windowingMode);
-        DeathRecipient dr = new DeathRecipient(organizer, windowingMode);
+        enforceStackPermission("registerTaskOrganizer()");
+        final long origId = Binder.clearCallingIdentity();
         try {
-            organizer.asBinder().linkToDeath(dr, 0);
-        } catch (RemoteException e) {
-            Slog.e(TAG, "TaskOrganizer failed to register death recipient");
+            synchronized (mGlobalLock) {
+                clearIfNeeded(windowingMode);
+                DeathRecipient dr = new DeathRecipient(organizer, windowingMode);
+                try {
+                    organizer.asBinder().linkToDeath(dr, 0);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "TaskOrganizer failed to register death recipient");
+                }
+
+                final TaskOrganizerState state = new TaskOrganizerState(organizer, dr);
+                mTaskOrganizersForWindowingMode.put(windowingMode, state);
+
+                mTaskOrganizerStates.put(organizer, state);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(origId);
         }
-
-        final TaskOrganizerState state = new TaskOrganizerState(organizer, dr);
-        mTaskOrganizersForWindowingMode.put(windowingMode, state);
-
-        mTaskOrganizerStates.put(organizer, state);
     }
 
     ITaskOrganizer getTaskOrganizer(int windowingMode) {
@@ -138,7 +181,7 @@
 
     private void sendTaskAppeared(ITaskOrganizer organizer, Task task) {
         try {
-            organizer.taskAppeared(task.getRemoteToken(), task.getTaskInfo());
+            organizer.taskAppeared(task.getTaskInfo());
         } catch (Exception e) {
             Slog.e(TAG, "Exception sending taskAppeared callback" + e);
         }
@@ -167,4 +210,254 @@
         // we do this AFTER sending taskVanished.
         state.removeTask(task);
     }
+
+    @Override
+    public RunningTaskInfo createRootTask(int displayId, int windowingMode) {
+        enforceStackPermission("createRootTask()");
+        final long origId = Binder.clearCallingIdentity();
+        try {
+            synchronized (mGlobalLock) {
+                DisplayContent display = mService.mRootWindowContainer.getDisplayContent(displayId);
+                if (display == null) {
+                    return null;
+                }
+                final int nextId = display.getNextStackId();
+                TaskTile tile = new TaskTile(mService, nextId, windowingMode);
+                display.addTile(tile);
+                RunningTaskInfo out = new RunningTaskInfo();
+                tile.fillTaskInfo(out);
+                mLastSentTaskInfos.put(tile, out);
+                return out;
+            }
+        } finally {
+            Binder.restoreCallingIdentity(origId);
+        }
+    }
+
+    @Override
+    public boolean deleteRootTask(IWindowContainer token) {
+        enforceStackPermission("deleteRootTask()");
+        final long origId = Binder.clearCallingIdentity();
+        try {
+            synchronized (mGlobalLock) {
+                TaskTile tile = TaskTile.forToken(token.asBinder());
+                if (tile == null) {
+                    return false;
+                }
+                tile.removeImmediately();
+                return true;
+            }
+        } finally {
+            Binder.restoreCallingIdentity(origId);
+        }
+    }
+
+    void dispatchPendingTaskInfoChanges() {
+        if (mService.mWindowManager.mWindowPlacerLocked.isLayoutDeferred()) {
+            return;
+        }
+        for (int i = 0, n = mPendingTaskInfoChanges.size(); i < n; ++i) {
+            dispatchTaskInfoChanged(mPendingTaskInfoChanges.get(i), false /* force */);
+        }
+        mPendingTaskInfoChanges.clear();
+    }
+
+    void dispatchTaskInfoChanged(Task task, boolean force) {
+        if (!force && mService.mWindowManager.mWindowPlacerLocked.isLayoutDeferred()) {
+            // Defer task info reporting while layout is deferred. This is because layout defer
+            // blocks tend to do lots of re-ordering which can mess up animations in receivers.
+            mPendingTaskInfoChanges.remove(task);
+            mPendingTaskInfoChanges.add(task);
+            return;
+        }
+        RunningTaskInfo lastInfo = mLastSentTaskInfos.get(task);
+        if (mTmpTaskInfo == null) {
+            mTmpTaskInfo = new RunningTaskInfo();
+        }
+        task.fillTaskInfo(mTmpTaskInfo);
+        boolean changed = lastInfo == null
+                || mTmpTaskInfo.topActivityType != lastInfo.topActivityType
+                || mTmpTaskInfo.isResizable() != lastInfo.isResizable();
+        if (!(changed || force)) {
+            return;
+        }
+        final RunningTaskInfo newInfo = mTmpTaskInfo;
+        mLastSentTaskInfos.put(task, mTmpTaskInfo);
+        // Since we've stored this, clean up the reference so a new one will be created next time.
+        // Transferring it this way means we only have to construct new RunningTaskInfos when they
+        // change.
+        mTmpTaskInfo = null;
+
+        if (task.mTaskOrganizer != null) {
+            try {
+                task.mTaskOrganizer.onTaskInfoChanged(newInfo);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    @Override
+    public IWindowContainer getImeTarget(int displayId) {
+        enforceStackPermission("getImeTarget()");
+        final long origId = Binder.clearCallingIdentity();
+        try {
+            synchronized (mGlobalLock) {
+                DisplayContent dc = mService.mWindowManager.mRoot
+                        .getDisplayContent(displayId);
+                if (dc == null || dc.mInputMethodTarget == null) {
+                    return null;
+                }
+                // Avoid WindowState#getRootTask() so we don't attribute system windows to a task.
+                final Task task = dc.mInputMethodTarget.getTask();
+                if (task == null) {
+                    return null;
+                }
+                ActivityStack rootTask = (ActivityStack) task.getRootTask();
+                final TaskTile tile = rootTask.getTile();
+                if (tile != null) {
+                    rootTask = tile;
+                }
+                return rootTask.mRemoteToken;
+            }
+        } finally {
+            Binder.restoreCallingIdentity(origId);
+        }
+    }
+
+    @Override
+    public void setLaunchRoot(int displayId, @Nullable IWindowContainer tile) {
+        enforceStackPermission("setLaunchRoot()");
+        final long origId = Binder.clearCallingIdentity();
+        try {
+            synchronized (mGlobalLock) {
+                DisplayContent display = mService.mRootWindowContainer.getDisplayContent(displayId);
+                if (display == null) {
+                    return;
+                }
+                TaskTile taskTile = tile == null ? null : TaskTile.forToken(tile.asBinder());
+                if (taskTile == null) {
+                    display.mLaunchTile = null;
+                    return;
+                }
+                if (taskTile.getDisplay() != display) {
+                    throw new RuntimeException("Can't set launch root for display " + displayId
+                            + " to task on display " + taskTile.getDisplay().getDisplayId());
+                }
+                display.mLaunchTile = taskTile;
+            }
+        } finally {
+            Binder.restoreCallingIdentity(origId);
+        }
+    }
+
+    private int sanitizeAndApplyChange(WindowContainer container,
+            WindowContainerTransaction.Change change) {
+        if (!(container instanceof Task)) {
+            throw new RuntimeException("Invalid token in task transaction");
+        }
+        // The "client"-facing API should prevent bad changes; however, just in case, sanitize
+        // masks here.
+        int configMask = change.getConfigSetMask();
+        int windowMask = change.getWindowSetMask();
+        configMask &= ActivityInfo.CONFIG_WINDOW_CONFIGURATION
+                | ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
+        windowMask &= WindowConfiguration.WINDOW_CONFIG_BOUNDS;
+        int effects = 0;
+        if (configMask != 0) {
+            Configuration c = new Configuration(container.getRequestedOverrideConfiguration());
+            c.setTo(change.getConfiguration(), configMask, windowMask);
+            container.onRequestedOverrideConfigurationChanged(c);
+            // TODO(b/145675353): remove the following once we could apply new bounds to the
+            // pinned stack together with its children.
+            resizePinnedStackIfNeeded(container, configMask, windowMask, c);
+            effects |= TRANSACT_EFFECTS_CLIENT_CONFIG;
+        }
+        if ((change.getChangeMask() & WindowContainerTransaction.Change.CHANGE_FOCUSABLE) != 0) {
+            if (container.setFocusable(change.getFocusable())) {
+                effects |= TRANSACT_EFFECTS_LIFECYCLE;
+            }
+        }
+        return effects;
+    }
+
+    private void resizePinnedStackIfNeeded(ConfigurationContainer container, int configMask,
+            int windowMask, Configuration config) {
+        if ((container instanceof ActivityStack)
+                && ((configMask & ActivityInfo.CONFIG_WINDOW_CONFIGURATION) != 0)
+                && ((windowMask & WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)) {
+            final ActivityStack stack = (ActivityStack) container;
+            if (stack.inPinnedWindowingMode()) {
+                stack.resize(config.windowConfiguration.getBounds(),
+                        null /* tempTaskBounds */, null /* tempTaskInsetBounds */,
+                        PRESERVE_WINDOWS, true /* deferResume */);
+            }
+        }
+    }
+
+    private int applyWindowContainerChange(WindowContainer wc,
+            WindowContainerTransaction.Change c) {
+        int effects = sanitizeAndApplyChange(wc, c);
+
+        Rect enterPipBounds = c.getEnterPipBounds();
+        if (enterPipBounds != null) {
+            Task tr = (Task) wc;
+            mService.mStackSupervisor.updatePictureInPictureMode(tr,
+                    enterPipBounds, true);
+        }
+        return effects;
+    }
+
+    @Override
+    public void applyContainerTransaction(WindowContainerTransaction t) {
+        enforceStackPermission("applyContainerTransaction()");
+        if (t == null) {
+            return;
+        }
+        long ident = Binder.clearCallingIdentity();
+        try {
+            synchronized (mGlobalLock) {
+                int effects = 0;
+                mService.deferWindowLayout();
+                try {
+                    ArraySet<WindowContainer> haveConfigChanges = new ArraySet<>();
+                    Iterator<Map.Entry<IBinder, WindowContainerTransaction.Change>> entries =
+                            t.getChanges().entrySet().iterator();
+                    while (entries.hasNext()) {
+                        final Map.Entry<IBinder, WindowContainerTransaction.Change> entry =
+                                entries.next();
+                        final WindowContainer wc = WindowContainer.RemoteToken.fromBinder(
+                                entry.getKey()).getContainer();
+                        int containerEffect = applyWindowContainerChange(wc, entry.getValue());
+                        effects |= containerEffect;
+                        // Lifecycle changes will trigger ensureConfig for everything.
+                        if ((effects & TRANSACT_EFFECTS_LIFECYCLE) == 0
+                                && (containerEffect & TRANSACT_EFFECTS_CLIENT_CONFIG) != 0) {
+                            haveConfigChanges.add(wc);
+                        }
+                    }
+                    if ((effects & TRANSACT_EFFECTS_LIFECYCLE) != 0) {
+                        // Already calls ensureActivityConfig
+                        mService.mRootWindowContainer.ensureActivitiesVisible(
+                                null, 0, PRESERVE_WINDOWS);
+                    } else if ((effects & TRANSACT_EFFECTS_CLIENT_CONFIG) != 0) {
+                        final PooledConsumer f = PooledLambda.obtainConsumer(
+                                ActivityRecord::ensureActivityConfiguration,
+                                PooledLambda.__(ActivityRecord.class), 0,
+                                false /* preserveWindow */);
+                        try {
+                            for (int i = haveConfigChanges.size() - 1; i >= 0; --i) {
+                                haveConfigChanges.valueAt(i).forAllActivities(f);
+                            }
+                        } finally {
+                            f.recycle();
+                        }
+                    }
+                } finally {
+                    mService.continueWindowLayout();
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/wm/TaskTile.java b/services/core/java/com/android/server/wm/TaskTile.java
new file mode 100644
index 0000000..add11d6
--- /dev/null
+++ b/services/core/java/com/android/server/wm/TaskTile.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
+
+import android.app.ActivityManager;
+import android.app.TaskInfo;
+import android.app.WindowConfiguration;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.util.Slog;
+import android.view.SurfaceControl;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+
+/**
+ * A Tile. Right now this acts as a proxy for manipulating non-child stacks. Eventually, this
+ * can become an actual parent.
+ */
+// TODO(task-hierarchy): Remove when tasks can nest >2 or when single tasks can handle their
+//                       own lifecycles.
+public class TaskTile extends ActivityStack {
+    private static final String TAG = "TaskTile";
+    final ArrayList<WindowContainer> mChildren = new ArrayList<>();
+
+    private static ActivityInfo createEmptyActivityInfo() {
+        ActivityInfo info = new ActivityInfo();
+        info.applicationInfo = new ApplicationInfo();
+        return info;
+    }
+
+    TaskTile(ActivityTaskManagerService atmService, int id, int windowingMode) {
+        super(atmService, id, new Intent() /*intent*/,  null /*affinityIntent*/, null /*affinity*/,
+                null /*rootAffinity*/, null /*realActivity*/, null /*origActivity*/,
+                false /*rootWasReset*/, false /*autoRemoveRecents*/, false /*askedCompatMode*/,
+                0 /*userId*/, 0 /*effectiveUid*/, null /*lastDescription*/,
+                System.currentTimeMillis(), true /*neverRelinquishIdentity*/,
+                new ActivityManager.TaskDescription(), id, INVALID_TASK_ID, INVALID_TASK_ID,
+                0 /*taskAffiliationColor*/, 0 /*callingUid*/, "" /*callingPackage*/,
+                RESIZE_MODE_RESIZEABLE, false /*supportsPictureInPicture*/,
+                false /*_realActivitySuspended*/, false /*userSetupComplete*/, INVALID_MIN_SIZE,
+                INVALID_MIN_SIZE, createEmptyActivityInfo(), null /*voiceSession*/,
+                null /*voiceInteractor*/, null /*stack*/);
+        getRequestedOverrideConfiguration().windowConfiguration.setWindowingMode(windowingMode);
+    }
+
+    @Override
+    void onDisplayChanged(DisplayContent dc) {
+        mDisplayContent = null;
+        if (dc != null) {
+            dc.getPendingTransaction().merge(getPendingTransaction());
+        }
+        mDisplayContent = dc;
+        // Virtual parent, so don't notify children.
+    }
+
+    /**
+     * If there is a disconnection, this will clean up any vestigial surfaces left on the tile
+     * leash by moving known children to a new surfacecontrol and then removing the old one.
+     */
+    void cleanupSurfaces() {
+        if (mSurfaceControl == null) {
+            return;
+        }
+        SurfaceControl oldSurface = mSurfaceControl;
+        WindowContainer parentWin = getParent();
+        if (parentWin == null) {
+            return;
+        }
+        mSurfaceControl = parentWin.makeChildSurface(null).setName("TaskTile " + mTaskId + " - "
+                    + getRequestedOverrideWindowingMode()).setContainerLayer().build();
+        SurfaceControl.Transaction t = parentWin.getPendingTransaction();
+        t.show(mSurfaceControl);
+        for (int i = 0; i < mChildren.size(); ++i) {
+            if (mChildren.get(i).getSurfaceControl() == null) {
+                continue;
+            }
+            mChildren.get(i).reparentSurfaceControl(t, mSurfaceControl);
+        }
+        t.remove(oldSurface);
+    }
+
+    @Override
+    protected void addChild(WindowContainer child, Comparator<WindowContainer> comparator) {
+        throw new RuntimeException("Improper use of addChild() on Tile");
+    }
+
+    @Override
+    void addChild(WindowContainer child, int index) {
+        mChildren.add(child);
+        if (child instanceof ActivityStack) {
+            ((ActivityStack) child).setTile(this);
+        }
+        mAtmService.mTaskOrganizerController.dispatchTaskInfoChanged(
+                this, false /* force */);
+    }
+
+    @Override
+    void removeChild(WindowContainer child) {
+        if (child instanceof ActivityStack) {
+            ((ActivityStack) child).setTile(null);
+        }
+        mChildren.remove(child);
+        mAtmService.mTaskOrganizerController.dispatchTaskInfoChanged(
+                this, false /* force */);
+    }
+
+    void removeAllChildren() {
+        for (int i = mChildren.size() - 1; i >= 0; --i) {
+            final WindowContainer child = mChildren.get(i);
+            if (child instanceof ActivityStack) {
+                ((ActivityStack) child).setTile(null);
+            }
+        }
+        mChildren.clear();
+        mAtmService.mTaskOrganizerController.dispatchTaskInfoChanged(
+                this, false /* force */);
+    }
+
+    @Override
+    protected int getChildCount() {
+        // Currently 0 as this isn't a proper hierarchy member yet.
+        return 0;
+    }
+
+    @Override
+    public void setWindowingMode(/*@WindowConfiguration.WindowingMode*/ int windowingMode) {
+        Configuration c = new Configuration(getRequestedOverrideConfiguration());
+        c.windowConfiguration.setWindowingMode(windowingMode);
+        onRequestedOverrideConfigurationChanged(c);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newParentConfig) {
+        super.onConfigurationChanged(newParentConfig);
+        for (int i = mChildren.size() - 1; i >= 0; --i) {
+            final WindowContainer child = mChildren.get(i);
+            child.onConfigurationChanged(child.getParent().getConfiguration());
+        }
+    }
+
+    /**
+     * Until this can be part of the hierarchy, the Stack level can use this utility during
+     * resolveOverrideConfig to simulate inheritance.
+     */
+    void updateResolvedConfig(Configuration inOutResolvedConfig) {
+        Rect resolveBounds = inOutResolvedConfig.windowConfiguration.getBounds();
+        if (resolveBounds == null || resolveBounds.isEmpty()) {
+            resolveBounds.set(getRequestedOverrideBounds());
+        }
+        int stackMode = inOutResolvedConfig.windowConfiguration.getWindowingMode();
+        if (stackMode == WindowConfiguration.WINDOWING_MODE_UNDEFINED
+                || stackMode == WindowConfiguration.WINDOWING_MODE_FULLSCREEN) {
+            // Also replace FULLSCREEN because we interpret FULLSCREEN as "fill parent"
+            inOutResolvedConfig.windowConfiguration.setWindowingMode(
+                    getRequestedOverrideWindowingMode());
+        }
+        if (inOutResolvedConfig.smallestScreenWidthDp
+                == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED) {
+            inOutResolvedConfig.smallestScreenWidthDp =
+                    getRequestedOverrideConfiguration().smallestScreenWidthDp;
+        }
+    }
+
+    @Override
+    void fillTaskInfo(TaskInfo info) {
+        super.fillTaskInfo(info);
+        WindowContainer top = null;
+        // Check mChildren.isEmpty directly because hasChild() -> getChildCount() always returns 0
+        if (!mChildren.isEmpty()) {
+            // Find the top-most root task which is a virtual child of this Tile. Because this is a
+            // virtual parent, the mChildren order here isn't changed during hierarchy operations.
+            WindowContainer parent = mChildren.get(0).getParent();
+            for (int i = parent.getChildCount() - 1; i >= 0; --i) {
+                if (mChildren.contains(parent.getChildAt(i))) {
+                    top = parent.getChildAt(i);
+                    break;
+                }
+            }
+        }
+        final Task topTask = top == null ? null : top.getTopMostTask();
+        boolean isResizable = topTask == null || topTask.isResizeable();
+        info.resizeMode = isResizable ? RESIZE_MODE_RESIZEABLE : RESIZE_MODE_UNRESIZEABLE;
+        info.topActivityType = top == null ? ACTIVITY_TYPE_UNDEFINED : top.getActivityType();
+        info.configuration.setTo(getRequestedOverrideConfiguration());
+    }
+
+    @Override
+    void removeImmediately() {
+        removeAllChildren();
+        super.removeImmediately();
+    }
+
+    static TaskTile forToken(IBinder token) {
+        try {
+            return (TaskTile) ((TaskToken) token).getContainer();
+        } catch (ClassCastException e) {
+            Slog.w(TAG, "Bad tile token: " + token, e);
+            return null;
+        }
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
index d22502d..4532400 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
@@ -16,9 +16,6 @@
 
 package com.android.server.wm;
 
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -27,17 +24,14 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.when;
 
 import android.app.Activity;
-import android.app.ActivityManager;
 import android.app.PictureInPictureParams;
 import android.app.servertransaction.ClientTransaction;
 import android.app.servertransaction.EnterPipRequestedItem;
@@ -46,7 +40,6 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.view.IDisplayWindowListener;
-import android.view.WindowContainerTransaction;
 
 import androidx.test.filters.MediumTest;
 
@@ -126,47 +119,6 @@
     }
 
     @Test
-    public void testTaskTransaction() {
-        removeGlobalMinSizeRestriction();
-        final ActivityStack stack = new StackBuilder(mRootWindowContainer)
-                .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
-        final Task task = stack.getTopMostTask();
-        WindowContainerTransaction t = new WindowContainerTransaction();
-        Rect newBounds = new Rect(10, 10, 100, 100);
-        t.setBounds(task.mRemoteToken, new Rect(10, 10, 100, 100));
-        mService.applyContainerTransaction(t);
-        assertEquals(newBounds, task.getBounds());
-    }
-
-    @Test
-    public void testStackTransaction() {
-        removeGlobalMinSizeRestriction();
-        final ActivityStack stack = new StackBuilder(mRootWindowContainer)
-                .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
-        ActivityManager.StackInfo info =
-                mService.getStackInfo(WINDOWING_MODE_FREEFORM, ACTIVITY_TYPE_STANDARD);
-        WindowContainerTransaction t = new WindowContainerTransaction();
-        assertEquals(stack.mRemoteToken, info.stackToken);
-        Rect newBounds = new Rect(10, 10, 100, 100);
-        t.setBounds(info.stackToken, new Rect(10, 10, 100, 100));
-        mService.applyContainerTransaction(t);
-        assertEquals(newBounds, stack.getBounds());
-    }
-
-    @Test
-    public void testContainerChanges() {
-        removeGlobalMinSizeRestriction();
-        final ActivityStack stack = new StackBuilder(mRootWindowContainer)
-                .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
-        final Task task = stack.getTopMostTask();
-        WindowContainerTransaction t = new WindowContainerTransaction();
-        assertTrue(task.isFocusable());
-        t.setFocusable(stack.mRemoteToken, false);
-        mService.applyContainerTransaction(t);
-        assertFalse(task.isFocusable());
-    }
-
-    @Test
     public void testDisplayWindowListener() {
         final ArrayList<Integer> added = new ArrayList<>();
         final ArrayList<Integer> changed = new ArrayList<>();
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 9e80cf2..078347e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskOrganizerTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,45 +16,50 @@
 
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.assertFalse;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
 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.verify;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
 
-import android.graphics.Point;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.ActivityManager.StackInfo;
+import android.content.res.Configuration;
 import android.graphics.Rect;
-import android.platform.test.annotations.Presubmit;
 import android.os.Binder;
 import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+import android.view.Display;
 import android.view.ITaskOrganizer;
+import android.view.IWindowContainer;
 import android.view.SurfaceControl;
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import android.view.WindowContainerTransaction;
 
 import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
- * Test class for {@link TaskOrganizer}.
+ * Test class for {@link ITaskOrganizer} and {@link android.app.ITaskOrganizerController}.
  *
  * Build/Install/Run:
  *  atest WmTests:TaskOrganizerTests
@@ -67,7 +72,8 @@
         final ITaskOrganizer organizer = mock(ITaskOrganizer.class);
         when(organizer.asBinder()).thenReturn(new Binder());
 
-        mWm.mAtmService.registerTaskOrganizer(organizer, windowingMode);
+        mWm.mAtmService.mTaskOrganizerController.registerTaskOrganizer(
+                organizer, windowingMode);
 
         return organizer;
     }
@@ -83,7 +89,7 @@
         final ITaskOrganizer organizer = registerMockOrganizer();
 
         task.setTaskOrganizer(organizer);
-        verify(organizer).taskAppeared(any(), any());
+        verify(organizer).taskAppeared(any());
 
         task.removeImmediately();
         verify(organizer).taskVanished(any());
@@ -97,10 +103,10 @@
         final ITaskOrganizer organizer2 = registerMockOrganizer(WINDOWING_MODE_PINNED);
 
         task.setTaskOrganizer(organizer);
-        verify(organizer).taskAppeared(any(), any());
+        verify(organizer).taskAppeared(any());
         task.setTaskOrganizer(organizer2);
         verify(organizer).taskVanished(any());
-        verify(organizer2).taskAppeared(any(), any());
+        verify(organizer2).taskAppeared(any());
     }
 
     @Test
@@ -111,10 +117,10 @@
         final ITaskOrganizer organizer2 = registerMockOrganizer(WINDOWING_MODE_PINNED);
  
         stack.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
-        verify(organizer).taskAppeared(any(), any());
+        verify(organizer).taskAppeared(any());
         stack.setWindowingMode(WINDOWING_MODE_PINNED);
         verify(organizer).taskVanished(any());
-        verify(organizer2).taskAppeared(any(), any());
+        verify(organizer2).taskAppeared(any());
     }
 
     @Test
@@ -124,7 +130,7 @@
         final ITaskOrganizer organizer = registerMockOrganizer();
 
         stack.setTaskOrganizer(organizer);
-        verify(organizer).taskAppeared(any(), any());
+        verify(organizer).taskAppeared(any());
         assertTrue(stack.isControlledByTaskOrganizer());
 
         stack.setTaskOrganizer(null);
@@ -140,9 +146,176 @@
         final Task task = createTaskInStack(stack, 0 /* userId */);
         final Task task2 = createTaskInStack(stack, 0 /* userId */);
         stack.setWindowingMode(WINDOWING_MODE_PINNED);
-        verify(organizer, times(1)).taskAppeared(any(), any());
+        verify(organizer, times(1)).taskAppeared(any());
 
         stack.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
         verify(organizer, times(1)).taskVanished(any());
     }
+
+    @Test
+    public void testTaskTransaction() {
+        removeGlobalMinSizeRestriction();
+        final ActivityStack stack = new ActivityTestsBase.StackBuilder(mWm.mRoot)
+                .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+        final Task task = stack.getTopMostTask();
+        WindowContainerTransaction t = new WindowContainerTransaction();
+        Rect newBounds = new Rect(10, 10, 100, 100);
+        t.setBounds(task.mRemoteToken, new Rect(10, 10, 100, 100));
+        mWm.mAtmService.mTaskOrganizerController.applyContainerTransaction(t);
+        assertEquals(newBounds, task.getBounds());
+    }
+
+    @Test
+    public void testStackTransaction() {
+        removeGlobalMinSizeRestriction();
+        final ActivityStack stack = new ActivityTestsBase.StackBuilder(mWm.mRoot)
+                .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+        StackInfo info =
+                mWm.mAtmService.getStackInfo(WINDOWING_MODE_FREEFORM, ACTIVITY_TYPE_STANDARD);
+        WindowContainerTransaction t = new WindowContainerTransaction();
+        assertEquals(stack.mRemoteToken, info.stackToken);
+        Rect newBounds = new Rect(10, 10, 100, 100);
+        t.setBounds(info.stackToken, new Rect(10, 10, 100, 100));
+        mWm.mAtmService.mTaskOrganizerController.applyContainerTransaction(t);
+        assertEquals(newBounds, stack.getBounds());
+    }
+
+    @Test
+    public void testContainerChanges() {
+        removeGlobalMinSizeRestriction();
+        final ActivityStack stack = new ActivityTestsBase.StackBuilder(mWm.mRoot)
+                .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+        final Task task = stack.getTopMostTask();
+        WindowContainerTransaction t = new WindowContainerTransaction();
+        assertTrue(task.isFocusable());
+        t.setFocusable(stack.mRemoteToken, false);
+        mWm.mAtmService.mTaskOrganizerController.applyContainerTransaction(t);
+        assertFalse(task.isFocusable());
+    }
+
+    @Test
+    public void testCreateDeleteRootTasks() {
+        RunningTaskInfo info1 = mWm.mAtmService.mTaskOrganizerController.createRootTask(
+                Display.DEFAULT_DISPLAY,
+                WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
+        assertEquals(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY,
+                info1.configuration.windowConfiguration.getWindowingMode());
+        assertEquals(ACTIVITY_TYPE_UNDEFINED, info1.topActivityType);
+
+        RunningTaskInfo info2 = mWm.mAtmService.mTaskOrganizerController.createRootTask(
+                Display.DEFAULT_DISPLAY,
+                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+        assertEquals(WINDOWING_MODE_SPLIT_SCREEN_SECONDARY,
+                info2.configuration.windowConfiguration.getWindowingMode());
+        assertEquals(ACTIVITY_TYPE_UNDEFINED, info2.topActivityType);
+
+        DisplayContent dc = mWm.mRoot.getDisplayContent(Display.DEFAULT_DISPLAY);
+        List<TaskTile> infos = getTaskTiles(dc);
+        assertEquals(2, infos.size());
+
+        assertTrue(mWm.mAtmService.mTaskOrganizerController.deleteRootTask(info1.token));
+        infos = getTaskTiles(dc);
+        assertEquals(1, infos.size());
+        assertEquals(WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, infos.get(0).getWindowingMode());
+    }
+
+    @Test
+    public void testTileAddRemoveChild() {
+        RunningTaskInfo info1 = mWm.mAtmService.mTaskOrganizerController.createRootTask(
+                mDisplayContent.mDisplayId, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+
+        final ActivityStack stack = createTaskStackOnDisplay(
+                WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_STANDARD, mDisplayContent);
+        assertEquals(mDisplayContent.getWindowingMode(), stack.getWindowingMode());
+        TaskTile tile1 = TaskTile.forToken(info1.token.asBinder());
+        tile1.addChild(stack, 0 /* index */);
+        assertEquals(info1.configuration.windowConfiguration.getWindowingMode(),
+                stack.getWindowingMode());
+
+        // Info should reflect new membership
+        List<TaskTile> tiles = getTaskTiles(mDisplayContent);
+        info1 = new RunningTaskInfo();
+        tiles.get(0).fillTaskInfo(info1);
+        assertEquals(ACTIVITY_TYPE_STANDARD, info1.topActivityType);
+
+        // Children inherit configuration
+        Rect newSize = new Rect(10, 10, 300, 300);
+        Configuration c = new Configuration(tile1.getRequestedOverrideConfiguration());
+        c.windowConfiguration.setBounds(newSize);
+        tile1.onRequestedOverrideConfigurationChanged(c);
+        assertEquals(newSize, stack.getBounds());
+
+        tile1.removeChild(stack);
+        assertEquals(mDisplayContent.getWindowingMode(), stack.getWindowingMode());
+        info1 = new RunningTaskInfo();
+        tiles = getTaskTiles(mDisplayContent);
+        tiles.get(0).fillTaskInfo(info1);
+        assertEquals(ACTIVITY_TYPE_UNDEFINED, info1.topActivityType);
+    }
+
+    @Test
+    public void testTaskInfoCallback() {
+        final ArrayList<RunningTaskInfo> lastReportedTiles = new ArrayList<>();
+        final boolean[] called = {false};
+        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) throws RemoteException {
+                lastReportedTiles.add(info);
+                called[0] = true;
+            }
+        };
+        mWm.mAtmService.mTaskOrganizerController.registerTaskOrganizer(listener,
+                WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+        RunningTaskInfo info1 = mWm.mAtmService.mTaskOrganizerController.createRootTask(
+                mDisplayContent.mDisplayId, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
+        lastReportedTiles.clear();
+        called[0] = false;
+
+        final ActivityStack stack = createTaskStackOnDisplay(
+                WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_STANDARD, mDisplayContent);
+        TaskTile tile1 = TaskTile.forToken(info1.token.asBinder());
+        tile1.addChild(stack, 0 /* index */);
+        assertTrue(called[0]);
+        assertEquals(ACTIVITY_TYPE_STANDARD, lastReportedTiles.get(0).topActivityType);
+
+        lastReportedTiles.clear();
+        called[0] = false;
+        final ActivityStack stack2 = createTaskStackOnDisplay(
+                WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_HOME, mDisplayContent);
+        tile1.addChild(stack2, 0 /* index */);
+        assertTrue(called[0]);
+        assertEquals(ACTIVITY_TYPE_HOME, lastReportedTiles.get(0).topActivityType);
+
+        lastReportedTiles.clear();
+        called[0] = false;
+        mDisplayContent.positionStackAtTop(stack, false /* includingParents */);
+        assertTrue(called[0]);
+        assertEquals(ACTIVITY_TYPE_STANDARD, lastReportedTiles.get(0).topActivityType);
+
+        lastReportedTiles.clear();
+        called[0] = false;
+        tile1.removeAllChildren();
+        assertTrue(called[0]);
+        assertEquals(ACTIVITY_TYPE_UNDEFINED, lastReportedTiles.get(0).topActivityType);
+    }
+
+    private List<TaskTile> getTaskTiles(DisplayContent dc) {
+        ArrayList<TaskTile> out = new ArrayList<>();
+        for (int i = dc.getStackCount() - 1; i >= 0; --i) {
+            final Task t = dc.getStackAt(i);
+            if (t instanceof TaskTile) {
+                out.add((TaskTile) t);
+            }
+        }
+        return out;
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 8e362ae..1ca2e318 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -40,6 +40,7 @@
 import static org.mockito.Mockito.mock;
 
 import android.content.Context;
+import android.content.Intent;
 import android.util.Log;
 import android.view.Display;
 import android.view.DisplayInfo;
@@ -340,6 +341,7 @@
                     .setWindowingMode(windowingMode)
                     .setActivityType(activityType)
                     .setCreateActivity(false)
+                    .setIntent(new Intent())
                     .build();
         }
     }
diff --git a/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerMultiWindowTest.java b/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerMultiWindowTest.java
index bc8d3c3..3fb0050 100644
--- a/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerMultiWindowTest.java
+++ b/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerMultiWindowTest.java
@@ -25,6 +25,7 @@
 import android.content.Intent;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.os.RemoteException;
 import android.view.ITaskOrganizer;
 import android.view.IWindowContainer;
 import android.view.SurfaceControl;
@@ -47,13 +48,16 @@
 
     class Organizer extends ITaskOrganizer.Stub {
         @Override
-        public void taskAppeared(IWindowContainer wc, ActivityManager.RunningTaskInfo ti) {
-            mView.reparentTask(wc);
+        public void taskAppeared(ActivityManager.RunningTaskInfo ti) {
+            mView.reparentTask(ti.token);
         }
         public void taskVanished(IWindowContainer wc) {
         }
         public void transactionReady(int id, SurfaceControl.Transaction t) {
         }
+        @Override
+        public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) {
+        }
     }
 
     Organizer mOrganizer = new Organizer();
diff --git a/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerPipTest.java b/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerPipTest.java
index fc1be28..8f3cb34 100644
--- a/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerPipTest.java
+++ b/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskOrganizerPipTest.java
@@ -21,18 +21,14 @@
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.app.Service;
-import android.app.WindowConfiguration;
-import android.content.Context;
 import android.content.Intent;
 import android.graphics.Rect;
 import android.os.IBinder;
 import android.view.ITaskOrganizer;
 import android.view.IWindowContainer;
-import android.view.WindowContainerTransaction;
 import android.view.SurfaceControl;
-import android.view.SurfaceHolder;
-import android.view.SurfaceView;
 import android.view.ViewGroup;
+import android.view.WindowContainerTransaction;
 import android.view.WindowManager;
 import android.widget.FrameLayout;
 
@@ -43,13 +39,13 @@
     TaskView mTaskView;
 
     class Organizer extends ITaskOrganizer.Stub {
-        public void taskAppeared(IWindowContainer wc, ActivityManager.RunningTaskInfo ti) {
-            mTaskView.reparentTask(wc);
+        public void taskAppeared(ActivityManager.RunningTaskInfo ti) {
+            mTaskView.reparentTask(ti.token);
 
             final WindowContainerTransaction wct = new WindowContainerTransaction();
-            wct.scheduleFinishEnterPip(wc, new Rect(0, 0, PIP_WIDTH, PIP_HEIGHT));
+            wct.scheduleFinishEnterPip(ti.token, new Rect(0, 0, PIP_WIDTH, PIP_HEIGHT));
             try {
-                ActivityTaskManager.getService().applyContainerTransaction(wct);
+                ActivityTaskManager.getTaskOrganizerController().applyContainerTransaction(wct);
             } catch (Exception e) {
             }
         }
@@ -57,6 +53,8 @@
         }
         public void transactionReady(int id, SurfaceControl.Transaction t) {
         }
+        public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) {
+        }
     }
 
     Organizer mOrganizer = new Organizer();
diff --git a/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskView.java b/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskView.java
index ff73340..9f32bb8 100644
--- a/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskView.java
+++ b/tests/TaskOrganizerTest/src/com/android/test/taskembed/TaskView.java
@@ -44,7 +44,7 @@
     @Override
     public void surfaceCreated(SurfaceHolder holder) {
         try {
-            ActivityTaskManager.getService().registerTaskOrganizer(mTaskOrganizer,
+            ActivityTaskManager.getTaskOrganizerController().registerTaskOrganizer(mTaskOrganizer,
                     mWindowingMode);
         } catch (Exception e) {
         }